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, long size) 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 releaseRetrievedFileResources(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, long size) 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                this.endpoint.getMoveExistingFileStrategy().moveExistingFile(endpoint, this, 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    private void keepLastModified(Exchange exchange, File file) {
359        if (endpoint.isKeepLastModified()) {
360            Long last;
361            Date date = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Date.class);
362            if (date != null) {
363                last = date.getTime();
364            } else {
365                // fallback and try a long
366                last = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Long.class);
367            }
368            if (last != null) {
369                boolean result = file.setLastModified(last);
370                if (LOG.isTraceEnabled()) {
371                    LOG.trace("Keeping last modified timestamp: {} on file: {} with result: {}", last, file, result);
372                }
373            }
374        }
375    }
376
377    private boolean writeFileByLocalWorkPath(File source, File file) throws IOException {
378        LOG.trace("Using local work file being renamed from: {} to: {}", source, file);
379        return FileUtil.renameFile(source, file, endpoint.isCopyAndDeleteOnRenameFail());
380    }
381
382    private void writeFileByFile(File source, File target) throws IOException {
383        Files.copy(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING);
384    }
385
386    private void writeFileByStream(InputStream in, File target) throws IOException {
387        try (SeekableByteChannel out = prepareOutputFileChannel(target)) {
388            
389            LOG.debug("Using InputStream to write file: {}", target);
390            int size = endpoint.getBufferSize();
391            byte[] buffer = new byte[size];
392            ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
393            int bytesRead;
394            while ((bytesRead = in.read(buffer)) != -1) {
395                if (bytesRead < size) {
396                    byteBuffer.limit(bytesRead);
397                }
398                out.write(byteBuffer);
399                byteBuffer.clear();
400            }
401        } finally {
402            IOHelper.close(in, target.getName(), LOG);
403        }
404    }
405
406    private void writeFileByReaderWithCharset(Reader in, File target, String charset) throws IOException {
407        boolean append = endpoint.getFileExist() == GenericFileExist.Append;
408        try (Writer out = Files.newBufferedWriter(target.toPath(), Charset.forName(charset), 
409                                                  StandardOpenOption.WRITE,
410                                                  append ? StandardOpenOption.APPEND : StandardOpenOption.TRUNCATE_EXISTING, 
411                                                  StandardOpenOption.CREATE)) {
412            LOG.debug("Using Reader to write file: {} with charset: {}", target, charset);
413            int size = endpoint.getBufferSize();
414            IOHelper.copy(in, out, size);
415        } finally {
416            IOHelper.close(in, target.getName(), LOG);
417        }
418    }
419
420    /**
421     * Creates a new file if the file doesn't exist.
422     * If the endpoint's existing file logic is set to 'Override' then the target file will be truncated
423     */
424    private void writeFileEmptyBody(File target) throws IOException {
425        if (!target.exists()) {
426            LOG.debug("Creating new empty file: {}", target);
427            FileUtil.createNewFile(target);
428        } else if (endpoint.getFileExist() == GenericFileExist.Override) {
429            LOG.debug("Truncating existing file: {}", target);
430            try (SeekableByteChannel out = Files.newByteChannel(target.toPath(), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
431                //nothing to write
432            }
433        }
434    }
435
436    /**
437     * Creates and prepares the output file channel. Will position itself in correct position if the file is writable
438     * eg. it should append or override any existing content.
439     */
440    private SeekableByteChannel prepareOutputFileChannel(File target) throws IOException {
441        if (endpoint.getFileExist() == GenericFileExist.Append) {
442            SeekableByteChannel out = Files.newByteChannel(target.toPath(), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
443            return out.position(out.size());
444        }
445        return Files.newByteChannel(target.toPath(), StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
446    }
447}