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