/*
* JBoss, Home of Professional Open Source
* Copyright 2006, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.soa.esb.listeners.gateway;
import java.io.File;
import java.io.FileFilter;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import org.apache.log4j.Logger;
import org.jboss.soa.esb.ConfigurationException;
import org.jboss.soa.esb.Service;
import org.jboss.soa.esb.addressing.eprs.FileEpr;
import org.jboss.soa.esb.schedule.SchedulingException;
import org.jboss.soa.esb.common.Environment;
import org.jboss.soa.esb.couriers.CourierException;
import org.jboss.soa.esb.filter.FilterManager;
import org.jboss.soa.esb.helpers.ConfigTree;
import org.jboss.soa.esb.listeners.ListenerTagNames;
import org.jboss.soa.esb.listeners.ListenerUtil;
import org.jboss.soa.esb.client.ServiceInvoker;
import org.jboss.soa.esb.listeners.message.MessageDeliverException;
import org.jboss.soa.esb.listeners.message.MessageComposer;
import org.jboss.soa.esb.listeners.lifecycle.*;
import org.jboss.soa.esb.message.Message;
import org.jboss.soa.esb.services.registry.RegistryException;
import org.jboss.internal.soa.esb.message.LegacyMessageComposerAdapter;
/**
* Base class for all file gateways: local filesystem, ftp, sftp and ftps.
* <p/>Implementations for file manipulation (getFileList, getFileContents,
* renameFile and deleteFile) must be provided by factory
*
* @author <a href="mailto:schifest@heuristica.com.ar">schifest@heuristica.com.ar</a>
* @since Version 4.0
*/
public abstract class AbstractFileGateway extends AbstractScheduledManagedLifecycle {
protected final static Logger _logger = Logger
.getLogger(AbstractFileGateway.class);
protected ConfigTree config;
protected long _maxMillisForResponse;
protected Service targetService;
protected ServiceInvoker serviceInvoker;
protected MessageComposer messageComposer;
protected boolean _deleteAfterOK;
protected boolean _renameAfterOK;
protected File _inputDirectory, _errorDirectory, _postProcessDirectory;
protected String _inputSuffix, _postProcessSuffix, _workingSuffix,
_errorSuffix;
protected AbstractFileGateway(ConfigTree config) throws ConfigurationException, RegistryException, GatewayException {
super(config);
this.config = config;
checkMyParms();
}
/**
* Handle the initialisation of the managed instance.
*
* @throws ManagedLifecycleException for errors while initialisation.
*/
protected void doInitialise() throws ManagedLifecycleException {
try {
serviceInvoker = new ServiceInvoker(targetService);
} catch (MessageDeliverException e) {
throw new ManagedLifecycleException(e);
}
}
/**
* Execute on trigger from the scheduler.
*/
public void onSchedule() throws SchedulingException {
File[] fileList;
try {
fileList = getFileList();
if(fileList == null) {
_logger.warn("No files to process.");
return;
}
}
catch (GatewayException e) {
_logger.error("Can't retrieve file list", e);
return;
}
for (File fileIn : fileList) {
// Only continue to process files if we're in a STARTED state...
if(getState() != ManagedLifecycleState.STARTED) {
break;
}
// Set the file working. If that fails, move to the next file...
File workingFile = setFileWorking(fileIn);
if (workingFile == null) {
continue;
}
try {
Message message;
try {
message = messageComposer.compose(workingFile);
} catch (MessageDeliverException e) {
processException("Composer <" + messageComposer.getClass().getName() + "> Failed.", e, fileIn, workingFile);
continue;
}
if (message == null) {
_logger.warn("Composer <" + messageComposer.getClass().getName() + "> returned a null object");
continue;
}
Map<String, Object> params = new HashMap<String, Object>();
params.put(Environment.ORIGINAL_FILE, fileIn);
params.put(Environment.GATEWAY_CONFIG, config);
message = FilterManager.getInstance().doOutputWork(message, params);
try {
if (_maxMillisForResponse > 0) {
Message replyMsg = serviceInvoker.deliverSync(message, _maxMillisForResponse);
replyMsg.getAttachment().put(Environment.ORIGINAL_FILE, fileIn); // For backward compatibility!
try {
processReply(replyMsg, fileIn);
} catch (GatewayException e) {
processException("Failed to process reply.", e, fileIn, workingFile);
continue;
}
} else {
serviceInvoker.deliverAsync(message);
}
} catch (MessageDeliverException e) {
processException("Message Delivery Failure.", e, fileIn, workingFile);
continue;
} catch (RegistryException e) {
processException("Message Delivery Failure.", e, fileIn, workingFile);
continue;
}
} catch (CourierException e) {
processException("Message Delivery Failure.", e, fileIn, workingFile);
continue;
}
// The message has been successfully processed...
processingComplete(fileIn, workingFile);
}
}
private void processingComplete(File fileIn, File workingFile) {
File fileOK = new File(_postProcessDirectory, fileIn.getName() + _postProcessSuffix);
if (_deleteAfterOK) {
try {
deleteFile(workingFile);
}
catch (GatewayException e) {
_logger.error("File "+ fileIn + " has been processed and renamed to " + workingFile
+ ", but there were problems deleting it from the input directory ", e);
}
} else {
try {
if (_renameAfterOK) {
renameFile(workingFile, fileOK);
}
}
catch (GatewayException e) {
_logger.error("File " + fileIn + " has been processed and renamed to " + workingFile
+ ", but there were problems renaming it to " + fileOK, e);
}
}
}
private void processException(String message, Throwable thrown, File fileIn, File workingFile) {
_logger.error(message, thrown);
File fileError = new File(_errorDirectory, fileIn.getName() + _errorSuffix);
try {
deleteFile(fileError);
} catch (GatewayException e) {
_logger.warn("File : " + fileError + " did not exist.");
}
try {
renameFile(workingFile, fileError);
}catch (GatewayException e) {
_logger.error("Problems renaming file " + workingFile + " to " + fileError, e);
}
}
private void processReply(Message replyMsg, File fileIn) throws MessageDeliverException, GatewayException {
Object responseData = messageComposer.decompose(replyMsg, fileIn);
if(responseData == null) {
// Legacy composers may handled response delivery themselves...
} else if(responseData instanceof byte[]) {
File responseFile = new File(fileIn.getParent(), fileIn.getName() + FileEpr.DEFAULT_REPLY_TO_FILE_SUFFIX + "_gw");
bytesToFile((byte[])responseData, responseFile);
} else {
_logger.error("File based composers must return a byte[] from their decompose implementations.");
}
}
protected File setFileWorking(File file) {
File workingFile = getWorkFileName(file, _workingSuffix);
try {
if (renameFile(file, workingFile)) {
return workingFile;
}
} catch (GatewayException e) {
_logger.error("Unable to rename file '" + file.getAbsolutePath() + "' to it's working name '" + workingFile + "'. May be a contention issue with another listener. You should avoid having multiple listeners polling on the same file subset. Ignoring this file for now!");
}
return null;
}
protected File getWorkFileName(File fileIn, String suffix) {
return new File(fileIn.toString() + _workingSuffix);
}
/*
* Is the input suffix valid for this type of gateway?
*/
protected void checkInputSuffix() throws ConfigurationException {
if (_inputSuffix.length() < 1)
throw new ConfigurationException("Invalid "
+ ListenerTagNames.FILE_INPUT_SFX_TAG + " attribute");
}
/**
* Check for mandatory and optional attributes in parameter tree
*
* @throws ConfigurationException Mandatory atts are not right or actionClass not in classpath.
*/
private void checkMyParms() throws ConfigurationException {
targetService = Service.getGatewayTargetService(config);
String composerClass = config.getAttribute(ListenerTagNames.GATEWAY_COMPOSER_CLASS_TAG);
if(composerClass == null) {
composerClass = getDefaultComposer();
}
messageComposer = MessageComposer.Factory.getInstance(composerClass, config, LegacyFileMessageComposerAdapter.class);
_maxMillisForResponse = ListenerUtil.getMaxMillisGatewayWait(config, _logger);
String sInpDir = getInputDir(config);
_inputDirectory = fileFromString(sInpDir);
seeIfOkToWorkOnDir(_inputDirectory);
_inputSuffix = ListenerUtil.getValue(config, ListenerTagNames.FILE_INPUT_SFX_TAG, "").trim();
checkInputSuffix();
// WORK suffix (will rename in input directory)
_workingSuffix = ListenerUtil.getValue(config, ListenerTagNames.FILE_WORK_SFX_TAG, ".esbWork").trim();
if (_workingSuffix.length() < 1) {
throw new ConfigurationException("Invalid "+ ListenerTagNames.FILE_WORK_SFX_TAG + " attribute");
}
if (_inputSuffix.equals(_workingSuffix)) {
throw new ConfigurationException("Work suffix must differ from input suffix <" + _workingSuffix + ">");
}
// ERROR directory and suffix (defaults to input dir and
// ".esbError"
// suffix)
String sErrDir = ListenerUtil.getValue(config, ListenerTagNames.FILE_ERROR_DIR_TAG, sInpDir);
_errorDirectory = fileFromString(sErrDir);
seeIfOkToWorkOnDir(_errorDirectory);
_errorSuffix = ListenerUtil.getValue(config, ListenerTagNames.FILE_ERROR_SFX_TAG, ".esbError").trim();
if (_errorSuffix.length() < 1) {
throw new ConfigurationException("Invalid " + ListenerTagNames.FILE_ERROR_SFX_TAG + " attribute");
}
if (_errorDirectory.equals(_inputDirectory) && _inputSuffix.equals(_errorSuffix)) {
throw new ConfigurationException("Error suffix must differ from input suffix <" + _errorSuffix + ">");
}
// Do users wish to delete files that were processed OK ?
String sPostDel = ListenerUtil.getValue(config, ListenerTagNames.FILE_POST_DEL_TAG, "false").trim();
_deleteAfterOK = Boolean.parseBoolean(sPostDel);
if (_deleteAfterOK) {
return;
}
// Do users wish to rename files that were processed OK ?
String sPostRename = ListenerUtil.getValue(config, ListenerTagNames.FILE_POST_RENAME_TAG, "true").trim();
_renameAfterOK = Boolean.parseBoolean(sPostRename);
// POST (done) directory and suffix (defaults to input dir and
// ".esbDone" suffix)
String sPostDir = ListenerUtil.getValue(config, ListenerTagNames.FILE_POST_DIR_TAG, sInpDir);
_postProcessDirectory = fileFromString(sPostDir);
seeIfOkToWorkOnDir(_postProcessDirectory);
_postProcessSuffix = ListenerUtil.getValue(config, ListenerTagNames.FILE_POST_SFX_TAG, ".esbDone").trim();
if (_postProcessDirectory.equals(_inputDirectory)) {
if (_postProcessSuffix.length() < 1) {
throw new ConfigurationException("Invalid " + ListenerTagNames.FILE_POST_SFX_TAG + " attribute");
}
if (_postProcessSuffix.equals(_inputSuffix)) {
throw new ConfigurationException("Post process suffix must differ from input suffix <" + _postProcessSuffix + ">");
}
}
}
public static File getFileInputDirectory(ConfigTree config) throws ConfigurationException {
String sInpDir = getInputDir(config);
return fileFromString(sInpDir);
}
private static String getInputDir(ConfigTree config) throws ConfigurationException {
String uri = config.getAttribute(ListenerTagNames.URL_TAG);
if(uri != null) {
try {
return new URI(uri).getPath();
} catch (URISyntaxException e) {
throw new ConfigurationException("Invalid '" + ListenerTagNames.URL_TAG + "' value '" + uri + "'. Must be a valid URI.");
}
}
return config.getRequiredAttribute(ListenerTagNames.FILE_INPUT_DIR_TAG);
}
private static File fileFromString(String file) {
try {
return new File(new URI(file));
}
catch (Exception e) {
return new File(file);
}
}
abstract File[] getFileList() throws GatewayException;
abstract byte[] getFileContents(File file) throws GatewayException;
abstract boolean renameFile(File from, File to) throws GatewayException;
abstract boolean deleteFile(File file) throws GatewayException;
abstract void seeIfOkToWorkOnDir(File p_oDir) throws ConfigurationException;
abstract String getDefaultComposer() throws ConfigurationException;
abstract void bytesToFile(byte[] bytes, File file) throws GatewayException;
/**
* Legacy Message composer adapter for the file based gateways.
* <p/>
* The old AbstractFileGateway used to leave it to the composer to handle the
* response. It also used to supply the composer with the input file and leave
* it to the composer to determine the name of the output file.
*
* @author <a href="mailto:tom.fennelly@jboss.com">tom.fennelly@jboss.com</a>
*/
private class LegacyFileMessageComposerAdapter<T extends File> extends LegacyMessageComposerAdapter<T> {
public Object decompose(Message message, T inputFile) throws MessageDeliverException {
try {
return _responderMethod.invoke(_composer, message, inputFile);
} catch (IllegalAccessException e) {
throw new MessageDeliverException("Legacy composer class ('" + _composerClass.getName() + "') responder method '" + _responderMethod.getName() + "' is not callable.", e);
} catch (InvocationTargetException e) {
throw new MessageDeliverException("Legacy composer class ('" + _composerClass.getName() + "') responder method '" + _responderMethod.getName() + "' failed with exception.", e.getCause());
}
}
public Class[] getResponderParameters() {
return new Class[]{Message.class, File.class};
}
}
}