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.IOException;
021import java.io.InputStream;
022import java.io.InputStreamReader;
023import java.io.Reader;
024import java.io.Writer;
025import java.nio.ByteBuffer;
026import java.nio.channels.SeekableByteChannel;
027import java.nio.charset.Charset;
028import java.nio.file.Files;
029import java.nio.file.StandardCopyOption;
030import java.nio.file.StandardOpenOption;
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.util.FileUtil;
041import org.apache.camel.util.IOHelper;
042import org.apache.camel.util.ObjectHelper;
043import org.apache.camel.util.StringHelper;
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    protected boolean buildDirectory(File dir, Set<PosixFilePermission> permissions, boolean absolute) {
093        if (dir.exists()) {
094            return true;
095        }
096
097        if (permissions == null || permissions.isEmpty()) {
098            return dir.mkdirs();
099        }
100
101        // create directory one part of a time and set permissions
102        try {
103            String[] parts = dir.getPath().split("\\" + File.separatorChar);
104
105            File base;
106            // reusing absolute flag to handle relative and absolute paths
107            if (absolute) {
108                base = new File("");
109            } else {
110                base = new File(".");
111            }
112
113            for (String part : parts) {
114                File subDir = new File(base, part);
115                if (!subDir.exists()) {
116                    if (subDir.mkdir()) {
117                        if (LOG.isTraceEnabled()) {
118                            LOG.trace("Setting chmod: {} on directory: {} ", PosixFilePermissions.toString(permissions), subDir);
119                        }
120                        Files.setPosixFilePermissions(subDir.toPath(), permissions);
121                    } else {
122                        return false;
123                    }
124                }
125                base = new File(base, subDir.getName());
126            }
127        } catch (IOException e) {
128            throw new GenericFileOperationFailedException("Error setting chmod on directory: " + dir, e);
129        }
130
131        return true;
132    }
133
134    public boolean buildDirectory(String directory, boolean absolute) throws GenericFileOperationFailedException {
135        ObjectHelper.notNull(endpoint, "endpoint");
136
137        // always create endpoint defined directory
138        if (endpoint.isAutoCreate() && !endpoint.getFile().exists()) {
139            LOG.trace("Building starting directory: {}", endpoint.getFile());
140            buildDirectory(endpoint.getFile(), endpoint.getDirectoryPermissions(), absolute);
141        }
142
143        if (ObjectHelper.isEmpty(directory)) {
144            // no directory to build so return true to indicate ok
145            return true;
146        }
147
148        File endpointPath = endpoint.getFile();
149        File target = new File(directory);
150
151        File path;
152        if (absolute) {
153            // absolute path
154            path = target;
155        } else if (endpointPath.equals(target)) {
156            // its just the root of the endpoint path
157            path = endpointPath;
158        } else {
159            // relative after the endpoint path
160            String afterRoot = StringHelper.after(directory, endpointPath.getPath() + File.separator);
161            if (ObjectHelper.isNotEmpty(afterRoot)) {
162                // dir is under the root path
163                path = new File(endpoint.getFile(), afterRoot);
164            } else {
165                // dir is relative to the root path
166                path = new File(endpoint.getFile(), directory);
167            }
168        }
169
170        // We need to make sure that this is thread-safe and only one thread tries to create the path directory at the same time.
171        synchronized (this) {
172            if (path.isDirectory() && path.exists()) {
173                // the directory already exists
174                return true;
175            } else {
176                LOG.trace("Building directory: {}", path);
177                return buildDirectory(path, endpoint.getDirectoryPermissions(), absolute);
178            }
179        }
180    }
181
182    public List<File> listFiles() throws GenericFileOperationFailedException {
183        // noop
184        return null;
185    }
186
187    public List<File> listFiles(String path) throws GenericFileOperationFailedException {
188        // noop
189        return null;
190    }
191
192    public void changeCurrentDirectory(String path) throws GenericFileOperationFailedException {
193        // noop
194    }
195
196    public void changeToParentDirectory() throws GenericFileOperationFailedException {
197        // noop
198    }
199
200    public String getCurrentDirectory() throws GenericFileOperationFailedException {
201        // noop
202        return null;
203    }
204
205    public boolean retrieveFile(String name, Exchange exchange) throws GenericFileOperationFailedException {
206        // noop as we use type converters to read the body content for java.io.File
207        return true;
208    }
209    
210    @Override
211    public void releaseRetreivedFileResources(Exchange exchange) throws GenericFileOperationFailedException {
212        // noop as we used type converters to read the body content for java.io.File
213    }
214
215    public boolean storeFile(String fileName, Exchange exchange) throws GenericFileOperationFailedException {
216        ObjectHelper.notNull(endpoint, "endpoint");
217
218        File file = new File(fileName);
219
220        // if an existing file already exists what should we do?
221        if (file.exists()) {
222            if (endpoint.getFileExist() == GenericFileExist.Ignore) {
223                // ignore but indicate that the file was written
224                LOG.trace("An existing file already exists: {}. Ignore and do not override it.", file);
225                return true;
226            } else if (endpoint.getFileExist() == GenericFileExist.Fail) {
227                throw new GenericFileOperationFailedException("File already exist: " + file + ". Cannot write new file.");
228            } else if (endpoint.getFileExist() == GenericFileExist.Move) {
229                // move any existing file first
230                doMoveExistingFile(fileName);
231            }
232        }
233        
234        // Do an explicit test for a null body and decide what to do
235        if (exchange.getIn().getBody() == null) {
236            if (endpoint.isAllowNullBody()) {
237                LOG.trace("Writing empty file.");
238                try {
239                    writeFileEmptyBody(file);
240                    return true;
241                } catch (IOException e) {
242                    throw new GenericFileOperationFailedException("Cannot store file: " + file, e);
243                }
244            } else {
245                throw new GenericFileOperationFailedException("Cannot write null body to file: " + file);
246            }
247        }
248
249        // we can write the file by 3 different techniques
250        // 1. write file to file
251        // 2. rename a file from a local work path
252        // 3. write stream to file
253        try {
254
255            // is there an explicit charset configured we must write the file as
256            String charset = endpoint.getCharset();
257
258            // we can optimize and use file based if no charset must be used, and the input body is a file
259            File source = null;
260            boolean fileBased = false;
261            if (charset == null) {
262                // if no charset, then we can try using file directly (optimized)
263                Object body = exchange.getIn().getBody();
264                if (body instanceof WrappedFile) {
265                    body = ((WrappedFile<?>) body).getFile();
266                }
267                if (body instanceof File) {
268                    source = (File) body;
269                    fileBased = true;
270                }
271            }
272
273            if (fileBased) {
274                // okay we know the body is a file based
275
276                // so try to see if we can optimize by renaming the local work path file instead of doing
277                // a full file to file copy, as the local work copy is to be deleted afterwards anyway
278                // local work path
279                File local = exchange.getIn().getHeader(Exchange.FILE_LOCAL_WORK_PATH, File.class);
280                if (local != null && local.exists()) {
281                    boolean renamed = writeFileByLocalWorkPath(local, file);
282                    if (renamed) {
283                        // try to keep last modified timestamp if configured to do so
284                        keepLastModified(exchange, file);
285                        // set permissions if the chmod option was set
286                        if (ObjectHelper.isNotEmpty(endpoint.getChmod())) {
287                            Set<PosixFilePermission> permissions = endpoint.getPermissions();
288                            if (!permissions.isEmpty()) {
289                                if (LOG.isTraceEnabled()) {
290                                    LOG.trace("Setting chmod: {} on file: {} ", PosixFilePermissions.toString(permissions), file);
291                                }
292                                Files.setPosixFilePermissions(file.toPath(), permissions);
293                            }
294                        }
295                        // clear header as we have renamed the file
296                        exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, null);
297                        // return as the operation is complete, we just renamed the local work file
298                        // to the target.
299                        return true;
300                    }
301                } else if (source != null && source.exists()) {
302                    // no there is no local work file so use file to file copy if the source exists
303                    writeFileByFile(source, file);
304                    // try to keep last modified timestamp if configured to do so
305                    keepLastModified(exchange, file);
306                    // set permissions if the chmod option was set
307                    if (ObjectHelper.isNotEmpty(endpoint.getChmod())) {
308                        Set<PosixFilePermission> permissions = endpoint.getPermissions();
309                        if (!permissions.isEmpty()) {
310                            if (LOG.isTraceEnabled()) {
311                                LOG.trace("Setting chmod: {} on file: {} ", PosixFilePermissions.toString(permissions), file);
312                            }
313                            Files.setPosixFilePermissions(file.toPath(), permissions);
314                        }
315                    }
316                    return true;
317                }
318            }
319
320            if (charset != null) {
321                // charset configured so we must use a reader so we can write with encoding
322                Reader in = exchange.getContext().getTypeConverter().tryConvertTo(Reader.class, exchange, exchange.getIn().getBody());
323                if (in == null) {
324                    // okay no direct reader conversion, so use an input stream (which a lot can be converted as)
325                    InputStream is = exchange.getIn().getMandatoryBody(InputStream.class);
326                    in = new InputStreamReader(is);
327                }
328                // buffer the reader
329                in = IOHelper.buffered(in);
330                writeFileByReaderWithCharset(in, file, charset);
331            } else {
332                // fallback and use stream based
333                InputStream in = exchange.getIn().getMandatoryBody(InputStream.class);
334                writeFileByStream(in, file);
335            }
336
337            // try to keep last modified timestamp if configured to do so
338            keepLastModified(exchange, file);
339            // set permissions if the chmod option was set
340            if (ObjectHelper.isNotEmpty(endpoint.getChmod())) {
341                Set<PosixFilePermission> permissions = endpoint.getPermissions();
342                if (!permissions.isEmpty()) {
343                    if (LOG.isTraceEnabled()) {
344                        LOG.trace("Setting chmod: {} on file: {} ", PosixFilePermissions.toString(permissions), file);
345                    }
346                    Files.setPosixFilePermissions(file.toPath(), permissions);
347                }
348            }
349
350            return true;
351        } catch (IOException e) {
352            throw new GenericFileOperationFailedException("Cannot store file: " + file, e);
353        } catch (InvalidPayloadException e) {
354            throw new GenericFileOperationFailedException("Cannot store file: " + file, e);
355        }
356    }
357
358    /**
359     * Moves any existing file due fileExists=Move is in use.
360     */
361    private void doMoveExistingFile(String fileName) throws GenericFileOperationFailedException {
362        // need to evaluate using a dummy and simulate the file first, to have access to all the file attributes
363        // create a dummy exchange as Exchange is needed for expression evaluation
364        // we support only the following 3 tokens.
365        Exchange dummy = endpoint.createExchange();
366        String parent = FileUtil.onlyPath(fileName);
367        String onlyName = FileUtil.stripPath(fileName);
368        dummy.getIn().setHeader(Exchange.FILE_NAME, fileName);
369        dummy.getIn().setHeader(Exchange.FILE_NAME_ONLY, onlyName);
370        dummy.getIn().setHeader(Exchange.FILE_PARENT, parent);
371
372        String to = endpoint.getMoveExisting().evaluate(dummy, String.class);
373        // we must normalize it (to avoid having both \ and / in the name which confuses java.io.File)
374        to = FileUtil.normalizePath(to);
375        if (ObjectHelper.isEmpty(to)) {
376            throw new GenericFileOperationFailedException("moveExisting evaluated as empty String, cannot move existing file: " + fileName);
377        }
378
379        // ensure any paths is created before we rename as the renamed file may be in a different path (which may be non exiting)
380        // use java.io.File to compute the file path
381        File toFile = new File(to);
382        String directory = toFile.getParent();
383        boolean absolute = FileUtil.isAbsolute(toFile);
384        if (directory != null) {
385            if (!buildDirectory(directory, absolute)) {
386                LOG.debug("Cannot build directory [{}] (could be because of denied permissions)", directory);
387            }
388        }
389
390        // deal if there already exists a file
391        if (existsFile(to)) {
392            if (endpoint.isEagerDeleteTargetFile()) {
393                LOG.trace("Deleting existing file: {}", to);
394                if (!deleteFile(to)) {
395                    throw new GenericFileOperationFailedException("Cannot delete file: " + to);
396                }
397            } else {
398                throw new GenericFileOperationFailedException("Cannot moved existing file from: " + fileName + " to: " + to + " as there already exists a file: " + to);
399            }
400        }
401
402        LOG.trace("Moving existing file: {} to: {}", fileName, to);
403        if (!renameFile(fileName, to)) {
404            throw new GenericFileOperationFailedException("Cannot rename file from: " + fileName + " to: " + to);
405        }
406    }
407
408    private void keepLastModified(Exchange exchange, File file) {
409        if (endpoint.isKeepLastModified()) {
410            Long last;
411            Date date = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Date.class);
412            if (date != null) {
413                last = date.getTime();
414            } else {
415                // fallback and try a long
416                last = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Long.class);
417            }
418            if (last != null) {
419                boolean result = file.setLastModified(last);
420                if (LOG.isTraceEnabled()) {
421                    LOG.trace("Keeping last modified timestamp: {} on file: {} with result: {}", new Object[]{last, file, result});
422                }
423            }
424        }
425    }
426
427    private boolean writeFileByLocalWorkPath(File source, File file) throws IOException {
428        LOG.trace("Using local work file being renamed from: {} to: {}", source, file);
429        return FileUtil.renameFile(source, file, endpoint.isCopyAndDeleteOnRenameFail());
430    }
431
432    private void writeFileByFile(File source, File target) throws IOException {
433        Files.copy(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING);
434    }
435
436    private void writeFileByStream(InputStream in, File target) throws IOException {
437        try (SeekableByteChannel out = prepareOutputFileChannel(target)) {
438            
439            LOG.debug("Using InputStream to write file: {}", target);
440            int size = endpoint.getBufferSize();
441            byte[] buffer = new byte[size];
442            ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
443            int bytesRead;
444            while ((bytesRead = in.read(buffer)) != -1) {
445                if (bytesRead < size) {
446                    byteBuffer.limit(bytesRead);
447                }
448                out.write(byteBuffer);
449                byteBuffer.clear();
450            }
451        } finally {
452            IOHelper.close(in, target.getName(), LOG);
453        }
454    }
455
456    private void writeFileByReaderWithCharset(Reader in, File target, String charset) throws IOException {
457        boolean append = endpoint.getFileExist() == GenericFileExist.Append;
458        try (Writer out = Files.newBufferedWriter(target.toPath(), Charset.forName(charset), 
459                                                  StandardOpenOption.WRITE,
460                                                  append ? StandardOpenOption.APPEND : StandardOpenOption.TRUNCATE_EXISTING, 
461                                                  StandardOpenOption.CREATE)) {
462            LOG.debug("Using Reader to write file: {} with charset: {}", target, charset);
463            int size = endpoint.getBufferSize();
464            IOHelper.copy(in, out, size);
465        } finally {
466            IOHelper.close(in, target.getName(), LOG);
467        }
468    }
469
470    /**
471     * Creates a new file if the file doesn't exist.
472     * If the endpoint's existing file logic is set to 'Override' then the target file will be truncated
473     */
474    private void writeFileEmptyBody(File target) throws IOException {
475        if (!target.exists()) {
476            LOG.debug("Creating new empty file: {}", target);
477            FileUtil.createNewFile(target);
478        } else if (endpoint.getFileExist() == GenericFileExist.Override) {
479            LOG.debug("Truncating existing file: {}", target);
480            try (SeekableByteChannel out = Files.newByteChannel(target.toPath(), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
481                //nothing to write
482            }
483        }
484    }
485
486    /**
487     * Creates and prepares the output file channel. Will position itself in correct position if the file is writable
488     * eg. it should append or override any existing content.
489     */
490    private SeekableByteChannel prepareOutputFileChannel(File target) throws IOException {
491        if (endpoint.getFileExist() == GenericFileExist.Append) {
492            SeekableByteChannel out = Files.newByteChannel(target.toPath(), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
493            return out.position(out.size());
494        }
495        return Files.newByteChannel(target.toPath(), StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
496    }
497}