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        // check if directory is a path
152        boolean isPath = directory.contains("/") || directory.contains("\\");
153
154        File path;
155        if (absolute) {
156            // absolute path
157            path = target;
158        } else if (endpointPath.equals(target)) {
159            // its just the root of the endpoint path
160            path = endpointPath;
161        } else if (isPath) {
162            // relative after the endpoint path
163            String afterRoot = StringHelper.after(directory, endpointPath.getPath() + File.separator);
164            if (ObjectHelper.isNotEmpty(afterRoot)) {
165                // dir is under the root path
166                path = new File(endpoint.getFile(), afterRoot);
167            } else {
168                // dir path is relative to the root path
169                path = new File(directory);
170            }
171        } else {
172            // dir is a child of the root path
173            path = new File(endpoint.getFile(), directory);
174        }
175
176        // We need to make sure that this is thread-safe and only one thread tries to create the path directory at the same time.
177        synchronized (this) {
178            if (path.isDirectory() && path.exists()) {
179                // the directory already exists
180                return true;
181            } else {
182                LOG.trace("Building directory: {}", path);
183                return buildDirectory(path, endpoint.getDirectoryPermissions(), absolute);
184            }
185        }
186    }
187
188    public List<File> listFiles() throws GenericFileOperationFailedException {
189        // noop
190        return null;
191    }
192
193    public List<File> listFiles(String path) throws GenericFileOperationFailedException {
194        // noop
195        return null;
196    }
197
198    public void changeCurrentDirectory(String path) throws GenericFileOperationFailedException {
199        // noop
200    }
201
202    public void changeToParentDirectory() throws GenericFileOperationFailedException {
203        // noop
204    }
205
206    public String getCurrentDirectory() throws GenericFileOperationFailedException {
207        // noop
208        return null;
209    }
210
211    public boolean retrieveFile(String name, Exchange exchange, long size) throws GenericFileOperationFailedException {
212        // noop as we use type converters to read the body content for java.io.File
213        return true;
214    }
215    
216    @Override
217    public void releaseRetrievedFileResources(Exchange exchange) throws GenericFileOperationFailedException {
218        // noop as we used type converters to read the body content for java.io.File
219    }
220
221    public boolean storeFile(String fileName, Exchange exchange, long size) throws GenericFileOperationFailedException {
222        ObjectHelper.notNull(endpoint, "endpoint");
223
224        File file = new File(fileName);
225
226        // if an existing file already exists what should we do?
227        if (file.exists()) {
228            if (endpoint.getFileExist() == GenericFileExist.Ignore) {
229                // ignore but indicate that the file was written
230                LOG.trace("An existing file already exists: {}. Ignore and do not override it.", file);
231                return true;
232            } else if (endpoint.getFileExist() == GenericFileExist.Fail) {
233                throw new GenericFileOperationFailedException("File already exist: " + file + ". Cannot write new file.");
234            } else if (endpoint.getFileExist() == GenericFileExist.Move) {
235                // move any existing file first
236                this.endpoint.getMoveExistingFileStrategy().moveExistingFile(endpoint, this, fileName);
237            }
238        }
239        
240        // Do an explicit test for a null body and decide what to do
241        if (exchange.getIn().getBody() == null) {
242            if (endpoint.isAllowNullBody()) {
243                LOG.trace("Writing empty file.");
244                try {
245                    writeFileEmptyBody(file);
246                    return true;
247                } catch (IOException e) {
248                    throw new GenericFileOperationFailedException("Cannot store file: " + file, e);
249                }
250            } else {
251                throw new GenericFileOperationFailedException("Cannot write null body to file: " + file);
252            }
253        }
254
255        // we can write the file by 3 different techniques
256        // 1. write file to file
257        // 2. rename a file from a local work path
258        // 3. write stream to file
259        try {
260
261            // is there an explicit charset configured we must write the file as
262            String charset = endpoint.getCharset();
263
264            // we can optimize and use file based if no charset must be used, and the input body is a file
265            // however optimization cannot be applied when content should be appended to target file
266            File source = null;
267            boolean fileBased = false;
268            if (charset == null && endpoint.getFileExist() != GenericFileExist.Append) {
269                // if no charset and not in appending mode, then we can try using file directly (optimized)
270                Object body = exchange.getIn().getBody();
271                if (body instanceof WrappedFile) {
272                    body = ((WrappedFile<?>) body).getFile();
273                }
274                if (body instanceof File) {
275                    source = (File) body;
276                    fileBased = true;
277                }
278            }
279
280            if (fileBased) {
281                // okay we know the body is a file based
282
283                // so try to see if we can optimize by renaming the local work path file instead of doing
284                // a full file to file copy, as the local work copy is to be deleted afterwards anyway
285                // local work path
286                File local = exchange.getIn().getHeader(Exchange.FILE_LOCAL_WORK_PATH, File.class);
287                if (local != null && local.exists()) {
288                    boolean renamed = writeFileByLocalWorkPath(local, file);
289                    if (renamed) {
290                        // try to keep last modified timestamp if configured to do so
291                        keepLastModified(exchange, file);
292                        // set permissions if the chmod option was set
293                        if (ObjectHelper.isNotEmpty(endpoint.getChmod())) {
294                            Set<PosixFilePermission> permissions = endpoint.getPermissions();
295                            if (!permissions.isEmpty()) {
296                                if (LOG.isTraceEnabled()) {
297                                    LOG.trace("Setting chmod: {} on file: {}", PosixFilePermissions.toString(permissions), file);
298                                }
299                                Files.setPosixFilePermissions(file.toPath(), permissions);
300                            }
301                        }
302                        // clear header as we have renamed the file
303                        exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, null);
304                        // return as the operation is complete, we just renamed the local work file
305                        // to the target.
306                        return true;
307                    }
308                } else if (source != null && source.exists()) {
309                    // no there is no local work file so use file to file copy if the source exists
310                    writeFileByFile(source, file);
311                    // try to keep last modified timestamp if configured to do so
312                    keepLastModified(exchange, file);
313                    // set permissions if the chmod option was set
314                    if (ObjectHelper.isNotEmpty(endpoint.getChmod())) {
315                        Set<PosixFilePermission> permissions = endpoint.getPermissions();
316                        if (!permissions.isEmpty()) {
317                            if (LOG.isTraceEnabled()) {
318                                LOG.trace("Setting chmod: {} on file: {}", PosixFilePermissions.toString(permissions), file);
319                            }
320                            Files.setPosixFilePermissions(file.toPath(), permissions);
321                        }
322                    }
323                    return true;
324                }
325            }
326
327            if (charset != null) {
328                // charset configured so we must use a reader so we can write with encoding
329                Reader in = exchange.getContext().getTypeConverter().tryConvertTo(Reader.class, exchange, exchange.getIn().getBody());
330                if (in == null) {
331                    // okay no direct reader conversion, so use an input stream (which a lot can be converted as)
332                    InputStream is = exchange.getIn().getMandatoryBody(InputStream.class);
333                    in = new InputStreamReader(is);
334                }
335                // buffer the reader
336                in = IOHelper.buffered(in);
337                writeFileByReaderWithCharset(in, file, charset);
338            } else {
339                // fallback and use stream based
340                InputStream in = exchange.getIn().getMandatoryBody(InputStream.class);
341                writeFileByStream(in, file);
342            }
343
344            // try to keep last modified timestamp if configured to do so
345            keepLastModified(exchange, file);
346            // set permissions if the chmod option was set
347            if (ObjectHelper.isNotEmpty(endpoint.getChmod())) {
348                Set<PosixFilePermission> permissions = endpoint.getPermissions();
349                if (!permissions.isEmpty()) {
350                    if (LOG.isTraceEnabled()) {
351                        LOG.trace("Setting chmod: {} on file: {}", PosixFilePermissions.toString(permissions), file);
352                    }
353                    Files.setPosixFilePermissions(file.toPath(), permissions);
354                }
355            }
356
357            return true;
358        } catch (IOException e) {
359            throw new GenericFileOperationFailedException("Cannot store file: " + file, e);
360        } catch (InvalidPayloadException e) {
361            throw new GenericFileOperationFailedException("Cannot store file: " + file, e);
362        }
363    }
364    
365    private void keepLastModified(Exchange exchange, File file) {
366        if (endpoint.isKeepLastModified()) {
367            Long last;
368            Date date = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Date.class);
369            if (date != null) {
370                last = date.getTime();
371            } else {
372                // fallback and try a long
373                last = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Long.class);
374            }
375            if (last != null) {
376                boolean result = file.setLastModified(last);
377                if (LOG.isTraceEnabled()) {
378                    LOG.trace("Keeping last modified timestamp: {} on file: {} with result: {}", last, file, result);
379                }
380            }
381        }
382    }
383
384    private boolean writeFileByLocalWorkPath(File source, File file) throws IOException {
385        LOG.trace("Using local work file being renamed from: {} to: {}", source, file);
386        return FileUtil.renameFile(source, file, endpoint.isCopyAndDeleteOnRenameFail());
387    }
388
389    private void writeFileByFile(File source, File target) throws IOException {
390        Files.copy(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING);
391    }
392
393    private void writeFileByStream(InputStream in, File target) throws IOException {
394        try (SeekableByteChannel out = prepareOutputFileChannel(target)) {
395            
396            LOG.debug("Using InputStream to write file: {}", target);
397            int size = endpoint.getBufferSize();
398            byte[] buffer = new byte[size];
399            ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
400            int bytesRead;
401            while ((bytesRead = in.read(buffer)) != -1) {
402                if (bytesRead < size) {
403                    byteBuffer.limit(bytesRead);
404                }
405                out.write(byteBuffer);
406                byteBuffer.clear();
407            }
408        } finally {
409            IOHelper.close(in, target.getName(), LOG);
410        }
411    }
412
413    private void writeFileByReaderWithCharset(Reader in, File target, String charset) throws IOException {
414        boolean append = endpoint.getFileExist() == GenericFileExist.Append;
415        try (Writer out = Files.newBufferedWriter(target.toPath(), Charset.forName(charset), 
416                                                  StandardOpenOption.WRITE,
417                                                  append ? StandardOpenOption.APPEND : StandardOpenOption.TRUNCATE_EXISTING, 
418                                                  StandardOpenOption.CREATE)) {
419            LOG.debug("Using Reader to write file: {} with charset: {}", target, charset);
420            int size = endpoint.getBufferSize();
421            IOHelper.copy(in, out, size);
422        } finally {
423            IOHelper.close(in, target.getName(), LOG);
424        }
425    }
426
427    /**
428     * Creates a new file if the file doesn't exist.
429     * If the endpoint's existing file logic is set to 'Override' then the target file will be truncated
430     */
431    private void writeFileEmptyBody(File target) throws IOException {
432        if (!target.exists()) {
433            LOG.debug("Creating new empty file: {}", target);
434            FileUtil.createNewFile(target);
435        } else if (endpoint.getFileExist() == GenericFileExist.Override) {
436            LOG.debug("Truncating existing file: {}", target);
437            try (SeekableByteChannel out = Files.newByteChannel(target.toPath(), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
438                //nothing to write
439            }
440        }
441    }
442
443    /**
444     * Creates and prepares the output file channel. Will position itself in correct position if the file is writable
445     * eg. it should append or override any existing content.
446     */
447    private SeekableByteChannel prepareOutputFileChannel(File target) throws IOException {
448        if (endpoint.getFileExist() == GenericFileExist.Append) {
449            SeekableByteChannel out = Files.newByteChannel(target.toPath(), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
450            return out.position(out.size());
451        }
452        return Files.newByteChannel(target.toPath(), StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
453    }
454}