org.springframework.integration.file.FileWritingMessageHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.integration.file.FileWritingMessageHandler.java

Source

/*
 * Copyright 2002-2013 the original author or authors.
 *
 * Licensed 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.springframework.integration.file;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.Message;
import org.springframework.integration.MessageHandlingException;
import org.springframework.integration.core.MessageHandler;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.file.support.FileExistsMode;
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.util.DefaultLockRegistry;
import org.springframework.integration.util.LockRegistry;
import org.springframework.integration.util.PassThruLockRegistry;
import org.springframework.integration.util.WhileLockedProcessor;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;

/**
 * A {@link MessageHandler} implementation that writes the Message payload to a
 * file. If the payload is a File object, it will copy the File to the specified
 * destination directory. If the payload is a byte array or String, it will write
 * it directly. Otherwise, the payload type is unsupported, and an Exception
 * will be thrown.
 * <p>
 * If the 'deleteSourceFiles' flag is set to true, the original Files will be
 * deleted. The default value for that flag is <em>false</em>. See the
 * {@link #setDeleteSourceFiles(boolean)} method javadoc for more information.
 * <p>
 * Other transformers may be useful to precede this handler. For example, any
 * Serializable object payload can be converted into a byte array by the
 * {@link org.springframework.integration.transformer.PayloadSerializingTransformer}.
 * Likewise, any Object can be converted to a String based on its
 * <code>toString()</code> method by the
 * {@link org.springframework.integration.transformer.ObjectToStringTransformer}.
 *
 * @author Mark Fisher
 * @author Iwein Fuld
 * @author Alex Peters
 * @author Oleg Zhurakousky
 * @author Artem Bilan
 * @author Gunnar Hillert
 * @author Gary Russell
 */
public class FileWritingMessageHandler extends AbstractReplyProducingMessageHandler {

    private volatile String temporaryFileSuffix = ".writing";

    private volatile boolean temporaryFileSuffixSet = false;

    private volatile FileExistsMode fileExistsMode = FileExistsMode.REPLACE;

    private final Log logger = LogFactory.getLog(this.getClass());

    private volatile FileNameGenerator fileNameGenerator = new DefaultFileNameGenerator();

    private volatile boolean fileNameGeneratorSet;

    private volatile StandardEvaluationContext evaluationContext;

    private final Expression destinationDirectoryExpression;

    private volatile boolean autoCreateDirectory = true;

    private volatile boolean deleteSourceFiles;

    private volatile Charset charset = Charset.defaultCharset();

    private volatile boolean expectReply = true;

    private volatile LockRegistry lockRegistry = new PassThruLockRegistry();

    /**
     * Constructor which sets the {@link #destinationDirectoryExpression} using
     * a {@link LiteralExpression}.
     *
     * @param destinationDirectory Must not be null
     * @see #FileWritingMessageHandler(Expression)
     */
    public FileWritingMessageHandler(File destinationDirectory) {
        Assert.notNull(destinationDirectory, "Destination directory must not be null.");
        this.destinationDirectoryExpression = new LiteralExpression(destinationDirectory.getPath());
    }

    /**
     * Constructor which sets the {@link #destinationDirectoryExpression}.
     *
     * @param destinationDirectoryExpression Must not be null
     * @see #FileWritingMessageHandler(File)
     */
    public FileWritingMessageHandler(Expression destinationDirectoryExpression) {
        Assert.notNull(destinationDirectoryExpression, "Destination directory expression must not be null.");
        this.destinationDirectoryExpression = destinationDirectoryExpression;
    }

    /**
     * Specify whether to create the destination directory automatically if it
     * does not yet exist upon initialization. By default, this value is
     * <em>true</em>. If set to <em>false</em> and the
     * destination directory does not exist, an Exception will be thrown upon
     * initialization.
     */
    public void setAutoCreateDirectory(boolean autoCreateDirectory) {
        this.autoCreateDirectory = autoCreateDirectory;
    }

    /**
     * By default, every file that is in the process of being transferred will
     * appear in the file system with an additional suffix, which by default is
     * ".writing". This can be changed by setting this property.
     *
     * @param temporaryFileSuffix
     */
    public void setTemporaryFileSuffix(String temporaryFileSuffix) {
        Assert.notNull(temporaryFileSuffix, "'temporaryFileSuffix' must not be null"); // empty string is OK
        this.temporaryFileSuffix = temporaryFileSuffix;
        this.temporaryFileSuffixSet = true;
    }

