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.FileInputStream;
021    import java.io.FileOutputStream;
022    import java.io.IOException;
023    import java.io.InputStream;
024    import java.io.InputStreamReader;
025    import java.io.RandomAccessFile;
026    import java.io.Reader;
027    import java.io.Writer;
028    import java.nio.ByteBuffer;
029    import java.nio.channels.FileChannel;
030    import java.util.Date;
031    import java.util.List;
032    
033    import org.apache.camel.Exchange;
034    import org.apache.camel.InvalidPayloadException;
035    import org.apache.camel.WrappedFile;
036    import org.apache.camel.converter.IOConverter;
037    import org.apache.camel.util.FileUtil;
038    import org.apache.camel.util.IOHelper;
039    import org.apache.camel.util.ObjectHelper;
040    import org.slf4j.Logger;
041    import org.slf4j.LoggerFactory;
042    
043    /**
044     * File operations for {@link java.io.File}.
045     */
046    public class FileOperations implements GenericFileOperations<File> {
047        private static final Logger LOG = LoggerFactory.getLogger(FileOperations.class);
048        private FileEndpoint endpoint;
049    
050        public FileOperations() {
051        }
052    
053        public FileOperations(FileEndpoint endpoint) {
054            this.endpoint = endpoint;
055        }
056    
057        public void setEndpoint(GenericFileEndpoint<File> endpoint) {
058            this.endpoint = (FileEndpoint) endpoint;
059        }
060    
061        public boolean deleteFile(String name) throws GenericFileOperationFailedException {
062            File file = new File(name);
063            return FileUtil.deleteFile(file);
064        }
065    
066        public boolean renameFile(String from, String to) throws GenericFileOperationFailedException {
067            File file = new File(from);
068            File target = new File(to);
069            try {
070                return FileUtil.renameFile(file, target, endpoint.isCopyAndDeleteOnRenameFail());
071            } catch (IOException e) {
072                throw new GenericFileOperationFailedException("Error renaming file from " + from + " to " + to, e);
073            }
074        }
075    
076        public boolean existsFile(String name) throws GenericFileOperationFailedException {
077            File file = new File(name);
078            return file.exists();
079        }
080    
081        public boolean buildDirectory(String directory, boolean absolute) throws GenericFileOperationFailedException {
082            ObjectHelper.notNull(endpoint, "endpoint");
083    
084            // always create endpoint defined directory
085            if (endpoint.isAutoCreate() && !endpoint.getFile().exists()) {
086                LOG.trace("Building starting directory: {}", endpoint.getFile());
087                endpoint.getFile().mkdirs();
088            }
089    
090            if (ObjectHelper.isEmpty(directory)) {
091                // no directory to build so return true to indicate ok
092                return true;
093            }
094    
095            File endpointPath = endpoint.getFile();
096            File target = new File(directory);
097    
098            File path;
099            if (absolute) {
100                // absolute path
101                path = target;
102            } else if (endpointPath.equals(target)) {
103                // its just the root of the endpoint path
104                path = endpointPath;
105            } else {
106                // relative after the endpoint path
107                String afterRoot = ObjectHelper.after(directory, endpointPath.getPath() + File.separator);
108                if (ObjectHelper.isNotEmpty(afterRoot)) {
109                    // dir is under the root path
110                    path = new File(endpoint.getFile(), afterRoot);
111                } else {
112                    // dir is relative to the root path
113                    path = new File(endpoint.getFile(), directory);
114                }
115            }
116    
117            // We need to make sure that this is thread-safe and only one thread tries to create the path directory at the same time.
118            synchronized (this) {
119                if (path.isDirectory() && path.exists()) {
120                    // the directory already exists
121                    return true;
122                } else {
123                    if (LOG.isTraceEnabled()) {
124                        LOG.trace("Building directory: {}", path);
125                    }
126                    return path.mkdirs();
127                }
128            }
129        }
130    
131        public List<File> listFiles() throws GenericFileOperationFailedException {
132            // noop
133            return null;
134        }
135    
136        public List<File> listFiles(String path) throws GenericFileOperationFailedException {
137            // noop
138            return null;
139        }
140    
141        public void changeCurrentDirectory(String path) throws GenericFileOperationFailedException {
142            // noop
143        }
144    
145        public void changeToParentDirectory() throws GenericFileOperationFailedException {
146            // noop
147        }
148    
149        public String getCurrentDirectory() throws GenericFileOperationFailedException {
150            // noop
151            return null;
152        }
153    
154        public boolean retrieveFile(String name, Exchange exchange) throws GenericFileOperationFailedException {
155            // noop as we use type converters to read the body content for java.io.File
156            return true;
157        }
158        
159        @Override
160        public void releaseRetreivedFileResources(Exchange exchange) throws GenericFileOperationFailedException {
161            // noop as we used type converters to read the body content for java.io.File
162        }
163    
164        public boolean storeFile(String fileName, Exchange exchange) throws GenericFileOperationFailedException {
165            ObjectHelper.notNull(endpoint, "endpoint");
166    
167            File file = new File(fileName);
168            
169            // if an existing file already exists what should we do?
170            if (file.exists()) {
171                if (endpoint.getFileExist() == GenericFileExist.Ignore) {
172                    // ignore but indicate that the file was written
173                    LOG.trace("An existing file already exists: {}. Ignore and do not override it.", file);
174                    return true;
175                } else if (endpoint.getFileExist() == GenericFileExist.Fail) {
176                    throw new GenericFileOperationFailedException("File already exist: " + file + ". Cannot write new file.");
177                } else if (endpoint.getFileExist() == GenericFileExist.Move) {
178                    // move any existing file first
179                    doMoveExistingFile(fileName);
180                }
181            }
182            
183            // Do an explicit test for a null body and decide what to do
184            if (exchange.getIn().getBody() == null) {
185                if (endpoint.isAllowNullBody()) {
186                    LOG.trace("Writing empty file.");
187                    try {
188                        writeFileEmptyBody(file);
189                        return true;
190                    } catch (IOException e) {
191                        throw new GenericFileOperationFailedException("Cannot store file: " + file, e);
192                    }
193                } else {
194                    throw new GenericFileOperationFailedException("Cannot write null body to file: " + file);
195                }
196            }
197    
198            // we can write the file by 3 different techniques
199            // 1. write file to file
200            // 2. rename a file from a local work path
201            // 3. write stream to file
202            try {
203    
204                // is there an explicit charset configured we must write the file as
205                String charset = endpoint.getCharset();
206    
207                // we can optimize and use file based if no charset must be used, and the input body is a file
208                File source = null;
209                boolean fileBased = false;
210                if (charset == null) {
211                    // if no charset, then we can try using file directly (optimized)
212                    Object body = exchange.getIn().getBody();
213                    if (body instanceof WrappedFile) {
214                        body = ((WrappedFile<?>) body).getFile();
215                        fileBased = true;
216                    }
217                    if (body instanceof File) {
218                        source = (File) body;
219                    }
220                }
221    
222                if (fileBased) {
223                    // okay we know the body is a file based
224    
225                    // so try to see if we can optimize by renaming the local work path file instead of doing
226                    // a full file to file copy, as the local work copy is to be deleted afterwards anyway
227                    // local work path
228                    File local = exchange.getIn().getHeader(Exchange.FILE_LOCAL_WORK_PATH, File.class);
229                    if (local != null && local.exists()) {
230                        boolean renamed = writeFileByLocalWorkPath(local, file);
231                        if (renamed) {
232                            // try to keep last modified timestamp if configured to do so
233                            keepLastModified(exchange, file);
234                            // clear header as we have renamed the file
235                            exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, null);
236                            // return as the operation is complete, we just renamed the local work file
237                            // to the target.
238                            return true;
239                        }
240                    } else if (source != null && source.exists()) {
241                        // no there is no local work file so use file to file copy if the source exists
242                        writeFileByFile(source, file);
243                        // try to keep last modified timestamp if configured to do so
244                        keepLastModified(exchange, file);
245                        return true;
246                    }
247                }
248    
249                if (charset != null) {
250                    // charset configured so we must use a reader so we can write with encoding
251                    Reader in = exchange.getIn().getBody(Reader.class);
252                    if (in == null) {
253                        // okay no direct reader conversion, so use an input stream (which a lot can be converted as)
254                        InputStream is = exchange.getIn().getMandatoryBody(InputStream.class);
255                        in = new InputStreamReader(is);
256                    }
257                    // buffer the reader
258                    in = IOHelper.buffered(in);
259                    writeFileByReaderWithCharset(in, file, charset);
260                } else {
261                    // fallback and use stream based
262                    InputStream in = exchange.getIn().getMandatoryBody(InputStream.class);
263                    writeFileByStream(in, file);
264                }
265                // try to keep last modified timestamp if configured to do so
266                keepLastModified(exchange, file);
267                return true;
268            } catch (IOException e) {
269                throw new GenericFileOperationFailedException("Cannot store file: " + file, e);
270            } catch (InvalidPayloadException e) {
271                throw new GenericFileOperationFailedException("Cannot store file: " + file, e);
272            }
273        }
274    
275        /**
276         * Moves any existing file due fileExists=Move is in use.
277         */
278        private void doMoveExistingFile(String fileName) throws GenericFileOperationFailedException {
279            // need to evaluate using a dummy and simulate the file first, to have access to all the file attributes
280            // create a dummy exchange as Exchange is needed for expression evaluation
281            // we support only the following 3 tokens.
282            Exchange dummy = endpoint.createExchange();
283            String parent = FileUtil.onlyPath(fileName);
284            String onlyName = FileUtil.stripPath(fileName);
285            dummy.getIn().setHeader(Exchange.FILE_NAME, fileName);
286            dummy.getIn().setHeader(Exchange.FILE_NAME_ONLY, onlyName);
287            dummy.getIn().setHeader(Exchange.FILE_PARENT, parent);
288    
289            String to = endpoint.getMoveExisting().evaluate(dummy, String.class);
290            // we must normalize it (to avoid having both \ and / in the name which confuses java.io.File)
291            to = FileUtil.normalizePath(to);
292            if (ObjectHelper.isEmpty(to)) {
293                throw new GenericFileOperationFailedException("moveExisting evaluated as empty String, cannot move existing file: " + fileName);
294            }
295    
296            // ensure any paths is created before we rename as the renamed file may be in a different path (which may be non exiting)
297            // use java.io.File to compute the file path
298            File toFile = new File(to);
299            String directory = toFile.getParent();
300            boolean absolute = FileUtil.isAbsolute(toFile);
301            if (directory != null) {
302                if (!buildDirectory(directory, absolute)) {
303                    LOG.debug("Cannot build directory [{}] (could be because of denied permissions)", directory);
304                }
305            }
306    
307            // deal if there already exists a file
308            if (existsFile(to)) {
309                if (endpoint.isEagerDeleteTargetFile()) {
310                    LOG.trace("Deleting existing file: {}", to);
311                    if (!deleteFile(to)) {
312                        throw new GenericFileOperationFailedException("Cannot delete file: " + to);
313                    }
314                } else {
315                    throw new GenericFileOperationFailedException("Cannot moved existing file from: " + fileName + " to: " + to + " as there already exists a file: " + to);
316                }
317            }
318    
319            LOG.trace("Moving existing file: {} to: {}", fileName, to);
320            if (!renameFile(fileName, to)) {
321                throw new GenericFileOperationFailedException("Cannot rename file from: " + fileName + " to: " + to);
322            }
323        }
324    
325        private void keepLastModified(Exchange exchange, File file) {
326            if (endpoint.isKeepLastModified()) {
327                Long last;
328                Date date = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Date.class);
329                if (date != null) {
330                    last = date.getTime();
331                } else {
332                    // fallback and try a long
333                    last = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Long.class);
334                }
335                if (last != null) {
336                    boolean result = file.setLastModified(last);
337                    if (LOG.isTraceEnabled()) {
338                        LOG.trace("Keeping last modified timestamp: {} on file: {} with result: {}", new Object[]{last, file, result});
339                    }
340                }
341            }
342        }
343    
344        private boolean writeFileByLocalWorkPath(File source, File file) throws IOException {
345            LOG.trace("Using local work file being renamed from: {} to: {}", source, file);
346            return FileUtil.renameFile(source, file, endpoint.isCopyAndDeleteOnRenameFail());
347        }
348    
349        private void writeFileByFile(File source, File target) throws IOException {
350            FileChannel in = new FileInputStream(source).getChannel();
351            FileChannel out = null;
352            try {
353                out = prepareOutputFileChannel(target);
354                LOG.debug("Using FileChannel to write file: {}", target);
355                long size = in.size();
356                long position = 0;
357                while (position < size) {
358                    position += in.transferTo(position, endpoint.getBufferSize(), out);
359                }
360            } finally {
361                IOHelper.close(in, source.getName(), LOG);
362                IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites());
363            }
364        }
365    
366        private void writeFileByStream(InputStream in, File target) throws IOException {
367            FileChannel out = null;
368            try {
369                out = prepareOutputFileChannel(target);
370                LOG.debug("Using InputStream to write file: {}", target);
371                int size = endpoint.getBufferSize();
372                byte[] buffer = new byte[size];
373                ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
374                int bytesRead;
375                while ((bytesRead = in.read(buffer)) != -1) {
376                    if (bytesRead < size) {
377                        byteBuffer.limit(bytesRead);
378                    }
379                    out.write(byteBuffer);
380                    byteBuffer.clear();
381                }
382            } finally {
383                IOHelper.close(in, target.getName(), LOG);
384                IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites());
385            }
386        }
387    
388        private void writeFileByReaderWithCharset(Reader in, File target, String charset) throws IOException {
389            boolean append = endpoint.getFileExist() == GenericFileExist.Append;
390            FileOutputStream os = new FileOutputStream(target, append);
391            Writer out = IOConverter.toWriter(os, charset);
392            try {
393                LOG.debug("Using Reader to write file: {} with charset: {}", target, charset);
394                int size = endpoint.getBufferSize();
395                IOHelper.copy(in, out, size);
396            } finally {
397                IOHelper.close(in, target.getName(), LOG);
398                IOHelper.close(out, os, target.getName(), LOG, endpoint.isForceWrites());
399            }
400        }
401    
402        /**
403         * Creates a new file if the file doesn't exist.
404         * If the endpoint's existing file logic is set to 'Override' then the target file will be truncated
405         */
406        private void writeFileEmptyBody(File target) throws IOException {
407            if (!target.exists()) {
408                LOG.debug("Creating new empty file: {}", target);
409                FileUtil.createNewFile(target);
410            } else if (endpoint.getFileExist() == GenericFileExist.Override) {
411                LOG.debug("Truncating existing file: {}", target);
412                FileChannel out = new FileOutputStream(target).getChannel();
413                try {
414                    out.truncate(0);
415                } finally {
416                    IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites());
417                }
418            }
419        }
420    
421        /**
422         * Creates and prepares the output file channel. Will position itself in correct position if the file is writable
423         * eg. it should append or override any existing content.
424         */
425        private FileChannel prepareOutputFileChannel(File target) throws IOException {
426            if (endpoint.getFileExist() == GenericFileExist.Append) {
427                FileChannel out = new RandomAccessFile(target, "rw").getChannel();
428                return out.position(out.size());
429            }
430            return new FileOutputStream(target).getChannel();
431        }
432    }