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 */
017package org.apache.camel.component.file;
018
019import java.io.IOException;
020import java.lang.reflect.Method;
021import java.util.ArrayList;
022import java.util.Comparator;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026
027import org.apache.camel.CamelContext;
028import org.apache.camel.Component;
029import org.apache.camel.Exchange;
030import org.apache.camel.Expression;
031import org.apache.camel.ExpressionIllegalSyntaxException;
032import org.apache.camel.LoggingLevel;
033import org.apache.camel.Message;
034import org.apache.camel.Predicate;
035import org.apache.camel.Processor;
036import org.apache.camel.impl.ScheduledPollEndpoint;
037import org.apache.camel.processor.idempotent.MemoryIdempotentRepository;
038import org.apache.camel.spi.BrowsableEndpoint;
039import org.apache.camel.spi.ExceptionHandler;
040import org.apache.camel.spi.FactoryFinder;
041import org.apache.camel.spi.IdempotentRepository;
042import org.apache.camel.spi.Language;
043import org.apache.camel.spi.UriParam;
044import org.apache.camel.util.FileUtil;
045import org.apache.camel.util.IOHelper;
046import org.apache.camel.util.ObjectHelper;
047import org.apache.camel.util.ServiceHelper;
048import org.apache.camel.util.StringHelper;
049import org.slf4j.Logger;
050import org.slf4j.LoggerFactory;
051
052/**
053 * Base class for file endpoints
054 */
055public abstract class GenericFileEndpoint<T> extends ScheduledPollEndpoint implements BrowsableEndpoint {
056
057    protected static final String DEFAULT_STRATEGYFACTORY_CLASS = "org.apache.camel.component.file.strategy.GenericFileProcessStrategyFactory";
058    protected static final int DEFAULT_IDEMPOTENT_CACHE_SIZE = 1000;
059    
060    protected final Logger log = LoggerFactory.getLogger(getClass());
061
062    // common options
063
064    @UriParam(label = "advanced", defaultValue = "true")
065    protected boolean autoCreate = true;
066    @UriParam(label = "advanced", defaultValue = "" + FileUtil.BUFFER_SIZE)
067    protected int bufferSize = FileUtil.BUFFER_SIZE;
068    @UriParam
069    protected String charset;
070    @UriParam(javaType = "java.lang.String")
071    protected Expression fileName;
072    @UriParam
073    protected String doneFileName;
074
075    // producer options
076
077    @UriParam(label = "producer")
078    protected boolean flatten;
079    @UriParam(label = "producer", defaultValue = "Override")
080    protected GenericFileExist fileExist = GenericFileExist.Override;
081    @UriParam(label = "producer")
082    protected String tempPrefix;
083    @UriParam(label = "producer", javaType = "java.lang.String")
084    protected Expression tempFileName;
085    @UriParam(label = "producer,advanced", defaultValue = "true")
086    protected boolean eagerDeleteTargetFile = true;
087    @UriParam(label = "producer,advanced")
088    protected boolean keepLastModified;
089    @UriParam(label = "producer,advanced")
090    protected boolean allowNullBody;
091
092    // consumer options
093
094    @UriParam
095    protected GenericFileConfiguration configuration;
096    @UriParam(label = "consumer,advanced")
097    protected GenericFileProcessStrategy<T> processStrategy;
098    @UriParam(label = "consumer,advanced")
099    protected IdempotentRepository<String> inProgressRepository = new MemoryIdempotentRepository();
100    @UriParam(label = "consumer,advanced")
101    protected String localWorkDirectory;
102    @UriParam(label = "consumer,advanced")
103    protected boolean startingDirectoryMustExist;
104    @UriParam(label = "consumer,advanced")
105    protected boolean directoryMustExist;
106    @UriParam(label = "consumer")
107    protected boolean noop;
108    @UriParam(label = "consumer")
109    protected boolean recursive;
110    @UriParam(label = "consumer")
111    protected boolean delete;
112    @UriParam(label = "consumer,filter")
113    protected int maxMessagesPerPoll;
114    @UriParam(label = "consumer,filter", defaultValue = "true")
115    protected boolean eagerMaxMessagesPerPoll = true;
116    @UriParam(label = "consumer,filter", defaultValue = "" + Integer.MAX_VALUE)
117    protected int maxDepth = Integer.MAX_VALUE;
118    @UriParam(label = "consumer,filter")
119    protected int minDepth;
120    @UriParam(label = "consumer,filter")
121    protected String include;
122    @UriParam(label = "consumer,filter")
123    protected String exclude;
124    @UriParam(label = "consumer,filter", javaType = "java.lang.String")
125    protected Expression move;
126    @UriParam(label = "consumer", javaType = "java.lang.String")
127    protected Expression moveFailed;
128    @UriParam(label = "consumer", javaType = "java.lang.String")
129    protected Expression preMove;
130    @UriParam(label = "producer", javaType = "java.lang.String")
131    protected Expression moveExisting;
132    @UriParam(label = "consumer,filter", defaultValue = "false")
133    protected Boolean idempotent;
134    @UriParam(label = "consumer,filter", javaType = "java.lang.String")
135    protected Expression idempotentKey;
136    @UriParam(label = "consumer,filter")
137    protected IdempotentRepository<String> idempotentRepository;
138    @UriParam(label = "consumer,filter")
139    protected GenericFileFilter<T> filter;
140    @UriParam(label = "consumer,filter", javaType = "java.lang.String")
141    protected Predicate filterDirectory;
142    @UriParam(label = "consumer,filter", javaType = "java.lang.String")
143    protected Predicate filterFile;
144    @UriParam(label = "consumer,filter", defaultValue = "true")
145    protected boolean antFilterCaseSensitive = true;
146    protected volatile AntPathMatcherGenericFileFilter<T> antFilter;
147    @UriParam(label = "consumer,filter")
148    protected String antInclude;
149    @UriParam(label = "consumer,filter")
150    protected String antExclude;
151    @UriParam(label = "consumer,sort")
152    protected Comparator<GenericFile<T>> sorter;
153    @UriParam(label = "consumer,sort", javaType = "java.lang.String")
154    protected Comparator<Exchange> sortBy;
155    @UriParam(label = "consumer,sort")
156    protected boolean shuffle;
157    @UriParam(label = "consumer,lock", enums = "none,markerFile,fileLock,rename,changed,idempotent")
158    protected String readLock = "none";
159    @UriParam(label = "consumer,lock", defaultValue = "1000")
160    protected long readLockCheckInterval = 1000;
161    @UriParam(label = "consumer,lock", defaultValue = "10000")
162    protected long readLockTimeout = 10000;
163    @UriParam(label = "consumer,lock", defaultValue = "true")
164    protected boolean readLockMarkerFile = true;
165    @UriParam(label = "consumer,lock", defaultValue = "true")
166    protected boolean readLockDeleteOrphanLockFiles = true;
167    @UriParam(label = "consumer,lock", defaultValue = "WARN")
168    protected LoggingLevel readLockLoggingLevel = LoggingLevel.WARN;
169    @UriParam(label = "consumer,lock", defaultValue = "1")
170    protected long readLockMinLength = 1;
171    @UriParam(label = "consumer,lock", defaultValue = "0")
172    protected long readLockMinAge;
173    @UriParam(label = "consumer,lock", defaultValue = "true")
174    protected boolean readLockRemoveOnRollback = true;
175    @UriParam(label = "consumer,lock")
176    protected boolean readLockRemoveOnCommit;
177    @UriParam(label = "consumer,lock")
178    protected GenericFileExclusiveReadLockStrategy<T> exclusiveReadLockStrategy;
179    @UriParam(label = "consumer,advanced")
180    protected ExceptionHandler onCompletionExceptionHandler;
181
182    public GenericFileEndpoint() {
183    }
184
185    public GenericFileEndpoint(String endpointUri, Component component) {
186        super(endpointUri, component);
187    }
188
189    public boolean isSingleton() {
190        return true;
191    }
192
193    public abstract GenericFileConsumer<T> createConsumer(Processor processor) throws Exception;
194
195    public abstract GenericFileProducer<T> createProducer() throws Exception;
196
197    public abstract Exchange createExchange(GenericFile<T> file);
198
199    public abstract String getScheme();
200
201    public abstract char getFileSeparator();
202
203    public abstract boolean isAbsolute(String name);
204
205    /**
206     * Return the file name that will be auto-generated for the given message if
207     * none is provided
208     */
209    public String getGeneratedFileName(Message message) {
210        return StringHelper.sanitize(message.getMessageId());
211    }
212
213    public GenericFileProcessStrategy<T> getGenericFileProcessStrategy() {
214        if (processStrategy == null) {
215            processStrategy = createGenericFileStrategy();
216            log.debug("Using Generic file process strategy: {}", processStrategy);
217        }
218        return processStrategy;
219    }
220
221    /**
222     * This implementation will <b>not</b> load the file content.
223     * Any file locking is neither in use by this implementation..
224     */
225    @Override
226    public List<Exchange> getExchanges() {
227        final List<Exchange> answer = new ArrayList<Exchange>();
228
229        GenericFileConsumer<?> consumer = null;
230        try {
231            // create a new consumer which can poll the exchanges we want to browse
232            // do not provide a processor as we do some custom processing
233            consumer = createConsumer(null);
234            consumer.setCustomProcessor(new Processor() {
235                @Override
236                public void process(Exchange exchange) throws Exception {
237                    answer.add(exchange);
238                }
239            });
240            // do not start scheduler, as we invoke the poll manually
241            consumer.setStartScheduler(false);
242            // start consumer
243            ServiceHelper.startService(consumer);
244            // invoke poll which performs the custom processing, so we can browse the exchanges
245            consumer.poll();
246        } catch (Exception e) {
247            throw ObjectHelper.wrapRuntimeCamelException(e);
248        } finally {
249            try {
250                ServiceHelper.stopService(consumer);
251            } catch (Exception e) {
252                log.debug("Error stopping consumer used for browsing exchanges. This exception will be ignored", e);
253            }
254        }
255
256        return answer;
257    }
258
259    /**
260     * A strategy method to lazily create the file strategy
261     */
262    @SuppressWarnings("unchecked")
263    protected GenericFileProcessStrategy<T> createGenericFileStrategy() {
264        Class<?> factory = null;
265        try {
266            FactoryFinder finder = getCamelContext().getFactoryFinder("META-INF/services/org/apache/camel/component/");
267            log.trace("Using FactoryFinder: {}", finder);
268            factory = finder.findClass(getScheme(), "strategy.factory.", CamelContext.class);
269        } catch (ClassNotFoundException e) {
270            log.trace("'strategy.factory.class' not found", e);
271        } catch (IOException e) {
272            log.trace("No strategy factory defined in 'META-INF/services/org/apache/camel/component/'", e);
273        }
274
275        if (factory == null) {
276            // use default
277            try {
278                log.trace("Using ClassResolver to resolve class: {}", DEFAULT_STRATEGYFACTORY_CLASS);
279                factory = this.getCamelContext().getClassResolver().resolveClass(DEFAULT_STRATEGYFACTORY_CLASS);
280            } catch (Exception e) {
281                log.trace("Cannot load class: {}", DEFAULT_STRATEGYFACTORY_CLASS, e);
282            }
283            // fallback and us this class loader
284            try {
285                if (log.isTraceEnabled()) {
286                    log.trace("Using classloader: {} to resolve class: {}", this.getClass().getClassLoader(), DEFAULT_STRATEGYFACTORY_CLASS);
287                }
288                factory = this.getCamelContext().getClassResolver().resolveClass(DEFAULT_STRATEGYFACTORY_CLASS, this.getClass().getClassLoader());
289            } catch (Exception e) {
290                if (log.isTraceEnabled()) {
291                    log.trace("Cannot load class: {} using classloader: " + this.getClass().getClassLoader(), DEFAULT_STRATEGYFACTORY_CLASS, e);
292                }
293            }
294
295            if (factory == null) {
296                throw new TypeNotPresentException(DEFAULT_STRATEGYFACTORY_CLASS + " class not found", null);
297            }
298        }
299
300        try {
301            Method factoryMethod = factory.getMethod("createGenericFileProcessStrategy", CamelContext.class, Map.class);
302            Map<String, Object> params = getParamsAsMap();
303            log.debug("Parameters for Generic file process strategy {}", params);
304            return (GenericFileProcessStrategy<T>) ObjectHelper.invokeMethod(factoryMethod, null, getCamelContext(), params);
305        } catch (NoSuchMethodException e) {
306            throw new TypeNotPresentException(factory.getSimpleName() + ".createGenericFileProcessStrategy method not found", e);
307        }
308    }
309
310    public boolean isNoop() {
311        return noop;
312    }
313
314    /**
315     * If true, the file is not moved or deleted in any way.
316     * This option is good for readonly data, or for ETL type requirements.
317     * If noop=true, Camel will set idempotent=true as well, to avoid consuming the same files over and over again.
318     */
319    public void setNoop(boolean noop) {
320        this.noop = noop;
321    }
322
323    public boolean isRecursive() {
324        return recursive;
325    }
326
327    /**
328     * If a directory, will look for files in all the sub-directories as well.
329     */
330    public void setRecursive(boolean recursive) {
331        this.recursive = recursive;
332    }
333
334    public String getInclude() {
335        return include;
336    }
337
338    /**
339     * Is used to include files, if filename matches the regex pattern (matching is case in-senstive).
340     * <p/>
341     * Notice if you use symbols such as plus sign and others you would need to configure
342     * this using the RAW() syntax if configuring this as an endpoint uri.
343     * See more details at <a href="http://camel.apache.org/how-do-i-configure-endpoints.html">configuring endpoint uris</a>
344     */
345    public void setInclude(String include) {
346        this.include = include;
347    }
348
349    public String getExclude() {
350        return exclude;
351    }
352
353    /**
354     * Is used to exclude files, if filename matches the regex pattern (matching is case in-senstive).
355     * <p/>
356     * Notice if you use symbols such as plus sign and others you would need to configure
357     * this using the RAW() syntax if configuring this as an endpoint uri.
358     * See more details at <a href="http://camel.apache.org/how-do-i-configure-endpoints.html">configuring endpoint uris</a>
359     */
360    public void setExclude(String exclude) {
361        this.exclude = exclude;
362    }
363
364    public String getAntInclude() {
365        return antInclude;
366    }
367
368    /**
369     * Ant style filter inclusion.
370     * Multiple inclusions may be specified in comma-delimited format.
371     */
372    public void setAntInclude(String antInclude) {
373        this.antInclude = antInclude;
374    }
375
376    public String getAntExclude() {
377        return antExclude;
378    }
379
380    /**
381     * Ant style filter exclusion. If both antInclude and antExclude are used, antExclude takes precedence over antInclude.
382     * Multiple exclusions may be specified in comma-delimited format.
383     */
384    public void setAntExclude(String antExclude) {
385        this.antExclude = antExclude;
386    }
387
388    public boolean isAntFilterCaseSensitive() {
389        return antFilterCaseSensitive;
390    }
391
392    /**
393     * Sets case sensitive flag on ant fiter
394     */
395    public void setAntFilterCaseSensitive(boolean antFilterCaseSensitive) {
396        this.antFilterCaseSensitive = antFilterCaseSensitive;
397    }
398
399    public GenericFileFilter<T> getAntFilter() {
400        return antFilter;
401    }
402
403    public boolean isDelete() {
404        return delete;
405    }
406
407    /**
408     * If true, the file will be deleted after it is processed successfully.
409     */
410    public void setDelete(boolean delete) {
411        this.delete = delete;
412    }
413
414    public boolean isFlatten() {
415        return flatten;
416    }
417
418    /**
419     * Flatten is used to flatten the file name path to strip any leading paths, so it's just the file name.
420     * This allows you to consume recursively into sub-directories, but when you eg write the files to another directory
421     * they will be written in a single directory.
422     * Setting this to true on the producer enforces that any file name in CamelFileName header
423     * will be stripped for any leading paths.
424     */
425    public void setFlatten(boolean flatten) {
426        this.flatten = flatten;
427    }
428
429    public Expression getMove() {
430        return move;
431    }
432
433    /**
434     * Expression (such as Simple Language) used to dynamically set the filename when moving it after processing.
435     * To move files into a .done subdirectory just enter .done.
436     */
437    public void setMove(Expression move) {
438        this.move = move;
439    }
440
441    /**
442     * @see #setMove(org.apache.camel.Expression)
443     */
444    public void setMove(String fileLanguageExpression) {
445        String expression = configureMoveOrPreMoveExpression(fileLanguageExpression);
446        this.move = createFileLanguageExpression(expression);
447    }
448
449    public Expression getMoveFailed() {
450        return moveFailed;
451    }
452
453    /**
454     * Sets the move failure expression based on Simple language.
455     * For example, to move files into a .error subdirectory use: .error.
456     * Note: When moving the files to the fail location Camel will handle the error and will not pick up the file again.
457     */
458    public void setMoveFailed(Expression moveFailed) {
459        this.moveFailed = moveFailed;
460    }
461
462    public void setMoveFailed(String fileLanguageExpression) {
463        String expression = configureMoveOrPreMoveExpression(fileLanguageExpression);
464        this.moveFailed = createFileLanguageExpression(expression);
465    }
466
467    public Predicate getFilterDirectory() {
468        return filterDirectory;
469    }
470
471    /**
472     * Filters the directory based on Simple language.
473     * For example to filter on current date, you can use a simple date pattern such as ${date:now:yyyMMdd}
474     */
475    public void setFilterDirectory(Predicate filterDirectory) {
476        this.filterDirectory = filterDirectory;
477    }
478
479    /**
480     * Filters the directory based on Simple language.
481     * For example to filter on current date, you can use a simple date pattern such as ${date:now:yyyMMdd}
482     * @see #setFilterDirectory(Predicate)
483     */
484    public void setFilterDirectory(String expression) {
485        this.filterDirectory = createFileLanguagePredicate(expression);
486    }
487
488    public Predicate getFilterFile() {
489        return filterFile;
490    }
491
492    /**
493     * Filters the file based on Simple language.
494     * For example to filter on file size, you can use ${file:size} > 5000
495     */
496    public void setFilterFile(Predicate filterFile) {
497        this.filterFile = filterFile;
498    }
499
500    /**
501     * Filters the file based on Simple language.
502     * For example to filter on file size, you can use ${file:size} > 5000
503     * @see #setFilterFile(Predicate)
504     */
505    public void setFilterFile(String expression) {
506        this.filterFile = createFileLanguagePredicate(expression);
507    }
508
509    public Expression getPreMove() {
510        return preMove;
511    }
512
513    /**
514     * Expression (such as File Language) used to dynamically set the filename when moving it before processing.
515     * For example to move in-progress files into the order directory set this value to order.
516     */
517    public void setPreMove(Expression preMove) {
518        this.preMove = preMove;
519    }
520
521    public void setPreMove(String fileLanguageExpression) {
522        String expression = configureMoveOrPreMoveExpression(fileLanguageExpression);
523        this.preMove = createFileLanguageExpression(expression);
524    }
525
526    public Expression getMoveExisting() {
527        return moveExisting;
528    }
529
530    /**
531     * Expression (such as File Language) used to compute file name to use when fileExist=Move is configured.
532     * To move files into a backup subdirectory just enter backup.
533     * This option only supports the following File Language tokens: "file:name", "file:name.ext", "file:name.noext", "file:onlyname",
534     * "file:onlyname.noext", "file:ext", and "file:parent". Notice the "file:parent" is not supported by the FTP component,
535     * as the FTP component can only move any existing files to a relative directory based on current dir as base.
536     */
537    public void setMoveExisting(Expression moveExisting) {
538        this.moveExisting = moveExisting;
539    }
540
541    public void setMoveExisting(String fileLanguageExpression) {
542        String expression = configureMoveOrPreMoveExpression(fileLanguageExpression);
543        this.moveExisting = createFileLanguageExpression(expression);
544    }
545
546    public Expression getFileName() {
547        return fileName;
548    }
549
550    /**
551     * Use Expression such as File Language to dynamically set the filename.
552     * For consumers, it's used as a filename filter.
553     * For producers, it's used to evaluate the filename to write.
554     * If an expression is set, it take precedence over the CamelFileName header. (Note: The header itself can also be an Expression).
555     * The expression options support both String and Expression types.
556     * If the expression is a String type, it is always evaluated using the File Language.
557     * If the expression is an Expression type, the specified Expression type is used - this allows you,
558     * for instance, to use OGNL expressions. For the consumer, you can use it to filter filenames,
559     * so you can for instance consume today's file using the File Language syntax: mydata-${date:now:yyyyMMdd}.txt.
560     * The producers support the CamelOverruleFileName header which takes precedence over any existing CamelFileName header;
561     * the CamelOverruleFileName is a header that is used only once, and makes it easier as this avoids to temporary
562     * store CamelFileName and have to restore it afterwards.
563     */
564    public void setFileName(Expression fileName) {
565        this.fileName = fileName;
566    }
567
568    public void setFileName(String fileLanguageExpression) {
569        this.fileName = createFileLanguageExpression(fileLanguageExpression);
570    }
571
572    public String getDoneFileName() {
573        return doneFileName;
574    }
575
576    /**
577     * Producer: If provided, then Camel will write a 2nd done file when the original file has been written.
578     * The done file will be empty. This option configures what file name to use.
579     * Either you can specify a fixed name. Or you can use dynamic placeholders.
580     * The done file will always be written in the same folder as the original file.
581     * <p/>
582     * Consumer: If provided, Camel will only consume files if a done file exists. 
583     * This option configures what file name to use. Either you can specify a fixed name.
584     * Or you can use dynamic placeholders.The done file is always expected in the same folder
585     * as the original file.
586     * <p/>
587     * Only ${file.name} and ${file.name.noext} is supported as dynamic placeholders.
588     */
589    public void setDoneFileName(String doneFileName) {
590        this.doneFileName = doneFileName;
591    }
592
593    public Boolean isIdempotent() {
594        return idempotent != null ? idempotent : false;
595    }
596
597    public String getCharset() {
598        return charset;
599    }
600
601    /**
602     * This option is used to specify the encoding of the file.
603     * You can use this on the consumer, to specify the encodings of the files, which allow Camel to know the charset
604     * it should load the file content in case the file content is being accessed.
605     * Likewise when writing a file, you can use this option to specify which charset to write the file as well.
606     */
607    public void setCharset(String charset) {
608        IOHelper.validateCharset(charset);
609        this.charset = charset;
610    }
611
612    protected boolean isIdempotentSet() {
613        return idempotent != null;
614    }
615
616    /**
617     * Option to use the Idempotent Consumer EIP pattern to let Camel skip already processed files.
618     * Will by default use a memory based LRUCache that holds 1000 entries. If noop=true then idempotent will be enabled
619     * as well to avoid consuming the same files over and over again.
620     */
621    public void setIdempotent(Boolean idempotent) {
622        this.idempotent = idempotent;
623    }
624
625    public Expression getIdempotentKey() {
626        return idempotentKey;
627    }
628
629    /**
630     * To use a custom idempotent key. By default the absolute path of the file is used.
631     * You can use the File Language, for example to use the file name and file size, you can do:
632     * <tt>idempotentKey=${file:name}-${file:size}</tt>
633     */
634    public void setIdempotentKey(Expression idempotentKey) {
635        this.idempotentKey = idempotentKey;
636    }
637
638    public void setIdempotentKey(String expression) {
639        this.idempotentKey = createFileLanguageExpression(expression);
640    }
641
642    public IdempotentRepository<String> getIdempotentRepository() {
643        return idempotentRepository;
644    }
645
646    /**
647     * A pluggable repository org.apache.camel.spi.IdempotentRepository which by default use MemoryMessageIdRepository
648     * if none is specified and idempotent is true.
649     */
650    public void setIdempotentRepository(IdempotentRepository<String> idempotentRepository) {
651        this.idempotentRepository = idempotentRepository;
652    }
653
654    public GenericFileFilter<T> getFilter() {
655        return filter;
656    }
657
658    /**
659     * Pluggable filter as a org.apache.camel.component.file.GenericFileFilter class.
660     * Will skip files if filter returns false in its accept() method.
661     */
662    public void setFilter(GenericFileFilter<T> filter) {
663        this.filter = filter;
664    }
665
666    public Comparator<GenericFile<T>> getSorter() {
667        return sorter;
668    }
669
670    /**
671     * Pluggable sorter as a java.util.Comparator<org.apache.camel.component.file.GenericFile> class.
672     */
673    public void setSorter(Comparator<GenericFile<T>> sorter) {
674        this.sorter = sorter;
675    }
676
677    public Comparator<Exchange> getSortBy() {
678        return sortBy;
679    }
680
681    /**
682     * Built-in sort by using the File Language.
683     * Supports nested sorts, so you can have a sort by file name and as a 2nd group sort by modified date.
684     */
685    public void setSortBy(Comparator<Exchange> sortBy) {
686        this.sortBy = sortBy;
687    }
688
689    public void setSortBy(String expression) {
690        setSortBy(expression, false);
691    }
692
693    public void setSortBy(String expression, boolean reverse) {
694        setSortBy(GenericFileDefaultSorter.sortByFileLanguage(getCamelContext(), expression, reverse));
695    }
696
697    public boolean isShuffle() {
698        return shuffle;
699    }
700
701    /**
702     * To shuffle the list of files (sort in random order)
703     */
704    public void setShuffle(boolean shuffle) {
705        this.shuffle = shuffle;
706    }
707
708    public String getTempPrefix() {
709        return tempPrefix;
710    }
711
712    /**
713     * This option is used to write the file using a temporary name and then, after the write is complete,
714     * rename it to the real name. Can be used to identify files being written and also avoid consumers
715     * (not using exclusive read locks) reading in progress files. Is often used by FTP when uploading big files.
716     */
717    public void setTempPrefix(String tempPrefix) {
718        this.tempPrefix = tempPrefix;
719        // use only name as we set a prefix in from on the name
720        setTempFileName(tempPrefix + "${file:onlyname}");
721    }
722
723    public Expression getTempFileName() {
724        return tempFileName;
725    }
726
727    /**
728     * The same as tempPrefix option but offering a more fine grained control on the naming of the temporary filename as it uses the File Language.
729     */
730    public void setTempFileName(Expression tempFileName) {
731        this.tempFileName = tempFileName;
732    }
733
734    public void setTempFileName(String tempFileNameExpression) {
735        this.tempFileName = createFileLanguageExpression(tempFileNameExpression);
736    }
737
738    public boolean isEagerDeleteTargetFile() {
739        return eagerDeleteTargetFile;
740    }
741
742    /**
743     * Whether or not to eagerly delete any existing target file.
744     * This option only applies when you use fileExists=Override and the tempFileName option as well.
745     * You can use this to disable (set it to false) deleting the target file before the temp file is written.
746     * For example you may write big files and want the target file to exists during the temp file is being written.
747     * This ensure the target file is only deleted until the very last moment, just before the temp file is being
748     * renamed to the target filename. This option is also used to control whether to delete any existing files when
749     * fileExist=Move is enabled, and an existing file exists.
750     * If this option copyAndDeleteOnRenameFails false, then an exception will be thrown if an existing file existed,
751     * if its true, then the existing file is deleted before the move operation.
752     */
753    public void setEagerDeleteTargetFile(boolean eagerDeleteTargetFile) {
754        this.eagerDeleteTargetFile = eagerDeleteTargetFile;
755    }
756
757    public GenericFileConfiguration getConfiguration() {
758        if (configuration == null) {
759            configuration = new GenericFileConfiguration();
760        }
761        return configuration;
762    }
763
764    public void setConfiguration(GenericFileConfiguration configuration) {
765        this.configuration = configuration;
766    }
767
768    public GenericFileExclusiveReadLockStrategy<T> getExclusiveReadLockStrategy() {
769        return exclusiveReadLockStrategy;
770    }
771
772    /**
773     * Pluggable read-lock as a org.apache.camel.component.file.GenericFileExclusiveReadLockStrategy implementation.
774     */
775    public void setExclusiveReadLockStrategy(GenericFileExclusiveReadLockStrategy<T> exclusiveReadLockStrategy) {
776        this.exclusiveReadLockStrategy = exclusiveReadLockStrategy;
777    }
778
779    public String getReadLock() {
780        return readLock;
781    }
782
783    /**
784     * Used by consumer, to only poll the files if it has exclusive read-lock on the file (i.e. the file is not in-progress or being written).
785     * Camel will wait until the file lock is granted.
786     * <p/>
787     * This option provides the build in strategies:
788     * <ul>
789     *     <li>none - No read lock is in use
790     *     <li>markerFile - Camel creates a marker file (fileName.camelLock) and then holds a lock on it. This option is not available for the FTP component
791     *     <li>changed - Changed is using file length/modification timestamp to detect whether the file is currently being copied or not. Will at least use 1 sec
792     *     to determine this, so this option cannot consume files as fast as the others, but can be more reliable as the JDK IO API cannot
793     *     always determine whether a file is currently being used by another process. The option readLockCheckInterval can be used to set the check frequency.</li>
794     *     <li>fileLock - is for using java.nio.channels.FileLock. This option is not avail for the FTP component. This approach should be avoided when accessing
795     *     a remote file system via a mount/share unless that file system supports distributed file locks.</li>
796     *     <li>rename - rename is for using a try to rename the file as a test if we can get exclusive read-lock.</li>
797     *     <li>idempotent - (only for file component) idempotent is for using a idempotentRepository as the read-lock.
798     *     This allows to use read locks that supports clustering if the idempotent repository implementation supports that.</li>
799     * </ul>
800     * Notice: The various read locks is not all suited to work in clustered mode, where concurrent consumers on different nodes is competing
801     * for the same files on a shared file system. The markerFile using a close to atomic operation to create the empty marker file,
802     * but its not guaranteed to work in a cluster. The fileLock may work better but then the file system need to support distributed file locks, and so on.
803     * Using the idempotent read lock can support clustering if the idempotent repository supports clustering, such as Hazelcast Component or Infinispan.
804     */
805    public void setReadLock(String readLock) {
806        this.readLock = readLock;
807    }
808
809    public long getReadLockCheckInterval() {
810        return readLockCheckInterval;
811    }
812
813    /**
814     * Interval in millis for the read-lock, if supported by the read lock.
815     * This interval is used for sleeping between attempts to acquire the read lock.
816     * For example when using the changed read lock, you can set a higher interval period to cater for slow writes.
817     * The default of 1 sec. may be too fast if the producer is very slow writing the file.
818     * <p/>
819     * Notice: For FTP the default readLockCheckInterval is 5000.
820     * <p/>
821     * The readLockTimeout value must be higher than readLockCheckInterval, but a rule of thumb is to have a timeout
822     * that is at least 2 or more times higher than the readLockCheckInterval. This is needed to ensure that amble
823     * time is allowed for the read lock process to try to grab the lock before the timeout was hit.
824     */
825    public void setReadLockCheckInterval(long readLockCheckInterval) {
826        this.readLockCheckInterval = readLockCheckInterval;
827    }
828
829    public long getReadLockTimeout() {
830        return readLockTimeout;
831    }
832
833    /**
834     * Optional timeout in millis for the read-lock, if supported by the read-lock.
835     * If the read-lock could not be granted and the timeout triggered, then Camel will skip the file.
836     * At next poll Camel, will try the file again, and this time maybe the read-lock could be granted.
837     * Use a value of 0 or lower to indicate forever. Currently fileLock, changed and rename support the timeout.
838     * <p/>
839     * Notice: For FTP the default readLockTimeout value is 20000 instead of 10000.
840     * <p/>
841     * The readLockTimeout value must be higher than readLockCheckInterval, but a rule of thumb is to have a timeout
842     * that is at least 2 or more times higher than the readLockCheckInterval. This is needed to ensure that amble
843     * time is allowed for the read lock process to try to grab the lock before the timeout was hit.
844     */
845    public void setReadLockTimeout(long readLockTimeout) {
846        this.readLockTimeout = readLockTimeout;
847    }
848
849    public boolean isReadLockMarkerFile() {
850        return readLockMarkerFile;
851    }
852
853    /**
854     * Whether to use marker file with the changed, rename, or exclusive read lock types.
855     * By default a marker file is used as well to guard against other processes picking up the same files.
856     * This behavior can be turned off by setting this option to false.
857     * For example if you do not want to write marker files to the file systems by the Camel application.
858     */
859    public void setReadLockMarkerFile(boolean readLockMarkerFile) {
860        this.readLockMarkerFile = readLockMarkerFile;
861    }
862
863    public boolean isReadLockDeleteOrphanLockFiles() {
864        return readLockDeleteOrphanLockFiles;
865    }
866
867    /**
868     * Whether or not read lock with marker files should upon startup delete any orphan read lock files, which may
869     * have been left on the file system, if Camel was not properly shutdown (such as a JVM crash).
870     * <p/>
871     * If turning this option to <tt>false</tt> then any orphaned lock file will cause Camel to not attempt to pickup
872     * that file, this could also be due another node is concurrently reading files from the same shared directory.
873     */
874    public void setReadLockDeleteOrphanLockFiles(boolean readLockDeleteOrphanLockFiles) {
875        this.readLockDeleteOrphanLockFiles = readLockDeleteOrphanLockFiles;
876    }
877
878    public LoggingLevel getReadLockLoggingLevel() {
879        return readLockLoggingLevel;
880    }
881
882    /**
883     * Logging level used when a read lock could not be acquired.
884     * By default a WARN is logged.
885     * You can change this level, for example to OFF to not have any logging.
886     * This option is only applicable for readLock of types: changed, fileLock, rename.
887     */
888    public void setReadLockLoggingLevel(LoggingLevel readLockLoggingLevel) {
889        this.readLockLoggingLevel = readLockLoggingLevel;
890    }
891
892    public long getReadLockMinLength() {
893        return readLockMinLength;
894    }
895
896    /**
897     * This option applied only for readLock=changed. This option allows you to configure a minimum file length.
898     * By default Camel expects the file to contain data, and thus the default value is 1.
899     * You can set this option to zero, to allow consuming zero-length files.
900     */
901    public void setReadLockMinLength(long readLockMinLength) {
902        this.readLockMinLength = readLockMinLength;
903    }
904
905    public long getReadLockMinAge() {
906        return readLockMinAge;
907    }
908
909    /**
910     * This option applied only for readLock=change.
911     * This option allows to specify a minimum age the file must be before attempting to acquire the read lock.
912     * For example use readLockMinAge=300s to require the file is at last 5 minutes old.
913     * This can speedup the changed read lock as it will only attempt to acquire files which are at least that given age.
914     */
915    public void setReadLockMinAge(long readLockMinAge) {
916        this.readLockMinAge = readLockMinAge;
917    }
918
919    public boolean isReadLockRemoveOnRollback() {
920        return readLockRemoveOnRollback;
921    }
922
923    /**
924     * This option applied only for readLock=idempotent.
925     * This option allows to specify whether to remove the file name entry from the idempotent repository
926     * when processing the file failed and a rollback happens.
927     * If this option is false, then the file name entry is confirmed (as if the file did a commit).
928     */
929    public void setReadLockRemoveOnRollback(boolean readLockRemoveOnRollback) {
930        this.readLockRemoveOnRollback = readLockRemoveOnRollback;
931    }
932
933    public boolean isReadLockRemoveOnCommit() {
934        return readLockRemoveOnCommit;
935    }
936
937    /**
938     * This option applied only for readLock=idempotent.
939     * This option allows to specify whether to remove the file name entry from the idempotent repository
940     * when processing the file is succeeded and a commit happens.
941     * <p/>
942     * By default the file is not removed which ensures that any race-condition do not occur so another active
943     * node may attempt to grab the file. Instead the idempotent repository may support eviction strategies
944     * that you can configure to evict the file name entry after X minutes - this ensures no problems with race conditions.
945     */
946    public void setReadLockRemoveOnCommit(boolean readLockRemoveOnCommit) {
947        this.readLockRemoveOnCommit = readLockRemoveOnCommit;
948    }
949
950    public int getBufferSize() {
951        return bufferSize;
952    }
953
954    /**
955     * Write buffer sized in bytes.
956     */
957    public void setBufferSize(int bufferSize) {
958        if (bufferSize <= 0) {
959            throw new IllegalArgumentException("BufferSize must be a positive value, was " + bufferSize);
960        }
961        this.bufferSize = bufferSize;
962    }
963
964    public GenericFileExist getFileExist() {
965        return fileExist;
966    }
967
968    /**
969     * What to do if a file already exists with the same name.
970     *
971     * Override, which is the default, replaces the existing file.
972     * <ul>
973     *   <li>Append - adds content to the existing file.</li>
974     *   <li>Fail - throws a GenericFileOperationException, indicating that there is already an existing file.</li>
975     *   <li>Ignore - silently ignores the problem and does not override the existing file, but assumes everything is okay.</li>
976     *   <li>Move - option requires to use the moveExisting option to be configured as well.
977     *   The option eagerDeleteTargetFile can be used to control what to do if an moving the file, and there exists already an existing file,
978     *   otherwise causing the move operation to fail.
979     *   The Move option will move any existing files, before writing the target file.</li>
980     *   <li>TryRename Camel is only applicable if tempFileName option is in use. This allows to try renaming the file from the temporary name to the actual name,
981     *   without doing any exists check.This check may be faster on some file systems and especially FTP servers.</li>
982     * </ul>
983     */
984    public void setFileExist(GenericFileExist fileExist) {
985        this.fileExist = fileExist;
986    }
987
988    public boolean isAutoCreate() {
989        return autoCreate;
990    }
991
992    /**
993     * Automatically create missing directories in the file's pathname. For the file consumer, that means creating the starting directory.
994     * For the file producer, it means the directory the files should be written to.
995     */
996    public void setAutoCreate(boolean autoCreate) {
997        this.autoCreate = autoCreate;
998    }
999
1000    public boolean isStartingDirectoryMustExist() {
1001        return startingDirectoryMustExist;
1002    }
1003
1004    /**
1005     * Whether the starting directory must exist. Mind that the autoCreate option is default enabled,
1006     * which means the starting directory is normally auto created if it doesn't exist.
1007     * You can disable autoCreate and enable this to ensure the starting directory must exist. Will thrown an exception if the directory doesn't exist.
1008     */
1009    public void setStartingDirectoryMustExist(boolean startingDirectoryMustExist) {
1010        this.startingDirectoryMustExist = startingDirectoryMustExist;
1011    }
1012
1013    public boolean isDirectoryMustExist() {
1014        return directoryMustExist;
1015    }
1016
1017    /**
1018     * Similar to startingDirectoryMustExist but this applies during polling recursive sub directories.
1019     */
1020    public void setDirectoryMustExist(boolean directoryMustExist) {
1021        this.directoryMustExist = directoryMustExist;
1022    }
1023
1024    public GenericFileProcessStrategy<T> getProcessStrategy() {
1025        return processStrategy;
1026    }
1027
1028    /**
1029     * A pluggable org.apache.camel.component.file.GenericFileProcessStrategy allowing you to implement your own readLock option or similar.
1030     * Can also be used when special conditions must be met before a file can be consumed, such as a special ready file exists.
1031     * If this option is set then the readLock option does not apply.
1032     */
1033    public void setProcessStrategy(GenericFileProcessStrategy<T> processStrategy) {
1034        this.processStrategy = processStrategy;
1035    }
1036
1037    public String getLocalWorkDirectory() {
1038        return localWorkDirectory;
1039    }
1040
1041    /**
1042     * When consuming, a local work directory can be used to store the remote file content directly in local files,
1043     * to avoid loading the content into memory. This is beneficial, if you consume a very big remote file and thus can conserve memory.
1044     */
1045    public void setLocalWorkDirectory(String localWorkDirectory) {
1046        this.localWorkDirectory = localWorkDirectory;
1047    }
1048
1049    public int getMaxMessagesPerPoll() {
1050        return maxMessagesPerPoll;
1051    }
1052
1053    /**
1054     * To define a maximum messages to gather per poll.
1055     * By default no maximum is set. Can be used to set a limit of e.g. 1000 to avoid when starting up the server that there are thousands of files.
1056     * Set a value of 0 or negative to disabled it.
1057     * Notice: If this option is in use then the File and FTP components will limit before any sorting.
1058     * For example if you have 100000 files and use maxMessagesPerPoll=500, then only the first 500 files will be picked up, and then sorted.
1059     * You can use the eagerMaxMessagesPerPoll option and set this to false to allow to scan all files first and then sort afterwards.
1060     */
1061    public void setMaxMessagesPerPoll(int maxMessagesPerPoll) {
1062        this.maxMessagesPerPoll = maxMessagesPerPoll;
1063    }
1064
1065    public boolean isEagerMaxMessagesPerPoll() {
1066        return eagerMaxMessagesPerPoll;
1067    }
1068
1069    /**
1070     * Allows for controlling whether the limit from maxMessagesPerPoll is eager or not.
1071     * If eager then the limit is during the scanning of files. Where as false would scan all files, and then perform sorting.
1072     * Setting this option to false allows for sorting all files first, and then limit the poll. Mind that this requires a
1073     * higher memory usage as all file details are in memory to perform the sorting.
1074     */
1075    public void setEagerMaxMessagesPerPoll(boolean eagerMaxMessagesPerPoll) {
1076        this.eagerMaxMessagesPerPoll = eagerMaxMessagesPerPoll;
1077    }
1078
1079    public int getMaxDepth() {
1080        return maxDepth;
1081    }
1082
1083    /**
1084     * The maximum depth to traverse when recursively processing a directory.
1085     */
1086    public void setMaxDepth(int maxDepth) {
1087        this.maxDepth = maxDepth;
1088    }
1089
1090    public int getMinDepth() {
1091        return minDepth;
1092    }
1093
1094    /**
1095     * The minimum depth to start processing when recursively processing a directory.
1096     * Using minDepth=1 means the base directory. Using minDepth=2 means the first sub directory.
1097     */
1098    public void setMinDepth(int minDepth) {
1099        this.minDepth = minDepth;
1100    }
1101
1102    public IdempotentRepository<String> getInProgressRepository() {
1103        return inProgressRepository;
1104    }
1105
1106    /**
1107     * A pluggable in-progress repository org.apache.camel.spi.IdempotentRepository.
1108     * The in-progress repository is used to account the current in progress files being consumed. By default a memory based repository is used.
1109     */
1110    public void setInProgressRepository(IdempotentRepository<String> inProgressRepository) {
1111        this.inProgressRepository = inProgressRepository;
1112    }
1113
1114    public boolean isKeepLastModified() {
1115        return keepLastModified;
1116    }
1117
1118    /**
1119     * Will keep the last modified timestamp from the source file (if any).
1120     * Will use the Exchange.FILE_LAST_MODIFIED header to located the timestamp.
1121     * This header can contain either a java.util.Date or long with the timestamp.
1122     * If the timestamp exists and the option is enabled it will set this timestamp on the written file.
1123     * Note: This option only applies to the file producer. You cannot use this option with any of the ftp producers.
1124     */
1125    public void setKeepLastModified(boolean keepLastModified) {
1126        this.keepLastModified = keepLastModified;
1127    }
1128
1129    public boolean isAllowNullBody() {
1130        return allowNullBody;
1131    }
1132
1133    /**
1134     * Used to specify if a null body is allowed during file writing.
1135     * If set to true then an empty file will be created, when set to false, and attempting to send a null body to the file component,
1136     * a GenericFileWriteException of 'Cannot write null body to file.' will be thrown.
1137     * If the `fileExist` option is set to 'Override', then the file will be truncated, and if set to `append` the file will remain unchanged.
1138     */
1139    public void setAllowNullBody(boolean allowNullBody) {
1140        this.allowNullBody = allowNullBody;
1141    }
1142
1143    public ExceptionHandler getOnCompletionExceptionHandler() {
1144        return onCompletionExceptionHandler;
1145    }
1146
1147    /**
1148     * To use a custom {@link org.apache.camel.spi.ExceptionHandler} to handle any thrown exceptions that happens
1149     * during the file on completion process where the consumer does either a commit or rollback. The default
1150     * implementation will log any exception at WARN level and ignore.
1151     */
1152    public void setOnCompletionExceptionHandler(ExceptionHandler onCompletionExceptionHandler) {
1153        this.onCompletionExceptionHandler = onCompletionExceptionHandler;
1154    }
1155
1156    /**
1157     * Configures the given message with the file which sets the body to the
1158     * file object.
1159     */
1160    public void configureMessage(GenericFile<T> file, Message message) {
1161        message.setBody(file);
1162
1163        if (flatten) {
1164            // when flatten the file name should not contain any paths
1165            message.setHeader(Exchange.FILE_NAME, file.getFileNameOnly());
1166        } else {
1167            // compute name to set on header that should be relative to starting directory
1168            String name = file.isAbsolute() ? file.getAbsoluteFilePath() : file.getRelativeFilePath();
1169
1170            // skip leading endpoint configured directory
1171            String endpointPath = getConfiguration().getDirectory() + getFileSeparator();
1172
1173            // need to normalize paths to ensure we can match using startsWith
1174            endpointPath = FileUtil.normalizePath(endpointPath);
1175            String copyOfName = FileUtil.normalizePath(name);
1176            if (ObjectHelper.isNotEmpty(endpointPath) && copyOfName.startsWith(endpointPath)) {
1177                name = name.substring(endpointPath.length());
1178            }
1179
1180            // adjust filename
1181            message.setHeader(Exchange.FILE_NAME, name);
1182        }
1183    }
1184
1185    /**
1186     * Set up the exchange properties with the options of the file endpoint
1187     */
1188    public void configureExchange(Exchange exchange) {
1189        // Now we just set the charset property here
1190        if (getCharset() != null) {
1191            exchange.setProperty(Exchange.CHARSET_NAME, getCharset());
1192        }
1193    }
1194
1195    /**
1196     * Strategy to configure the move, preMove, or moveExisting option based on a String input.
1197     *
1198     * @param expression the original string input
1199     * @return configured string or the original if no modifications is needed
1200     */
1201    protected String configureMoveOrPreMoveExpression(String expression) {
1202        // if the expression already have ${ } placeholders then pass it unmodified
1203        if (StringHelper.hasStartToken(expression, "simple")) {
1204            return expression;
1205        }
1206
1207        // remove trailing slash
1208        expression = FileUtil.stripTrailingSeparator(expression);
1209
1210        StringBuilder sb = new StringBuilder();
1211
1212        // if relative then insert start with the parent folder
1213        if (!isAbsolute(expression)) {
1214            sb.append("${file:parent}");
1215            sb.append(getFileSeparator());
1216        }
1217        // insert the directory the end user provided
1218        sb.append(expression);
1219        // append only the filename (file:name can contain a relative path, so we must use onlyname)
1220        sb.append(getFileSeparator());
1221        sb.append("${file:onlyname}");
1222
1223        return sb.toString();
1224    }
1225
1226    protected Map<String, Object> getParamsAsMap() {
1227        Map<String, Object> params = new HashMap<String, Object>();
1228
1229        if (isNoop()) {
1230            params.put("noop", Boolean.toString(true));
1231        }
1232        if (isDelete()) {
1233            params.put("delete", Boolean.toString(true));
1234        }
1235        if (move != null) {
1236            params.put("move", move);
1237        }
1238        if (moveFailed != null) {
1239            params.put("moveFailed", moveFailed);
1240        }
1241        if (preMove != null) {
1242            params.put("preMove", preMove);
1243        }
1244        if (exclusiveReadLockStrategy != null) {
1245            params.put("exclusiveReadLockStrategy", exclusiveReadLockStrategy);
1246        }
1247        if (readLock != null) {
1248            params.put("readLock", readLock);
1249        }
1250        if ("idempotent".equals(readLock)) {
1251            params.put("readLockIdempotentRepository", idempotentRepository);
1252        }
1253        if (readLockCheckInterval > 0) {
1254            params.put("readLockCheckInterval", readLockCheckInterval);
1255        }
1256        if (readLockTimeout > 0) {
1257            params.put("readLockTimeout", readLockTimeout);
1258        }
1259        params.put("readLockMarkerFile", readLockMarkerFile);
1260        params.put("readLockDeleteOrphanLockFiles", readLockDeleteOrphanLockFiles);
1261        params.put("readLockMinLength", readLockMinLength);
1262        params.put("readLockLoggingLevel", readLockLoggingLevel);
1263        params.put("readLockMinAge", readLockMinAge);
1264        params.put("readLockRemoveOnRollback", readLockRemoveOnRollback);
1265        params.put("readLockRemoveOnCommit", readLockRemoveOnCommit);
1266        return params;
1267    }
1268
1269    private Expression createFileLanguageExpression(String expression) {
1270        Language language;
1271        // only use file language if the name is complex (eg. using $)
1272        if (expression.contains("$")) {
1273            language = getCamelContext().resolveLanguage("file");
1274        } else {
1275            language = getCamelContext().resolveLanguage("constant");
1276        }
1277        return language.createExpression(expression);
1278    }
1279
1280    private Predicate createFileLanguagePredicate(String expression) {
1281        Language language = getCamelContext().resolveLanguage("file");
1282        return language.createPredicate(expression);
1283    }
1284
1285    /**
1286     * Creates the associated name of the done file based on the given file name.
1287     * <p/>
1288     * This method should only be invoked if a done filename property has been set on this endpoint.
1289     *
1290     * @param fileName the file name
1291     * @return name of the associated done file name
1292     */
1293    protected String createDoneFileName(String fileName) {
1294        String pattern = getDoneFileName();
1295        ObjectHelper.notEmpty(pattern, "doneFileName", pattern);
1296
1297        // we only support ${file:name} or ${file:name.noext} as dynamic placeholders for done files
1298        String path = FileUtil.onlyPath(fileName);
1299        String onlyName = FileUtil.stripPath(fileName);
1300
1301        pattern = pattern.replaceFirst("\\$\\{file:name\\}", onlyName);
1302        pattern = pattern.replaceFirst("\\$simple\\{file:name\\}", onlyName);
1303        pattern = pattern.replaceFirst("\\$\\{file:name.noext\\}", FileUtil.stripExt(onlyName));
1304        pattern = pattern.replaceFirst("\\$simple\\{file:name.noext\\}", FileUtil.stripExt(onlyName));
1305
1306        // must be able to resolve all placeholders supported
1307        if (StringHelper.hasStartToken(pattern, "simple")) {
1308            throw new ExpressionIllegalSyntaxException(fileName + ". Cannot resolve reminder: " + pattern);
1309        }
1310
1311        String answer = pattern;
1312        if (ObjectHelper.isNotEmpty(path) && ObjectHelper.isNotEmpty(pattern)) {
1313            // done file must always be in same directory as the real file name
1314            answer = path + getFileSeparator() + pattern;
1315        }
1316
1317        if (getConfiguration().needToNormalize()) {
1318            // must normalize path to cater for Windows and other OS
1319            answer = FileUtil.normalizePath(answer);
1320        }
1321
1322        return answer;
1323    }
1324
1325    /**
1326     * Is the given file a done file?
1327     * <p/>
1328     * This method should only be invoked if a done filename property has been set on this endpoint.
1329     *
1330     * @param fileName the file name
1331     * @return <tt>true</tt> if its a done file, <tt>false</tt> otherwise
1332     */
1333    protected boolean isDoneFile(String fileName) {
1334        String pattern = getDoneFileName();
1335        ObjectHelper.notEmpty(pattern, "doneFileName", pattern);
1336
1337        if (!StringHelper.hasStartToken(pattern, "simple")) {
1338            // no tokens, so just match names directly
1339            return pattern.equals(fileName);
1340        }
1341
1342        // the static part of the pattern, is that a prefix or suffix?
1343        // its a prefix if ${ start token is not at the start of the pattern
1344        boolean prefix = pattern.indexOf("${") > 0;
1345
1346        // remove dynamic parts of the pattern so we only got the static part left
1347        pattern = pattern.replaceFirst("\\$\\{file:name\\}", "");
1348        pattern = pattern.replaceFirst("\\$simple\\{file:name\\}", "");
1349        pattern = pattern.replaceFirst("\\$\\{file:name.noext\\}", "");
1350        pattern = pattern.replaceFirst("\\$simple\\{file:name.noext\\}", "");
1351
1352        // must be able to resolve all placeholders supported
1353        if (StringHelper.hasStartToken(pattern, "simple")) {
1354            throw new ExpressionIllegalSyntaxException(fileName + ". Cannot resolve reminder: " + pattern);
1355        }
1356
1357        if (prefix) {
1358            return fileName.startsWith(pattern);
1359        } else {
1360            return fileName.endsWith(pattern);
1361        }
1362    }
1363
1364    @Override
1365    protected void doStart() throws Exception {
1366        // validate that the read lock options is valid for the process strategy
1367        if (!"none".equals(readLock) && !"off".equals(readLock)) {
1368            if (readLockTimeout > 0 && readLockTimeout <= readLockCheckInterval) {
1369                throw new IllegalArgumentException("The option readLockTimeout must be higher than readLockCheckInterval"
1370                        + ", was readLockTimeout=" + readLockTimeout + ", readLockCheckInterval=" + readLockCheckInterval
1371                        + ". A good practice is to let the readLockTimeout be at least 3 times higher than the readLockCheckInterval"
1372                        + " to ensure that the read lock procedure has enough time to acquire the lock.");
1373            }
1374        }
1375        if ("idempotent".equals(readLock) && idempotentRepository == null) {
1376            throw new IllegalArgumentException("IdempotentRepository must be configured when using readLock=idempotent");
1377        }
1378
1379        if (antInclude != null) {
1380            if (antFilter == null) {
1381                antFilter = new AntPathMatcherGenericFileFilter<>();
1382            }
1383            antFilter.setIncludes(antInclude);
1384        }
1385        if (antExclude != null) {
1386            if (antFilter == null) {
1387                antFilter = new AntPathMatcherGenericFileFilter<>();
1388            }
1389            antFilter.setExcludes(antExclude);
1390        }
1391        if (antFilter != null) {
1392            antFilter.setCaseSensitive(antFilterCaseSensitive);
1393        }
1394
1395        // idempotent repository may be used by others, so add it as a service so its stopped when CamelContext stops
1396        if (idempotentRepository != null) {
1397            getCamelContext().addService(idempotentRepository, true);
1398        }
1399        ServiceHelper.startServices(inProgressRepository);
1400        super.doStart();
1401    }
1402
1403    @Override
1404    protected void doStop() throws Exception {
1405        super.doStop();
1406        ServiceHelper.stopServices(inProgressRepository);
1407    }
1408}