001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.camel.component.file;
018    
019    import java.io.IOException;
020    import java.lang.reflect.Method;
021    import java.util.ArrayList;
022    import java.util.Comparator;
023    import java.util.HashMap;
024    import java.util.List;
025    import java.util.Map;
026    
027    import org.apache.camel.CamelContext;
028    import org.apache.camel.Component;
029    import org.apache.camel.Exchange;
030    import org.apache.camel.Expression;
031    import org.apache.camel.ExpressionIllegalSyntaxException;
032    import org.apache.camel.Message;
033    import org.apache.camel.Processor;
034    import org.apache.camel.impl.ScheduledPollEndpoint;
035    import org.apache.camel.processor.idempotent.MemoryIdempotentRepository;
036    import org.apache.camel.spi.BrowsableEndpoint;
037    import org.apache.camel.spi.FactoryFinder;
038    import org.apache.camel.spi.IdempotentRepository;
039    import org.apache.camel.spi.Language;
040    import org.apache.camel.util.FileUtil;
041    import org.apache.camel.util.IOHelper;
042    import org.apache.camel.util.ObjectHelper;
043    import org.apache.camel.util.ServiceHelper;
044    import org.apache.camel.util.StringHelper;
045    import org.slf4j.Logger;
046    import org.slf4j.LoggerFactory;
047    
048    /**
049     * Base class for file endpoints
050     */
051    public abstract class GenericFileEndpoint<T> extends ScheduledPollEndpoint implements BrowsableEndpoint {
052    
053        protected static final transient String DEFAULT_STRATEGYFACTORY_CLASS = "org.apache.camel.component.file.strategy.GenericFileProcessStrategyFactory";
054        protected static final transient int DEFAULT_IDEMPOTENT_CACHE_SIZE = 1000;
055    
056        protected final transient Logger log = LoggerFactory.getLogger(getClass());
057    
058        protected GenericFileProcessStrategy<T> processStrategy;
059        protected GenericFileConfiguration configuration;
060    
061        protected IdempotentRepository<String> inProgressRepository = new MemoryIdempotentRepository();
062        protected String localWorkDirectory;
063        protected boolean autoCreate = true;
064        protected boolean startingDirectoryMustExist;
065        protected boolean directoryMustExist;
066        protected int bufferSize = FileUtil.BUFFER_SIZE;
067        protected GenericFileExist fileExist = GenericFileExist.Override;
068        protected boolean noop;
069        protected boolean recursive;
070        protected boolean delete;
071        protected boolean flatten;
072        protected int maxMessagesPerPoll;
073        protected boolean eagerMaxMessagesPerPoll = true;
074        protected int maxDepth = Integer.MAX_VALUE;
075        protected int minDepth;
076        protected String tempPrefix;
077        protected Expression tempFileName;
078        protected boolean eagerDeleteTargetFile = true;
079        protected String include;
080        protected String exclude;
081        protected String charset;
082        protected Expression fileName;
083        protected Expression move;
084        protected Expression moveFailed;
085        protected Expression preMove;
086        protected Expression moveExisting;
087        protected Boolean idempotent;
088        protected IdempotentRepository<String> idempotentRepository;
089        protected GenericFileFilter<T> filter;
090        protected AntPathMatcherGenericFileFilter<T> antFilter;
091        protected Comparator<GenericFile<T>> sorter;
092        protected Comparator<Exchange> sortBy;
093        protected String readLock = "none";
094        protected long readLockCheckInterval = 1000;
095        protected long readLockTimeout = 10000;
096        protected long readLockMinLength = 1;
097        protected GenericFileExclusiveReadLockStrategy<T> exclusiveReadLockStrategy;
098        protected boolean keepLastModified;
099        protected String doneFileName;
100        protected boolean allowNullBody;
101    
102        public GenericFileEndpoint() {
103        }
104    
105        public GenericFileEndpoint(String endpointUri, Component component) {
106            super(endpointUri, component);
107        }
108    
109        public boolean isSingleton() {
110            return true;
111        }
112    
113        public abstract GenericFileConsumer<T> createConsumer(Processor processor) throws Exception;
114    
115        public abstract GenericFileProducer<T> createProducer() throws Exception;
116    
117        public abstract Exchange createExchange(GenericFile<T> file);
118    
119        public abstract String getScheme();
120    
121        public abstract char getFileSeparator();
122    
123        public abstract boolean isAbsolute(String name);
124    
125        /**
126         * Return the file name that will be auto-generated for the given message if
127         * none is provided
128         */
129        public String getGeneratedFileName(Message message) {
130            return StringHelper.sanitize(message.getMessageId());
131        }
132    
133        public GenericFileProcessStrategy<T> getGenericFileProcessStrategy() {
134            if (processStrategy == null) {
135                processStrategy = createGenericFileStrategy();
136                log.debug("Using Generic file process strategy: {}", processStrategy);
137            }
138            return processStrategy;
139        }
140    
141        /**
142         * This implementation will <b>not</b> load the file content.
143         * Any file locking is neither in use by this implementation..
144         */
145        @Override
146        public List<Exchange> getExchanges() {
147            final List<Exchange> answer = new ArrayList<Exchange>();
148    
149            GenericFileConsumer<?> consumer = null;
150            try {
151                // create a new consumer which can poll the exchanges we want to browse
152                // do not provide a processor as we do some custom processing
153                consumer = createConsumer(null);
154                consumer.setCustomProcessor(new Processor() {
155                    @Override
156                    public void process(Exchange exchange) throws Exception {
157                        answer.add(exchange);
158                    }
159                });
160                // do not start scheduler, as we invoke the poll manually
161                consumer.setStartScheduler(false);
162                // start consumer
163                ServiceHelper.startService(consumer);
164                // invoke poll which performs the custom processing, so we can browse the exchanges
165                consumer.poll();
166            } catch (Exception e) {
167                throw ObjectHelper.wrapRuntimeCamelException(e);
168            } finally {
169                try {
170                    ServiceHelper.stopService(consumer);
171                } catch (Exception e) {
172                    log.debug("Error stopping consumer used for browsing exchanges. This exception will be ignored", e);
173                }
174            }
175    
176            return answer;
177        }
178    
179        /**
180         * A strategy method to lazily create the file strategy
181         */
182        @SuppressWarnings("unchecked")
183        protected GenericFileProcessStrategy<T> createGenericFileStrategy() {
184            Class<?> factory = null;
185            try {
186                FactoryFinder finder = getCamelContext().getFactoryFinder("META-INF/services/org/apache/camel/component/");
187                log.trace("Using FactoryFinder: {}", finder);
188                factory = finder.findClass(getScheme(), "strategy.factory.", CamelContext.class);
189            } catch (ClassNotFoundException e) {
190                log.trace("'strategy.factory.class' not found", e);
191            } catch (IOException e) {
192                log.trace("No strategy factory defined in 'META-INF/services/org/apache/camel/component/'", e);
193            }
194    
195            if (factory == null) {
196                // use default
197                try {
198                    log.trace("Using ClassResolver to resolve class: {}", DEFAULT_STRATEGYFACTORY_CLASS);
199                    factory = this.getCamelContext().getClassResolver().resolveClass(DEFAULT_STRATEGYFACTORY_CLASS);
200                } catch (Exception e) {
201                    log.trace("Cannot load class: {}", DEFAULT_STRATEGYFACTORY_CLASS, e);
202                }
203                // fallback and us this class loader
204                try {
205                    if (log.isTraceEnabled()) {
206                        log.trace("Using classloader: {} to resolve class: {}", this.getClass().getClassLoader(), DEFAULT_STRATEGYFACTORY_CLASS);
207                    }
208                    factory = this.getCamelContext().getClassResolver().resolveClass(DEFAULT_STRATEGYFACTORY_CLASS, this.getClass().getClassLoader());
209                } catch (Exception e) {
210                    if (log.isTraceEnabled()) {
211                        log.trace("Cannot load class: {} using classloader: " + this.getClass().getClassLoader(), DEFAULT_STRATEGYFACTORY_CLASS, e);
212                    }
213                }
214    
215                if (factory == null) {
216                    throw new TypeNotPresentException(DEFAULT_STRATEGYFACTORY_CLASS + " class not found", null);
217                }
218            }
219    
220            try {
221                Method factoryMethod = factory.getMethod("createGenericFileProcessStrategy", CamelContext.class, Map.class);
222                Map<String, Object> params = getParamsAsMap();
223                log.debug("Parameters for Generic file process strategy {}", params);
224                return (GenericFileProcessStrategy<T>) ObjectHelper.invokeMethod(factoryMethod, null, getCamelContext(), params);
225            } catch (NoSuchMethodException e) {
226                throw new TypeNotPresentException(factory.getSimpleName() + ".createGenericFileProcessStrategy method not found", e);
227            }
228        }
229    
230        public boolean isNoop() {
231            return noop;
232        }
233    
234        public void setNoop(boolean noop) {
235            this.noop = noop;
236        }
237    
238        public boolean isRecursive() {
239            return recursive;
240        }
241    
242        public void setRecursive(boolean recursive) {
243            this.recursive = recursive;
244        }
245    
246        public String getInclude() {
247            return include;
248        }
249    
250        public void setInclude(String include) {
251            this.include = include;
252        }
253    
254        public String getExclude() {
255            return exclude;
256        }
257    
258        public void setExclude(String exclude) {
259            this.exclude = exclude;
260        }
261    
262        public void setAntInclude(String antInclude) {
263            if (this.antFilter == null) {
264                this.antFilter = new AntPathMatcherGenericFileFilter<T>();
265            }
266            this.antFilter.setIncludes(antInclude);
267        }
268    
269        public void setAntExclude(String antExclude) {
270            if (this.antFilter == null) {
271                this.antFilter = new AntPathMatcherGenericFileFilter<T>();
272            }
273            this.antFilter.setExcludes(antExclude);
274        }
275    
276        public GenericFileFilter<T> getAntFilter() {
277            return antFilter;
278        }
279    
280        public boolean isDelete() {
281            return delete;
282        }
283    
284        public void setDelete(boolean delete) {
285            this.delete = delete;
286        }
287    
288        public boolean isFlatten() {
289            return flatten;
290        }
291    
292        public void setFlatten(boolean flatten) {
293            this.flatten = flatten;
294        }
295    
296        public Expression getMove() {
297            return move;
298        }
299    
300        public void setMove(Expression move) {
301            this.move = move;
302        }
303    
304        /**
305         * Sets the move failure expression based on
306         * {@link org.apache.camel.language.simple.SimpleLanguage}
307         */
308        public void setMoveFailed(String fileLanguageExpression) {
309            String expression = configureMoveOrPreMoveExpression(fileLanguageExpression);
310            this.moveFailed = createFileLanguageExpression(expression);
311        }
312    
313        public Expression getMoveFailed() {
314            return moveFailed;
315        }
316    
317        public void setMoveFailed(Expression moveFailed) {
318            this.moveFailed = moveFailed;
319        }
320    
321        /**
322         * Sets the move expression based on
323         * {@link org.apache.camel.language.simple.SimpleLanguage}
324         */
325        public void setMove(String fileLanguageExpression) {
326            String expression = configureMoveOrPreMoveExpression(fileLanguageExpression);
327            this.move = createFileLanguageExpression(expression);
328        }
329    
330        public Expression getPreMove() {
331            return preMove;
332        }
333    
334        public void setPreMove(Expression preMove) {
335            this.preMove = preMove;
336        }
337    
338        /**
339         * Sets the pre move expression based on
340         * {@link org.apache.camel.language.simple.SimpleLanguage}
341         */
342        public void setPreMove(String fileLanguageExpression) {
343            String expression = configureMoveOrPreMoveExpression(fileLanguageExpression);
344            this.preMove = createFileLanguageExpression(expression);
345        }
346    
347        public Expression getMoveExisting() {
348            return moveExisting;
349        }
350    
351        public void setMoveExisting(Expression moveExisting) {
352            this.moveExisting = moveExisting;
353        }
354    
355        /**
356         * Sets the move existing expression based on
357         * {@link org.apache.camel.language.simple.SimpleLanguage}
358         */
359        public void setMoveExisting(String fileLanguageExpression) {
360            String expression = configureMoveOrPreMoveExpression(fileLanguageExpression);
361            this.moveExisting = createFileLanguageExpression(expression);
362        }
363    
364        public Expression getFileName() {
365            return fileName;
366        }
367    
368        public void setFileName(Expression fileName) {
369            this.fileName = fileName;
370        }
371    
372        /**
373         * Sets the file expression based on
374         * {@link org.apache.camel.language.simple.SimpleLanguage}
375         */
376        public void setFileName(String fileLanguageExpression) {
377            this.fileName = createFileLanguageExpression(fileLanguageExpression);
378        }
379    
380        public String getDoneFileName() {
381            return doneFileName;
382        }
383    
384        /**
385         * Sets the done file name.
386         * <p/>
387         * Only ${file.name} and ${file.name.noext} is supported as dynamic placeholders.
388         */
389        public void setDoneFileName(String doneFileName) {
390            this.doneFileName = doneFileName;
391        }
392    
393        public Boolean isIdempotent() {
394            return idempotent != null ? idempotent : false;
395        }
396    
397        public String getCharset() {
398            return charset;
399        }
400    
401        public void setCharset(String charset) {
402            IOHelper.validateCharset(charset);
403            this.charset = charset;
404        }
405    
406        protected boolean isIdempotentSet() {
407            return idempotent != null;
408        }
409    
410        public void setIdempotent(Boolean idempotent) {
411            this.idempotent = idempotent;
412        }
413    
414        public IdempotentRepository<String> getIdempotentRepository() {
415            return idempotentRepository;
416        }
417    
418        public void setIdempotentRepository(IdempotentRepository<String> idempotentRepository) {
419            this.idempotentRepository = idempotentRepository;
420        }
421    
422        public GenericFileFilter<T> getFilter() {
423            return filter;
424        }
425    
426        public void setFilter(GenericFileFilter<T> filter) {
427            this.filter = filter;
428        }
429    
430        public Comparator<GenericFile<T>> getSorter() {
431            return sorter;
432        }
433    
434        public void setSorter(Comparator<GenericFile<T>> sorter) {
435            this.sorter = sorter;
436        }
437    
438        public Comparator<Exchange> getSortBy() {
439            return sortBy;
440        }
441    
442        public void setSortBy(Comparator<Exchange> sortBy) {
443            this.sortBy = sortBy;
444        }
445    
446        public void setSortBy(String expression) {
447            setSortBy(expression, false);
448        }
449    
450        public void setSortBy(String expression, boolean reverse) {
451            setSortBy(GenericFileDefaultSorter.sortByFileLanguage(getCamelContext(), expression, reverse));
452        }
453    
454        public String getTempPrefix() {
455            return tempPrefix;
456        }
457    
458        /**
459         * Enables and uses temporary prefix when writing files, after write it will
460         * be renamed to the correct name.
461         */
462        public void setTempPrefix(String tempPrefix) {
463            this.tempPrefix = tempPrefix;
464            // use only name as we set a prefix in from on the name
465            setTempFileName(tempPrefix + "${file:onlyname}");
466        }
467    
468        public Expression getTempFileName() {
469            return tempFileName;
470        }
471    
472        public void setTempFileName(Expression tempFileName) {
473            this.tempFileName = tempFileName;
474        }
475    
476        public void setTempFileName(String tempFileNameExpression) {
477            this.tempFileName = createFileLanguageExpression(tempFileNameExpression);
478        }
479    
480        public boolean isEagerDeleteTargetFile() {
481            return eagerDeleteTargetFile;
482        }
483    
484        public void setEagerDeleteTargetFile(boolean eagerDeleteTargetFile) {
485            this.eagerDeleteTargetFile = eagerDeleteTargetFile;
486        }
487    
488        public GenericFileConfiguration getConfiguration() {
489            if (configuration == null) {
490                configuration = new GenericFileConfiguration();
491            }
492            return configuration;
493        }
494    
495        public void setConfiguration(GenericFileConfiguration configuration) {
496            this.configuration = configuration;
497        }
498    
499        public GenericFileExclusiveReadLockStrategy<T> getExclusiveReadLockStrategy() {
500            return exclusiveReadLockStrategy;
501        }
502    
503        public void setExclusiveReadLockStrategy(GenericFileExclusiveReadLockStrategy<T> exclusiveReadLockStrategy) {
504            this.exclusiveReadLockStrategy = exclusiveReadLockStrategy;
505        }
506    
507        public String getReadLock() {
508            return readLock;
509        }
510    
511        public void setReadLock(String readLock) {
512            this.readLock = readLock;
513        }
514    
515        public long getReadLockCheckInterval() {
516            return readLockCheckInterval;
517        }
518    
519        public void setReadLockCheckInterval(long readLockCheckInterval) {
520            this.readLockCheckInterval = readLockCheckInterval;
521        }
522    
523        public long getReadLockTimeout() {
524            return readLockTimeout;
525        }
526    
527        public void setReadLockTimeout(long readLockTimeout) {
528            this.readLockTimeout = readLockTimeout;
529        }
530    
531        public long getReadLockMinLength() {
532            return readLockMinLength;
533        }
534    
535        public void setReadLockMinLength(long readLockMinLength) {
536            this.readLockMinLength = readLockMinLength;
537        }
538    
539        public int getBufferSize() {
540            return bufferSize;
541        }
542    
543        public void setBufferSize(int bufferSize) {
544            if (bufferSize <= 0) {
545                throw new IllegalArgumentException("BufferSize must be a positive value, was " + bufferSize);
546            }
547            this.bufferSize = bufferSize;
548        }
549    
550        public GenericFileExist getFileExist() {
551            return fileExist;
552        }
553    
554        public void setFileExist(GenericFileExist fileExist) {
555            this.fileExist = fileExist;
556        }
557    
558        public boolean isAutoCreate() {
559            return autoCreate;
560        }
561    
562        public void setAutoCreate(boolean autoCreate) {
563            this.autoCreate = autoCreate;
564        }
565    
566        public boolean isStartingDirectoryMustExist() {
567            return startingDirectoryMustExist;
568        }
569    
570        public void setStartingDirectoryMustExist(boolean startingDirectoryMustExist) {
571            this.startingDirectoryMustExist = startingDirectoryMustExist;
572        }
573    
574        public boolean isDirectoryMustExist() {
575            return directoryMustExist;
576        }
577    
578        public void setDirectoryMustExist(boolean directoryMustExist) {
579            this.directoryMustExist = directoryMustExist;
580        }
581    
582        public GenericFileProcessStrategy<T> getProcessStrategy() {
583            return processStrategy;
584        }
585    
586        public void setProcessStrategy(GenericFileProcessStrategy<T> processStrategy) {
587            this.processStrategy = processStrategy;
588        }
589    
590        public String getLocalWorkDirectory() {
591            return localWorkDirectory;
592        }
593    
594        public void setLocalWorkDirectory(String localWorkDirectory) {
595            this.localWorkDirectory = localWorkDirectory;
596        }
597    
598        public int getMaxMessagesPerPoll() {
599            return maxMessagesPerPoll;
600        }
601    
602        public void setMaxMessagesPerPoll(int maxMessagesPerPoll) {
603            this.maxMessagesPerPoll = maxMessagesPerPoll;
604        }
605    
606        public boolean isEagerMaxMessagesPerPoll() {
607            return eagerMaxMessagesPerPoll;
608        }
609    
610        public void setEagerMaxMessagesPerPoll(boolean eagerMaxMessagesPerPoll) {
611            this.eagerMaxMessagesPerPoll = eagerMaxMessagesPerPoll;
612        }
613    
614        public int getMaxDepth() {
615            return maxDepth;
616        }
617    
618        public void setMaxDepth(int maxDepth) {
619            this.maxDepth = maxDepth;
620        }
621    
622        public int getMinDepth() {
623            return minDepth;
624        }
625    
626        public void setMinDepth(int minDepth) {
627            this.minDepth = minDepth;
628        }
629    
630        public IdempotentRepository<String> getInProgressRepository() {
631            return inProgressRepository;
632        }
633    
634        public void setInProgressRepository(IdempotentRepository<String> inProgressRepository) {
635            this.inProgressRepository = inProgressRepository;
636        }
637    
638        public boolean isKeepLastModified() {
639            return keepLastModified;
640        }
641    
642        public void setKeepLastModified(boolean keepLastModified) {
643            this.keepLastModified = keepLastModified;
644        }
645    
646        public boolean isAllowNullBody() {
647            return allowNullBody;
648        }
649        
650        public void setAllowNullBody(boolean allowNullBody) {
651            this.allowNullBody = allowNullBody;
652        }
653        
654        /**
655         * Configures the given message with the file which sets the body to the
656         * file object.
657         */
658        public void configureMessage(GenericFile<T> file, Message message) {
659            message.setBody(file);
660    
661            if (flatten) {
662                // when flatten the file name should not contain any paths
663                message.setHeader(Exchange.FILE_NAME, file.getFileNameOnly());
664            } else {
665                // compute name to set on header that should be relative to starting directory
666                String name = file.isAbsolute() ? file.getAbsoluteFilePath() : file.getRelativeFilePath();
667    
668                // skip leading endpoint configured directory
669                String endpointPath = getConfiguration().getDirectory() + getFileSeparator();
670    
671                // need to normalize paths to ensure we can match using startsWith
672                endpointPath = FileUtil.normalizePath(endpointPath);
673                String copyOfName = FileUtil.normalizePath(name);
674                if (ObjectHelper.isNotEmpty(endpointPath) && copyOfName.startsWith(endpointPath)) {
675                    name = name.substring(endpointPath.length());
676                }
677    
678                // adjust filename
679                message.setHeader(Exchange.FILE_NAME, name);
680            }
681        }
682    
683        /**
684         * Set up the exchange properties with the options of the file endpoint
685         */
686        public void configureExchange(Exchange exchange) {
687            // Now we just set the charset property here
688            if (getCharset() != null) {
689                exchange.setProperty(Exchange.CHARSET_NAME, getCharset());
690            }
691        }
692    
693        /**
694         * Strategy to configure the move, preMove, or moveExisting option based on a String input.
695         *
696         * @param expression the original string input
697         * @return configured string or the original if no modifications is needed
698         */
699        protected String configureMoveOrPreMoveExpression(String expression) {
700            // if the expression already have ${ } placeholders then pass it unmodified
701            if (StringHelper.hasStartToken(expression, "simple")) {
702                return expression;
703            }
704    
705            // remove trailing slash
706            expression = FileUtil.stripTrailingSeparator(expression);
707    
708            StringBuilder sb = new StringBuilder();
709    
710            // if relative then insert start with the parent folder
711            if (!isAbsolute(expression)) {
712                sb.append("${file:parent}");
713                sb.append(getFileSeparator());
714            }
715            // insert the directory the end user provided
716            sb.append(expression);
717            // append only the filename (file:name can contain a relative path, so we must use onlyname)
718            sb.append(getFileSeparator());
719            sb.append("${file:onlyname}");
720    
721            return sb.toString();
722        }
723    
724        protected Map<String, Object> getParamsAsMap() {
725            Map<String, Object> params = new HashMap<String, Object>();
726    
727            if (isNoop()) {
728                params.put("noop", Boolean.toString(true));
729            }
730            if (isDelete()) {
731                params.put("delete", Boolean.toString(true));
732            }
733            if (move != null) {
734                params.put("move", move);
735            }
736            if (moveFailed != null) {
737                params.put("moveFailed", moveFailed);
738            }
739            if (preMove != null) {
740                params.put("preMove", preMove);
741            }
742            if (exclusiveReadLockStrategy != null) {
743                params.put("exclusiveReadLockStrategy", exclusiveReadLockStrategy);
744            }
745            if (readLock != null) {
746                params.put("readLock", readLock);
747            }
748            if (readLockCheckInterval > 0) {
749                params.put("readLockCheckInterval", readLockCheckInterval);
750            }
751            if (readLockTimeout > 0) {
752                params.put("readLockTimeout", readLockTimeout);
753            }
754            params.put("readLockMinLength", readLockMinLength);
755    
756            return params;
757        }
758    
759        private Expression createFileLanguageExpression(String expression) {
760            Language language;
761            // only use file language if the name is complex (eg. using $)
762            if (expression.contains("$")) {
763                language = getCamelContext().resolveLanguage("file");
764            } else {
765                language = getCamelContext().resolveLanguage("constant");
766            }
767            return language.createExpression(expression);
768        }
769    
770        /**
771         * Creates the associated name of the done file based on the given file name.
772         * <p/>
773         * This method should only be invoked if a done filename property has been set on this endpoint.
774         *
775         * @param fileName the file name
776         * @return name of the associated done file name
777         */
778        protected String createDoneFileName(String fileName) {
779            String pattern = getDoneFileName();
780            ObjectHelper.notEmpty(pattern, "doneFileName", pattern);
781    
782            // we only support ${file:name} or ${file:name.noext} as dynamic placeholders for done files
783            String path = FileUtil.onlyPath(fileName);
784            String onlyName = FileUtil.stripPath(fileName);
785    
786            pattern = pattern.replaceFirst("\\$\\{file:name\\}", onlyName);
787            pattern = pattern.replaceFirst("\\$simple\\{file:name\\}", onlyName);
788            pattern = pattern.replaceFirst("\\$\\{file:name.noext\\}", FileUtil.stripExt(onlyName));
789            pattern = pattern.replaceFirst("\\$simple\\{file:name.noext\\}", FileUtil.stripExt(onlyName));
790    
791            // must be able to resolve all placeholders supported
792            if (StringHelper.hasStartToken(pattern, "simple")) {
793                throw new ExpressionIllegalSyntaxException(fileName + ". Cannot resolve reminder: " + pattern);
794            }
795    
796            String answer = pattern;
797            if (ObjectHelper.isNotEmpty(path) && ObjectHelper.isNotEmpty(pattern)) {
798                // done file must always be in same directory as the real file name
799                answer = path + getFileSeparator() + pattern;
800            }
801    
802            if (getConfiguration().needToNormalize()) {
803                // must normalize path to cater for Windows and other OS
804                answer = FileUtil.normalizePath(answer);
805            }
806    
807            return answer;
808        }
809    
810        /**
811         * Is the given file a done file?
812         * <p/>
813         * This method should only be invoked if a done filename property has been set on this endpoint.
814         *
815         * @param fileName the file name
816         * @return <tt>true</tt> if its a done file, <tt>false</tt> otherwise
817         */
818        protected boolean isDoneFile(String fileName) {
819            String pattern = getDoneFileName();
820            ObjectHelper.notEmpty(pattern, "doneFileName", pattern);
821    
822            if (!StringHelper.hasStartToken(pattern, "simple")) {
823                // no tokens, so just match names directly
824                return pattern.equals(fileName);
825            }
826    
827            // the static part of the pattern, is that a prefix or suffix?
828            // its a prefix if ${ start token is not at the start of the pattern
829            boolean prefix = pattern.indexOf("${") > 0;
830    
831            // remove dynamic parts of the pattern so we only got the static part left
832            pattern = pattern.replaceFirst("\\$\\{file:name\\}", "");
833            pattern = pattern.replaceFirst("\\$simple\\{file:name\\}", "");
834            pattern = pattern.replaceFirst("\\$\\{file:name.noext\\}", "");
835            pattern = pattern.replaceFirst("\\$simple\\{file:name.noext\\}", "");
836    
837            // must be able to resolve all placeholders supported
838            if (StringHelper.hasStartToken(pattern, "simple")) {
839                throw new ExpressionIllegalSyntaxException(fileName + ". Cannot resolve reminder: " + pattern);
840            }
841    
842            if (prefix) {
843                return fileName.startsWith(pattern);
844            } else {
845                return fileName.endsWith(pattern);
846            }
847        }
848    
849        @Override
850        protected void doStart() throws Exception {
851            ServiceHelper.startServices(inProgressRepository, idempotentRepository);
852            super.doStart();
853        }
854    
855        @Override
856        protected void doStop() throws Exception {
857            super.doStop();
858            ServiceHelper.stopServices(inProgressRepository, idempotentRepository);
859        }
860    }