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