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 transient 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        public boolean storeFile(String fileName, Exchange exchange) throws GenericFileOperationFailedException {
160            ObjectHelper.notNull(endpoint, "endpoint");
161    
162            File file = new File(fileName);
163            
164            // if an existing file already exists what should we do?
165            if (file.exists()) {
166                if (endpoint.getFileExist() == GenericFileExist.Ignore) {
167                    // ignore but indicate that the file was written
168                    LOG.trace("An existing file already exists: {}. Ignore and do not override it.", file);
169                    return true;
170                } else if (endpoint.getFileExist() == GenericFileExist.Fail) {
171                    throw new GenericFileOperationFailedException("File already exist: " + file + ". Cannot write new file.");
172                } else if (endpoint.getFileExist() == GenericFileExist.Move) {
173                    // move any existing file first
174                    doMoveExistingFile(fileName);
175                }
176            }
177            
178            // Do an explicit test for a null body and decide what to do
179            if (exchange.getIn().getBody() == null) {
180                if (endpoint.isAllowNullBody()) {
181                    LOG.trace("Writing empty file.");
182                    try {
183                        writeFileEmptyBody(file);
184                        return true;
185                    } catch (IOException e) {
186                        throw new GenericFileOperationFailedException("Cannot store file: " + file, e);
187                    }
188                } else {
189                    throw new GenericFileOperationFailedException("Cannot write null body to file: " + file);
190                }
191            }
192    
193            // we can write the file by 3 different techniques
194            // 1. write file to file
195            // 2. rename a file from a local work path
196            // 3. write stream to file
197            try {
198    
199                // is there an explicit charset configured we must write the file as
200                String charset = endpoint.getCharset();
201    
202                // we can optimize and use file based if no charset must be used, and the input body is a file
203                File source = null;
204                boolean fileBased = false;
205                if (charset == null) {
206                    // if no charset, then we can try using file directly (optimized)
207                    Object body = exchange.getIn().getBody();
208                    if (body instanceof WrappedFile) {
209                        body = ((WrappedFile<?>) body).getFile();
210                        fileBased = true;
211                    }
212                    if (body instanceof File) {
213                        source = (File) body;
214                    }
215                }
216    
217                if (fileBased) {
218                    // okay we know the body is a file based
219    
220                    // so try to see if we can optimize by renaming the local work path file instead of doing
221                    // a full file to file copy, as the local work copy is to be deleted afterwards anyway
222                    // local work path
223                    File local = exchange.getIn().getHeader(Exchange.FILE_LOCAL_WORK_PATH, File.class);
224                    if (local != null && local.exists()) {
225                        boolean renamed = writeFileByLocalWorkPath(local, file);
226                        if (renamed) {
227                            // try to keep last modified timestamp if configured to do so
228                            keepLastModified(exchange, file);
229                            // clear header as we have renamed the file
230                            exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, null);
231                            // return as the operation is complete, we just renamed the local work file
232                            // to the target.
233                            return true;
234                        }
235                    } else if (source != null && source.exists()) {
236                        // no there is no local work file so use file to file copy if the source exists
237                        writeFileByFile(source, file);
238                        // try to keep last modified timestamp if configured to do so
239                        keepLastModified(exchange, file);
240                        return true;
241                    }
242                }
243    
244                if (charset != null) {
245                    // charset configured so we must use a reader so we can write with encoding
246                    Reader in = exchange.getIn().getBody(Reader.class);
247                    if (in == null) {
248                        // okay no direct reader conversion, so use an input stream (which a lot can be converted as)
249                        InputStream is = exchange.getIn().getMandatoryBody(InputStream.class);
250                        in = new InputStreamReader(is);
251                    }
252                    // buffer the reader
253                    in = IOHelper.buffered(in);
254                    writeFileByReaderWithCharset(in, file, charset);
255                } else {
256                    // fallback and use stream based
257                    InputStream in = exchange.getIn().getMandatoryBody(InputStream.class);
258                    writeFileByStream(in, file);
259                }
260                // try to keep last modified timestamp if configured to do so
261                keepLastModified(exchange, file);
262                return true;
263            } catch (IOException e) {
264                throw new GenericFileOperationFailedException("Cannot store file: " + file, e);
265            } catch (InvalidPayloadException e) {
266                throw new GenericFileOperationFailedException("Cannot store file: " + file, e);
267            }
268        }
269    
270        /**
271         * Moves any existing file due fileExists=Move is in use.
272         */
273        private void doMoveExistingFile(String fileName) throws GenericFileOperationFailedException {
274            // need to evaluate using a dummy and simulate the file first, to have access to all the file attributes
275            // create a dummy exchange as Exchange is needed for expression evaluation
276            // we support only the following 3 tokens.
277            Exchange dummy = endpoint.createExchange();
278            String parent = FileUtil.onlyPath(fileName);
279            String onlyName = FileUtil.stripPath(fileName);
280            dummy.getIn().setHeader(Exchange.FILE_NAME, fileName);
281            dummy.getIn().setHeader(Exchange.FILE_NAME_ONLY, onlyName);
282            dummy.getIn().setHeader(Exchange.FILE_PARENT, parent);
283    
284            String to = endpoint.getMoveExisting().evaluate(dummy, String.class);
285            // we must normalize it (to avoid having both \ and / in the name which confuses java.io.File)
286            to = FileUtil.normalizePath(to);
287            if (ObjectHelper.isEmpty(to)) {
288                throw new GenericFileOperationFailedException("moveExisting evaluated as empty String, cannot move existing file: " + fileName);
289            }
290    
291            // ensure any paths is created before we rename as the renamed file may be in a different path (which may be non exiting)
292            // use java.io.File to compute the file path
293            File toFile = new File(to);
294            String directory = toFile.getParent();
295            boolean absolute = FileUtil.isAbsolute(toFile);
296            if (directory != null) {
297                if (!buildDirectory(directory, absolute)) {
298                    LOG.debug("Cannot build directory [{}] (could be because of denied permissions)", directory);
299                }
300            }
301    
302            // deal if there already exists a file
303            if (existsFile(to)) {
304                if (endpoint.isEagerDeleteTargetFile()) {
305                    LOG.trace("Deleting existing file: {}", to);
306                    if (!deleteFile(to)) {
307                        throw new GenericFileOperationFailedException("Cannot delete file: " + to);
308                    }
309                } else {
310                    throw new GenericFileOperationFailedException("Cannot moved existing file from: " + fileName + " to: " + to + " as there already exists a file: " + to);
311                }
312            }
313    
314            LOG.trace("Moving existing file: {} to: {}", fileName, to);
315            if (!renameFile(fileName, to)) {
316                throw new GenericFileOperationFailedException("Cannot rename file from: " + fileName + " to: " + to);
317            }
318        }
319    
320        private void keepLastModified(Exchange exchange, File file) {
321            if (endpoint.isKeepLastModified()) {
322                Long last;
323                Date date = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Date.class);
324                if (date != null) {
325                    last = date.getTime();
326                } else {
327                    // fallback and try a long
328                    last = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Long.class);
329                }
330                if (last != null) {
331                    boolean result = file.setLastModified(last);
332                    if (LOG.isTraceEnabled()) {
333                        LOG.trace("Keeping last modified timestamp: {} on file: {} with result: {}", new Object[]{last, file, result});
334                    }
335                }
336            }
337        }
338    
339        private boolean writeFileByLocalWorkPath(File source, File file) throws IOException {
340            LOG.trace("Using local work file being renamed from: {} to: {}", source, file);
341            return FileUtil.renameFile(source, file, endpoint.isCopyAndDeleteOnRenameFail());
342        }
343    
344        private void writeFileByFile(File source, File target) throws IOException {
345            FileChannel in = new FileInputStream(source).getChannel();
346            FileChannel out = null;
347            try {
348                out = prepareOutputFileChannel(target);
349                LOG.debug("Using FileChannel to write file: {}", target);
350                long size = in.size();
351                long position = 0;
352                while (position < size) {
353                    position += in.transferTo(position, endpoint.getBufferSize(), out);
354                }
355            } finally {
356                IOHelper.close(in, source.getName(), LOG);
357                IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites());
358            }
359        }
360    
361        private void writeFileByStream(InputStream in, File target) throws IOException {
362            FileChannel out = null;
363            try {
364                out = prepareOutputFileChannel(target);
365                LOG.debug("Using InputStream to write file: {}", target);
366                int size = endpoint.getBufferSize();
367                byte[] buffer = new byte[size];
368                ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
369                int bytesRead;
370                while ((bytesRead = in.read(buffer)) != -1) {
371                    if (bytesRead < size) {
372                        byteBuffer.limit(bytesRead);
373                    }
374                    out.write(byteBuffer);
375                    byteBuffer.clear();
376                }
377            } finally {
378                IOHelper.close(in, target.getName(), LOG);
379                IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites());
380            }
381        }
382    
383        private void writeFileByReaderWithCharset(Reader in, File target, String charset) throws IOException {
384            boolean append = endpoint.getFileExist() == GenericFileExist.Append;
385            FileOutputStream os = new FileOutputStream(target, append);
386            Writer out = IOConverter.toWriter(os, charset);
387            try {
388                LOG.debug("Using Reader to write file: {} with charset: {}", target, charset);
389                int size = endpoint.getBufferSize();
390                IOHelper.copy(in, out, size);
391            } finally {
392                IOHelper.close(in, target.getName(), LOG);
393                IOHelper.close(out, os, target.getName(), LOG, endpoint.isForceWrites());
394            }
395        }
396    
397        /**
398         * Creates a new file if the file doesn't exist.
399         * If the endpoint's existing file logic is set to 'Override' then the target file will be truncated
400         */
401        private void writeFileEmptyBody(File target) throws IOException {
402            if (!target.exists()) {
403                LOG.debug("Creating new empty file: {}", target);
404                FileUtil.createNewFile(target);
405            } else if (endpoint.getFileExist() == GenericFileExist.Override) {
406                LOG.debug("Truncating existing file: {}", target);
407                FileChannel out = new FileOutputStream(target).getChannel();
408                try {
409                    out.truncate(0);
410                } finally {
411                    IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites());
412                }
413            }
414        }
415    
416        /**
417         * Creates and prepares the output file channel. Will position itself in correct position if the file is writable
418         * eg. it should append or override any existing content.
419         */
420        private FileChannel prepareOutputFileChannel(File target) throws IOException {
421            if (endpoint.getFileExist() == GenericFileExist.Append) {
422                FileChannel out = new RandomAccessFile(target, "rw").getChannel();
423                return out.position(out.size());
424            }
425            return new FileOutputStream(target).getChannel();
426        }
427    }