    /**
     * Will set the {@link FileExistsMode} that specifies what will happen in
     * case the destination exists. For example {@link FileExistsMode#APPEND}
     * instructs this handler to append data to the existing file rather then
     * creating a new file for each {@link Message}.
     *
     * If set to {@link FileExistsMode#APPEND}, the adapter will also
     * create a real instance of the {@link LockRegistry} to ensure that there
     * is no collisions when multiple threads are writing to the same file.
     *
     * Otherwise the LockRegistry is set to {@link PassThruLockRegistry} which
     * has no effect.
     *
     * @param fileExistsMode Must not be null
     */
    public void setFileExistsMode(FileExistsMode fileExistsMode) {

        Assert.notNull(fileExistsMode, "'fileExistsMode' must not be null.");
        this.fileExistsMode = fileExistsMode;

        if (FileExistsMode.APPEND.equals(fileExistsMode)) {
            this.lockRegistry = this.lockRegistry instanceof PassThruLockRegistry ? new DefaultLockRegistry()
                    : this.lockRegistry;
        }
    }

    /**
     * Specify whether a reply Message is expected. If not, this handler will simply return null for a
     * successful response or throw an Exception for a non-successful response. The default is true.
     */
    public void setExpectReply(boolean expectReply) {
        this.expectReply = expectReply;
    }

    protected String getTemporaryFileSuffix() {
        return temporaryFileSuffix;
    }

    /**
     * Provide the {@link FileNameGenerator} strategy to use when generating
     * the destination file's name.
     */
    public void setFileNameGenerator(FileNameGenerator fileNameGenerator) {
        Assert.notNull(fileNameGenerator, "FileNameGenerator must not be null");
        this.fileNameGenerator = fileNameGenerator;
        this.fileNameGeneratorSet = true;
    }

    /**
     * Specify whether to delete source Files after writing to the destination
     * directory. The default is <em>false</em>. When set to <em>true</em>, it
     * will only have an effect if the inbound Message has a File payload or
     * a {@link FileHeaders#ORIGINAL_FILE} header value containing either a
     * File instance or a String representing the original file path.
     */
    public void setDeleteSourceFiles(boolean deleteSourceFiles) {
        this.deleteSourceFiles = deleteSourceFiles;
    }

    /**
     * Set the charset name to use when writing a File from a String-based
     * Message payload.
     */
    public void setCharset(String charset) {
        Assert.notNull(charset, "charset must not be null");
        Assert.isTrue(Charset.isSupported(charset), "Charset '" + charset + "' is not supported.");
        this.charset = Charset.forName(charset);
    }

    @Override
    public final void onInit() {

        super.onInit();

        this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(this.getBeanFactory());

        if (this.destinationDirectoryExpression instanceof LiteralExpression) {
            final File directory = new File(
                    this.destinationDirectoryExpression.getValue(this.evaluationContext, null, String.class));
            validateDestinationDirectory(directory, this.autoCreateDirectory);
        }

        if (!this.fileNameGeneratorSet && this.fileNameGenerator instanceof BeanFactoryAware) {
            ((BeanFactoryAware) this.fileNameGenerator).setBeanFactory(this.getBeanFactory());
        }
    }

    private void validateDestinationDirectory(File destinationDirectory, boolean autoCreateDirectory) {

        if (!destinationDirectory.exists() && autoCreateDirectory) {
            destinationDirectory.mkdirs();
        }

        Assert.isTrue(destinationDirectory.exists(),
                "Destination directory [" + destinationDirectory + "] does not exist.");
        Assert.isTrue(destinationDirectory.isDirectory(),
                "Destination path [" + destinationDirectory + "] does not point to a directory.");
        Assert.isTrue(destinationDirectory.canWrite(),
                "Destination directory [" + destinationDirectory + "] is not writable.");
        Assert.state(!(this.temporaryFileSuffixSet && FileExistsMode.APPEND.equals(this.fileExistsMode)),
                "'temporaryFileSuffix' can not be set when appending to an existing file");
    }

