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