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