diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 14678d69..679a3fb2 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,21 +1,19 @@ # OpenAS2 Server -# Version 3.7.0 +# Version 3.8.0 # RELEASE NOTES ----- -The OpenAS2 project is pleased to announce the release of OpenAS2 3.7.0 +The OpenAS2 project is pleased to announce the release of OpenAS2 3.8.0 -The release download file is: OpenAS2Server-3.7.0.zip +The release download file is: OpenAS2Server-3.8.0.zip The zip file contains a PDF document (OpenAS2HowTo.pdf) providing information on installing and using the application. ## NOTE: Testing covers Java 8 to 17. The application should work for older versions down to Java 7 but they are not tested as part of the CI/CD pipeline. -Version 3.7.0 - 2023-09-12 -This is an enhancement and bugfix release: +Version 3.8.0 - 2023-11-07 +This is an enhancement release: **IMPORTANT NOTE**: Please review upgrade notes below if you are upgrading - 1. Support parallel mode processing for the directory polling configuration to achieve high volume throughput. - 2. Enhance error handling when chacking for files that never received an DMN response. - 3. Added logging to indicate reading a fixed byte count message from HTTP stream to aid debugging. + 1. Support for configurable dynamic Content-Type based on the file extension. See documentation section 7.5 "Setting Content Type" ##Upgrade Notes See the openAS2HowTo appendix for the general process on upgrading OpenAS2. diff --git a/Remote/pom.xml b/Remote/pom.xml index cddb1ad2..f9511e55 100644 --- a/Remote/pom.xml +++ b/Remote/pom.xml @@ -4,7 +4,7 @@ net.sf.openas2 OpenAS2 - 3.7.0 + 3.8.0 4.0.0 diff --git a/Server/pom.xml b/Server/pom.xml index 8b576221..0de6aa6b 100644 --- a/Server/pom.xml +++ b/Server/pom.xml @@ -7,7 +7,7 @@ net.sf.openas2 OpenAS2 - 3.7.0 + 3.8.0 ../pom.xml diff --git a/Server/src/config/content_type_mappings.properties b/Server/src/config/content_type_mappings.properties new file mode 100644 index 00000000..556d6d8a --- /dev/null +++ b/Server/src/config/content_type_mappings.properties @@ -0,0 +1,3 @@ +xml=application/xml +edi=application/edifact +txt=text/plain \ No newline at end of file diff --git a/Server/src/main/java/org/openas2/XMLSession.java b/Server/src/main/java/org/openas2/XMLSession.java index b7a164f1..9399b30a 100644 --- a/Server/src/main/java/org/openas2/XMLSession.java +++ b/Server/src/main/java/org/openas2/XMLSession.java @@ -12,11 +12,13 @@ import org.openas2.params.InvalidParameterException; import org.openas2.params.ParameterParser; import org.openas2.message.MessageFactory; +import org.openas2.partner.Partnership; import org.openas2.partner.PartnershipFactory; import org.openas2.processor.Processor; import org.openas2.processor.ProcessorModule; import org.openas2.processor.receiver.PollingModule; import org.openas2.schedule.SchedulerComponent; +import org.openas2.util.FileUtil; import org.openas2.util.Properties; import org.openas2.util.XMLUtil; import org.w3c.dom.Document; @@ -141,8 +143,9 @@ protected void load(InputStream in) throws Exception { * * @param propNode - the "properties" element of the configuration file containing property values * @throws InvalidParameterException + * @throws IOException */ - private void loadProperties(Node propNode) throws InvalidParameterException { + private void loadProperties(Node propNode) throws InvalidParameterException, IOException { LOGGER.info("Loading properties..."); Map properties = XMLUtil.mapAttributes(propNode, false); @@ -151,7 +154,7 @@ private void loadProperties(Node propNode) throws InvalidParameterException { properties.put(Properties.APP_TITLE_PROP, getAppTitle()); properties.put(Properties.APP_VERSION_PROP, getAppVersion()); Properties.setProperties(properties); - String appPropsFile = System.getProperty("openas2.properties.file"); + String appPropsFile = System.getProperty(Properties.OPENAS2_PROPERTIES_FILE_PROP); if (appPropsFile != null && appPropsFile.length() > 1) { java.util.Properties appProps = new java.util.Properties(); FileInputStream fis = null; @@ -210,6 +213,11 @@ private void loadProperties(Node propNode) throws InvalidParameterException { Properties.setProperty(key, entry.getValue()); } } + // Now check if we need to load Content-Type mappings + String contentTypeMapFilename = Properties.getProperty(Partnership.PA_CONTENT_TYPE_MAPPING_FILE, null); + if (contentTypeMapFilename != null) { + Properties.setContentTypeMap(FileUtil.loadProperties(contentTypeMapFilename)); + } } private void loadCertificates(Node rootNode) throws OpenAS2Exception { diff --git a/Server/src/main/java/org/openas2/app/partner/AddPartnershipCommand.java b/Server/src/main/java/org/openas2/app/partner/AddPartnershipCommand.java index 3b211f5f..30b96ed8 100644 --- a/Server/src/main/java/org/openas2/app/partner/AddPartnershipCommand.java +++ b/Server/src/main/java/org/openas2/app/partner/AddPartnershipCommand.java @@ -16,7 +16,7 @@ import java.util.regex.Pattern; /** - * adds a new partnership entry in partneship store + * adds a new partnership entry in partnership store * * @author joseph mcverry */ @@ -56,7 +56,7 @@ public CommandResult execute(PartnershipFactory partFx, Object[] params) throws for (int i = 0; i < params.length; i++) { String param = (String) params[i]; - int pos = param.indexOf('='); + int equalsPos = param.indexOf('='); if (i == 0) { partnershipRoot.setAttribute("name", param); } else if (i == 1) { @@ -67,9 +67,9 @@ public CommandResult execute(PartnershipFactory partFx, Object[] params) throws Element elem = doc.createElement(Partnership.PCFG_RECEIVER); elem.setAttribute("name", param); partnershipRoot.appendChild(elem); - } else if (pos == 0) { + } else if (equalsPos == 0) { return new CommandResult(CommandResult.TYPE_ERROR, "incoming parameter missing name"); - } else if (pos > 0) { + } else if (equalsPos > 0) { if (param.startsWith("pollerConfig.")) { // Add a pollerConfig element String regex = "^pollerConfig.([^=]*)=((?:[^\"']+)|'(?:[^']*)'|\"(?:[^\"]*)\")"; @@ -86,8 +86,8 @@ public CommandResult execute(PartnershipFactory partFx, Object[] params) throws pollerConfigElem.setAttribute(name, val); } else { Element elem = doc.createElement("attribute"); - elem.setAttribute("name", param.substring(0, pos)); - elem.setAttribute("value", param.substring(pos + 1)); + elem.setAttribute("name", param.substring(0, equalsPos)); + elem.setAttribute("value", param.substring(equalsPos + 1)); partnershipRoot.appendChild(elem); } } else { diff --git a/Server/src/main/java/org/openas2/message/FileAttribute.java b/Server/src/main/java/org/openas2/message/FileAttribute.java index 8dd9893b..1e122e63 100644 --- a/Server/src/main/java/org/openas2/message/FileAttribute.java +++ b/Server/src/main/java/org/openas2/message/FileAttribute.java @@ -11,4 +11,5 @@ public interface FileAttribute { String MA_ERROR_FILENAME = "errorfilename"; String MA_SENT_DIR = "sentdir"; String MA_SENT_FILENAME = "sentfilename"; + String MA_FILENAME_EXTENSION = "filename_extension"; } diff --git a/Server/src/main/java/org/openas2/partner/Partnership.java b/Server/src/main/java/org/openas2/partner/Partnership.java index 09dedc43..98e23504 100644 --- a/Server/src/main/java/org/openas2/partner/Partnership.java +++ b/Server/src/main/java/org/openas2/partner/Partnership.java @@ -2,8 +2,10 @@ import org.openas2.OpenAS2Exception; import org.openas2.cert.CertificateNotFoundException; +import org.openas2.util.FileUtil; import org.openas2.util.Properties; +import java.io.IOException; import java.io.Serializable; import java.util.HashMap; import java.util.Iterator; @@ -37,11 +39,13 @@ public class Partnership implements Serializable { /* partnership definition attributes */ public static final String PA_SUBJECT = "subject"; // Subject sent in messages public static final String PA_CONTENT_TYPE = "content_type"; // optional content type for mime parts + public static final String PA_USE_DYNAMIC_CONTENT_TYPE_MAPPING = "use_dynamic_content_type_mapping"; // use file extension to Content-Type mapping + public static final String PA_CONTENT_TYPE_MAPPING_FILE = "content_type_mapping_file"; // file containing file extension to Content-Type mapping public static final String PA_CONTENT_TRANSFER_ENCODING = "content_transfer_encoding"; // optional content transfer enc value public static final String PA_SET_CONTENT_TRANSFER_ENCODING_HTTP = "set_content_transfer_encoding_http_header"; // See as an HTTP header public static final String PA_REMOVE_PROTECTION_ATTRIB = "remove_cms_algorithm_protection_attrib"; // Some AS2 systems do not support the attribute public static final String PA_SET_CONTENT_TRANSFER_ENCODING_OMBP = "set_content_transfer_encoding_on_outer_mime_bodypart"; // optional content transfer enc value - public static final String PA_RESEND_REQUIRES_NEW_MESSAGE_ID = "resend_requires_new_message_id"; // list of nme/value pairs for setting custom mime headers + public static final String PA_RESEND_REQUIRES_NEW_MESSAGE_ID = "resend_requires_new_message_id"; // list of name/value pairs for setting custom mime headers public static final String PA_COMPRESSION_TYPE = "compression"; public static final String PA_SIGNATURE_ALGORITHM = "sign"; public static final String PA_ENCRYPTION_ALGORITHM = "encrypt"; @@ -80,6 +84,9 @@ public class Partnership implements Serializable { private Map receiverIDs; private Map senderIDs; private String name; + private java.util.Properties overrideContentTypeFromFileExtensionMap = null; + private java.util.Properties contentTypeFromFileExtensionMap = null; + private boolean useDynamicContentTypeLookup = false; public String getName() { return name; @@ -173,7 +180,59 @@ public boolean matches(Partnership partnership) { } - public String getAlias(String partnershipType) throws OpenAS2Exception { + public boolean isUseDynamicContentTypeLookup() { + return useDynamicContentTypeLookup; + } + + /** This method is called if the partnership is configured to use dynamic mappings. + * It will check that there are either system or partnership specific mappings available + * load them into a partnership mapping cache. + * @param useDynamicContentTypeLookup - if true then enable dynamic mapping + * @throws OpenAS2Exception + * @throws IOException + */ + public void setUseDynamicContentTypeLookup(boolean useDynamicContentTypeLookup) throws OpenAS2Exception, IOException { + if (useDynamicContentTypeLookup) { + // Make sure there is a lookup available + // If there is a partnership specific override then make the partnership use + // that otherwise point it at the system mapping if available + String contentTypeMapFilename = getAttribute(Partnership.PA_CONTENT_TYPE_MAPPING_FILE); + if (contentTypeMapFilename != null) { + if (Properties.getContentTypeMap() != null) { + // Copy the system level mapping in first then override/add the custom mappings + overrideContentTypeFromFileExtensionMap = new java.util.Properties(); + overrideContentTypeFromFileExtensionMap.putAll(Properties.getContentTypeMap()); + overrideContentTypeFromFileExtensionMap.putAll(FileUtil.loadProperties(contentTypeMapFilename)); + } else { + // Get the override map + setOverrideContentTypeFromFileExtension(FileUtil.loadProperties(contentTypeMapFilename)); + } + // Configure this partnership to use the override lookup + contentTypeFromFileExtensionMap = overrideContentTypeFromFileExtensionMap; + } else { + // Set the partnership to use the global map + contentTypeFromFileExtensionMap = Properties.getContentTypeMap(); + } + // If there is no map to do the lookup throw an excpetion + if (this.contentTypeFromFileExtensionMap == null) { + throw new OpenAS2Exception("Trying to use Content-Type mapping functionality but no mappings loaded."); + } + } + this.useDynamicContentTypeLookup = useDynamicContentTypeLookup; + } + + public String getContentTypeFromFileExtension(String key) { + if (contentTypeFromFileExtensionMap == null) { + return null; + } + return (String) contentTypeFromFileExtensionMap.get(key); + } + + public void setOverrideContentTypeFromFileExtension(java.util.Properties contentTypeFromFileExtension) { + this.overrideContentTypeFromFileExtensionMap = contentTypeFromFileExtension; + } + + public String getAlias(String partnershipType) throws OpenAS2Exception { String alias = null; if (partnershipType == PTYPE_RECEIVER) { diff --git a/Server/src/main/java/org/openas2/partner/XMLPartnershipFactory.java b/Server/src/main/java/org/openas2/partner/XMLPartnershipFactory.java index 7f5d21a4..bbcd7453 100644 --- a/Server/src/main/java/org/openas2/partner/XMLPartnershipFactory.java +++ b/Server/src/main/java/org/openas2/partner/XMLPartnershipFactory.java @@ -209,7 +209,15 @@ public void loadPartnership(Map partners, List part // read in the partnership attributes loadAttributes(node, partnership); - + // Now check if we need to enable Content-Type mappings for this partnership + if ("true".equalsIgnoreCase(partnership.getAttributeOrProperty(Partnership.PA_USE_DYNAMIC_CONTENT_TYPE_MAPPING, "false"))) { + try { + partnership.setUseDynamicContentTypeLookup(true); + } catch (IOException e) { + logger.error("Error setting up dynamic Content-Type lookup: " + e.getMessage(), e); + throw new OpenAS2Exception("Partnership failed to be set up correctly for dynamic Content-Type lookup: " + getName()); + } + } // add the partnership to the list of available partnerships partnerships.add(partnership); diff --git a/Server/src/main/java/org/openas2/processor/receiver/MessageBuilderModule.java b/Server/src/main/java/org/openas2/processor/receiver/MessageBuilderModule.java index 915cc1d1..4b01c668 100644 --- a/Server/src/main/java/org/openas2/processor/receiver/MessageBuilderModule.java +++ b/Server/src/main/java/org/openas2/processor/receiver/MessageBuilderModule.java @@ -20,6 +20,7 @@ import org.openas2.processor.resender.ResenderModule; import org.openas2.processor.sender.SenderModule; import org.openas2.util.AS2Util; +import org.openas2.util.FileUtil; import org.openas2.util.IOUtil; import org.openas2.util.Properties; @@ -280,6 +281,8 @@ public Message buildBaseMessage(String filename) throws OpenAS2Exception { public void addMessageMetadata(Message msg, String filename) throws OpenAS2Exception { msg.setAttribute(FileAttribute.MA_FILENAME, filename); msg.setPayloadFilename(filename); + // Set the filename extension if it has one + msg.setAttribute(FileAttribute.MA_FILENAME_EXTENSION, FileUtil.getFilenameExtension(filename)); // Set a new message ID msg.updateMessageID(); // Set the sender and receiver in the Message object headers @@ -352,24 +355,35 @@ public void buildMessageData(Message msg, DataSource dataSource, String contentT msg.setData(body); } - private String getMessageContentType(Message msg) throws OpenAS2Exception { + public String getMessageContentType(Message msg) throws OpenAS2Exception { MessageParameters params = new MessageParameters(msg); - // Allow Content-Type to be overridden at partnership level or as property - String contentType = msg.getPartnership().getAttributeOrProperty(Partnership.PA_CONTENT_TYPE, null); - if (contentType == null) { - contentType = getParameter(PARAM_MIMETYPE, false); - } - if (contentType == null) { - contentType = "application/octet-stream"; - } else { - try { - contentType = ParameterParser.parse(contentType, params); - } catch (InvalidParameterException e) { - throw new OpenAS2Exception("Bad content-type" + contentType, e); + // Allow Content-Type to be overridden at partnership level or as property + String contentType = msg.getPartnership().getAttributeOrProperty(Partnership.PA_CONTENT_TYPE, null); + // The content type could be determined dynamically based on filename extension + if (msg.getPartnership().isUseDynamicContentTypeLookup()) { + String fileExtension = msg.getAttribute(FileAttribute.MA_FILENAME_EXTENSION); + if (fileExtension != null) { + String dynamicContentType = msg.getPartnership().getContentTypeFromFileExtension(fileExtension); + if (dynamicContentType != null) { + // Dynamic override found so use it + contentType = dynamicContentType; } } - return contentType; + } + if (contentType == null) { + contentType = getParameter(PARAM_MIMETYPE, false); + } + if (contentType == null) { + contentType = "application/octet-stream"; + } else { + try { + contentType = ParameterParser.parse(contentType, params); + } catch (InvalidParameterException e) { + throw new OpenAS2Exception("Bad content-type" + contentType, e); + } + } + return contentType; } private void setAdditionalMetaData(Message msg, MimeBodyPart mimeBodyPart) throws OpenAS2Exception { diff --git a/Server/src/main/java/org/openas2/processor/receiver/PollingModule.java b/Server/src/main/java/org/openas2/processor/receiver/PollingModule.java index 61e5ef62..377993cf 100644 --- a/Server/src/main/java/org/openas2/processor/receiver/PollingModule.java +++ b/Server/src/main/java/org/openas2/processor/receiver/PollingModule.java @@ -10,7 +10,7 @@ public abstract class PollingModule extends MessageBuilderModule { - private static final String PARAM_POLLING_INTERVAL = "interval"; + protected final String PARAM_POLLING_INTERVAL = "interval"; private Timer timer; private boolean busy; private String outboxDir; diff --git a/Server/src/main/java/org/openas2/util/FileUtil.java b/Server/src/main/java/org/openas2/util/FileUtil.java index 9d3bc2a6..dab44161 100644 --- a/Server/src/main/java/org/openas2/util/FileUtil.java +++ b/Server/src/main/java/org/openas2/util/FileUtil.java @@ -5,16 +5,46 @@ import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; +import java.util.Properties; public class FileUtil { //private static final Log logger = LogFactory.getLog(FileUtil.class.getSimpleName()); + public static Properties loadProperties(String filename) throws IOException { + Properties fileProps = new java.util.Properties(); + FileInputStream fis = null; + fis = new FileInputStream(filename); + try { + fileProps.load(fis); + } finally { + if (fis != null) { + fis.close(); + } + } + return fileProps; + } + + /** Attempts to extract the filename extension by searching for the last occurrence + * of a period and returning all characters following that period. + * If no period is found then it returns null. + * @param filename - the full name of the file including extension + * @return the extension of the filename excluding the period + */ + public static String getFilenameExtension(String filename) { + int period_index = filename.lastIndexOf("."); + if (period_index == -1) { + return null; + } + return filename.substring(filename.lastIndexOf(".") + 1); + } + public static void splitLineBasedFile(File sourceFile, String outputDir, long maxFileSize, boolean containsHeaderRow, String newFileBaseName, String filenamePrefix) throws OpenAS2Exception { FileReader fileReader; try { diff --git a/Server/src/main/java/org/openas2/util/Properties.java b/Server/src/main/java/org/openas2/util/Properties.java index bce69332..941a84a5 100644 --- a/Server/src/main/java/org/openas2/util/Properties.java +++ b/Server/src/main/java/org/openas2/util/Properties.java @@ -8,6 +8,7 @@ public class Properties { public static final String APP_TITLE_PROP = "app.title"; public static final String APP_BASE_DIR_PROP = "app.base.dir"; public static final String HTTP_USER_AGENT_PROP = "http.user.agent"; + public static final String OPENAS2_PROPERTIES_FILE_PROP = "openas2.properties.file"; public static final String AS2_MESSAGE_ID_FORMAT = "as2_message_id_format"; public static final String AS2_MDN_MESSAGE_ID_FORMAT = "as2_mdn_message_id_format"; @@ -20,6 +21,19 @@ public class Properties { private static final Map _properties = new HashMap(); + private static final java.util.Properties contentTypeMap = new java.util.Properties(); + + public static java.util.Properties getContentTypeMap() { + if (contentTypeMap.isEmpty()) { + return null; + } + return contentTypeMap; + } + + public static void setContentTypeMap(java.util.Properties contentTypeMappings) { + contentTypeMap.putAll(contentTypeMappings); + } + public static void setProperties(Map map) { _properties.putAll(map); } diff --git a/Server/src/test/java/org/openas2/app/BaserServerSetup.java b/Server/src/test/java/org/openas2/app/BaserServerSetup.java new file mode 100644 index 00000000..cf01f76e --- /dev/null +++ b/Server/src/test/java/org/openas2/app/BaserServerSetup.java @@ -0,0 +1,68 @@ +package org.openas2.app; + +import java.io.File; +import java.nio.file.Files; + +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.io.TempDir; +import org.openas2.TestResource; +import org.openas2.XMLSession; +import org.openas2.message.AS2Message; +import org.openas2.message.Message; +import org.openas2.partner.Partnership; +import org.openas2.util.Properties; + + +public class BaserServerSetup { + private static final TestResource RESOURCE = TestResource.forGroup("SingleServerTest"); + static String myCompanyOid = "MyCompany_OID"; + static String myPartnerOid = "PartnerA_OID"; + + // private static File openAS2AHome; + protected static XMLSession session; + protected static Message msg; + + @TempDir + public static File tmpDir; + public File openAS2PropertiesFile; + + public void refresh() throws Exception { + session.stop(); + setup(); + } + + public void createFileSystemResources() throws Exception { + tmpDir = Files.createTempDirectory("testResources").toFile(); + openAS2PropertiesFile = new File(tmpDir, "test.openas2.properties"); + } + + public void setup() throws Exception { + try { + System.setProperty("org.apache.commons.logging.Log", "org.openas2.logging.Log"); + //System.setProperty("org.openas2.logging.defaultlog", "TRACE"); + if (openAS2PropertiesFile.exists()) { + System.setProperty(Properties.OPENAS2_PROPERTIES_FILE_PROP, openAS2PropertiesFile.getAbsolutePath()); + } + session = new XMLSession(RESOURCE.get("MyCompany", "config", "config.xml").getAbsolutePath()); + msg = new AS2Message(); + Partnership myPartnership = msg.getPartnership(); + myPartnership.setSenderID(Partnership.PID_AS2, myCompanyOid); + myPartnership.setReceiverID(Partnership.PID_AS2, myPartnerOid); + myPartnership.setSenderID(Partnership.PID_AS2, myCompanyOid); + session.getPartnershipFactory().updatePartnership(msg, true); + } catch (Throwable e) { + // aid for debugging JUnit tests + System.err.println("ERROR occurred: " + ExceptionUtils.getStackTrace(e)); + throw new Exception(e); + } + } + + @AfterAll + public void tearDown() throws Exception { + session.stop(); + openAS2PropertiesFile.delete(); + System.clearProperty(Properties.OPENAS2_PROPERTIES_FILE_PROP); + } + +} diff --git a/Server/src/test/java/org/openas2/app/FilenameParsingTest.java b/Server/src/test/java/org/openas2/app/FilenameParsingTest.java index 64a06a35..3f629301 100644 --- a/Server/src/test/java/org/openas2/app/FilenameParsingTest.java +++ b/Server/src/test/java/org/openas2/app/FilenameParsingTest.java @@ -4,24 +4,20 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; -import org.openas2.TestResource; -import org.openas2.XMLSession; import org.openas2.message.AS2Message; import org.openas2.message.FileAttribute; -import org.openas2.message.Message; import org.openas2.partner.Partnership; import org.openas2.partner.PartnershipFactory; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; @ExtendWith(MockitoExtension.class) - -public class FilenameParsingTest { - private static final TestResource RESOURCE = TestResource.forGroup("SingleServerTest"); - private static String myCompanyOid = "MyCompany_OID"; - private static String myPartnerOid = "PartnerA_OID"; +@TestInstance(Lifecycle.PER_CLASS) +public class FilenameParsingTest extends BaserServerSetup { private static String testFileNamePart1 = "abc"; private static String testFileNamePart2 = "123"; private static String testFileName = testFileNamePart1 + "-" + testFileNamePart2 + ".txt"; @@ -32,23 +28,19 @@ public class FilenameParsingTest { private static String subjectAttrib = "First part filename: $attributes." + attribNamesFromFileName1 + "$ Second part filename: $attributes." + attribNamesFromFileName2 + "$"; private static String expectedSubject = "First part filename: " + testFileNamePart1 + " Second part filename: " + testFileNamePart2; - // private static File openAS2AHome; - private static XMLSession session; - private static Message msg; @BeforeAll - public static void setup() throws Exception { + public void setup() throws Exception { + super.createFileSystemResources(); + super.setup(); try { - System.setProperty("org.apache.commons.logging.Log", "org.openas2.logging.Log"); - //System.setProperty("org.openas2.logging.defaultlog", "TRACE"); - FilenameParsingTest.session = new XMLSession(RESOURCE.get("MyCompany", "config", "config.xml").getAbsolutePath()); msg = new AS2Message(); msg.setAttribute(FileAttribute.MA_FILENAME, testFileName); PartnershipFactory pf = session.getPartnershipFactory(); Partnership myPartnership = msg.getPartnership(); - myPartnership.setSenderID(Partnership.PID_AS2, myCompanyOid); - myPartnership.setReceiverID(Partnership.PID_AS2, myPartnerOid); - myPartnership.setSenderID(Partnership.PID_AS2, myCompanyOid); + myPartnership.setSenderID(Partnership.PID_AS2, BaserServerSetup.myCompanyOid); + myPartnership.setReceiverID(Partnership.PID_AS2, BaserServerSetup.myPartnerOid); + myPartnership.setSenderID(Partnership.PID_AS2, BaserServerSetup.myCompanyOid); Partnership configuredPartnership = pf.getPartnership(myPartnership, false); @@ -66,8 +58,8 @@ public static void setup() throws Exception { } @AfterAll - public static void tearDown() throws Exception { - session = null; + public void tearDown() throws Exception { + super.tearDown();; } @Test diff --git a/Server/src/test/java/org/openas2/app/OpenAS2ServerTest.java b/Server/src/test/java/org/openas2/app/OpenAS2ServerTest.java index 94287281..8e62f117 100644 --- a/Server/src/test/java/org/openas2/app/OpenAS2ServerTest.java +++ b/Server/src/test/java/org/openas2/app/OpenAS2ServerTest.java @@ -54,7 +54,7 @@ public class OpenAS2ServerTest { @BeforeAll public static void startServers() throws Exception { - tmp = Files.createTempDirectory("testResources").toFile(); + tmp = Files.createTempDirectory("testResources").toFile(); //System.setProperty("org.openas2.logging.defaultlog", "TRACE"); System.setProperty("org.apache.commons.logging.Log", "org.openas2.logging.Log"); try { @@ -111,7 +111,7 @@ public void sendMessages(TestPartner sender, TestPartner receiver) throws Except // write messages to outbox and build callables with test message objects for (int i = 0; i < msgCnt; i++) { - TestMessage testMsg = sendMessage(sender, receiver); + TestMessage testMsg = sendMessage(sender, receiver); callers.add(new Callable() { @Override public TestMessage call() throws Exception { @@ -121,7 +121,7 @@ public TestMessage call() throws Exception { } // send and verify all messages in parallel for (Future result : executorService.invokeAll(callers)) { - verifyMessageDelivery(result.get()); + verifyMessageDelivery(result.get()); } } @@ -135,12 +135,12 @@ public static void tearDown() throws Exception { // NOTE: For debugging "missing" files it is best to comment this out for (int i = 0; i < dataFolders.length; i++) { try { - FileUtils.deleteDirectory(new File(dataFolders[i])); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } + FileUtils.deleteDirectory(new File(dataFolders[i])); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } } private TestMessage sendMessage(TestPartner fromPartner, TestPartner toPartner) throws IOException { @@ -150,7 +150,7 @@ private TestMessage sendMessage(TestPartner fromPartner, TestPartner toPartner) FileUtils.write(outgoingMsg, outgoingMsgBody, "UTF-8"); System.out.println("Copying a file to send to:" + fromPartner.getOutbox()); FileUtils.copyFileToDirectory(outgoingMsg, fromPartner.getOutbox()); - //System.out.println("**** **** FILE COPIED: " + fromPartner.getOutbox() + "/" + outgoingMsg.getName()); + //System.out.println("**** **** FILE COPIED: " + fromPartner.getOutbox() + "/" + outgoingMsg.getName()); return new TestMessage(outgoingMsgFileName, outgoingMsgBody, fromPartner, toPartner); @@ -158,12 +158,12 @@ private TestMessage sendMessage(TestPartner fromPartner, TestPartner toPartner) private TestMessage getDeliveredMessage(TestMessage testMessage) throws IOException { // Wait a while - will depend on the sender poller interval how long it takes to arrive - testMessage.deliveredMsg = waitForFile(testMessage.toPartner.getInbox(), new PrefixFileFilter(testMessage.fileName), 20, TimeUnit.SECONDS); - return testMessage; + testMessage.deliveredMsg = waitForFile(testMessage.toPartner.getInbox(), new PrefixFileFilter(testMessage.fileName), 20, TimeUnit.SECONDS); + return testMessage; } private void verifyMessageDelivery(TestMessage testMessage) throws IOException { - assertThat("A file was received by " + testMessage.toPartner.getName() + " from " + testMessage.fromPartner.getName(), testMessage.deliveredMsg != null, is(true)); + assertThat("A file was received by " + testMessage.toPartner.getName() + " from " + testMessage.fromPartner.getName(), testMessage.deliveredMsg != null, is(true)); String deliveredMsgBody = FileUtils.readFileToString(testMessage.deliveredMsg, "UTF-8"); assertThat("Verify content of delivered message", deliveredMsgBody, is(testMessage.body)); @@ -181,27 +181,27 @@ private void verifyMessageDelivery(TestMessage testMessage) throws IOException { * @throws Exception */ private static TestPartner getFromFirstSendingPartnership(OpenAS2Server server) throws Exception { - PartnershipFactory pf = server.getSession().getPartnershipFactory(); + PartnershipFactory pf = server.getSession().getPartnershipFactory(); List partnerships = pf.getPartnerships(); for (Iterator iterator = partnerships.iterator(); iterator.hasNext();) { - Partnership partnership = (Partnership) iterator.next(); + Partnership partnership = (Partnership) iterator.next(); DirectoryPollingModule pollerModule = getPollingModule((XMLSession) server.getSession(), partnership); if (pollerModule != null) { - return new TestPartner(server, partnership, pollerModule); + return new TestPartner(server, partnership, pollerModule); } } return null; } private static TestPartner getFromPartnerIds(OpenAS2Server server, String senderAs2Id, String receiverAs2Id) throws Exception { - PartnershipFactory pf = server.getSession().getPartnershipFactory(); + PartnershipFactory pf = server.getSession().getPartnershipFactory(); List partnerships = pf.getPartnerships(); for (Iterator iterator = partnerships.iterator(); iterator.hasNext();) { - Partnership partnership = (Partnership) iterator.next(); - if (senderAs2Id.equals(partnership.getSenderID(Partnership.PID_AS2)) && receiverAs2Id.equals(partnership.getReceiverID(Partnership.PID_AS2))) { - DirectoryPollingModule pollerModule = getPollingModule((XMLSession) server.getSession(), partnership); - return new TestPartner(server, partnership, pollerModule); - } + Partnership partnership = (Partnership) iterator.next(); + if (senderAs2Id.equals(partnership.getSenderID(Partnership.PID_AS2)) && receiverAs2Id.equals(partnership.getReceiverID(Partnership.PID_AS2))) { + DirectoryPollingModule pollerModule = getPollingModule((XMLSession) server.getSession(), partnership); + return new TestPartner(server, partnership, pollerModule); + } } return null; } @@ -209,10 +209,10 @@ private static TestPartner getFromPartnerIds(OpenAS2Server server, String sender private static DirectoryPollingModule getPollingModule(XMLSession session, Partnership partnership) throws ComponentNotFoundException { DirectoryPollingModule dirPollMod = session.getPartnershipPoller(partnership.getName()); if (dirPollMod != null) { - return dirPollMod; + return dirPollMod; } - // Try to find a module defined poller since there is no matching poller by name. (config.xml defined pollers do not have the correct partnership name in the poller cache) - return session.getPartnershipPoller(partnership.getSenderID(Partnership.PID_AS2), partnership.getReceiverID(Partnership.PID_AS2)); + // Try to find a module defined poller since there is no matching poller by name. (config.xml defined pollers do not have the correct partnership name in the poller cache) + return session.getPartnershipPoller(partnership.getSenderID(Partnership.PID_AS2), partnership.getReceiverID(Partnership.PID_AS2)); } @SuppressWarnings("unused") @@ -229,7 +229,7 @@ private static class TestMessage { private final String body; private final TestPartner fromPartner, toPartner; @SuppressWarnings("unused") - public File deliveredMsg = null; + public File deliveredMsg = null; private TestMessage(String fileName, String body, TestPartner fromPartner, TestPartner toPartner) { this.fileName = fileName; diff --git a/Server/src/test/java/org/openas2/app/RestApiTest.java b/Server/src/test/java/org/openas2/app/RestApiTest.java index c3e2e591..054555b2 100644 --- a/Server/src/test/java/org/openas2/app/RestApiTest.java +++ b/Server/src/test/java/org/openas2/app/RestApiTest.java @@ -50,7 +50,7 @@ public class RestApiTest { // private static File openAS2AHome; private static OpenAS2Server serverInstance; private static String TEST_PARTNER_NAME = "partnerX"; - private static String TEST_PARTNERSHIP_NAME = "partnerX-partnerA"; + private static String TEST_PARTNERSHIP_NAME = TEST_PARTNER_NAME + "-partnerA"; @TempDir private static Path scratchpad; private static CloseableHttpClient httpclient; @@ -111,7 +111,6 @@ protected CredentialsProvider getCredentials() { } protected String doGet(String uriSuffix, boolean withAuth) throws IOException { - String buffer = ""; HttpGet request = new HttpGet(baseUrl + uriSuffix); HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); if (withAuth) { diff --git a/Server/src/test/java/org/openas2/message/DynamicContentTypeTest.java b/Server/src/test/java/org/openas2/message/DynamicContentTypeTest.java new file mode 100644 index 00000000..513fac00 --- /dev/null +++ b/Server/src/test/java/org/openas2/message/DynamicContentTypeTest.java @@ -0,0 +1,163 @@ +package org.openas2.message; + +import org.mockito.junit.jupiter.MockitoExtension; +import org.openas2.app.BaserServerSetup; +import org.openas2.partner.Partnership; +import org.openas2.processor.receiver.DirectoryPollingModule; +import org.openas2.util.Properties; + +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(MockitoExtension.class) +@TestInstance(Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.MethodName.class) +public class DynamicContentTypeTest extends BaserServerSetup { + private DirectoryPollingModule poller; + + public static File systemContentTypesMappingFile; + public static File partnershipContentTypesMappingFile; + + private final static String unmappedFileExtension = "data"; + private final static String xmlFileExtension = "xml"; + private final static String ediFileExtension = "edi"; + java.util.Properties contentTypeMap = Properties.getContentTypeMap(); + private Map systemMappedContentTypes = new HashMap(); + private Map partnershipMappedContentTypes = new HashMap(); + + + @BeforeAll + public void setUp() throws Exception { + super.createFileSystemResources(); + // Set up the system level mappings + systemContentTypesMappingFile = new File(tmpDir, "content_type_map.properties"); + systemMappedContentTypes.put(xmlFileExtension, "application/xml"); + systemMappedContentTypes.put(ediFileExtension, "application/edifact"); + systemMappedContentTypes.put("txt", "text/plain"); + BufferedWriter writer = new BufferedWriter(new FileWriter(systemContentTypesMappingFile)); + for (Map.Entry entry : systemMappedContentTypes.entrySet()) { + writer.write(entry.getKey() + "=" + entry.getValue() + "\n"); + } + writer.close(); + // Set up the partnership override mappings + partnershipContentTypesMappingFile = new File(tmpDir, "override_content_type_map.properties"); + partnershipMappedContentTypes.put(xmlFileExtension, "application/xml-custom"); + BufferedWriter writer2 = new BufferedWriter(new FileWriter(partnershipContentTypesMappingFile)); + for (Map.Entry entry : partnershipMappedContentTypes.entrySet()) { + writer2.write(entry.getKey() + "=" + entry.getValue() + "\n"); + } + writer2.close(); + super.setup(); + this.poller = session.getPartnershipPoller(msg.getPartnership().getName()); + } + + @AfterAll + public void tearDown() throws Exception { + super.tearDown(); + } + + @Test + public void a1_shouldHaveNoMappingEnabled() throws Exception { + // The default is to not have dynamic mapping so check + Partnership myPartnership = msg.getPartnership(); + assertFalse(myPartnership.isUseDynamicContentTypeLookup(), "Check default is mapping off."); + } + + + @Test + public void a2_shouldFailNoMapping() throws Exception { + // Make sure that there is an error if no mapping file defined but trying to use mapping + Partnership myPartnership = msg.getPartnership(); + assertThrows(Exception.class, () -> { myPartnership.setUseDynamicContentTypeLookup(true);}, "No config for Content-Type mapping should throw exception."); + } + + + @Test + public void b_shouldGetDefaultContentType() throws Exception { + // Partnership not set for dynamic mapping so should return system poller default + String testFilename = "random." + ediFileExtension; + poller.addMessageMetadata(msg, testFilename); + assertThat("Check default Content-Type returned when no mapping.", poller.getMessageContentType(msg).matches(Properties.getProperty("pollerConfigBase.mimetype", "FakeValue")), is(true)); + } + + @Test + public void c_shouldGetPartnershipMappedContentTypeWhenNoSystemMapping() throws Exception { + // Set the partnership to have dynamic mapping file + msg.getPartnership().setAttribute(Partnership.PA_CONTENT_TYPE_MAPPING_FILE, partnershipContentTypesMappingFile.getAbsolutePath()); + // Force load the partnership mapping + msg.getPartnership().setUseDynamicContentTypeLookup(true); + // Now check that we get the system property when no override and override when set + String testFilename = "random." + ediFileExtension; + poller.addMessageMetadata(msg, testFilename); + assertThat("Check system default Content-Type returned when not overridden.", poller.getMessageContentType(msg).matches(Properties.getProperty("pollerConfigBase.mimetype", "FakeValue")), is(true)); + testFilename = "random." + xmlFileExtension; + poller.addMessageMetadata(msg, testFilename); + assertThat("Check partnership mapped Content-Type returned when partnership mapping setup.", poller.getMessageContentType(msg).matches(partnershipMappedContentTypes.get(xmlFileExtension)), is(true)); + testFilename = "random." + unmappedFileExtension; + poller.addMessageMetadata(msg, testFilename); + assertThat("Check system mapped default Content-Type returned when no system or partnership mapping defined.", poller.getMessageContentType(msg).matches(Properties.getProperty("pollerConfigBase.mimetype", "FakeValue")), is(true)); + } + + @Test + public void d_shouldGetSystemMappedContentType() throws Exception { + // Append the mapping file property to custom load properties + BufferedWriter propsWriter = new BufferedWriter(new FileWriter(super.openAS2PropertiesFile, true)); + String propVal = systemContentTypesMappingFile.getAbsolutePath().replace("\\", "\\\\"); + propsWriter.write("\n" + Partnership.PA_CONTENT_TYPE_MAPPING_FILE + "=" + propVal); + propsWriter.close(); + // Now reload the session to get new properties file that then loads system mapping + super.refresh(); + // Force the partnership to have dynamic mapping enabled + msg.getPartnership().setUseDynamicContentTypeLookup(true); + String testFilename = "random." + ediFileExtension; + poller.addMessageMetadata(msg, testFilename); + assertThat("Check system mapped Content-Type returned when system mapping setup.", poller.getMessageContentType(msg).matches(systemMappedContentTypes.get(ediFileExtension)), is(true)); + testFilename = "random." + xmlFileExtension; + poller.addMessageMetadata(msg, testFilename); + assertThat("Check system mapped Content-Type returned when system mapping setup.", poller.getMessageContentType(msg).matches(systemMappedContentTypes.get(xmlFileExtension)), is(true)); + testFilename = "random." + unmappedFileExtension; + poller.addMessageMetadata(msg, testFilename); + assertThat("Check system mapped Content-Type returned when system mapping setup.", poller.getMessageContentType(msg).matches(Properties.getProperty("pollerConfigBase.mimetype", "FakeValue")), is(true)); + } + + @Test + public void e_shouldGetPartnershipOverrideMappedContentType() throws Exception { + // Append the property to globally enable dynamic mapping in custom load properties + BufferedWriter propsWriter = new BufferedWriter(new FileWriter(super.openAS2PropertiesFile, true)); + propsWriter.write("\n" + Partnership.PA_USE_DYNAMIC_CONTENT_TYPE_MAPPING + "=true"); + propsWriter.close(); + // Now reload the session to get new properties file that then loads system mapping + super.refresh(); + // Set the partnership to have dynamic mapping file + msg.getPartnership().setAttribute(Partnership.PA_CONTENT_TYPE_MAPPING_FILE, partnershipContentTypesMappingFile.getAbsolutePath()); + // Force load the override + msg.getPartnership().setUseDynamicContentTypeLookup(true); + // Now check that we get the system property when no override and override when set + String testFilename = "random." + ediFileExtension; + poller.addMessageMetadata(msg, testFilename); + assertThat("Check system mapped Content-Type returned when not overridden.", poller.getMessageContentType(msg).matches(systemMappedContentTypes.get(ediFileExtension)), is(true)); + testFilename = "random." + xmlFileExtension; + poller.addMessageMetadata(msg, testFilename); + assertThat("Check partnership mapped Content-Type returned when partnership mapping setup.", poller.getMessageContentType(msg).matches(partnershipMappedContentTypes.get(xmlFileExtension)), is(true)); + testFilename = "random." + unmappedFileExtension; + poller.addMessageMetadata(msg, testFilename); + assertThat("Check system mapped default Content-Type returned when no system or partnership mapping defined.", poller.getMessageContentType(msg).matches(Properties.getProperty("pollerConfigBase.mimetype", "FakeValue")), is(true)); + } +} diff --git a/Server/src/test/java/org/openas2/util/IOUtilTest.java b/Server/src/test/java/org/openas2/util/IOUtilTest.java index 62e0a489..d128c8a4 100644 --- a/Server/src/test/java/org/openas2/util/IOUtilTest.java +++ b/Server/src/test/java/org/openas2/util/IOUtilTest.java @@ -56,7 +56,7 @@ public Record(String filename, String format, String delimiters, boolean mergeEx }; /* Test records for polling filters - * Format is , , "excluded extensions list>, , , "excluded extensions list>, + + diff --git a/Server/src/test/resources/SingleServerTest/MyCompany/config/config.xml b/Server/src/test/resources/SingleServerTest/MyCompany/config/config.xml index ed9927a8..baa92e51 100644 --- a/Server/src/test/resources/SingleServerTest/MyCompany/config/config.xml +++ b/Server/src/test/resources/SingleServerTest/MyCompany/config/config.xml @@ -28,8 +28,8 @@ module.AS2MDNReceiverModule.https.enabled="false" module.HealthCheckModule.enabled="false" partnership_file="%home%/partnerships.xml" - pollerConfigBase.outboxdir="$properties.storageBaseDir$/outbox/$partnership.receiver.as2_id$" - pollerConfigBase.errordir="$properties.storageBaseDir$/outbox/error/$date.YYYY$-$date.MM$-$date.dd$/$partnership.receiver.as2_id$" + pollerConfigBase.outboxdir="$properties.storageBaseDir$/outbox/$partnership.sender.as2_id$/$partnership.receiver.as2_id$" + pollerConfigBase.errordir="$properties.storageBaseDir$/outbox/$partnership.sender.as2_id$/error/$date.YYYY$-$date.MM$-$date.dd$/$partnership.receiver.as2_id$" pollerConfigBase.interval="5" pollerConfigBase.defaults="sender.as2_id=$partnership.sender.as2_id$, receiver.as2_id=$partnership.receiver.as2_id$" pollerConfigBase.sendfilename="true" @@ -104,18 +104,7 @@ sendfilename="true" format="sender.as2_id, receiver.as2_id, attributes.filename" mimetype="application/EDI-X12"/> - - - + + diff --git a/changes.txt b/changes.txt index 51658195..cea151fb 100644 --- a/changes.txt +++ b/changes.txt @@ -1,3 +1,9 @@ +Version 3.8.0 - 2023-11-07 +This is an enhancement and minor bugfix release: + **IMPORTANT NOTE**: Please review upgrade notes in the RELEASE-NOTES.md if you are upgrading + + 1. Support for configurable dynamic Content-Type based on the file extension. See documentation section 7.5 "Setting Content Type" + Version 3.7.0 - 2023-09-12 This is an enhancement and minor bugfix release: **IMPORTANT NOTE**: Please review upgrade notes in the RELEASE-NOTES.md if you are upgrading diff --git a/docs/OpenAS2HowTo.odt b/docs/OpenAS2HowTo.odt index dc20f5cd..113d69f9 100644 Binary files a/docs/OpenAS2HowTo.odt and b/docs/OpenAS2HowTo.odt differ diff --git a/docs/OpenAS2HowTo.pdf b/docs/OpenAS2HowTo.pdf index be39fe55..186ef574 100644 Binary files a/docs/OpenAS2HowTo.pdf and b/docs/OpenAS2HowTo.pdf differ diff --git a/pom.xml b/pom.xml index 937eac2a..4958892b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 net.sf.openas2 OpenAS2 - 3.7.0 + 3.8.0 OpenAS2 pom @@ -82,7 +82,7 @@ commons-cli commons-cli - 1.5.0 + 1.6.0 commons-logging @@ -97,7 +97,7 @@ com.h2database h2 - 2.2.222 + 2.2.224 @@ -148,7 +148,7 @@ commons-io commons-io - 2.13.0 + 2.15.0 @@ -171,13 +171,13 @@ com.fasterxml.jackson.core jackson-databind - 2.15.2 + 2.15.3 jar com.fasterxml.jackson.module jackson-module-jaxb-annotations - 2.15.2 + 2.15.3 org.glassfish.jersey.media @@ -199,17 +199,17 @@ com.sun.xml.bind jaxb-core - 4.0.3 + 4.0.4 com.sun.xml.bind jaxb-impl - 4.0.3 + 4.0.4 io.sentry sentry - 6.28.0 + 6.33.0