    @Override
    protected Object handleRequestMessage(Message<?> requestMessage) {
        Assert.notNull(requestMessage, "message must not be null");
        Object payload = requestMessage.getPayload();
        Assert.notNull(payload, "message payload must not be null");
        String generatedFileName = this.fileNameGenerator.generateFileName(requestMessage);
        File originalFileFromHeader = this.retrieveOriginalFileFromHeader(requestMessage);

        final File destinationDirectoryToUse = evaluateDestinationDirectoryExpression(requestMessage);

        File tempFile = new File(destinationDirectoryToUse, generatedFileName + temporaryFileSuffix);
        File resultFile = new File(destinationDirectoryToUse, generatedFileName);

        if (FileExistsMode.FAIL.equals(this.fileExistsMode) && resultFile.exists()) {
            throw new MessageHandlingException(requestMessage,
                    "The destination file already exists at '" + resultFile.getAbsolutePath() + "'.");
        }

        final boolean ignore = FileExistsMode.IGNORE.equals(this.fileExistsMode)
                && (resultFile.exists() || (StringUtils.hasText(this.temporaryFileSuffix) && tempFile.exists()));

        if (!ignore) {

            try {
                if (payload instanceof File) {
                    resultFile = this.handleFileMessage((File) payload, tempFile, resultFile);
                } else if (payload instanceof byte[]) {
                    resultFile = this.handleByteArrayMessage((byte[]) payload, originalFileFromHeader, tempFile,
                            resultFile);
                } else if (payload instanceof String) {
                    resultFile = this.handleStringMessage((String) payload, originalFileFromHeader, tempFile,
                            resultFile);
                } else {
                    throw new IllegalArgumentException(
                            "unsupported Message payload type [" + payload.getClass().getName() + "]");
                }
            } catch (Exception e) {
                throw new MessageHandlingException(requestMessage, "failed to write Message payload to file", e);
            }

        }

        if (!this.expectReply) {
            return null;
        }

        if (resultFile != null) {
            if (originalFileFromHeader == null && payload instanceof File) {
                return MessageBuilder.withPayload(resultFile).setHeader(FileHeaders.ORIGINAL_FILE, payload);
            }
        }
        return resultFile;
    }

    /**
     * Retrieves the File instance from the {@link FileHeaders#ORIGINAL_FILE}
     * header if available. If the value is not a File instance or a String
     * representation of a file path, this will return <code>null</code>.
     */
    private File retrieveOriginalFileFromHeader(Message<?> message) {
        Object value = message.getHeaders().get(FileHeaders.ORIGINAL_FILE);
        if (value instanceof File) {
            return (File) value;
        }
        if (value instanceof String) {
            return new File((String) value);
        }
        return null;
    }

    private File handleFileMessage(final File sourceFile, File tempFile, final File resultFile) throws IOException {
        if (FileExistsMode.APPEND.equals(this.fileExistsMode)) {
            File fileToWriteTo = this.determineFileToWrite(resultFile, tempFile);
            final FileOutputStream fos = new FileOutputStream(fileToWriteTo, true);
            final FileInputStream fis = new FileInputStream(sourceFile);
            WhileLockedProcessor whileLockedProcessor = new WhileLockedProcessor(this.lockRegistry,
                    fileToWriteTo.getAbsolutePath()) {
                @Override
                protected void whileLocked() throws IOException {
                    FileCopyUtils.copy(fis, fos);
                }
            };
            whileLockedProcessor.doWhileLocked();
            this.cleanUpAfterCopy(fileToWriteTo, resultFile, sourceFile);
            return resultFile;
        } else {
            if (this.deleteSourceFiles) {
                if (sourceFile.renameTo(resultFile)) {
                    return resultFile;
                }
                if (logger.isInfoEnabled()) {
                    logger.info(String.format("Failed to move file '%s'. Using copy and delete fallback.",
                            sourceFile.getAbsolutePath()));
                }
            }
            FileCopyUtils.copy(sourceFile, tempFile);
            this.cleanUpAfterCopy(tempFile, resultFile, sourceFile);
            return resultFile;
        }
    }

    private File handleByteArrayMessage(final byte[] bytes, File originalFile, File tempFile, final File resultFile)
            throws IOException {
        File fileToWriteTo = this.determineFileToWrite(resultFile, tempFile);

        final boolean append = FileExistsMode.APPEND.equals(this.fileExistsMode);

        final FileOutputStream fos = new FileOutputStream(fileToWriteTo, append);
        WhileLockedProcessor whileLockedProcessor = new WhileLockedProcessor(this.lockRegistry,
                fileToWriteTo.getAbsolutePath()) {
            @Override
            protected void whileLocked() throws IOException {
                FileCopyUtils.copy(bytes, fos);
            }

        };
        whileLockedProcessor.doWhileLocked();
        this.cleanUpAfterCopy(fileToWriteTo, resultFile, originalFile);
        return resultFile;
    }

