diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..fda2fd7944c5fcb5df29d289e91c58f20f864e01
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+.idea/
+.mule/
+services/
+target/
\ No newline at end of file
diff --git a/README.md b/README.md
index 55f109a6aef9ec807adea3a319aba4204024b3be..3ad9258df97ef4f703116e191f269fed7bab2fb5 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,14 @@
-# AssertObjectEquals Extension
+# Assert Object Equals Extension
-Add description ...
-
-
-...
-
-
-...
-
-
-Add this dependency to your application pom.xml
+To install this Module in the Anypoint Studio add this dependency to your application pom.xml
```
de.codecentric.mule.modulesassert-object-equals-module
-1.0.0-SNAPSHOT
+1.0.175-SNAPSHOTmule-plugin
```
+
+Since this is not yet published on Maven repository, you have to install this project to your local Maven repository.
+Please use following command:
+mvn clean install -DskipTests
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 2bc96a40be01f71984d93c9674b489ab76ad6451..85be6dc50fe9fce9b6d295b26034c6cabe325c47 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,9 +6,9 @@
4.0.0de.codecentric.mule.modulesassert-object-equals-module
- 1.0.0-SNAPSHOT
+ 1.0.175-SNAPSHOTmule-extension
- AssertObjectEquals Extension
+ Assert Object Equals Extensionorg.mule.extensions
@@ -16,4 +16,26 @@
1.1.3
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ 2.9.8
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.9.8
+
+
+ org.apache.commons
+ commons-lang3
+ 3.0
+
+
+ org.xmlunit
+ xmlunit-core
+ 2.3.0
+
+
diff --git a/src/main/java/de/codecentric/mule/modules/assertobjectequals/internal/AssertObjectEqualsExtension.java b/src/main/java/de/codecentric/mule/modules/assertobjectequals/internal/AssertObjectEqualsExtension.java
index 95dafef62d9d919a614d1f8eaa0226189fb391c3..ba59a11cf4a8cae623d2fe565e0445271241b0ef 100644
--- a/src/main/java/de/codecentric/mule/modules/assertobjectequals/internal/AssertObjectEqualsExtension.java
+++ b/src/main/java/de/codecentric/mule/modules/assertobjectequals/internal/AssertObjectEqualsExtension.java
@@ -1,9 +1,15 @@
package de.codecentric.mule.modules.assertobjectequals.internal;
-import org.mule.runtime.extension.api.annotation.Extension;
import org.mule.runtime.extension.api.annotation.Configurations;
+import org.mule.runtime.extension.api.annotation.Extension;
import org.mule.runtime.extension.api.annotation.dsl.xml.Xml;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* This is the main class of an extension, is the entry point from which configurations, connection providers, operations
@@ -14,4 +20,27 @@ import org.mule.runtime.extension.api.annotation.dsl.xml.Xml;
@Configurations(AssertObjectEqualsConfiguration.class)
public class AssertObjectEqualsExtension {
+ /**
+ * This method is used for calling implementation logic ONLY in testing/development purposes.
+ * Once we reach "production" stage this method should be completely removed.
+ */
+ public static void main(String[] args) throws Exception{
+
+ //This is the expected file from my local disk. I'm imitating sending of real file from srt/test/resources directory to the mule component.
+ File expectedFile = new File("/Users/ddanijel/AnypointStudio/studio-workspace/test-aoe-module/src/test/resources/expected-file.xml");
+ FileInputStream expectedFileInputStream = new FileInputStream(expectedFile);
+ BufferedInputStream expectedFileBuffered = new BufferedInputStream(expectedFileInputStream);
+
+ //This is the actual file from my local disk. I'm imitating sending of real file from srt/test/resources directory to the mule component.
+ File actualFile = new File("/Users/ddanijel/AnypointStudio/studio-workspace/test-aoe-module/src/test/resources/actual-file.xml");
+ FileInputStream actualFileInputStream = new FileInputStream(actualFile);
+ BufferedInputStream actualFileBuffered = new BufferedInputStream(actualFileInputStream);
+
+ List additionalOptions = new ArrayList<>();
+ additionalOptions.add("['addresses'][#]['timestamp'] ignore");
+
+ AssertObjectEqualsOperations aoe = new AssertObjectEqualsOperations();
+ //aoe.compareObjects(expectedFileBuffered, actualFileBuffered, additionalOptions);
+ aoe.compareXml(expectedFileBuffered, actualFileBuffered, "IGNORE_COMMENTS");
+ }
}
diff --git a/src/main/java/de/codecentric/mule/modules/assertobjectequals/internal/AssertObjectEqualsOperations.java b/src/main/java/de/codecentric/mule/modules/assertobjectequals/internal/AssertObjectEqualsOperations.java
index ef332fd52be1463cd5b9620fc1dfdbe077b95ae4..1272de8f030b35a86e6fc9d82dd37542ced7311b 100644
--- a/src/main/java/de/codecentric/mule/modules/assertobjectequals/internal/AssertObjectEqualsOperations.java
+++ b/src/main/java/de/codecentric/mule/modules/assertobjectequals/internal/AssertObjectEqualsOperations.java
@@ -1,11 +1,18 @@
package de.codecentric.mule.modules.assertobjectequals.internal;
-import static org.mule.runtime.extension.api.annotation.param.MediaType.ANY;
-
-import org.mule.runtime.extension.api.annotation.param.MediaType;
-import org.mule.runtime.extension.api.annotation.param.Config;
-import org.mule.runtime.extension.api.annotation.param.Connection;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.mule.runtime.extension.api.annotation.param.Optional;
+import org.mule.runtime.extension.api.annotation.param.display.DisplayName;
+import org.mule.runtime.extension.api.annotation.param.display.Example;
+import org.mule.runtime.extension.api.annotation.values.OfValues;
+import org.xmlunit.builder.DiffBuilder;
+import org.xmlunit.diff.DefaultComparisonFormatter;
+import org.xmlunit.diff.Diff;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.*;
/**
* This class is a container for operations, every public method in this class will be taken as an extension operation.
@@ -13,25 +20,167 @@ import org.mule.runtime.extension.api.annotation.param.Connection;
public class AssertObjectEqualsOperations {
/**
- * Example of an operation that uses the configuration and a connection instance to perform some action.
+ * Compare two objects. Drill down into {@link Map} and {@link List}, use {@link Object#equals(Object)} for all other
+ * classes.
+ *
+ * @param expected
+ * The expected value. Automatic conversions are provided:
+ *
+ *
InputStream is read/parsed as Json
+ *
byte[] is parsed as Json
+ *
String is parsed as Json when it starts with [ or { (after trim()
+ *
+ * Remember: Encoding for Json is always UTF8
+ *
+ * @param actual
+ * The actual value. Automatic conversions are provided:
+ *
+ *
InputStream is read/parsed as Json
+ *
byte[] is parsed as Json
+ *
String is parsed as Json when it starts with [ or { (after trim()
+ *
+ * Remember: Encoding for Json is always UTF8
+ *
+ * @param pathOptions
+ * Options for path patterns to control the comparison. Syntax of one List entry: Zero to n path
+ * parts. The parts can have the following syntax:
+ *
+ *
?: Wildcard one, matches one element in a path
+ *
*: Wildcard any, matches zero to n elements in a path
+ *
[#]: List wildcard, matches a list entry with any index
+ *
[0]: Matches a list entry with the given number. 0 or positive numbers: Count from
+ * beginning, negative number: Cound from end (-1 is last element)
+ *
['.*']: Matches a map entry where the key must match the given regular expression. If you
+ * need a ' in the expression, just write ''. The example '.*' matches all keys.
+ *
+ * A space as separator. One or more of the following options (case not relevant):
+ *
+ * CONTAINS_ONLY_ON_MAPS: The actual value entry set of maps can contain more values than the expected set. So you tests do not fail
+ * when there are more elements than expected in the result.
+ *
+ * CHECK_MAP_ORDER: The order of map entries is checked. The default is to ignore order of map entries.
+ *
+ * IGNORE: The actual node and its subtree is ignored completely.
+ *
+ * @return The original payload.
+ *
+ * @throws Exception
+ * When comparison fails or on technical problems (e.g. parsing)
*/
- @MediaType(value = ANY, strict = false)
- public String retrieveInfo(@Config AssertObjectEqualsConfiguration configuration, @Connection AssertObjectEqualsConnection connection){
- return "Using Configuration [" + configuration.getConfigId() + "] with Connection id [" + connection.getId() + "]";
+ public void compareObjects(
+ @DisplayName("Expected value") @Example("#[MunitTools::getResourceAsStream()]") Object expected,
+ @DisplayName("Actual value") @Example("#[payload]") Object actual,
+ @DisplayName("Contains only on maps") @Optional(defaultValue = "false") boolean containsOnlyOnMaps,
+ @DisplayName("Check map order") @Optional(defaultValue = "false") boolean checkMapOrder,
+ @DisplayName("Path patterns+options") List pathOptions)
+
+ throws Exception {
+
+ Object expectedObj = convert2Object(expected);
+ Object actualObj = convert2Object(actual);
+ ObjectComparator comparator = createComparator(containsOnlyOnMaps, checkMapOrder, pathOptions == null ? new ArrayList() : pathOptions);
+ Collection diff = comparator.compare(expectedObj, actualObj);
+
+ if (!diff.isEmpty()) {
+ StringBuilder messageBuilder = new StringBuilder();
+ for (String s : diff) {
+ if (messageBuilder.length() > 0) {
+ messageBuilder.append(System.lineSeparator());
+ }
+ messageBuilder.append(s);
+ }
+ throw new AssertionError("\n" + messageBuilder);
+ }
}
/**
- * Example of a simple operation that receives a string parameter and returns a new string message that will be set on the payload.
+ * Compare two XML documents. See XMLUnit Wiki} how this works
+ *
+ * @param expected
+ * The expected value, XML as String, InputStream, byte[] or DOM tree.
+ * @param actual
+ * The actual value, XML as String, InputStream, byte[] or DOM tree.
+ * @param xmlCompareOption
+ * How to compare the XML documents.
+ *
+ * IGNORE_COMMENTS: Will remove all comment-Tags "" from test- and control-XML before
+ * comparing.
+ *
+ * IGNORE_WHITESPACE: Ignore whitespace by removing all empty text nodes and trimming the non-empty ones.
+ *
+ * NORMALIZE_WHITESPACE: Normalize Text-Elements by removing all empty text nodes and normalizing the
+ * non-empty ones.
+ *
+ * @return The original payload.
+ *
+ * @throws Exception
+ * When comparison fails or on technical problems (e.g. parsing)
*/
- @MediaType(value = ANY, strict = false)
- public String sayHi(String person) {
- return buildHelloMessage(person);
+ public void compareXml(
+ @DisplayName("Expected value") @Example("#[MunitTools::getResourceAsStream()]") Object expected,
+ @DisplayName("Actual value") @Example("#[payload]") Object actual,
+ @DisplayName("XML compare option") @OfValues(XmlCompareOption.class) String xmlCompareOption)
+
+ throws Exception {
+
+ DiffBuilder diffBuilder = DiffBuilder.compare(expected).withTest(actual);
+
+ switch (xmlCompareOption) {
+ case "IGNORE_COMMENTS":
+ diffBuilder = diffBuilder.ignoreComments();
+ break;
+ case "IGNORE_WHITESPACE":
+ diffBuilder = diffBuilder.ignoreWhitespace();
+ break;
+ case "NORMALIZE_WHITESPACE":
+ diffBuilder = diffBuilder.normalizeWhitespace();
+ break;
+ default:
+ throw new IllegalArgumentException("I forgot to implement for a new enum constant.");
+ }
+
+ Diff diff = diffBuilder.build();
+
+ if (diff.hasDifferences()) {
+ throw new AssertionError(diff.toString(new DefaultComparisonFormatter()));
+ }
}
- /**
- * Private Methods are not exposed as operations
- */
- private String buildHelloMessage(String person) {
- return "Hello " + person + "!!!";
+ private Object convert2Object(Object value) throws JsonProcessingException, IOException {
+ if (value == null) {
+ return null;
+ } else if (value instanceof InputStream) {
+ return new ObjectMapper().readerFor(Object.class).readValue((InputStream) value);
+ } else if (value instanceof byte[]) {
+ return new ObjectMapper().readerFor(Object.class).readValue((byte[]) value);
+ } else if (value instanceof CharSequence) {
+ String trimmed = ((CharSequence) value).toString().trim();
+ if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
+ return new ObjectMapper().readerFor(Object.class).readValue(trimmed);
+ } else {
+ return value;
+ }
+ } else {
+ return value;
+ }
}
+
+ private ObjectComparator createComparator(boolean containsOnlyOnMaps, boolean checkMapOrder, List pathOptionsStrings) {
+ PathPatternParser ppp = new PathPatternParser();
+ Collection patterns = new ArrayList<>();
+
+ for (String pathOptionString : pathOptionsStrings) {
+ patterns.add(ppp.parse(pathOptionString));
+ }
+ EnumSet rootOptions = EnumSet.noneOf(PathOption.class);
+ if (containsOnlyOnMaps) {
+ rootOptions.add(PathOption.CONTAINS_ONLY_ON_MAPS);
+ }
+ if (checkMapOrder) {
+ rootOptions.add(PathOption.CHECK_MAP_ORDER);
+ }
+
+ ObjectCompareOptionsFactory optionFactory = new PatternBasedOptionsFactory(rootOptions, patterns);
+ return new ObjectComparator(optionFactory);
+ }
}
diff --git a/src/main/java/de/codecentric/mule/modules/assertobjectequals/internal/ObjectComparator.java b/src/main/java/de/codecentric/mule/modules/assertobjectequals/internal/ObjectComparator.java
new file mode 100644
index 0000000000000000000000000000000000000000..cddca4f0040bd6774d50dbe5ca85fa7306ef0021
--- /dev/null
+++ b/src/main/java/de/codecentric/mule/modules/assertobjectequals/internal/ObjectComparator.java
@@ -0,0 +1,209 @@
+package de.codecentric.mule.modules.assertobjectequals.internal;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Compare two objects, drill down into List an
+ */
+public class ObjectComparator {
+ private ObjectCompareOptionsFactory optionFactory;
+
+ private class State {
+ final Path path;
+ final Object expected;
+ final Object actual;
+ final EnumSet options;
+
+ private State(Path path, Object expected, Object actual, EnumSet options) {
+ this.path = path;
+ this.expected = expected;
+ this.actual = actual;
+ this.options = options;
+ }
+
+ public State(Object expected, Object actual) {
+ path = new Path();
+ this.expected = expected;
+ this.actual = actual;
+ options = optionFactory.createOptions(null, path);
+ }
+
+ public State listEntry(int listIndex, int listSize, Object expected, Object actual) {
+ Path next = path.listEntry(listIndex, listSize);
+ return new State(next, expected, actual, optionFactory.createOptions(options, next));
+ }
+
+ public State mapEntry(String key, Object expected, Object actual) {
+ Path next = path.mapEntry(key);
+ return new State(next, expected, actual, optionFactory.createOptions(options, next));
+ }
+ }
+
+ public ObjectComparator(ObjectCompareOptionsFactory optionFactory) {
+ this.optionFactory = optionFactory;
+ }
+
+ /**
+ * Compare two objects. Drill down into {@link Map} and {@link List}, use {@link Object#equals(Object)} for all other
+ * classes.
+ *
+ * @param expected
+ * The expected value.
+ * @param actual
+ * The actual value.
+ * @return Textual description of the differences.
+ */
+ public Collection compare(Object expected, Object actual) {
+ State state = new State(expected, actual);
+ Collection diffs = new ArrayList();
+ compare(state, diffs);
+ return diffs;
+ }
+
+ private void compare(State state, Collection diffs) {
+ if (state.options.contains(PathOption.IGNORE)) {
+ return;
+ }
+ if (state.expected == null) {
+ if (state.actual == null) {
+ // ok, null equals null
+ } else { // actual != null
+ diffs.add("at '" + state.path + "', expected is null, actual " + state.actual);
+ }
+ } else { // expected != null
+ if (state.actual == null) {
+ diffs.add("at '" + state.path + "', expected " + state.expected + ", actual is null");
+ } else { // actual != null
+ compareNonNullObjects(state, diffs);
+ }
+ }
+ }
+
+ private void compareNonNullObjects(State state, Collection diffs) {
+ if (state.expected instanceof List) {
+ if (state.actual instanceof List) {
+ compareLists(state, diffs);
+ } else {
+ diffs.add("at '" + state.path + "', expected List, but found " + state.actual.getClass().getName());
+ }
+ } else if (state.expected instanceof Map) {
+ if (state.actual instanceof Map) {
+ compareMaps(state, diffs);
+ } else {
+ diffs.add("at '" + state.path + "', expected Map, but found " + state.actual.getClass().getName());
+ }
+ } else {
+ if (!state.expected.equals(state.actual)) {
+ diffs.add("at '" + state.path + "', expected " + state.expected + ", but found " + state.actual);
+ }
+ }
+ }
+
+ private void compareLists(State state, Collection diffs) {
+ @SuppressWarnings("unchecked")
+ List