diff --git a/README.md b/README.md
index 40dcc3ca..d73846d8 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,8 @@ from any language or framework you are in.
## Table of Contents
+* [BridgeService](#bridgeservice)
+ * [Table of Contents](#table-of-contents)
* [Background](#background)
* [Demo Project](#demo-project)
* [Release Notes](#release-notes)
@@ -36,6 +38,8 @@ from any language or framework you are in.
* [Lists and Arrays](#lists-and-arrays)
* [Complex Types](#complex-types)
* [Files](#files)
+ * [Results](#results)
+ * [Deserialization Plugins](#deserialization-plugins)
* [Managing Timeouts](#managing-timeouts)
* [Setting Timeout Globally](#setting-timeout-globally)
* [Setting a Timeout for the Call Session](#setting-a-timeout-for-the-call-session)
@@ -49,7 +53,7 @@ from any language or framework you are in.
* [Making Assertions](#making-assertions)
* [Duration-Based Assertions](#duration-based-assertions)
* [Error Management](#error-management)
- * [Contribution](#contribution)
+ * [Contributing to the Project](#contributing-to-the-project)
* [Known Errors](#known-errors)
* [Linked Error](#linked-error)
* [Known Issues and Limitations](#known-issues-and-limitations)
@@ -495,6 +499,21 @@ The result is then:
}
```
+## Results
+Results are returned as a JSON Object. Serializable return objects are deserialized. For objects that are not Serializable, we perform an operatio called scraping, which involves sequentially calling the simple getters of the object, and include the results in the result object.
+
+In the case of complex classes where the scraping is not sufficient, you can define a deserialization plugin for that class. This allow you to be specific regarding how the object can be returned. For mor information on this you can refer to the chapter [Deserialization Plugins](#deserialization-plugins).
+
+### Deserialization Plugins
+As of version 2.11.17, we introduced the notion of plugins. For now you can customize how an object is deserialized. This can be usefull when the default object serialization is incomplete or not to your liking.
+
+To create your plugin you need to:
+* Implement the interface methods of the interface `com.adobe.campaign.tests.bridge.service.plugins.IBSDeserializerPlugin`.
+* Add the package of the plugin to the environment variable `IBS.DESERIALIZATION.PLUGINS`.
+
+There is an example of the plugin in the tests under `integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/plugins/deserializer/MimeExtractionPluginDeserializer.java`.
+
+
## Managing Timeouts
As of version 2.11.6 we now introduce the notion of timeouts. This means that after a declared time a call will be
@@ -873,7 +892,7 @@ The BridgeService exception is how the bridgeService manages underlying errors.
* The step at which the error occured
* The stack trace of the originating exception
-## Contribution
+## Contributing to the Project
There are two main docs for contributing:
diff --git a/ReleaseNotes.md b/ReleaseNotes.md
index b2bf8b0c..e5409fa5 100644
--- a/ReleaseNotes.md
+++ b/ReleaseNotes.md
@@ -1,5 +1,6 @@
# Bridge Service - RELEASE NOTES
-## 2.11.17
+## 2.11.17 In-Progress
+* **New Feature** [#160 Introduce Extraction Plugins](https://github.com/adobe/bridgeService/issues/160). We have now introduced a new plugin mechanism so you can define how an object you are expecting should be deserialized. Please refer to the chapter on ["Deserialization Plugins"](README.md#deserialization-plugins) in the README doc.
* [#159 Errors when deserializing Milti-Part Mime Object](https://github.com/adobe/bridgeService/issues/159): We have included a couple of resilience features to better handle the deserialization of complex Objects. This includes:
* Nested scraping. We allow a nested scraping of objects.
* Ignoring calls that throw errors. We now log the error and continue with the next call.
diff --git a/bridgeService-data/src/main/java/com/adobe/campaign/tests/bridge/testdata/one/MimeMessageMethods.java b/bridgeService-data/src/main/java/com/adobe/campaign/tests/bridge/testdata/one/MimeMessageMethods.java
index a4fc4927..d523f67e 100644
--- a/bridgeService-data/src/main/java/com/adobe/campaign/tests/bridge/testdata/one/MimeMessageMethods.java
+++ b/bridgeService-data/src/main/java/com/adobe/campaign/tests/bridge/testdata/one/MimeMessageMethods.java
@@ -26,7 +26,7 @@ public static MimeMessage createMessage(String in_suffix) throws MessagingExcept
message.setFrom(new InternetAddress(from));
message.addRecipient(Message.RecipientType.TO, new InternetAddress("a@b.com"));
message.setSubject("a subject by me " + in_suffix);
- message.setText("a content by yours truely " + in_suffix);
+ message.setText("a content by yours truly " + in_suffix);
return message;
}
@@ -115,6 +115,8 @@ public static MimeMessage createMultiPartAlternativeMessage(String in_suffix) th
// Step 2: Create a default MimeMessage object
MimeMessage message = new MimeMessage(session);
+ message.setSentDate(new java.util.Date());
+
// Step 3: Set From, To, Subject
message.setFrom(new InternetAddress("from@example.com"));
message.addRecipient(Message.RecipientType.TO, new InternetAddress("to@example.com"));
@@ -138,5 +140,24 @@ public static MimeMessage createMultiPartAlternativeMessage(String in_suffix) th
message.setContent(multipart);
return message;
}
+
+ public static Multipart createMutliPartHTML() throws MessagingException {
+
+ Multipart multipart = new MimeMultipart();
+
+ // PLAIN TEXT
+ BodyPart messageBodyPart = new MimeBodyPart();
+ messageBodyPart.setText("Here is your plain text message");
+ multipart.addBodyPart(messageBodyPart);
+
+ // HTML TEXT
+ // HTML TEXT
+ BodyPart messageBodyPart2 = new MimeBodyPart();
+ String htmlText = "
I am the html part
";
+ messageBodyPart2.setContent(htmlText, "text/html");
+ multipart.addBodyPart(messageBodyPart2);
+
+ return multipart;
+ }
}
diff --git a/integroBridgeService/pom.xml b/integroBridgeService/pom.xml
index 2520f3e3..40231c4b 100644
--- a/integroBridgeService/pom.xml
+++ b/integroBridgeService/pom.xml
@@ -162,6 +162,11 @@
5.12.0
test
+
+ org.reflections
+ reflections
+ 0.9.12
+
diff --git a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/plugins/IBSDeserializerPlugin.java b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/plugins/IBSDeserializerPlugin.java
new file mode 100644
index 00000000..8ddb4b22
--- /dev/null
+++ b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/plugins/IBSDeserializerPlugin.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2022 Adobe
+ * All Rights Reserved.
+ *
+ * NOTICE: Adobe permits you to use, modify, and distribute this file in
+ * accordance with the terms of the Adobe license agreement accompanying
+ * it.
+ */
+package com.adobe.campaign.tests.bridge.plugins;
+
+import java.util.Map;
+
+public interface IBSDeserializerPlugin {
+ /**
+ * Lets us know if the given Object applies to this Plugin
+ * @param in_object an object we want to deserialize
+ * @return true if we can use this plugin for the given Object
+ */
+ public boolean appliesTo(Object in_object);
+
+ /**
+ * Applies the plugin to the given object
+ * @param in_object an object we want to deserialize
+ * @return A Map of data extracted from the object
+ */
+ public Map apply(Object in_object);
+}
+
diff --git a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/BridgeServiceFactory.java b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/BridgeServiceFactory.java
index 81735084..d2da4245 100644
--- a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/BridgeServiceFactory.java
+++ b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/BridgeServiceFactory.java
@@ -21,6 +21,7 @@ public class BridgeServiceFactory {
* Creates a Java Call Object given a JSON as a String
* @param in_requestJSON A JSON Object as a String
* @return A Java Call Object
+ * @throws JsonProcessingException thrown when the JSON return object could not be created
*/
public static JavaCalls createJavaCalls(String in_requestJSON) throws JsonProcessingException {
LogManagement.logStep(LogManagement.STD_STEPS.ANALYZING_PAYLOAD);
@@ -54,6 +55,7 @@ public static String transformJavaCallResultsToJSON(JavaCallResults in_callResul
* Creates a ServiceAccess Object given a JSON as a String
* @param in_requestJSON A JSON Object as a String
* @return A Service Access Object
+ * @throws JsonProcessingException thrown when the JSON return object could not be created
*/
public static ServiceAccess createServiceAccess(String in_requestJSON) throws JsonProcessingException {
LogManagement.logStep(LogManagement.STD_STEPS.ANALYZING_PAYLOAD);
diff --git a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/CallContent.java b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/CallContent.java
index 8dd0576c..ea73c3f1 100644
--- a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/CallContent.java
+++ b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/CallContent.java
@@ -72,6 +72,7 @@ public void setArgs(Object[] args) {
/**
* Returns the method object that is defined by this class
*
+ * @param in_class Class for which we want to fetch the method from
* @return the method object
*/
public Method fetchMethod(Class in_class) {
@@ -87,6 +88,7 @@ public Method fetchMethod(Class in_class) {
/**
* Returns the method object that is defined by this class
*
+ * @param in_class the class we want to instantiate
* @return the method object
*/
public Constructor fetchConstructor(Class in_class) {
@@ -136,6 +138,7 @@ protected Method fetchMethod() throws ClassNotFoundException {
/**
* Calls the java method defined in this class
*
+ * @param iClassLoader The class loader used for loading and executing the class and method.
* @return the value of this call
*/
public Object call(IntegroBridgeClassLoader iClassLoader) {
diff --git a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/ConfigValueHandlerIBS.java b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/ConfigValueHandlerIBS.java
index cc040889..9b49e1ce 100644
--- a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/ConfigValueHandlerIBS.java
+++ b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/ConfigValueHandlerIBS.java
@@ -64,7 +64,9 @@ public void activate(String in_value) {
SECRETS_BLOCK_OUTPUT("IBS.SECRETS.BLOCK.OUTPUT", "true", false,
"When set to true, we forbid the system from returning the value in the return payload. This is to avoid XSS attacks. This should ONLY be true when you want to debug."),
DESERIALIZATION_DEPTH_LIMIT("IBS.DESERIALIZATION.DEPTH.LIMIT", "1", false,
- "This value sets the maximum depth of the deserialization.");
+ "This value sets the maximum depth of the deserialization."),
+ PLUGIN_DESERIALIZATION_PATH(
+ "IBS.PLUGINS.PATH", null, false, "The package path in which IBS should search for the plugins.");
public final String systemName;
public final String defaultValue;
diff --git a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/IBSPluginManager.java b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/IBSPluginManager.java
new file mode 100644
index 00000000..889f21cf
--- /dev/null
+++ b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/IBSPluginManager.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2022 Adobe
+ * All Rights Reserved.
+ *
+ * NOTICE: Adobe permits you to use, modify, and distribute this file in
+ * accordance with the terms of the Adobe license agreement accompanying
+ * it.
+ */
+package com.adobe.campaign.tests.bridge.service;
+
+import com.adobe.campaign.tests.bridge.plugins.IBSDeserializerPlugin;
+import com.adobe.campaign.tests.bridge.service.exceptions.IBSConfigurationException;
+import org.reflections.Reflections;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.*;
+
+public class IBSPluginManager {
+
+ public static void loadPlugins() {
+ ExtractionPlugins.loadPlugins();
+ }
+
+ static class ExtractionPlugins {
+
+ static Set plugins = new LinkedHashSet<>();
+
+ static Map apply(Object in_object) {
+ Optional applicablePlugin = plugins.stream().filter(a -> a.appliesTo(in_object)).findFirst();
+
+ if (!applicablePlugin.isPresent()) {
+ return new HashMap<>();
+ }
+ return applicablePlugin.get().apply(in_object);
+ }
+
+ static void clearPlugins() {
+ plugins = new LinkedHashSet<>();
+ }
+
+ /**
+ * Checks if there is a plugin that applies to the given object
+ *
+ * @param in_object an arbitrary object
+ * @return true if there is a plugin that applies to the given
+ */
+ static boolean appliesTo(Object in_object) {
+ return plugins.stream().anyMatch(a -> a.appliesTo(in_object));
+ }
+
+ static void loadPlugins() {
+ if (ConfigValueHandlerIBS.PLUGIN_DESERIALIZATION_PATH.isSet()) {
+ Reflections reflections = new Reflections(
+ ConfigValueHandlerIBS.PLUGIN_DESERIALIZATION_PATH.fetchValue());
+ Set> classes = reflections.getSubTypesOf(
+ IBSDeserializerPlugin.class);
+
+ for (Class extends IBSDeserializerPlugin> implementingClass : classes) {
+ addPlugin(implementingClass);
+ }
+ }
+ }
+
+ static void addPlugin(Class extends IBSDeserializerPlugin> implementingClass) {
+ try {
+ Constructor> ctor = implementingClass.getConstructor();
+
+ plugins.add((IBSDeserializerPlugin) ctor.newInstance());
+ } catch (InstantiationException e) {
+ throw new IBSConfigurationException("The given plugin " + implementingClass.getName()
+ + " cannot be instantiated.", e);
+ } catch (IllegalAccessException e) {
+ throw new IBSConfigurationException("We do not have access to the plugin " + implementingClass.getName()
+ + ". This is probably a scope issue. Please review this and rerun the IBS.", e);
+ } catch (InvocationTargetException e) {
+ throw new IBSConfigurationException(
+ "The constructor of the plugin " + implementingClass.getName() + " threw an exception.", e);
+ } catch (NoSuchMethodException e) {
+ throw new IBSConfigurationException(
+ "The plugin " + implementingClass.getName() + " does not have a default constructor.", e);
+ }
+ }
+ }
+
+}
diff --git a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/IntegroAPI.java b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/IntegroAPI.java
index 2e09d2cb..5be74f4f 100644
--- a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/IntegroAPI.java
+++ b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/IntegroAPI.java
@@ -56,6 +56,8 @@ public static void startServices(int port) {
throw new IBSConfigurationException("The port " + port + " is not currently free.");
}
+ IBSPluginManager.loadPlugins();
+
if (Boolean.parseBoolean(ConfigValueHandlerIBS.SSL_ACTIVE.fetchValue())) {
File l_file = new File(ConfigValueHandlerIBS.SSL_KEYSTORE_PATH.fetchValue());
if (!l_file.exists()) {
diff --git a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/JavaCallResults.java b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/JavaCallResults.java
index a80a2d38..0965986b 100644
--- a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/JavaCallResults.java
+++ b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/JavaCallResults.java
@@ -61,10 +61,10 @@ public void addResult(String in_key, Object callResult, long in_callDuration) {
}
/**
- * If the given object is a key, we return the duration stored for that key. Otherwise we return the
+ * Used for assertions where we assert the duration of the executions. If the given object is a key, we return the duration stored for that key. Otherwise we return the exact 'long' value of the given duration.
*
* @param in_keyOrValue An object that is either a key or an
- * @return
+ * @return A duration in milliseconds
*/
public Long expandDurations(Object in_keyOrValue) {
if (getCallDurations().containsKey(in_keyOrValue) ) {
diff --git a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MetaUtils.java b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MetaUtils.java
index 2128a006..59eba2b5 100644
--- a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MetaUtils.java
+++ b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MetaUtils.java
@@ -97,6 +97,12 @@ public static boolean isExtractable(Method in_method) {
* @return A Map of serialized Objects
*/
public static Object extractValuesFromObject(Object in_object) {
+ //Check if there is a plugin for this object
+
+ if (IBSPluginManager.ExtractionPlugins.appliesTo(in_object)) {
+ return IBSPluginManager.ExtractionPlugins.apply(in_object);
+ }
+
return extractValuesFromObject(in_object, 0);
}
diff --git a/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/plugins/deserializer/MimeExtractionPluginDeserializer.java b/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/plugins/deserializer/MimeExtractionPluginDeserializer.java
new file mode 100644
index 00000000..8c981e31
--- /dev/null
+++ b/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/plugins/deserializer/MimeExtractionPluginDeserializer.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2022 Adobe
+ * All Rights Reserved.
+ *
+ * NOTICE: Adobe permits you to use, modify, and distribute this file in
+ * accordance with the terms of the Adobe license agreement accompanying
+ * it.
+ */
+package com.adobe.campaign.tests.bridge.plugins.deserializer;
+
+import com.adobe.campaign.tests.bridge.plugins.IBSDeserializerPlugin;
+
+import java.io.IOException;
+import java.util.*;
+
+import javax.mail.*;
+import javax.mail.internet.MimeMessage;
+import javax.mail.internet.MimeMultipart;
+
+public class MimeExtractionPluginDeserializer implements IBSDeserializerPlugin {
+
+ @Override
+ public boolean appliesTo(Object in_object) {
+ if (in_object instanceof MimeMessage) {
+ return true;
+ }
+
+ if (in_object instanceof Message) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public Map apply(Object in_object) {
+ MimeMessage l_message = (MimeMessage) in_object;
+ Map l_returnMap = new HashMap<>();
+ try {
+ l_returnMap.put("subject", l_message.getSubject());
+ l_returnMap.put("from", l_message.getFrom());
+ l_returnMap.put("recipients", l_message.getAllRecipients());
+
+ if (l_message.getContent() instanceof MimeMultipart) {
+ MimeMultipart l_multipart = (MimeMultipart) l_message.getContent();
+ List