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.util.concurrent.locks.Lock;
021    import java.util.concurrent.locks.ReentrantLock;
022    
023    import org.apache.camel.Exchange;
024    import org.apache.camel.Expression;
025    import org.apache.camel.impl.DefaultExchange;
026    import org.apache.camel.impl.DefaultProducer;
027    import org.apache.camel.spi.Language;
028    import org.apache.camel.util.ExchangeHelper;
029    import org.apache.camel.util.FileUtil;
030    import org.apache.camel.util.LRUCache;
031    import org.apache.camel.util.ObjectHelper;
032    import org.apache.camel.util.ServiceHelper;
033    import org.apache.camel.util.StringHelper;
034    import org.slf4j.Logger;
035    import org.slf4j.LoggerFactory;
036    
037    /**
038     * Generic file producer
039     */
040    public class GenericFileProducer<T> extends DefaultProducer {
041        protected final transient Logger log = LoggerFactory.getLogger(getClass());
042        protected final GenericFileEndpoint<T> endpoint;
043        protected GenericFileOperations<T> operations;
044        // assume writing to 100 different files concurrently at most for the same file producer
045        private final LRUCache<String, Lock> locks = new LRUCache<String, Lock>(100);
046    
047        protected GenericFileProducer(GenericFileEndpoint<T> endpoint, GenericFileOperations<T> operations) {
048            super(endpoint);
049            this.endpoint = endpoint;
050            this.operations = operations;
051        }
052        
053        public String getFileSeparator() {
054            return File.separator;
055        }
056    
057        public String normalizePath(String name) {
058            return FileUtil.normalizePath(name);
059        }
060    
061        public void process(Exchange exchange) throws Exception {
062            String target = createFileName(exchange);
063    
064            // use lock for same file name to avoid concurrent writes to the same file
065            // for example when you concurrently append to the same file
066            Lock lock;
067            synchronized (locks) {
068                lock = locks.get(target);
069                if (lock == null) {
070                    lock = new ReentrantLock();
071                    locks.put(target, lock);
072                }
073            }
074    
075            lock.lock();
076            try {
077                processExchange(exchange, target);
078            } finally {
079                // do not remove as the locks cache has an upper bound
080                // this ensure the locks is appropriate reused
081                lock.unlock();
082            }
083        }
084    
085        /**
086         * Sets the operations to be used.
087         * <p/>
088         * Can be used to set a fresh operations in case of recovery attempts
089         *
090         * @param operations the operations
091         */
092        public void setOperations(GenericFileOperations<T> operations) {
093            this.operations = operations;
094        }
095    
096        /**
097         * Perform the work to process the fileExchange
098         *
099         * @param exchange fileExchange
100         * @param target   the target filename
101         * @throws Exception is thrown if some error
102         */
103        protected void processExchange(Exchange exchange, String target) throws Exception {
104            log.trace("Processing file: {} for exchange: {}", target, exchange);
105    
106            try {
107                preWriteCheck();
108    
109                // should we write to a temporary name and then afterwards rename to real target
110                boolean writeAsTempAndRename = ObjectHelper.isNotEmpty(endpoint.getTempFileName());
111                String tempTarget = null;
112                // remember if target exists to avoid checking twice
113                Boolean targetExists = null;
114                if (writeAsTempAndRename) {
115                    // compute temporary name with the temp prefix
116                    tempTarget = createTempFileName(exchange, target);
117    
118                    log.trace("Writing using tempNameFile: {}", tempTarget);
119    
120                    // cater for file exists option on the real target as
121                    // the file operations code will work on the temp file
122    
123                    // if an existing file already exists what should we do?
124                    targetExists = operations.existsFile(target);
125                    if (targetExists) {
126                        if (endpoint.getFileExist() == GenericFileExist.Ignore) {
127                            // ignore but indicate that the file was written
128                            log.trace("An existing file already exists: {}. Ignore and do not override it.", target);
129                            return;
130                        } else if (endpoint.getFileExist() == GenericFileExist.Fail) {
131                            throw new GenericFileOperationFailedException("File already exist: " + target + ". Cannot write new file.");
132                        } else if (endpoint.isEagerDeleteTargetFile() && endpoint.getFileExist() == GenericFileExist.Override) {
133                            // we override the target so we do this by deleting it so the temp file can be renamed later
134                            // with success as the existing target file have been deleted
135                            log.trace("Eagerly deleting existing file: {}", target);
136                            if (!operations.deleteFile(target)) {
137                                throw new GenericFileOperationFailedException("Cannot delete file: " + target);
138                            }
139                        }
140                    }
141    
142                    // delete any pre existing temp file
143                    if (operations.existsFile(tempTarget)) {
144                        log.trace("Deleting existing temp file: {}", tempTarget);
145                        if (!operations.deleteFile(tempTarget)) {
146                            throw new GenericFileOperationFailedException("Cannot delete file: " + tempTarget);
147                        }
148                    }
149                }
150    
151                // write/upload the file
152                writeFile(exchange, tempTarget != null ? tempTarget : target);
153    
154                // if we did write to a temporary name then rename it to the real
155                // name after we have written the file
156                if (tempTarget != null) {
157    
158                    // if we should not eager delete the target file then do it now just before renaming
159                    if (!endpoint.isEagerDeleteTargetFile() && targetExists
160                            && endpoint.getFileExist() == GenericFileExist.Override) {
161                        // we override the target so we do this by deleting it so the temp file can be renamed later
162                        // with success as the existing target file have been deleted
163                        log.trace("Deleting existing file: {}", target);
164                        if (!operations.deleteFile(target)) {
165                            throw new GenericFileOperationFailedException("Cannot delete file: " + target);
166                        }
167                    }
168    
169                    // now we are ready to rename the temp file to the target file
170                    log.trace("Renaming file: [{}] to: [{}]", tempTarget, target);
171                    boolean renamed = operations.renameFile(tempTarget, target);
172                    if (!renamed) {
173                        throw new GenericFileOperationFailedException("Cannot rename file from: " + tempTarget + " to: " + target);
174                    }
175                }
176    
177                // any done file to write?
178                if (endpoint.getDoneFileName() != null) {
179                    String doneFileName = endpoint.createDoneFileName(target);
180                    ObjectHelper.notEmpty(doneFileName, "doneFileName", endpoint);
181    
182                    // create empty exchange with empty body to write as the done file
183                    Exchange empty = new DefaultExchange(exchange);
184                    empty.getIn().setBody("");
185    
186                    log.trace("Writing done file: [{}]", doneFileName);
187                    // delete any existing done file
188                    if (operations.existsFile(doneFileName)) {
189                        if (!operations.deleteFile(doneFileName)) {
190                            throw new GenericFileOperationFailedException("Cannot delete existing done file: " + doneFileName);
191                        }
192                    }
193                    writeFile(empty, doneFileName);
194                }
195    
196                // let's store the name we really used in the header, so end-users
197                // can retrieve it
198                exchange.getIn().setHeader(Exchange.FILE_NAME_PRODUCED, target);
199            } catch (Exception e) {
200                handleFailedWrite(exchange, e);
201            }
202    
203            postWriteCheck();
204        }
205    
206        /**
207         * If we fail writing out a file, we will call this method. This hook is
208         * provided to disconnect from servers or clean up files we created (if needed).
209         */
210        public void handleFailedWrite(Exchange exchange, Exception exception) throws Exception {
211            throw exception;
212        }
213    
214        /**
215         * Perform any actions that need to occur before we write such as connecting to an FTP server etc.
216         */
217        public void preWriteCheck() throws Exception {
218            // nothing needed to check
219        }
220    
221        /**
222         * Perform any actions that need to occur after we are done such as disconnecting.
223         */
224        public void postWriteCheck() {
225            // nothing needed to check
226        }
227    
228        public void writeFile(Exchange exchange, String fileName) throws GenericFileOperationFailedException {
229            // build directory if auto create is enabled
230            if (endpoint.isAutoCreate()) {
231                // we must normalize it (to avoid having both \ and / in the name which confuses java.io.File)
232                String name = FileUtil.normalizePath(fileName);
233    
234                // use java.io.File to compute the file path
235                File file = new File(name);
236                String directory = file.getParent();
237                boolean absolute = FileUtil.isAbsolute(file);
238                if (directory != null) {
239                    if (!operations.buildDirectory(directory, absolute)) {
240                        log.debug("Cannot build directory [{}] (could be because of denied permissions)", directory);
241                    }
242                }
243            }
244    
245            // upload
246            if (log.isTraceEnabled()) {
247                log.trace("About to write [{}] to [{}] from exchange [{}]", new Object[]{fileName, getEndpoint(), exchange});
248            }
249    
250            boolean success = operations.storeFile(fileName, exchange);
251            if (!success) {
252                throw new GenericFileOperationFailedException("Error writing file [" + fileName + "]");
253            }
254            log.debug("Wrote [{}] to [{}]", fileName, getEndpoint());
255        }
256    
257        public String createFileName(Exchange exchange) {
258            String answer;
259    
260            String name = exchange.getIn().getHeader(Exchange.FILE_NAME, String.class);
261    
262            // expression support
263            Expression expression = endpoint.getFileName();
264            if (name != null) {
265                // the header name can be an expression too, that should override
266                // whatever configured on the endpoint
267                if (StringHelper.hasStartToken(name, "simple")) {
268                    log.trace("{} contains a Simple expression: {}", Exchange.FILE_NAME, name);
269                    Language language = getEndpoint().getCamelContext().resolveLanguage("file");
270                    expression = language.createExpression(name);
271                }
272            }
273            if (expression != null) {
274                log.trace("Filename evaluated as expression: {}", expression);
275                name = expression.evaluate(exchange, String.class);
276            }
277    
278            // flatten name
279            if (name != null && endpoint.isFlatten()) {
280                // check for both windows and unix separators
281                int pos = Math.max(name.lastIndexOf("/"), name.lastIndexOf("\\"));
282                if (pos != -1) {
283                    name = name.substring(pos + 1);
284                }
285            }
286    
287            // compute path by adding endpoint starting directory
288            String endpointPath = endpoint.getConfiguration().getDirectory();
289            String baseDir = "";
290            if (endpointPath.length() > 0) {
291                // Its a directory so we should use it as a base path for the filename
292                // If the path isn't empty, we need to add a trailing / if it isn't already there
293                baseDir = endpointPath;
294                boolean trailingSlash = endpointPath.endsWith("/") || endpointPath.endsWith("\\");
295                if (!trailingSlash) {
296                    baseDir += getFileSeparator();
297                }
298            }
299            if (name != null) {
300                answer = baseDir + name;
301            } else {
302                // use a generated filename if no name provided
303                answer = baseDir + endpoint.getGeneratedFileName(exchange.getIn());
304            }
305    
306            if (endpoint.getConfiguration().needToNormalize()) {
307                // must normalize path to cater for Windows and other OS
308                answer = normalizePath(answer);
309            }
310    
311            return answer;
312        }
313    
314        public String createTempFileName(Exchange exchange, String fileName) {
315            String answer = fileName;
316    
317            String tempName;
318            if (exchange.getIn().getHeader(Exchange.FILE_NAME) == null) {
319                // its a generated filename then add it to header so we can evaluate the expression
320                exchange.getIn().setHeader(Exchange.FILE_NAME, FileUtil.stripPath(fileName));
321                tempName = endpoint.getTempFileName().evaluate(exchange, String.class);
322                // and remove it again after evaluation
323                exchange.getIn().removeHeader(Exchange.FILE_NAME);
324            } else {
325                tempName = endpoint.getTempFileName().evaluate(exchange, String.class);
326            }
327    
328            // check for both windows and unix separators
329            int pos = Math.max(answer.lastIndexOf("/"), answer.lastIndexOf("\\"));
330            if (pos == -1) {
331                // no path so use temp name as calculated
332                answer = tempName;
333            } else {
334                // path should be prefixed before the temp name
335                StringBuilder sb = new StringBuilder(answer.substring(0, pos + 1));
336                sb.append(tempName);
337                answer = sb.toString();
338            }
339    
340            if (endpoint.getConfiguration().needToNormalize()) {
341                // must normalize path to cater for Windows and other OS
342                answer = normalizePath(answer);
343            }
344    
345            return answer;
346        }
347    
348        @Override
349        protected void doStart() throws Exception {
350            super.doStart();
351            ServiceHelper.startService(locks);
352        }
353    
354        @Override
355        protected void doStop() throws Exception {
356            ServiceHelper.stopService(locks);
357            super.doStop();
358        }
359    }