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