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                    fileBased = true;
227                }
228                if (body instanceof File) {
229                    source = (File) body;
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                        // clear header as we have renamed the file
246                        exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, null);
247                        // return as the operation is complete, we just renamed the local work file
248                        // to the target.
249                        return true;
250                    }
251                } else if (source != null && source.exists()) {
252                    // no there is no local work file so use file to file copy if the source exists
253                    writeFileByFile(source, file);
254                    // try to keep last modified timestamp if configured to do so
255                    keepLastModified(exchange, file);
256                    return true;
257                }
258            }
259
260            if (charset != null) {
261                // charset configured so we must use a reader so we can write with encoding
262                Reader in = exchange.getContext().getTypeConverter().tryConvertTo(Reader.class, exchange, exchange.getIn().getBody());
263                if (in == null) {
264                    // okay no direct reader conversion, so use an input stream (which a lot can be converted as)
265                    InputStream is = exchange.getIn().getMandatoryBody(InputStream.class);
266                    in = new InputStreamReader(is);
267                }
268                // buffer the reader
269                in = IOHelper.buffered(in);
270                writeFileByReaderWithCharset(in, file, charset);
271            } else {
272                // fallback and use stream based
273                InputStream in = exchange.getIn().getMandatoryBody(InputStream.class);
274                writeFileByStream(in, file);
275            }
276            // try to keep last modified timestamp if configured to do so
277            keepLastModified(exchange, file);
278
279            // set permissions if the chmod option was set
280            if (ObjectHelper.isNotEmpty(endpoint.getChmod())) {
281                Set<PosixFilePermission> permissions = endpoint.getPermissions();
282                if (!permissions.isEmpty()) {
283                    Files.setPosixFilePermissions(file.toPath(), permissions);
284                    LOG.trace("Setting chmod: {} on file: {} ", PosixFilePermissions.toString(permissions), file);
285                }
286            }
287
288            return true;
289        } catch (IOException e) {
290            throw new GenericFileOperationFailedException("Cannot store file: " + file, e);
291        } catch (InvalidPayloadException e) {
292            throw new GenericFileOperationFailedException("Cannot store file: " + file, e);
293        }
294    }
295
296    /**
297     * Moves any existing file due fileExists=Move is in use.
298     */
299    private void doMoveExistingFile(String fileName) throws GenericFileOperationFailedException {
300        // need to evaluate using a dummy and simulate the file first, to have access to all the file attributes
301        // create a dummy exchange as Exchange is needed for expression evaluation
302        // we support only the following 3 tokens.
303        Exchange dummy = endpoint.createExchange();
304        String parent = FileUtil.onlyPath(fileName);
305        String onlyName = FileUtil.stripPath(fileName);
306        dummy.getIn().setHeader(Exchange.FILE_NAME, fileName);
307        dummy.getIn().setHeader(Exchange.FILE_NAME_ONLY, onlyName);
308        dummy.getIn().setHeader(Exchange.FILE_PARENT, parent);
309
310        String to = endpoint.getMoveExisting().evaluate(dummy, String.class);
311        // we must normalize it (to avoid having both \ and / in the name which confuses java.io.File)
312        to = FileUtil.normalizePath(to);
313        if (ObjectHelper.isEmpty(to)) {
314            throw new GenericFileOperationFailedException("moveExisting evaluated as empty String, cannot move existing file: " + fileName);
315        }
316
317        // ensure any paths is created before we rename as the renamed file may be in a different path (which may be non exiting)
318        // use java.io.File to compute the file path
319        File toFile = new File(to);
320        String directory = toFile.getParent();
321        boolean absolute = FileUtil.isAbsolute(toFile);
322        if (directory != null) {
323            if (!buildDirectory(directory, absolute)) {
324                LOG.debug("Cannot build directory [{}] (could be because of denied permissions)", directory);
325            }
326        }
327
328        // deal if there already exists a file
329        if (existsFile(to)) {
330            if (endpoint.isEagerDeleteTargetFile()) {
331                LOG.trace("Deleting existing file: {}", to);
332                if (!deleteFile(to)) {
333                    throw new GenericFileOperationFailedException("Cannot delete file: " + to);
334                }
335            } else {
336                throw new GenericFileOperationFailedException("Cannot moved existing file from: " + fileName + " to: " + to + " as there already exists a file: " + to);
337            }
338        }
339
340        LOG.trace("Moving existing file: {} to: {}", fileName, to);
341        if (!renameFile(fileName, to)) {
342            throw new GenericFileOperationFailedException("Cannot rename file from: " + fileName + " to: " + to);
343        }
344    }
345
346    private void keepLastModified(Exchange exchange, File file) {
347        if (endpoint.isKeepLastModified()) {
348            Long last;
349            Date date = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Date.class);
350            if (date != null) {
351                last = date.getTime();
352            } else {
353                // fallback and try a long
354                last = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Long.class);
355            }
356            if (last != null) {
357                boolean result = file.setLastModified(last);
358                if (LOG.isTraceEnabled()) {
359                    LOG.trace("Keeping last modified timestamp: {} on file: {} with result: {}", new Object[]{last, file, result});
360                }
361            }
362        }
363    }
364
365    private boolean writeFileByLocalWorkPath(File source, File file) throws IOException {
366        LOG.trace("Using local work file being renamed from: {} to: {}", source, file);
367        return FileUtil.renameFile(source, file, endpoint.isCopyAndDeleteOnRenameFail());
368    }
369
370    private void writeFileByFile(File source, File target) throws IOException {
371        FileChannel in = new FileInputStream(source).getChannel();
372        FileChannel out = null;
373        try {
374            out = prepareOutputFileChannel(target);
375            LOG.debug("Using FileChannel to write file: {}", target);
376            long size = in.size();
377            long position = 0;
378            while (position < size) {
379                position += in.transferTo(position, endpoint.getBufferSize(), out);
380            }
381        } finally {
382            IOHelper.close(in, source.getName(), LOG);
383            IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites());
384        }
385    }
386
387    private void writeFileByStream(InputStream in, File target) throws IOException {
388        FileChannel out = null;
389        try {
390            out = prepareOutputFileChannel(target);
391            LOG.debug("Using InputStream to write file: {}", target);
392            int size = endpoint.getBufferSize();
393            byte[] buffer = new byte[size];
394            ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
395            int bytesRead;
396            while ((bytesRead = in.read(buffer)) != -1) {
397                if (bytesRead < size) {
398                    byteBuffer.limit(bytesRead);
399                }
400                out.write(byteBuffer);
401                byteBuffer.clear();
402            }
403        } finally {
404            IOHelper.close(in, target.getName(), LOG);
405            IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites());
406        }
407    }
408
409    private void writeFileByReaderWithCharset(Reader in, File target, String charset) throws IOException {
410        boolean append = endpoint.getFileExist() == GenericFileExist.Append;
411        FileOutputStream os = new FileOutputStream(target, append);
412        Writer out = IOConverter.toWriter(os, charset);
413        try {
414            LOG.debug("Using Reader to write file: {} with charset: {}", target, charset);
415            int size = endpoint.getBufferSize();
416            IOHelper.copy(in, out, size);
417        } finally {
418            IOHelper.close(in, target.getName(), LOG);
419            IOHelper.close(out, os, target.getName(), LOG, endpoint.isForceWrites());
420        }
421    }
422
423    /**
424     * Creates a new file if the file doesn't exist.
425     * If the endpoint's existing file logic is set to 'Override' then the target file will be truncated
426     */
427    private void writeFileEmptyBody(File target) throws IOException {
428        if (!target.exists()) {
429            LOG.debug("Creating new empty file: {}", target);
430            FileUtil.createNewFile(target);
431        } else if (endpoint.getFileExist() == GenericFileExist.Override) {
432            LOG.debug("Truncating existing file: {}", target);
433            FileChannel out = new FileOutputStream(target).getChannel();
434            try {
435                out.truncate(0);
436            } finally {
437                IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites());
438            }
439        }
440    }
441
442    /**
443     * Creates and prepares the output file channel. Will position itself in correct position if the file is writable
444     * eg. it should append or override any existing content.
445     */
446    private FileChannel prepareOutputFileChannel(File target) throws IOException {
447        if (endpoint.getFileExist() == GenericFileExist.Append) {
448            FileChannel out = new RandomAccessFile(target, "rw").getChannel();
449            return out.position(out.size());
450        }
451        return new FileOutputStream(target).getChannel();
452    }
453}