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