/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.component.file;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.io.Reader;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Date;
import java.util.List;
import java.util.Set;
import org.apache.camel.Exchange;
import org.apache.camel.InvalidPayloadException;
import org.apache.camel.WrappedFile;
import org.apache.camel.converter.IOConverter;
import org.apache.camel.util.FileUtil;
import org.apache.camel.util.IOHelper;
import org.apache.camel.util.ObjectHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* File operations for {@link java.io.File}.
*/
public class FileOperations implements GenericFileOperations<File> {
private static final Logger LOG = LoggerFactory.getLogger(FileOperations.class);
private FileEndpoint endpoint;
public FileOperations() {
}
public FileOperations(FileEndpoint endpoint) {
this.endpoint = endpoint;
}
public void setEndpoint(GenericFileEndpoint<File> endpoint) {
this.endpoint = (FileEndpoint) endpoint;
}
public boolean deleteFile(String name) throws GenericFileOperationFailedException {
File file = new File(name);
return FileUtil.deleteFile(file);
}
public boolean renameFile(String from, String to) throws GenericFileOperationFailedException {
boolean renamed = false;
File file = new File(from);
File target = new File(to);
try {
if (endpoint.isRenameUsingCopy()) {
renamed = FileUtil.renameFileUsingCopy(file, target);
} else {
renamed = FileUtil.renameFile(file, target, endpoint.isCopyAndDeleteOnRenameFail());
}
} catch (IOException e) {
throw new GenericFileOperationFailedException("Error renaming file from " + from + " to " + to, e);
}
return renamed;
}
public boolean existsFile(String name) throws GenericFileOperationFailedException {
File file = new File(name);
return file.exists();
}
public boolean buildDirectory(String directory, boolean absolute) throws GenericFileOperationFailedException {
ObjectHelper.notNull(endpoint, "endpoint");
// always create endpoint defined directory
if (endpoint.isAutoCreate() && !endpoint.getFile().exists()) {
LOG.trace("Building starting directory: {}", endpoint.getFile());
endpoint.getFile().mkdirs();
}
if (ObjectHelper.isEmpty(directory)) {
// no directory to build so return true to indicate ok
return true;
}
File endpointPath = endpoint.getFile();
File target = new File(directory);
File path;
if (absolute) {
// absolute path
path = target;
} else if (endpointPath.equals(target)) {
// its just the root of the endpoint path
path = endpointPath;
} else {
// relative after the endpoint path
String afterRoot = ObjectHelper.after(directory, endpointPath.getPath() + File.separator);
if (ObjectHelper.isNotEmpty(afterRoot)) {
// dir is under the root path
path = new File(endpoint.getFile(), afterRoot);
} else {
// dir is relative to the root path
path = new File(endpoint.getFile(), directory);
}
}
// We need to make sure that this is thread-safe and only one thread tries to create the path directory at the same time.
synchronized (this) {
if (path.isDirectory() && path.exists()) {
// the directory already exists
return true;
} else {
if (LOG.isTraceEnabled()) {
LOG.trace("Building directory: {}", path);
}
return path.mkdirs();
}
}
}
public List<File> listFiles() throws GenericFileOperationFailedException {
// noop
return null;
}
public List<File> listFiles(String path) throws GenericFileOperationFailedException {
// noop
return null;
}
public void changeCurrentDirectory(String path) throws GenericFileOperationFailedException {
// noop
}
public void changeToParentDirectory() throws GenericFileOperationFailedException {
// noop
}
public String getCurrentDirectory() throws GenericFileOperationFailedException {
// noop
return null;
}
public boolean retrieveFile(String name, Exchange exchange) throws GenericFileOperationFailedException {
// noop as we use type converters to read the body content for java.io.File
return true;
}
@Override
public void releaseRetreivedFileResources(Exchange exchange) throws GenericFileOperationFailedException {
// noop as we used type converters to read the body content for java.io.File
}
public boolean storeFile(String fileName, Exchange exchange) throws GenericFileOperationFailedException {
ObjectHelper.notNull(endpoint, "endpoint");
File file = new File(fileName);
// if an existing file already exists what should we do?
if (file.exists()) {
if (endpoint.getFileExist() == GenericFileExist.Ignore) {
// ignore but indicate that the file was written
LOG.trace("An existing file already exists: {}. Ignore and do not override it.", file);
return true;
} else if (endpoint.getFileExist() == GenericFileExist.Fail) {
throw new GenericFileOperationFailedException("File already exist: " + file + ". Cannot write new file.");
} else if (endpoint.getFileExist() == GenericFileExist.Move) {
// move any existing file first
doMoveExistingFile(fileName);
}
}
// Do an explicit test for a null body and decide what to do
if (exchange.getIn().getBody() == null) {
if (endpoint.isAllowNullBody()) {
LOG.trace("Writing empty file.");
try {
writeFileEmptyBody(file);
return true;
} catch (IOException e) {
throw new GenericFileOperationFailedException("Cannot store file: " + file, e);
}
} else {
throw new GenericFileOperationFailedException("Cannot write null body to file: " + file);
}
}
// we can write the file by 3 different techniques
// 1. write file to file
// 2. rename a file from a local work path
// 3. write stream to file
try {
// is there an explicit charset configured we must write the file as
String charset = endpoint.getCharset();
// we can optimize and use file based if no charset must be used, and the input body is a file
File source = null;
boolean fileBased = false;
if (charset == null) {
// if no charset, then we can try using file directly (optimized)
Object body = exchange.getIn().getBody();
if (body instanceof WrappedFile) {
body = ((WrappedFile<?>) body).getFile();
fileBased = true;
}
if (body instanceof File) {
source = (File) body;
}
}
if (fileBased) {
// okay we know the body is a file based
// so try to see if we can optimize by renaming the local work path file instead of doing
// a full file to file copy, as the local work copy is to be deleted afterwards anyway
// local work path
File local = exchange.getIn().getHeader(Exchange.FILE_LOCAL_WORK_PATH, File.class);
if (local != null && local.exists()) {
boolean renamed = writeFileByLocalWorkPath(local, file);
if (renamed) {
// try to keep last modified timestamp if configured to do so
keepLastModified(exchange, file);
// clear header as we have renamed the file
exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, null);
// return as the operation is complete, we just renamed the local work file
// to the target.
return true;
}
} else if (source != null && source.exists()) {
// no there is no local work file so use file to file copy if the source exists
writeFileByFile(source, file);
// try to keep last modified timestamp if configured to do so
keepLastModified(exchange, file);
return true;
}
}
if (charset != null) {
// charset configured so we must use a reader so we can write with encoding
Reader in = exchange.getIn().getBody(Reader.class);
if (in == null) {
// okay no direct reader conversion, so use an input stream (which a lot can be converted as)
InputStream is = exchange.getIn().getMandatoryBody(InputStream.class);
in = new InputStreamReader(is);
}
// buffer the reader
in = IOHelper.buffered(in);
writeFileByReaderWithCharset(in, file, charset);
} else {
// fallback and use stream based
InputStream in = exchange.getIn().getMandatoryBody(InputStream.class);
writeFileByStream(in, file);
}
// try to keep last modified timestamp if configured to do so
keepLastModified(exchange, file);
// set permissions if the chmod option was set
if (ObjectHelper.isNotEmpty(endpoint.getChmod())) {
Set<PosixFilePermission> permissions = endpoint.getPermissions();
if (!permissions.isEmpty()) {
Files.setPosixFilePermissions(file.toPath(), permissions);
LOG.trace("Setting chmod: {} on file: {} ", PosixFilePermissions.toString(permissions), file);
}
}
return true;
} catch (IOException e) {
throw new GenericFileOperationFailedException("Cannot store file: " + file, e);
} catch (InvalidPayloadException e) {
throw new GenericFileOperationFailedException("Cannot store file: " + file, e);
}
}
/**
* Moves any existing file due fileExists=Move is in use.
*/
private void doMoveExistingFile(String fileName) throws GenericFileOperationFailedException {
// need to evaluate using a dummy and simulate the file first, to have access to all the file attributes
// create a dummy exchange as Exchange is needed for expression evaluation
// we support only the following 3 tokens.
Exchange dummy = endpoint.createExchange();
String parent = FileUtil.onlyPath(fileName);
String onlyName = FileUtil.stripPath(fileName);
dummy.getIn().setHeader(Exchange.FILE_NAME, fileName);
dummy.getIn().setHeader(Exchange.FILE_NAME_ONLY, onlyName);
dummy.getIn().setHeader(Exchange.FILE_PARENT, parent);
String to = endpoint.getMoveExisting().evaluate(dummy, String.class);
// we must normalize it (to avoid having both \ and / in the name which confuses java.io.File)
to = FileUtil.normalizePath(to);
if (ObjectHelper.isEmpty(to)) {
throw new GenericFileOperationFailedException("moveExisting evaluated as empty String, cannot move existing file: " + fileName);
}
// ensure any paths is created before we rename as the renamed file may be in a different path (which may be non exiting)
// use java.io.File to compute the file path
File toFile = new File(to);
String directory = toFile.getParent();
boolean absolute = FileUtil.isAbsolute(toFile);
if (directory != null) {
if (!buildDirectory(directory, absolute)) {
LOG.debug("Cannot build directory [{}] (could be because of denied permissions)", directory);
}
}
// deal if there already exists a file
if (existsFile(to)) {
if (endpoint.isEagerDeleteTargetFile()) {
LOG.trace("Deleting existing file: {}", to);
if (!deleteFile(to)) {
throw new GenericFileOperationFailedException("Cannot delete file: " + to);
}
} else {
throw new GenericFileOperationFailedException("Cannot moved existing file from: " + fileName + " to: " + to + " as there already exists a file: " + to);
}
}
LOG.trace("Moving existing file: {} to: {}", fileName, to);
if (!renameFile(fileName, to)) {
throw new GenericFileOperationFailedException("Cannot rename file from: " + fileName + " to: " + to);
}
}
private void keepLastModified(Exchange exchange, File file) {
if (endpoint.isKeepLastModified()) {
Long last;
Date date = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Date.class);
if (date != null) {
last = date.getTime();
} else {
// fallback and try a long
last = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Long.class);
}
if (last != null) {
boolean result = file.setLastModified(last);
if (LOG.isTraceEnabled()) {
LOG.trace("Keeping last modified timestamp: {} on file: {} with result: {}", new Object[]{last, file, result});
}
}
}
}
private boolean writeFileByLocalWorkPath(File source, File file) throws IOException {
LOG.trace("Using local work file being renamed from: {} to: {}", source, file);
return FileUtil.renameFile(source, file, endpoint.isCopyAndDeleteOnRenameFail());
}
private void writeFileByFile(File source, File target) throws IOException {
FileChannel in = new FileInputStream(source).getChannel();
FileChannel out = null;
try {
out = prepareOutputFileChannel(target);
LOG.debug("Using FileChannel to write file: {}", target);
long size = in.size();
long position = 0;
while (position < size) {
position += in.transferTo(position, endpoint.getBufferSize(), out);
}
} finally {
IOHelper.close(in, source.getName(), LOG);
IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites());
}
}
private void writeFileByStream(InputStream in, File target) throws IOException {
FileChannel out = null;
try {
out = prepareOutputFileChannel(target);
LOG.debug("Using InputStream to write file: {}", target);
int size = endpoint.getBufferSize();
byte[] buffer = new byte[size];
ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
if (bytesRead < size) {
byteBuffer.limit(bytesRead);
}
out.write(byteBuffer);
byteBuffer.clear();
}
} finally {
IOHelper.close(in, target.getName(), LOG);
IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites());
}
}
private void writeFileByReaderWithCharset(Reader in, File target, String charset) throws IOException {
boolean append = endpoint.getFileExist() == GenericFileExist.Append;
FileOutputStream os = new FileOutputStream(target, append);
Writer out = IOConverter.toWriter(os, charset);
try {
LOG.debug("Using Reader to write file: {} with charset: {}", target, charset);
int size = endpoint.getBufferSize();
IOHelper.copy(in, out, size);
} finally {
IOHelper.close(in, target.getName(), LOG);
IOHelper.close(out, os, target.getName(), LOG, endpoint.isForceWrites());
}
}
/**
* Creates a new file if the file doesn't exist.
* If the endpoint's existing file logic is set to 'Override' then the target file will be truncated
*/
private void writeFileEmptyBody(File target) throws IOException {
if (!target.exists()) {
LOG.debug("Creating new empty file: {}", target);
FileUtil.createNewFile(target);
} else if (endpoint.getFileExist() == GenericFileExist.Override) {
LOG.debug("Truncating existing file: {}", target);
FileChannel out = new FileOutputStream(target).getChannel();
try {
out.truncate(0);
} finally {
IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites());
}
}
}
/**
* Creates and prepares the output file channel. Will position itself in correct position if the file is writable
* eg. it should append or override any existing content.
*/
private FileChannel prepareOutputFileChannel(File target) throws IOException {
if (endpoint.getFileExist() == GenericFileExist.Append) {
FileChannel out = new RandomAccessFile(target, "rw").getChannel();
return out.position(out.size());
}
return new FileOutputStream(target).getChannel();
}
}