Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a plugin system for deserializing internal objects #163

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ from any language or framework you are in.
## Table of Contents

<!-- TOC -->
* [BridgeService](#bridgeservice)
* [Table of Contents](#table-of-contents)
* [Background](#background)
* [Demo Project](#demo-project)
* [Release Notes](#release-notes)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:

Expand Down
3 changes: 2 additions & 1 deletion ReleaseNotes.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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("[email protected]"));
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;
}

Expand Down Expand Up @@ -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("[email protected]"));
message.addRecipient(Message.RecipientType.TO, new InternetAddress("[email protected]"));
Expand All @@ -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 = "<H1>I am the html part</H1>";
messageBodyPart2.setContent(htmlText, "text/html");
multipart.addBodyPart(messageBodyPart2);

return multipart;
}
}

5 changes: 5 additions & 0 deletions integroBridgeService/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@
<version>5.12.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.9.12</version>
</dependency>
</dependencies>

<parent>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Object> apply(Object in_object);
}

Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {

Check warning on line 19 in integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/IBSPluginManager.java

View check run for this annotation

Codecov / codecov/patch

integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/IBSPluginManager.java#L19

Added line #L19 was not covered by tests

public static void loadPlugins() {
ExtractionPlugins.loadPlugins();
}

static class ExtractionPlugins {

Check warning on line 25 in integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/IBSPluginManager.java

View check run for this annotation

Codecov / codecov/patch

integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/IBSPluginManager.java#L25

Added line #L25 was not covered by tests

static Set<IBSDeserializerPlugin> plugins = new LinkedHashSet<>();

static Map<String, Object> apply(Object in_object) {
Optional<IBSDeserializerPlugin> 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<Class<? extends IBSDeserializerPlugin>> 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);
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Object> apply(Object in_object) {
MimeMessage l_message = (MimeMessage) in_object;
Map<String, Object> 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<Map<String, Object>> l_parts = new ArrayList<>();
for (int p = 0; p < l_multipart.getCount(); p++) {
BodyPart l_bodyPart = l_multipart.getBodyPart(p);
Map<String, Object> l_partMap = new HashMap<>();
l_partMap.put("contentType", l_bodyPart.getContentType());
l_partMap.put("description", l_bodyPart.getDescription());
l_partMap.put("content", l_bodyPart.getContent());
l_parts.add(l_partMap);
}
l_returnMap.put("content", l_parts);
} else {
l_returnMap.put("content", l_message.getContent());
}
l_returnMap.put("contentType", l_message.getContentType());
l_returnMap.put("description", l_message.getDescription());
l_returnMap.put("receivedDate", l_message.getReceivedDate());
l_returnMap.put("sentDate", l_message.getSentDate());
l_returnMap.put("size", l_message.getSize());
l_returnMap.put("flags", l_message.getFlags());
l_returnMap.put("messageNumber", l_message.getMessageNumber());
l_returnMap.put("lineCount", l_message.getLineCount());
} catch (MessagingException | IOException e) {
throw new RuntimeException(e);
}
return l_returnMap;
}


}
Loading