/** * The FreeBSD Copyright * Copyright 1994-2008 The FreeBSD Project. All rights reserved. * Copyright (C) 2013-2017 Philip Helger philip[at]helger[dot]com * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FREEBSD PROJECT OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * The views and conclusions contained in the software and documentation * are those of the authors and should not be interpreted as representing * official policies, either expressed or implied, of the FreeBSD Project. */ package com.helger.as2lib.processor.receiver; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.Map; import java.util.Map.Entry; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.mail.MessagingException; import javax.mail.internet.MimeBodyPart; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.helger.as2lib.exception.OpenAS2Exception; import com.helger.as2lib.exception.WrappedOpenAS2Exception; import com.helger.as2lib.message.IMessage; import com.helger.as2lib.params.InvalidParameterException; import com.helger.as2lib.params.MessageParameters; import com.helger.as2lib.processor.CFileAttribute; import com.helger.as2lib.processor.sender.IProcessorSenderModule; import com.helger.as2lib.session.IAS2Session; import com.helger.as2lib.util.CAS2Header; import com.helger.as2lib.util.IOHelper; import com.helger.as2lib.util.IStringMap; import com.helger.commons.annotation.ReturnsMutableObject; import com.helger.commons.collection.CollectionHelper; import com.helger.commons.collection.ext.CommonsHashMap; import com.helger.commons.collection.ext.ICommonsMap; import com.helger.commons.io.file.FileIOError; import com.helger.commons.io.file.SimpleFileIO; import com.helger.commons.io.stream.StreamHelper; import com.helger.commons.mime.CMimeType; import com.helger.mail.cte.EContentTransferEncoding; import com.helger.mail.datasource.ByteArrayDataSource; public abstract class AbstractDirectoryPollingModule extends AbstractActivePollingModule { public static final String ATTR_OUTBOX_DIRECTORY = "outboxdir"; public static final String ATTR_ERROR_DIRECTORY = "errordir"; public static final String ATTR_SENT_DIRECTORY = "sentdir"; public static final String ATTR_FORMAT = "format"; public static final String ATTR_DELIMITERS = "delimiters"; public static final String ATTR_DEFAULTS = "defaults"; public static final String ATTR_MIMETYPE = "mimetype"; public static final String ATTR_SENDFILENAME = "sendfilename"; private static final Logger s_aLogger = LoggerFactory.getLogger (AbstractDirectoryPollingModule.class); private ICommonsMap <String, Long> m_aTrackedFiles; @Override public void initDynamicComponent (@Nonnull final IAS2Session aSession, @Nullable final IStringMap aOptions) throws OpenAS2Exception { super.initDynamicComponent (aSession, aOptions); getAttributeAsStringRequired (ATTR_OUTBOX_DIRECTORY); getAttributeAsStringRequired (ATTR_ERROR_DIRECTORY); } @Override public void poll () { try { // scan the directory for new files scanDirectory (getAttributeAsStringRequired (ATTR_OUTBOX_DIRECTORY)); // update tracking info. if a file is ready, process it updateTracking (); } catch (final Exception ex) { WrappedOpenAS2Exception.wrap (ex).terminate (); forceStop (ex); } } protected void scanDirectory (final String sDirectory) throws InvalidParameterException { final File aDir = IOHelper.getDirectoryFile (sDirectory); // get a list of entries in the directory final File [] aFiles = aDir.listFiles (); if (aFiles == null) { throw new InvalidParameterException ("Error getting list of files in directory", this, ATTR_OUTBOX_DIRECTORY, aDir.getAbsolutePath ()); } // iterator through each entry, and start tracking new files if (aFiles.length > 0) for (final File aCurrentFile : aFiles) if (checkFile (aCurrentFile)) { // start watching the file's size if it's not already being watched trackFile (aCurrentFile); } } protected boolean checkFile (@Nonnull final File aFile) { if (aFile.exists () && aFile.isFile ()) { FileOutputStream aFOS = null; try { // check for a write-lock on file, will skip file if it's write locked aFOS = new FileOutputStream (aFile, true); return true; } catch (final IOException ioe) { // a sharing violation occurred, ignore the file for now } finally { StreamHelper.close (aFOS); } } return false; } protected void trackFile (@Nonnull final File aFile) { final Map <String, Long> aTrackedFiles = getAllTrackedFiles (); final String sFilePath = aFile.getAbsolutePath (); if (!aTrackedFiles.containsKey (sFilePath)) aTrackedFiles.put (sFilePath, Long.valueOf (aFile.length ())); } protected void updateTracking () throws OpenAS2Exception { // clone the trackedFiles map, iterator through the clone and modify the // original to avoid iterator exceptions // is there a better way to do this? final Map <String, Long> aTrackedFiles = getAllTrackedFiles (); // We need to operate on a copy for (final Entry <String, Long> aFileEntry : CollectionHelper.newMap (aTrackedFiles).entrySet ()) { // get the file and it's stored length final File aFile = new File (aFileEntry.getKey ()); final long nFileLength = aFileEntry.getValue ().longValue (); // if the file no longer exists, remove it from the tracker if (!checkFile (aFile)) { aTrackedFiles.remove (aFileEntry.getKey ()); } else { // if the file length has changed, update the tracker final long nNewLength = aFile.length (); if (nNewLength != nFileLength) { aTrackedFiles.put (aFileEntry.getKey (), Long.valueOf (nNewLength)); } else { // if the file length has stayed the same, process the file and stop // tracking it try { processFile (aFile); } finally { aTrackedFiles.remove (aFileEntry.getKey ()); } } } } } protected void processFile (@Nonnull final File aFile) throws OpenAS2Exception { s_aLogger.info ("processing " + aFile.getAbsolutePath ()); final IMessage aMsg = createMessage (); aMsg.setAttribute (CFileAttribute.MA_FILEPATH, aFile.getAbsolutePath ()); aMsg.setAttribute (CFileAttribute.MA_FILENAME, aFile.getName ()); /* * asynch mdn logic 2007-03-12 save the file name into message object, it * will be stored into pending information file */ aMsg.setAttribute (CFileAttribute.MA_PENDING_FILENAME, aFile.getName ()); if (s_aLogger.isDebugEnabled ()) s_aLogger.debug ("AS2Message was created"); try { updateMessage (aMsg, aFile); s_aLogger.info ("file assigned to message " + aFile.getAbsolutePath () + aMsg.getLoggingText ()); if (aMsg.getData () == null) throw new InvalidMessageException ("No Data"); // Transmit the message getSession ().getMessageProcessor ().handle (IProcessorSenderModule.DO_SEND, aMsg, null); if (s_aLogger.isDebugEnabled ()) s_aLogger.debug ("AS2Message was successfully handled my the MessageProcessor"); /* * asynch mdn logic 2007-03-12 If the return status is pending in msg's * attribute "status" then copy the transmitted file to pending folder and * wait for the receiver to make another HTTP call to post AsyncMDN */ if (CFileAttribute.MA_STATUS_PENDING.equals (aMsg.getAttribute (CFileAttribute.MA_STATUS))) { final File aPendingFile = new File (aMsg.getPartnership ().getAttribute (CFileAttribute.MA_STATUS_PENDING), aMsg.getAttribute (CFileAttribute.MA_PENDING_FILENAME)); final FileIOError aIOErr = IOHelper.getFileOperationManager ().copyFile (aFile, aPendingFile); if (aIOErr.isFailure ()) throw new OpenAS2Exception ("File was successfully sent but not copied to pending folder: " + aPendingFile + " - " + aIOErr.toString ()); s_aLogger.info ("copied " + aFile.getAbsolutePath () + " to pending folder : " + aPendingFile.getAbsolutePath () + aMsg.getLoggingText ()); } // If the Sent Directory option is set, move the transmitted file to // the sent directory if (containsAttribute (ATTR_SENT_DIRECTORY)) { File aSentFile = null; try { aSentFile = new File (IOHelper.getDirectoryFile (getAttributeAsStringRequired (ATTR_SENT_DIRECTORY)), aFile.getName ()); aSentFile = IOHelper.moveFile (aFile, aSentFile, false, true); s_aLogger.info ("moved " + aFile.getAbsolutePath () + " to " + aSentFile.getAbsolutePath () + aMsg.getLoggingText ()); } catch (final IOException ex) { final OpenAS2Exception se = new OpenAS2Exception ("File was successfully sent but not moved to sent folder: " + aSentFile); se.initCause (ex); } } else { if (s_aLogger.isDebugEnabled ()) s_aLogger.debug ("Trying to delete file " + aFile.getAbsolutePath ()); if (!aFile.delete ()) { // Delete the file if a sent directory isn't set throw new OpenAS2Exception ("File was successfully sent but not deleted: " + aFile); } s_aLogger.info ("deleted " + aFile.getAbsolutePath () + aMsg.getLoggingText ()); } } catch (final OpenAS2Exception ex) { s_aLogger.info (ex.getLocalizedMessage () + aMsg.getLoggingText ()); ex.addSource (OpenAS2Exception.SOURCE_MESSAGE, aMsg); ex.addSource (OpenAS2Exception.SOURCE_FILE, aFile); ex.terminate (); IOHelper.handleError (aFile, getAttributeAsStringRequired (ATTR_ERROR_DIRECTORY)); } } @Nonnull protected abstract IMessage createMessage (); public void updateMessage (@Nonnull final IMessage aMsg, @Nonnull final File aFile) throws OpenAS2Exception { final MessageParameters aParams = new MessageParameters (aMsg); final String sDefaults = getAttributeAsString (ATTR_DEFAULTS); if (sDefaults != null) aParams.setParameters (sDefaults); final String sFilename = aFile.getName (); final String sFormat = getAttributeAsString (ATTR_FORMAT); if (sFormat != null) { final String sDelimiters = getAttributeAsString (ATTR_DELIMITERS, ".-"); aParams.setParameters (sFormat, sDelimiters, sFilename); } try { final byte [] aData = SimpleFileIO.getAllFileBytes (aFile); String sContentType = getAttributeAsString (ATTR_MIMETYPE); if (sContentType == null) { // Default to application/octet-stream sContentType = CMimeType.APPLICATION_OCTET_STREAM.getAsString (); } else { try { sContentType = aParams.format (sContentType); } catch (final InvalidParameterException ex) { s_aLogger.error ("Bad content-type '" + sContentType + "'" + aMsg.getLoggingText ()); // Default to application/octet-stream sContentType = CMimeType.APPLICATION_OCTET_STREAM.getAsString (); } } final ByteArrayDataSource aByteSource = new ByteArrayDataSource (aData, sContentType, null); final MimeBodyPart aBody = new MimeBodyPart (); aBody.setDataHandler (aByteSource.getAsDataHandler ()); // Headers must be set AFTER the DataHandler final String sEncodeType = aMsg.getPartnership () .getContentTransferEncoding (EContentTransferEncoding.AS2_DEFAULT.getID ()); aBody.setHeader (CAS2Header.HEADER_CONTENT_TRANSFER_ENCODING, sEncodeType); // below statement is not filename related, just want to make it // consist with the parameter "mimetype="application/EDI-X12"" // defined in config.xml 2007-06-01 aBody.setHeader (CAS2Header.HEADER_CONTENT_TYPE, sContentType); // add below statement will tell the receiver to save the filename // as the one sent by sender. 2007-06-01 final String sSendFilename = getAttributeAsString (ATTR_SENDFILENAME); if ("true".equals (sSendFilename)) { final String sMAFilename = aMsg.getAttribute (CFileAttribute.MA_FILENAME); final String sContentDisposition = "Attachment; filename=\"" + sMAFilename + "\""; aBody.setHeader (CAS2Header.HEADER_CONTENT_DISPOSITION, sContentDisposition); aMsg.setContentDisposition (sContentDisposition); } aMsg.setData (aBody); } catch (final MessagingException ex) { throw WrappedOpenAS2Exception.wrap (ex); } if (s_aLogger.isDebugEnabled ()) s_aLogger.debug ("Updating partnership for AS2 message" + aMsg.getLoggingText ()); // update the message's partnership with any stored information getSession ().getPartnershipFactory ().updatePartnership (aMsg, true); if (s_aLogger.isDebugEnabled ()) s_aLogger.debug ("Finished updating partnership for AS2 message"); aMsg.updateMessageID (); if (s_aLogger.isDebugEnabled ()) s_aLogger.debug ("Updated message ID to " + aMsg.getMessageID ()); } @Nonnull @ReturnsMutableObject ("design") public ICommonsMap <String, Long> getAllTrackedFiles () { if (m_aTrackedFiles == null) m_aTrackedFiles = new CommonsHashMap<> (); return m_aTrackedFiles; } }