    private File handleStringMessage(final String content, File originalFile, File tempFile, final File resultFile)
            throws IOException {
        File fileToWriteTo = this.determineFileToWrite(resultFile, tempFile);

        final boolean append = FileExistsMode.APPEND.equals(this.fileExistsMode);

        final OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(fileToWriteTo, append),
                this.charset);
        WhileLockedProcessor whileLockedProcessor = new WhileLockedProcessor(this.lockRegistry,
                fileToWriteTo.getAbsolutePath()) {
            @Override
            protected void whileLocked() throws IOException {
                FileCopyUtils.copy(content, writer);
            }

        };
        whileLockedProcessor.doWhileLocked();

        this.cleanUpAfterCopy(fileToWriteTo, resultFile, originalFile);
        return resultFile;
    }

    private File determineFileToWrite(File resultFile, File tempFile) {

        final File fileToWriteTo;

        switch (this.fileExistsMode) {
        case APPEND:
            fileToWriteTo = resultFile;
            break;
        case FAIL:
        case IGNORE:
        case REPLACE:
            fileToWriteTo = tempFile;
            break;
        default:
            throw new IllegalStateException("Unsupported FileExistsMode " + this.fileExistsMode);
        }
        return fileToWriteTo;
    }

    private void cleanUpAfterCopy(File fileToWriteTo, File resultFile, File originalFile) throws IOException {
        if (!FileExistsMode.APPEND.equals(this.fileExistsMode) && StringUtils.hasText(this.temporaryFileSuffix)) {
            this.renameTo(fileToWriteTo, resultFile);
        }

        if (this.deleteSourceFiles && originalFile != null) {
            originalFile.delete();
        }
    }

    private void renameTo(File tempFile, File resultFile) throws IOException {
        Assert.notNull(resultFile, "'resultFile' must not be null");
        Assert.notNull(tempFile, "'tempFile' must not be null");

        if (resultFile.exists()) {
            if (resultFile.setWritable(true, false) && resultFile.delete()) {
                if (!tempFile.renameTo(resultFile)) {
                    throw new IOException("Failed to rename file '" + tempFile.getAbsolutePath() + "' to '"
                            + resultFile.getAbsolutePath() + "'");
                }
            } else {
                throw new IOException("Failed to rename file '" + tempFile.getAbsolutePath() + "' to '"
                        + resultFile.getAbsolutePath() + "' since '" + resultFile.getName()
                        + "' is not writable or can not be deleted");
            }
        } else {
            if (!tempFile.renameTo(resultFile)) {
                throw new IOException("Failed to rename file '" + tempFile.getAbsolutePath() + "' to '"
                        + resultFile.getAbsolutePath() + "'");
            }
        }
    }

    private File evaluateDestinationDirectoryExpression(Message<?> message) {

        final File destinationDirectory;

        final Object destinationDirectoryToUse = this.destinationDirectoryExpression
                .getValue(this.evaluationContext, message);

        if (destinationDirectoryToUse == null) {
            throw new IllegalStateException(
                    String.format("The provided " + "destinationDirectoryExpression (%s) must not resolve to null.",
                            this.destinationDirectoryExpression.getExpressionString()));
        } else if (destinationDirectoryToUse instanceof String) {

            final String destinationDirectoryPath = (String) destinationDirectoryToUse;

            Assert.hasText(destinationDirectoryPath,
                    String.format("Unable to resolve destination directory name for the provided Expression '%s'.",
                            this.destinationDirectoryExpression.getExpressionString()));
            destinationDirectory = new File(destinationDirectoryPath);
        } else if (destinationDirectoryToUse instanceof File) {
            destinationDirectory = (File) destinationDirectoryToUse;
        } else {
            throw new IllegalStateException(String.format(
                    "The provided " + "destinationDirectoryExpression (%s) must be of type "
                            + "java.io.File or be a String.",
                    this.destinationDirectoryExpression.getExpressionString()));
        }

        validateDestinationDirectory(destinationDirectory, this.autoCreateDirectory);
        return destinationDirectory;
    }

}