From def61300727353418dce3e2768148c9da1ed1b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Schlath=C3=B6lter?= Date: Thu, 16 Dec 2021 14:02:40 +0100 Subject: [PATCH] fix(#821): Allow direct access to path/jsonPath in variables --- .../consol/citrus/context/TestContext.java | 27 ++- .../consol/citrus/util/IsJsonPredicate.java | 22 ++ .../consol/citrus/util/IsXmlPredicate.java | 19 ++ .../util/VariableExpressionIterator.java | 200 ------------------ .../variable/SegmentVariableExtractor.java | 77 +++++++ .../SegmentVariableExtractorRegistry.java | 161 ++++++++++++++ .../variable/VariableExpressionIterator.java | 151 +++++++++++++ .../VariableExpressionSegmentMatcher.java | 138 ++++++++++++ .../VariableExpressionSegmentMatcherTest.java | 112 ++++++++++ .../citrus/context/TestContextFactory.java | 22 ++ .../com/consol/citrus/util/MessageUtils.java | 5 +- .../citrus/context/TestContextTest.java | 108 +++++++--- .../JsonPathSegmentVariableExtractor.java | 42 ++++ .../variable/extractor/segment/jsonPath | 1 + .../JsonPathSegmentVariableExtractorIT.java | 12 ++ .../JsonPathSegmentVariableExtractorTest.java | 56 +++++ .../JsonPathSegmentVariableExtractorIT.xml | 44 ++++ .../xml/XpathSegmentVariableExtractor.java | 67 ++++++ .../citrus/variable/extractor/segment/xpath | 1 + .../XpathSegmentVariableExtractorIT.java | 12 ++ .../XmlPathSegmentVariableExtractorTest.java | 71 +++++++ .../XpathSegmentVariableExtractorIT.xml | 44 ++++ 22 files changed, 1156 insertions(+), 236 deletions(-) create mode 100644 core/citrus-api/src/main/java/com/consol/citrus/util/IsJsonPredicate.java create mode 100644 core/citrus-api/src/main/java/com/consol/citrus/util/IsXmlPredicate.java delete mode 100644 core/citrus-api/src/main/java/com/consol/citrus/util/VariableExpressionIterator.java create mode 100644 core/citrus-api/src/main/java/com/consol/citrus/variable/SegmentVariableExtractor.java create mode 100644 core/citrus-api/src/main/java/com/consol/citrus/variable/SegmentVariableExtractorRegistry.java create mode 100644 core/citrus-api/src/main/java/com/consol/citrus/variable/VariableExpressionIterator.java create mode 100644 core/citrus-api/src/main/java/com/consol/citrus/variable/VariableExpressionSegmentMatcher.java create mode 100644 core/citrus-api/src/test/java/com/consol/citrus/variable/VariableExpressionSegmentMatcherTest.java create mode 100644 validation/citrus-validation-json/src/main/java/com/consol/citrus/json/JsonPathSegmentVariableExtractor.java create mode 100644 validation/citrus-validation-json/src/main/resources/META-INF/citrus/variable/extractor/segment/jsonPath create mode 100644 validation/citrus-validation-json/src/test/java/com/consol/citrus/integration/JsonPathSegmentVariableExtractorIT.java create mode 100644 validation/citrus-validation-json/src/test/java/com/consol/citrus/json/JsonPathSegmentVariableExtractorTest.java create mode 100644 validation/citrus-validation-json/src/test/resources/com/consol/citrus/integration/JsonPathSegmentVariableExtractorIT.xml create mode 100644 validation/citrus-validation-xml/src/main/java/com/consol/citrus/xml/XpathSegmentVariableExtractor.java create mode 100644 validation/citrus-validation-xml/src/main/resources/META-INF/citrus/variable/extractor/segment/xpath create mode 100644 validation/citrus-validation-xml/src/test/java/com/consol/citrus/integration/XpathSegmentVariableExtractorIT.java create mode 100644 validation/citrus-validation-xml/src/test/java/com/consol/citrus/xml/XmlPathSegmentVariableExtractorTest.java create mode 100644 validation/citrus-validation-xml/src/test/resources/com/consol/citrus/integration/XpathSegmentVariableExtractorIT.xml diff --git a/core/citrus-api/src/main/java/com/consol/citrus/context/TestContext.java b/core/citrus-api/src/main/java/com/consol/citrus/context/TestContext.java index 0a6a1f5694..de892a8738 100644 --- a/core/citrus-api/src/main/java/com/consol/citrus/context/TestContext.java +++ b/core/citrus-api/src/main/java/com/consol/citrus/context/TestContext.java @@ -59,10 +59,11 @@ import com.consol.citrus.spi.ReferenceResolverAware; import com.consol.citrus.util.DefaultTypeConverter; import com.consol.citrus.util.TypeConverter; -import com.consol.citrus.util.VariableExpressionIterator; +import com.consol.citrus.variable.VariableExpressionIterator; import com.consol.citrus.validation.MessageValidatorRegistry; import com.consol.citrus.validation.matcher.ValidationMatcherRegistry; import com.consol.citrus.variable.GlobalVariables; +import com.consol.citrus.variable.SegmentVariableExtractorRegistry; import com.consol.citrus.variable.VariableUtils; import com.consol.citrus.xml.namespace.NamespaceContextBuilder; import org.slf4j.Logger; @@ -175,6 +176,11 @@ public class TestContext implements ReferenceResolverAware, TestActionListenerAw */ private LogModifier logModifier; + /** + * SegmentVariableExtractorRegistry + */ + private SegmentVariableExtractorRegistry segmentVariableExtractorRegistry; + /** * Default constructor */ @@ -225,7 +231,7 @@ public Object getVariableObject(final String variableExpression) { } else if (variables.containsKey(variableName)) { return variables.get(variableName); } else { - return VariableExpressionIterator.getLastExpressionValue(variableName, this); + return VariableExpressionIterator.getLastExpressionValue(variableName, this, segmentVariableExtractorRegistry.getSegmentValueExtractors()); } } @@ -236,7 +242,6 @@ public Object getVariableObject(final String variableExpression) { * * @param variableName the name of the new variable * @param value the new variable value - * @return * @throws CitrusRuntimeException */ public void setVariable(final String variableName, Object value) { @@ -652,6 +657,22 @@ public void setAfterTest(List afterTest) { this.afterTest = afterTest; } + /** + * Obtains the segmentVariableExtractorRegistry + * @return + */ + public SegmentVariableExtractorRegistry getSegmentVariableExtractorRegistry() { + return segmentVariableExtractorRegistry; + } + + /** + * Specifies the segmentVariableExtractorRegistry + * @param segmentVariableExtractorRegistry + */ + public void setSegmentVariableExtractorRegistry(SegmentVariableExtractorRegistry segmentVariableExtractorRegistry) { + this.segmentVariableExtractorRegistry = segmentVariableExtractorRegistry; + } + /** * Gets the global message processors for given direction. * @return diff --git a/core/citrus-api/src/main/java/com/consol/citrus/util/IsJsonPredicate.java b/core/citrus-api/src/main/java/com/consol/citrus/util/IsJsonPredicate.java new file mode 100644 index 0000000000..97e907ff9a --- /dev/null +++ b/core/citrus-api/src/main/java/com/consol/citrus/util/IsJsonPredicate.java @@ -0,0 +1,22 @@ +package com.consol.citrus.util; + +import java.util.function.Predicate; + +/** + * Tests if a string represents a Json. An empty string is considered to be a + * valid Json. + */ +public class IsJsonPredicate implements Predicate { + + public static IsJsonPredicate INSTANCE = new IsJsonPredicate(); + + @Override + public boolean test(String toTest) { + + if (toTest != null) { + toTest = toTest.trim(); + } + + return toTest != null && (toTest.length() == 0 || toTest.startsWith("{") || toTest.startsWith("[")); + } +} \ No newline at end of file diff --git a/core/citrus-api/src/main/java/com/consol/citrus/util/IsXmlPredicate.java b/core/citrus-api/src/main/java/com/consol/citrus/util/IsXmlPredicate.java new file mode 100644 index 0000000000..b309ad7ea0 --- /dev/null +++ b/core/citrus-api/src/main/java/com/consol/citrus/util/IsXmlPredicate.java @@ -0,0 +1,19 @@ +package com.consol.citrus.util; + +import java.util.function.Predicate; + +/** + * Tests if a string represents a XML. An empty string is considered to be a valid XML. + */ +public class IsXmlPredicate implements Predicate { + + public static IsXmlPredicate INSTANCE = new IsXmlPredicate(); + @Override + public boolean test(String toTest) { + + if (toTest != null) { + toTest = toTest.trim(); + } + return toTest!=null && (toTest.length() == 0 || toTest.startsWith("<")); + } +} \ No newline at end of file diff --git a/core/citrus-api/src/main/java/com/consol/citrus/util/VariableExpressionIterator.java b/core/citrus-api/src/main/java/com/consol/citrus/util/VariableExpressionIterator.java deleted file mode 100644 index be159e7f84..0000000000 --- a/core/citrus-api/src/main/java/com/consol/citrus/util/VariableExpressionIterator.java +++ /dev/null @@ -1,200 +0,0 @@ -package com.consol.citrus.util; - -import com.consol.citrus.context.TestContext; -import com.consol.citrus.exceptions.CitrusRuntimeException; -import com.consol.citrus.util.VariableExpressionIterator.VariableSegment; -import org.springframework.util.ReflectionUtils; -import org.springframework.util.StringUtils; -import java.lang.reflect.Array; -import java.lang.reflect.Field; -import java.util.Iterator; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * This {@link Iterator} uses a regular expression pattern to match individual - * segments of a variableExpression. Each segment of a variable expression - * represents a bean, either stored as variable in the TestContext (first - * segment) or a property of the previous bean (all other segments). The - * iterator provides VariableSegments which provide the name and an optional - * index as well as the variable/property value corresponding to the segment. - *

- * Example:

- * var1.persons[2].firstnames[0]
- *
- * The iterator will provide the following VariableSegments for this expression - *
    - *
  1. the variable with name var1 from the TestContext
  2. - *
  3. the third element of the persons property of the variable retrieved in the previous step
  4. - *
  5. the first element of the firstnames property of the property retrieved in the previous step
  6. - *
- - * - * @author Thorsten Schlathoelter - * - */ -public class VariableExpressionIterator implements Iterator { - - /** - * Pattern to parse a variable expression of type 'var1.var2[1].var3' - */ - private static final Pattern varPathPattern = Pattern.compile("(([^\\[\\]\\.]+)(\\[([0-9])\\])?)(\\.|$)"); - - /** - * The group index for the name of the property - */ - private static final int NAME_GROUP = 2; - - /** - * The group index for the index property when accessing array elements - */ - private static final int INDEX_GROUP = 4; - - /** - * The variable expression to iterate - */ - private String variableExpression; - - /** - * The matcher used to match the variableExpression - */ - private Matcher matcher; - - /** - * The nextSegment that is provided by the Iterator - */ - private VariableSegment nextSegment; - - /** - * The TestContext - */ - private TestContext testContext; - - public VariableExpressionIterator(String variableExpression, TestContext testContext) { - this.variableExpression = variableExpression; - this.testContext = testContext; - - matcher = varPathPattern.matcher(variableExpression); - - if (matcher.find()) { - nextSegment = createSegmentValue(matcher.group(NAME_GROUP), matcher.group(INDEX_GROUP), testContext); - } else { - throw new CitrusRuntimeException( - String.format("Cannot match a segment on variableExpression:", variableExpression)); - } - } - - @Override - public boolean hasNext() { - return nextSegment != null; - } - - @Override - public VariableSegment next() { - VariableSegment ret = nextSegment; - if (matcher.find()) { - nextSegment = createSegmentValue(matcher.group(NAME_GROUP), matcher.group(INDEX_GROUP), - nextSegment.getSegmentValue()); - } else { - nextSegment = null; - } - - return ret; - } - - private VariableSegment createSegmentValue(String name, String index, Object parentValue) { - - Object segmentValue = null; - if (parentValue instanceof TestContext) { - segmentValue = getValueFromContext(name); - } else { - segmentValue = getValueFromObjectField(name, parentValue); - } - - if (StringUtils.hasLength(index)) { - segmentValue = getIndexedElement(name, index, segmentValue); - } - - return new VariableSegment(name, index, segmentValue); - } - - private Object getValueFromObjectField(String name, Object parentValue) { - Field field = ReflectionUtils.findField(parentValue.getClass(), name); - if (field == null) { - throw new CitrusRuntimeException(String.format("Failed to get variable - unknown field '%s' on type %s", - name, parentValue.getClass().getName())); - } - - ReflectionUtils.makeAccessible(field); - return ReflectionUtils.getField(field, parentValue); - } - - private Object getValueFromContext(String name) { - Object matchedVariableValue = null; - if (testContext.getVariables().containsKey(name)) { - matchedVariableValue = testContext.getVariables().get(name); - } else { - throw new CitrusRuntimeException("Unknown variable '" + variableExpression + "'"); - } - return matchedVariableValue; - } - - private Object getIndexedElement(String name, String index, Object arrayValue) { - Object indexValue = null; - if (arrayValue.getClass().isArray()) { - indexValue = Array.get(arrayValue, Integer.valueOf(index)); - } else { - throw new CitrusRuntimeException( - String.format("Expected an instance of Array type. Cannot retrieve indexed property %s from %s ", - name, arrayValue.getClass().getName())); - } - return indexValue; - } - - public static Object getLastExpressionValue(String variableExpression, TestContext testContext) { - VariableSegment segment = null; - VariableExpressionIterator iterator = new VariableExpressionIterator(variableExpression, testContext); - while (iterator.hasNext()) { - segment = iterator.next(); - } - - return segment.getSegmentValue(); - } - - public static class VariableSegment { - - /** - * The name of the variableExpression segment - */ - private final String name; - - /** - * An optional index if the VariableSegment represents an array - */ - private final String index; - - /** - * The evaluated value for this segment - */ - private final Object segmentValue; - - public VariableSegment(String name, String index, Object segmentValue) { - super(); - this.name = name; - this.index = index; - this.segmentValue = segmentValue; - } - - public String getName() { - return name; - } - - public String getIndex() { - return index; - } - - public Object getSegmentValue() { - return segmentValue; - } - } -} diff --git a/core/citrus-api/src/main/java/com/consol/citrus/variable/SegmentVariableExtractor.java b/core/citrus-api/src/main/java/com/consol/citrus/variable/SegmentVariableExtractor.java new file mode 100644 index 0000000000..1b3b5af9f2 --- /dev/null +++ b/core/citrus-api/src/main/java/com/consol/citrus/variable/SegmentVariableExtractor.java @@ -0,0 +1,77 @@ +/* + * Copyright 2006-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.consol.citrus.variable; + +import com.consol.citrus.context.TestContext; +import com.consol.citrus.exceptions.CitrusRuntimeException; +import com.consol.citrus.spi.ResourcePathTypeResolver; +import com.consol.citrus.spi.TypeResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +/** + * Class extracting values of segments of VariableExpressions. + * + * @author Thorsten Schlathoelter + */ +public interface SegmentVariableExtractor { + + /** Logger */ + Logger LOG = LoggerFactory.getLogger(SegmentVariableExtractor.class); + + /** Segment variable extractor resource lookup path */ + String RESOURCE_PATH = "META-INF/citrus/variable/extractor/segment"; + + /** Type resolver to find custom segment variable extractors on classpath via resource path lookup */ + TypeResolver TYPE_RESOLVER = new ResourcePathTypeResolver(RESOURCE_PATH); + + /** + * Resolves extractor from resource path lookup with given extractor resource name. Scans classpath for extractor meta information + * with given name and returns instance of extractor. Returns optional instead of throwing exception when no extractor + * could be found. + * @return + */ + static Collection lookup() { + try { + Map extractors = TYPE_RESOLVER.resolveAll(); + return extractors.values(); + } catch (CitrusRuntimeException e) { + LOG.warn(String.format("Failed to resolve segment variable extractor from resource '%s'", RESOURCE_PATH)); + } + + return Collections.emptyList(); + } + + /** + * Extract variables from given object. + * @param object the object of which to extract the value + * @param matcher + */ + boolean canExtract(TestContext testContext, Object object, VariableExpressionSegmentMatcher matcher); + + /** + * Extract variables from given object. Implementations should throw a CitrusRuntimeException + * @param object the object of which to extract the value + * @param matcher + */ + Object extractValue(TestContext testContext, Object object, VariableExpressionSegmentMatcher matcher); + +} diff --git a/core/citrus-api/src/main/java/com/consol/citrus/variable/SegmentVariableExtractorRegistry.java b/core/citrus-api/src/main/java/com/consol/citrus/variable/SegmentVariableExtractorRegistry.java new file mode 100644 index 0000000000..8b036dacfb --- /dev/null +++ b/core/citrus-api/src/main/java/com/consol/citrus/variable/SegmentVariableExtractorRegistry.java @@ -0,0 +1,161 @@ +package com.consol.citrus.variable; + +import com.consol.citrus.context.TestContext; +import com.consol.citrus.exceptions.CitrusRuntimeException; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * Simple registry holding all available segment variable extractor implementations. Test context can ask this registry for + * the extractors managed by this registry in order to access variable content from the TestContext expressed by variable expressions. + *

+ * Registry provides all known {@link SegmentVariableExtractor}s. + * + * @author Thorsten Schlathoelter + */ +public class SegmentVariableExtractorRegistry { + + /** + * SegmentVariableExtractors to extract values from value representations of individual segments. + */ + private final List segmentValueExtractors = new ArrayList<>(Arrays.asList(MapVariableExtractor.INSTANCE, ObjectFieldValueExtractor.INSTANCE)); + + public SegmentVariableExtractorRegistry() { + segmentValueExtractors.addAll(SegmentVariableExtractor.lookup()); + } + + /** + * Obtain the segment variable extractors managed by the registry + * + * @return + */ + public List getSegmentValueExtractors() { + return segmentValueExtractors; + } + + /** + * Base class for segment variable extractors that ensures that an exception is thrown upon no match. + */ + public static abstract class AbstractSegmentVariableExtractor implements SegmentVariableExtractor { + + @Override + public final Object extractValue(TestContext testContext, Object object, VariableExpressionSegmentMatcher matcher) { + Object matchedValue = doExtractValue(testContext, object, matcher); + + if (matchedValue == null) { + throw new CitrusRuntimeException(String.format("Unable to match variable content for '%s' " + + "of variable expression '%s'", + matcher.getSegmentExpression(), matcher.getVariableExpression())); + } + + return matchedValue; + } + + protected abstract Object doExtractValue(TestContext testContext, Object object, VariableExpressionSegmentMatcher matcher); + } + + /** + * Base class for extractors that can operate on indexed values. + */ + public static abstract class IndexedSegmentVariableExtractor extends AbstractSegmentVariableExtractor { + + public final Object doExtractValue(TestContext testContext, Object object, VariableExpressionSegmentMatcher matcher) { + + Object extractedValue = doExtractIndexedValue(testContext, object, matcher); + + if (matcher.getSegmentIndex() != -1) { + extractedValue = getIndexedElement(matcher, extractedValue); + } + return extractedValue; + } + + /** + * Get the index element from an indexed value. + * + * @param matcher + * @param indexedValue + * @return + */ + private Object getIndexedElement(VariableExpressionSegmentMatcher matcher, Object indexedValue) { + if (indexedValue.getClass().isArray()) { + return Array.get(indexedValue, matcher.getSegmentIndex()); + } else { + throw new CitrusRuntimeException( + String.format("Expected an instance of Array type. Cannot retrieve indexed property %s from %s ", + matcher.getSegmentExpression(), indexedValue.getClass().getName())); + } + } + + /** + * Extract the indexed value from the object + * + * @param object + * @param matcher + * @return + */ + protected abstract Object doExtractIndexedValue(TestContext testContext, Object object, VariableExpressionSegmentMatcher matcher); + } + + /** + * SegmentVariableExtractor that accesses the segment value by a {@link Field} of the parentObject + */ + public static class ObjectFieldValueExtractor extends IndexedSegmentVariableExtractor { + + public static ObjectFieldValueExtractor INSTANCE = new ObjectFieldValueExtractor(); + + private ObjectFieldValueExtractor() { + // singleton + } + + @Override + protected Object doExtractIndexedValue(TestContext testContext, Object parentObject, VariableExpressionSegmentMatcher matcher) { + Field field = ReflectionUtils.findField(parentObject.getClass(), matcher.getSegmentExpression()); + if (field == null) { + throw new CitrusRuntimeException(String.format("Failed to get variable - unknown field '%s' on type %s", + matcher.getSegmentExpression(), parentObject.getClass().getName())); + } + + ReflectionUtils.makeAccessible(field); + return ReflectionUtils.getField(field, parentObject); + } + + @Override + public boolean canExtract(TestContext testContext, Object object, VariableExpressionSegmentMatcher matcher) { + return object != null && !(object instanceof String); + } + } + + /** + * SegmentVariableExtractor that accesses the segment value from a {@link Map}. The extractor uses the segment expression + * as key into the map. + */ + public static class MapVariableExtractor extends IndexedSegmentVariableExtractor { + + public static MapVariableExtractor INSTANCE = new MapVariableExtractor(); + + private MapVariableExtractor() { + // singleton + } + + @Override + protected Object doExtractIndexedValue(TestContext testContext, Object parentObject, VariableExpressionSegmentMatcher matcher) { + + Object matchedValue = null; + if (parentObject instanceof Map) { + matchedValue = ((Map) parentObject).get(matcher.getSegmentExpression()); + } + return matchedValue; + } + + @Override + public boolean canExtract(TestContext testContext, Object object, VariableExpressionSegmentMatcher matcher) { + return object instanceof Map; + } + } +} diff --git a/core/citrus-api/src/main/java/com/consol/citrus/variable/VariableExpressionIterator.java b/core/citrus-api/src/main/java/com/consol/citrus/variable/VariableExpressionIterator.java new file mode 100644 index 0000000000..ad8be2b525 --- /dev/null +++ b/core/citrus-api/src/main/java/com/consol/citrus/variable/VariableExpressionIterator.java @@ -0,0 +1,151 @@ +package com.consol.citrus.variable; + +import com.consol.citrus.context.TestContext; +import com.consol.citrus.exceptions.CitrusRuntimeException; +import com.consol.citrus.variable.VariableExpressionIterator.VariableSegment; + +import java.util.*; + +/** + * This {@link Iterator} uses a regular expression pattern to match individual + * segments of a variableExpression. Each segment of a variable expression + * represents a bean, either stored as variable in the TestContext (first + * segment) or a property of the previous bean (all other segments). The + * iterator provides VariableSegments which provide the name and an optional + * index as well as the variable/property value corresponding to the segment. + *

+ * Example: + * var1.persons[2].firstnames[0] + *

+ * The iterator will provide the following VariableSegments for this expression + *

    + *
  1. the variable with name var1 from the TestContext
  2. + *
  3. the third element of the persons property of the variable retrieved in the previous step
  4. + *
  5. the first element of the firstnames property of the property retrieved in the previous step
  6. + *
+ + * + * @author Thorsten Schlathoelter + * + */ +public class VariableExpressionIterator implements Iterator { + + /** + * The matcher used to match the variableExpression + */ + private final VariableExpressionSegmentMatcher matcher; + + /** + * The TestContext + */ + private final TestContext testContext; + + /** + * The SegmentVariableExtractor + */ + private final List segmentValueExtractors; + + /** + * The nextSegment that is provided by the Iterator. The nextSegment value is always looked ahead to be able to support hasNext. + */ + private VariableSegment nextSegment; + + public VariableExpressionIterator(String variableExpression, TestContext testContext, List segmentValueExtractors) { + this.testContext = testContext; + this.segmentValueExtractors = segmentValueExtractors; + + matcher = new VariableExpressionSegmentMatcher(variableExpression); + + if (matcher.nextMatch()) { + nextSegment = createSegmentValue(testContext.getVariables()); + } else { + throw new CitrusRuntimeException( + String.format("Cannot match a segment on variableExpression: %s", variableExpression)); + } + } + + /** + * Returns true if the iterator has a next + * @return + */ + @Override + public boolean hasNext() { + return nextSegment != null; + } + + /** + * Returns the next value and looks ahead for yet another next value. + * @return + */ + @Override + public VariableSegment next() { + VariableSegment currentSegment = nextSegment; + + // Look ahead next segment + if (matcher.nextMatch()) { + nextSegment = createSegmentValue(currentSegment.getSegmentValue()); + } else { + nextSegment = null; + } + + return currentSegment; + } + + /** + * Create the segment value from the current match + * @param parentValue + * @return + */ + private VariableSegment createSegmentValue(Object parentValue) { + + Object segmentValue = segmentValueExtractors.stream().filter(extractor->extractor.canExtract(testContext, parentValue, matcher)).findFirst().map(extractor->extractor.extractValue(testContext, parentValue, matcher)).orElse(null); + return new VariableSegment(matcher.getSegmentExpression(), matcher.getSegmentIndex(), segmentValue); + } + + public static Object getLastExpressionValue(String variableExpression, TestContext testContext, List extractors) { + VariableSegment segment = null; + VariableExpressionIterator iterator = new VariableExpressionIterator(variableExpression, testContext, extractors); + while (iterator.hasNext()) { + segment = iterator.next(); + } + + return segment != null ? segment.getSegmentValue() : null; + } + + public static class VariableSegment { + + /** + * The name of the variableExpression segment + */ + private final String name; + + /** + * An optional index if the VariableSegment represents an array. A value of -1 indicates "no index". + */ + private final int index; + + /** + * The evaluated value for this segment + */ + private final Object segmentValue; + + public VariableSegment(String name, int index, Object segmentValue) { + super(); + this.name = name; + this.index = index; + this.segmentValue = segmentValue; + } + + public String getName() { + return name; + } + + public int getIndex() { + return index; + } + + public Object getSegmentValue() { + return segmentValue; + } + } +} diff --git a/core/citrus-api/src/main/java/com/consol/citrus/variable/VariableExpressionSegmentMatcher.java b/core/citrus-api/src/main/java/com/consol/citrus/variable/VariableExpressionSegmentMatcher.java new file mode 100644 index 0000000000..5c8950a655 --- /dev/null +++ b/core/citrus-api/src/main/java/com/consol/citrus/variable/VariableExpressionSegmentMatcher.java @@ -0,0 +1,138 @@ +package com.consol.citrus.variable; + +import org.springframework.util.StringUtils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Matcher that matches segments of variable expressions. The matcher is capable to match the following segments:
+ *
+ *
    + *
  • indexed variables/properties segments of the form: 'var[1]'
  • + *
  • jsonPath segments of the form: 'jsonPath($.person.name)'
  • + *
  • xpath segments of the form: 'xpath(//person/name)'
  • + *
+ *
+ *

+ * Note that jsonPath and xpath segments must terminate the expression, i.e. they cannot be followed by further expressions. + * If a variable expression is used to access a variable from the test context it must start with a variable segment which + * extracts the first variable from the test context.

+ *

+ * Sample for valid variable expressions: + *
+ *

    + *
  • var1
  • + *
  • var1.var2
  • + *
  • var1[1]
  • + *
  • var1[1].var2[2]
  • + *
  • var1[1].var2[2].var3
  • + *
  • var1.jsonPath($.person.name)
  • + *
  • var1[1].jsonPath($.person.name)
  • + *
  • var1.xpath(//title[@lang='en'])
  • + *
  • var1[1].xpath(//title[@lang='en'])
  • + *
+ *
+ */ +public class VariableExpressionSegmentMatcher { + + /** + * Pattern to parse a variable expression + */ + private static final Pattern VAR_PATH_PATTERN = Pattern.compile("xpath\\((.*)\\)$|jsonPath\\((\\$[.\\[].*)\\)$|([^\\[\\].]+)(\\[([0-9])])?(\\.|$)"); + + /** + * The group index for the xpath part + */ + private static final int XPATH_GROUP = 1; + + /** + * The group index for the jsonPath part + */ + private static final int JSON_PATH_GROUP = 2; + + /** + * The group index for the name of the variable/property + */ + private static final int VAR_PROP_NAME_GROUP = 3; + + /** + * The group index for the index when accessing array elements + */ + private static final int INDEX_GROUP = 5; + + /** + * The variable expression the matcher is working on + */ + private final String variableExpression; + + /** + * The matcher that performs the actual matching + */ + private final Matcher matcher; + + /** + * The current expression the matcher has matched + */ + private String currentSegmentExpression; + + /** + * The current segment expression index. A value of -1 indicates "no index". + */ + private int currentSegmentIndex = -1; + + public VariableExpressionSegmentMatcher(String variableExpression) { + this.variableExpression = variableExpression; + matcher = VAR_PATH_PATTERN.matcher(variableExpression); + } + + /** + * Attempts to find the next segment in the variable expression and sets the current + * segment expression as well as the current segment index. + * + * @return + */ + public boolean nextMatch() { + boolean matches = matcher.find(); + + currentSegmentExpression = null; + currentSegmentIndex = -1; + + if (matches) { + + if (StringUtils.hasLength(matcher.group(JSON_PATH_GROUP))) { + currentSegmentExpression = matcher.group(JSON_PATH_GROUP); + } else if (StringUtils.hasLength(matcher.group(XPATH_GROUP))) { + currentSegmentExpression = matcher.group(XPATH_GROUP); + } else { + currentSegmentExpression = matcher.group(VAR_PROP_NAME_GROUP); + currentSegmentIndex = matcher.group(INDEX_GROUP) != null ? Integer.parseInt(matcher.group(INDEX_GROUP)) : -1; + } + } + return matches; + } + + /** + * Obtain the variable expression which backs the matcher. + * @return + */ + public String getVariableExpression() { + return variableExpression; + } + + /** + * Obtain the segment expression ot the current match. Null if the matcher has run out of matches. + * @return + */ + public String getSegmentExpression() { + return currentSegmentExpression; + } + + /** + * Obtain the segment index of the current match. -1 if match is not indexed of matcher has run out of matches. + * @return + */ + public int getSegmentIndex() { + return currentSegmentIndex; + } +} diff --git a/core/citrus-api/src/test/java/com/consol/citrus/variable/VariableExpressionSegmentMatcherTest.java b/core/citrus-api/src/test/java/com/consol/citrus/variable/VariableExpressionSegmentMatcherTest.java new file mode 100644 index 0000000000..33a3cd68a1 --- /dev/null +++ b/core/citrus-api/src/test/java/com/consol/citrus/variable/VariableExpressionSegmentMatcherTest.java @@ -0,0 +1,112 @@ +package com.consol.citrus.variable; + +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; + +public class VariableExpressionSegmentMatcherTest { + + @Test(dataProvider = "matcherDataprovider") + public void testExpression(TestData testData) { + + VariableExpressionSegmentMatcher matcher = new VariableExpressionSegmentMatcher(testData.expression); + + for (SegmentAttributes attributes : testData.segmentAttributes) { + assertTrue(matcher.nextMatch()); + assertEquals(attributes.name, matcher.getSegmentExpression()); + assertEquals(attributes.index, matcher.getSegmentIndex()); + } + assertFalse(matcher.nextMatch()); + } + + @DataProvider(name = "matcherDataprovider") + public static TestData[] matcherExpressions() { + return new TestData[] + { + new TestData("var.prop1[1].prop2[2].prop3") + .addSegmentAttributes("var", -1) + .addSegmentAttributes("prop1", 1) + .addSegmentAttributes("prop2", 2) + .addSegmentAttributes("prop3", -1), + new TestData("var[2].prop1.prop2[2].prop3") + .addSegmentAttributes("var", 2) + .addSegmentAttributes("prop1", -1) + .addSegmentAttributes("prop2", 2) + .addSegmentAttributes("prop3", -1), + new TestData("var.jsonPath($.name1.name2)") + .addSegmentAttributes("var", -1) + .addSegmentAttributes("$.name1.name2", -1), + new TestData("var.jsonPath($['store']['book'][0]['author'])") + .addSegmentAttributes("var", -1) + .addSegmentAttributes("$['store']['book'][0]['author']", -1), + new TestData("var.jsonPath($.store.book[*].author)") + .addSegmentAttributes("var", -1) + .addSegmentAttributes("$.store.book[*].author", -1), + new TestData("var.jsonPath($..author)") + .addSegmentAttributes("var", -1) + .addSegmentAttributes("$..author", -1), + new TestData("var.jsonPath($..book[(@.length-1)])") + .addSegmentAttributes("var", -1) + .addSegmentAttributes("$..book[(@.length-1)]", -1), + new TestData("var.jsonPath($..book[?(@.price<10)])") + .addSegmentAttributes("var", -1) + .addSegmentAttributes("$..book[?(@.price<10)]", -1), + new TestData("var1.prop1[1].prop2[2].jsonPath($..book[?(@.price<10)])") + .addSegmentAttributes("var1", -1) + .addSegmentAttributes("prop1", 1) + .addSegmentAttributes("prop2", 2) + .addSegmentAttributes("$..book[?(@.price<10)]", -1), + new TestData("var.xpath(//title[@lang='en'])") + .addSegmentAttributes("var", -1) + .addSegmentAttributes("//title[@lang='en']", -1), + new TestData("var.xpath(/bookstore/book[price>35.00])") + .addSegmentAttributes("var", -1) + .addSegmentAttributes("/bookstore/book[price>35.00]", -1), + new TestData("var.xpath(/bookstore/book[position()<3])") + .addSegmentAttributes("var", -1) + .addSegmentAttributes("/bookstore/book[position()<3]", -1), + new TestData("var.xpath(/bookstore/book[last()-1])") + .addSegmentAttributes("var", -1) + .addSegmentAttributes("/bookstore/book[last()-1]", -1), + new TestData("var1.prop1[1].prop2[2].xpath(//title[@lang='en']])") + .addSegmentAttributes("var1", -1) + .addSegmentAttributes("prop1", 1) + .addSegmentAttributes("prop2", 2) + .addSegmentAttributes("//title[@lang='en']]", -1), + }; + + } + + private static class TestData { + String expression; + List segmentAttributes = new ArrayList<>(); + + public TestData(String expression) { + this.expression = expression; + } + + TestData addSegmentAttributes(String name, int index) { + segmentAttributes.add(new SegmentAttributes(name, index)); + return this; + } + + @Override + public String toString() { + return expression; + } + } + + private static class SegmentAttributes { + String name; + int index; + + public SegmentAttributes(String name, int index) { + this.name = name; + this.index = index; + } + } +} diff --git a/core/citrus-base/src/main/java/com/consol/citrus/context/TestContextFactory.java b/core/citrus-base/src/main/java/com/consol/citrus/context/TestContextFactory.java index 331df4826c..3a606f3fb5 100644 --- a/core/citrus-base/src/main/java/com/consol/citrus/context/TestContextFactory.java +++ b/core/citrus-base/src/main/java/com/consol/citrus/context/TestContextFactory.java @@ -41,6 +41,7 @@ import com.consol.citrus.validation.MessageValidatorRegistry; import com.consol.citrus.validation.matcher.ValidationMatcherRegistry; import com.consol.citrus.variable.GlobalVariables; +import com.consol.citrus.variable.SegmentVariableExtractorRegistry; import com.consol.citrus.xml.namespace.NamespaceContextBuilder; /** @@ -81,6 +82,8 @@ public class TestContextFactory implements ReferenceResolverAware { private LogModifier logModifier; + private SegmentVariableExtractorRegistry segmentVariableExtractorRegistry; + /** * Create new empty instance with default components set. * @return @@ -101,6 +104,7 @@ public static TestContextFactory newInstance() { factory.setNamespaceContextBuilder(new NamespaceContextBuilder()); factory.setTypeConverter(new DefaultTypeConverter()); factory.setLogModifier(new DefaultLogModifier()); + factory.setSegmentVariableExtractorRegistry(new SegmentVariableExtractorRegistry()); return factory; } @@ -123,6 +127,7 @@ public TestContext getObject() { context.setMessageProcessors(messageProcessors); context.setEndpointFactory(endpointFactory); context.setReferenceResolver(referenceResolver); + context.setSegmentVariableExtractorRegistry(segmentVariableExtractorRegistry); if (namespaceContextBuilder != null) { context.setNamespaceContextBuilder(namespaceContextBuilder); @@ -371,4 +376,21 @@ public LogModifier getLogModifier() { public void setLogModifier(LogModifier logModifier) { this.logModifier = logModifier; } + + /** + * Gets the segmentVariableExtractorRegistry + * @return + */ + public SegmentVariableExtractorRegistry getSegmentVariableExtractorRegistry() { + return segmentVariableExtractorRegistry; + } + + /** + * Sets the segmentVariableExtractorRegistry + * @param segmentVariableExtractorRegistry + */ + public void setSegmentVariableExtractorRegistry(SegmentVariableExtractorRegistry segmentVariableExtractorRegistry) { + this.segmentVariableExtractorRegistry = segmentVariableExtractorRegistry; + } + } diff --git a/core/citrus-base/src/main/java/com/consol/citrus/util/MessageUtils.java b/core/citrus-base/src/main/java/com/consol/citrus/util/MessageUtils.java index 7f2eeddac7..1df76da559 100644 --- a/core/citrus-base/src/main/java/com/consol/citrus/util/MessageUtils.java +++ b/core/citrus-base/src/main/java/com/consol/citrus/util/MessageUtils.java @@ -47,7 +47,7 @@ public static boolean hasXmlPayload(Message message) { return Optional.ofNullable(message.getPayload(String.class)) .map(String::trim) - .map(payload -> payload.length() == 0 || payload.startsWith("<")) + .map(payload -> IsXmlPredicate.INSTANCE.test(payload)) .orElse(true); } @@ -63,7 +63,8 @@ public static boolean hasJsonPayload(Message message) { return Optional.ofNullable(message.getPayload(String.class)) .map(String::trim) - .map(payload -> payload.length() == 0 || payload.startsWith("{") || payload.startsWith("[")) + .map(payload->IsJsonPredicate.INSTANCE.test(payload)) .orElse(true); } + } diff --git a/core/citrus-base/src/test/java/com/consol/citrus/context/TestContextTest.java b/core/citrus-base/src/test/java/com/consol/citrus/context/TestContextTest.java index a8116f3a9f..da18fd90ef 100644 --- a/core/citrus-base/src/test/java/com/consol/citrus/context/TestContextTest.java +++ b/core/citrus-base/src/test/java/com/consol/citrus/context/TestContextTest.java @@ -34,6 +34,8 @@ import com.consol.citrus.message.Message; import com.consol.citrus.report.MessageListeners; import com.consol.citrus.variable.GlobalVariables; +import com.consol.citrus.variable.VariableExpressionSegmentMatcher; +import com.consol.citrus.variable.SegmentVariableExtractor; import org.mockito.Mockito; import org.testng.Assert; import org.testng.annotations.Test; @@ -139,21 +141,21 @@ public void testUnknownVariable() { public void testGetVariableFromPathExpression() { context.setVariable("helloData", new DataContainer("hello")); context.setVariable("container", new DataContainer(new DataContainer("nested"))); - + DataContainer[] subContainerArray = new DataContainer[] { new DataContainer("A"), new DataContainer("B"), new DataContainer("C"), new DataContainer("D"), }; - + DataContainer[] containerArray = new DataContainer[] { new DataContainer("0"), new DataContainer("1"), new DataContainer("2"), new DataContainer(subContainerArray), }; - + context.setVariable("containerArray", containerArray); Assert.assertEquals(context.getVariable("${helloData}"), DataContainer.class.getName()); @@ -166,38 +168,82 @@ public void testGetVariableFromPathExpression() { Assert.assertEquals(context.getVariable("${container.data.CONSTANT}"), "FOO"); Assert.assertEquals(context.getVariable("${container.intVals[1]}"), "1"); Assert.assertEquals(context.getVariable("${containerArray[3].data[1].data}"), "B"); + } + + @Test + public void testGetVariableFromJsonPathExpression() { + String json = "{\"name\": \"Peter\"}"; + context.setVariable("jsonVar", json); + + String variableExpression = "jsonVar.jsonPath($.name)"; + + SegmentVariableExtractor jsonExtractorMock = Mockito.mock(SegmentVariableExtractor.class); + context.getSegmentVariableExtractorRegistry().getSegmentValueExtractors().add(jsonExtractorMock); + + Mockito.doReturn(true).when(jsonExtractorMock).canExtract(Mockito.eq(context), Mockito.eq(json), Mockito.any()); + Mockito.doReturn("Peter").when(jsonExtractorMock).extractValue(Mockito.eq(context), Mockito.eq(json), Mockito.any()); + + Assert.assertEquals(context.getVariable(String.format("${%s}", variableExpression)), "Peter"); + } + + @Test + public void testGetVariableFromJsonPathExpressionNoMatch() { + String json = "{\"name\": \"Peter\"}"; + context.setVariable("jsonVar", json); + + String variableExpression = "jsonVar.jsonPath($.othername)"; + + SegmentVariableExtractor jsonExtractorMock = Mockito.mock(SegmentVariableExtractor.class); + context.getSegmentVariableExtractorRegistry().getSegmentValueExtractors().add(jsonExtractorMock); + + Mockito.doReturn(true).when(jsonExtractorMock).canExtract(Mockito.eq(context), Mockito.eq(json), Mockito.any()); + Mockito.doThrow(new CitrusRuntimeException()).when(jsonExtractorMock).extractValue(Mockito.eq(context), Mockito.eq(json), Mockito.any()); + + Assert.assertThrows(() -> context.getVariable(String.format("${%s}", variableExpression))); + } + + @Test + public void testGetVariableFromXpathExpression() { + String xml = "Peter"; + context.setVariable("xpathVar", xml); + + String variableExpression = "xpathVar.xpath(//person/name)"; + + SegmentVariableExtractor xpathExtractorMock = Mockito.mock(SegmentVariableExtractor.class); + context.getSegmentVariableExtractorRegistry().getSegmentValueExtractors().add(xpathExtractorMock); + + Mockito.doReturn(true).when(xpathExtractorMock).canExtract(Mockito.eq(context), Mockito.eq(xml), Mockito.any()); + Mockito.doReturn("Peter").when(xpathExtractorMock).extractValue(Mockito.eq(context), Mockito.eq(xml), Mockito.any()); + + Assert.assertEquals(context.getVariable(String.format("${%s}", variableExpression)), "Peter"); } + @Test + public void testGetVariableFromXpathExpressionNoMatch() { + String xml = "Peter"; + context.setVariable("xpathVar", xml); + + String variableExpression = "xpathVar.xpath(//person/name)"; + + SegmentVariableExtractor xpathExtractorMock = Mockito.mock(SegmentVariableExtractor.class); + context.getSegmentVariableExtractorRegistry().getSegmentValueExtractors().add(xpathExtractorMock); + + Mockito.doReturn(true).when(xpathExtractorMock).canExtract(Mockito.eq(context), Mockito.eq(xml), Mockito.any()); + Mockito.doThrow(new CitrusRuntimeException()).when(xpathExtractorMock).extractValue(Mockito.eq(context), Mockito.eq(xml), Mockito.any()); + + Assert.assertThrows(() -> context.getVariable(String.format("${%s}", variableExpression))); + } + @Test public void testUnknownFromPathExpression() { context.setVariable("helloData", new DataContainer("hello")); context.setVariable("container", new DataContainer(new DataContainer("nested"))); - try { - context.getVariable("${helloData.unknown}"); - Assert.fail("Missing exception due to unknown field in variable path"); - } catch (CitrusRuntimeException e) { - Assert.assertTrue(e.getMessage().endsWith("")); - } - - try { - context.getVariable("${container.data.unknown}"); - } catch (CitrusRuntimeException e) { - Assert.assertTrue(e.getMessage().endsWith("")); - } + Assert.assertThrows(() ->context.getVariable("${helloData.unknown}")) ; + Assert.assertThrows(() ->context.getVariable("${container.data.unknown}")) ; + Assert.assertThrows(() ->context.getVariable("${something.else}")) ; + Assert.assertThrows(() ->context.getVariable("${helloData[1]}")) ; - try { - context.getVariable("${something.else}"); - } catch (CitrusRuntimeException e) { - Assert.assertEquals(e.getMessage(), "Unknown variable 'something.else'"); - } - - try { - context.getVariable("${helloData[1]}"); - Assert.fail("Missing exception due to indexed access to non array variable"); - } catch (CitrusRuntimeException e) { - Assert.assertTrue(e.getMessage().endsWith("")); - } } @Test @@ -326,7 +372,7 @@ public void testReplaceVariablesInMap() { testMap = context.resolveDynamicValuesInMap(testMap); // Should be null due to variable substitution - Assert.assertEquals(testMap.get("${test}"), null); + Assert.assertNull(testMap.get("${test}")); // Should return "test" after variable substitution Assert.assertEquals(testMap.get("123"), "value"); } @@ -410,10 +456,10 @@ public void shouldCallMessageListeners() { * Data container for test variable object access. */ private static class DataContainer { - private int number = 99; - private Object data; + private final int number = 99; + private final Object data; - private int[] intVals = new int[] {0, 1, 2, 3, 4}; + private final int[] intVals = new int[] {0, 1, 2, 3, 4}; private static final String CONSTANT = "FOO"; diff --git a/validation/citrus-validation-json/src/main/java/com/consol/citrus/json/JsonPathSegmentVariableExtractor.java b/validation/citrus-validation-json/src/main/java/com/consol/citrus/json/JsonPathSegmentVariableExtractor.java new file mode 100644 index 0000000000..46ad996eb3 --- /dev/null +++ b/validation/citrus-validation-json/src/main/java/com/consol/citrus/json/JsonPathSegmentVariableExtractor.java @@ -0,0 +1,42 @@ +package com.consol.citrus.json; + +import com.consol.citrus.context.TestContext; +import com.consol.citrus.exceptions.CitrusRuntimeException; +import com.consol.citrus.util.IsJsonPredicate; +import com.consol.citrus.validation.json.JsonPathMessageValidationContext; +import com.consol.citrus.variable.SegmentVariableExtractorRegistry; +import com.consol.citrus.variable.VariableExpressionSegmentMatcher; +import com.jayway.jsonpath.InvalidPathException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Thorsten Schlathoelter + */ +public class JsonPathSegmentVariableExtractor extends SegmentVariableExtractorRegistry.AbstractSegmentVariableExtractor { + + /** + * Logger + */ + private static final Logger LOG = LoggerFactory.getLogger(JsonPathSegmentVariableExtractor.class); + + @Override + public boolean canExtract(TestContext testContext, Object object, VariableExpressionSegmentMatcher matcher) { + return object == null || (object instanceof String && IsJsonPredicate.INSTANCE.test((String)object) && JsonPathMessageValidationContext.isJsonPathExpression(matcher.getSegmentExpression())); + } + + @Override + public Object doExtractValue(TestContext testContext, Object object, VariableExpressionSegmentMatcher matcher) { + return object == null ? null : extractJsonPath(object.toString(), matcher.getSegmentExpression()); + } + + private Object extractJsonPath(String json, String segmentExpression) { + + try { + return JsonPathUtils.evaluate(json, segmentExpression); + } catch (InvalidPathException e) { + throw new CitrusRuntimeException(String.format("Unable to extract jsonPath from segmentExpression %s", segmentExpression), e); + } + } + +} diff --git a/validation/citrus-validation-json/src/main/resources/META-INF/citrus/variable/extractor/segment/jsonPath b/validation/citrus-validation-json/src/main/resources/META-INF/citrus/variable/extractor/segment/jsonPath new file mode 100644 index 0000000000..6a945288a9 --- /dev/null +++ b/validation/citrus-validation-json/src/main/resources/META-INF/citrus/variable/extractor/segment/jsonPath @@ -0,0 +1 @@ +type=com.consol.citrus.json.JsonPathSegmentVariableExtractor diff --git a/validation/citrus-validation-json/src/test/java/com/consol/citrus/integration/JsonPathSegmentVariableExtractorIT.java b/validation/citrus-validation-json/src/test/java/com/consol/citrus/integration/JsonPathSegmentVariableExtractorIT.java new file mode 100644 index 0000000000..95b08cc63b --- /dev/null +++ b/validation/citrus-validation-json/src/test/java/com/consol/citrus/integration/JsonPathSegmentVariableExtractorIT.java @@ -0,0 +1,12 @@ +package com.consol.citrus.integration; + +import com.consol.citrus.annotations.CitrusXmlTest; +import com.consol.citrus.testng.spring.TestNGCitrusSpringSupport; +import org.testng.annotations.Test; + +public class JsonPathSegmentVariableExtractorIT extends TestNGCitrusSpringSupport { + + @Test + @CitrusXmlTest + public void JsonPathSegmentVariableExtractorIT() {} +} diff --git a/validation/citrus-validation-json/src/test/java/com/consol/citrus/json/JsonPathSegmentVariableExtractorTest.java b/validation/citrus-validation-json/src/test/java/com/consol/citrus/json/JsonPathSegmentVariableExtractorTest.java new file mode 100644 index 0000000000..b14d29116e --- /dev/null +++ b/validation/citrus-validation-json/src/test/java/com/consol/citrus/json/JsonPathSegmentVariableExtractorTest.java @@ -0,0 +1,56 @@ +package com.consol.citrus.json; + +import com.consol.citrus.UnitTestSupport; +import com.consol.citrus.variable.VariableExpressionSegmentMatcher; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class JsonPathSegmentVariableExtractorTest extends UnitTestSupport { + + public static final String JSON_FIXTURE = "{\"name\": \"Peter\"}"; + + private final JsonPathSegmentVariableExtractor unitUnderTest = new JsonPathSegmentVariableExtractor(); + + @Test + public void testExtractFromJson() { + + String jsonPath = "$.name"; + VariableExpressionSegmentMatcher matcher = matchSegmentExpressionMatcher(jsonPath); + + Assert.assertTrue(unitUnderTest.canExtract(context, JSON_FIXTURE, matcher)); + Assert.assertEquals(unitUnderTest.extractValue(context, JSON_FIXTURE, matcher), "Peter"); + } + + @Test + public void testExtractFromNonJsonPathExpression() { + String json = "{\"name\": \"Peter\"}"; + + String nonJsonPath = "name"; + VariableExpressionSegmentMatcher matcher = matchSegmentExpressionMatcher(nonJsonPath); + + Assert.assertFalse(unitUnderTest.canExtract(context, json, matcher)); + } + + @Test + public void testExtractFromJsonExpressionFailure() { + String json = "{\"name\": \"Peter\"}"; + + String invalidJsonPath = "$.$$$name"; + VariableExpressionSegmentMatcher matcher = matchSegmentExpressionMatcher(invalidJsonPath); + + Assert.assertTrue(unitUnderTest.canExtract(context, json, matcher)); + Assert.assertThrows(() -> unitUnderTest.extractValue(context, json, matcher)); + } + + /** + * Create a variable expression jsonPath matcher and match the first jsonPath + * @param jsonPath + * @return + */ + private VariableExpressionSegmentMatcher matchSegmentExpressionMatcher(String jsonPath) { + String variableExpression = String.format("jsonPath(%s)", jsonPath); + VariableExpressionSegmentMatcher matcher = new VariableExpressionSegmentMatcher(variableExpression); + Assert.assertTrue(matcher.nextMatch()); + return matcher; + } +} diff --git a/validation/citrus-validation-json/src/test/resources/com/consol/citrus/integration/JsonPathSegmentVariableExtractorIT.xml b/validation/citrus-validation-json/src/test/resources/com/consol/citrus/integration/JsonPathSegmentVariableExtractorIT.xml new file mode 100644 index 0000000000..2263ef7f35 --- /dev/null +++ b/validation/citrus-validation-json/src/test/resources/com/consol/citrus/integration/JsonPathSegmentVariableExtractorIT.xml @@ -0,0 +1,44 @@ + + + + + Thorsten Schlathoelter + 2021-12-16 + FINAL + Thorsten Schlathoelter + 2021-12-16T00:00:00 + + + Extract value from a json stored as variable and send it with a different payload. + + + + + + + + + + { + "Friend" : "${jsonVar.jsonPath($.Person)}" + } + + + + + + + + { + "Friend" : "Peter" + } + + + + + + diff --git a/validation/citrus-validation-xml/src/main/java/com/consol/citrus/xml/XpathSegmentVariableExtractor.java b/validation/citrus-validation-xml/src/main/java/com/consol/citrus/xml/XpathSegmentVariableExtractor.java new file mode 100644 index 0000000000..6e78473ca9 --- /dev/null +++ b/validation/citrus-validation-xml/src/main/java/com/consol/citrus/xml/XpathSegmentVariableExtractor.java @@ -0,0 +1,67 @@ +package com.consol.citrus.xml; + +import com.consol.citrus.context.TestContext; +import com.consol.citrus.exceptions.CitrusRuntimeException; +import com.consol.citrus.util.IsXmlPredicate; +import com.consol.citrus.util.XMLUtils; +import com.consol.citrus.variable.SegmentVariableExtractorRegistry; +import com.consol.citrus.variable.VariableExpressionSegmentMatcher; +import com.consol.citrus.xml.namespace.NamespaceContextBuilder; +import com.consol.citrus.xml.xpath.XPathExpressionResult; +import com.consol.citrus.xml.xpath.XPathUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.xml.SimpleNamespaceContext; +import org.w3c.dom.Document; + +/** + * @author Thorsten Schlathoelter + */ +public class XpathSegmentVariableExtractor extends SegmentVariableExtractorRegistry.AbstractSegmentVariableExtractor { + + /** + * Logger + */ + private static final Logger LOG = LoggerFactory.getLogger(XpathSegmentVariableExtractor.class); + + @Override + public boolean canExtract(TestContext testContext, Object object, VariableExpressionSegmentMatcher matcher) { + return object == null || (object instanceof Document + || (object instanceof String && IsXmlPredicate.INSTANCE.test((String)object)) + && XPathUtils.isXPathExpression(matcher.getSegmentExpression())); + } + + @Override + public Object doExtractValue(TestContext testContext, Object object, VariableExpressionSegmentMatcher matcher) { + return object == null ? null : extractXpath(testContext, object, matcher); + } + + private Object extractXpath(TestContext testContext, Object xml, VariableExpressionSegmentMatcher matcher) { + + Document document = null; + if (xml instanceof Document) { + document = (Document) xml; + } else if (xml instanceof String) { + String documentCacheKey = createDocumentCacheKey(matcher.getVariableExpression()); + document = (Document)testContext.getVariables().get(documentCacheKey); + if (document == null) { + document = XMLUtils.parseMessagePayload((String)xml); + testContext.setVariable(documentCacheKey, document); + } + } + + if (document == null) { + throw new CitrusRuntimeException(String.format("Unable to extract xpath from object of type %s", xml.getClass())); + } + + // TODO: namespace context? + SimpleNamespaceContext simpleNamespaceContext = new SimpleNamespaceContext(); + NamespaceContextBuilder builder = new NamespaceContextBuilder(); + return XPathUtils.evaluate(document, matcher.getSegmentExpression(), simpleNamespaceContext, XPathExpressionResult.STRING); + } + + public static String createDocumentCacheKey(String variableExpression) { + return variableExpression+"_document"; + } + +} diff --git a/validation/citrus-validation-xml/src/main/resources/META-INF/citrus/variable/extractor/segment/xpath b/validation/citrus-validation-xml/src/main/resources/META-INF/citrus/variable/extractor/segment/xpath new file mode 100644 index 0000000000..1a9675db57 --- /dev/null +++ b/validation/citrus-validation-xml/src/main/resources/META-INF/citrus/variable/extractor/segment/xpath @@ -0,0 +1 @@ +type=com.consol.citrus.xml.XpathSegmentVariableExtractor \ No newline at end of file diff --git a/validation/citrus-validation-xml/src/test/java/com/consol/citrus/integration/XpathSegmentVariableExtractorIT.java b/validation/citrus-validation-xml/src/test/java/com/consol/citrus/integration/XpathSegmentVariableExtractorIT.java new file mode 100644 index 0000000000..9e87937a84 --- /dev/null +++ b/validation/citrus-validation-xml/src/test/java/com/consol/citrus/integration/XpathSegmentVariableExtractorIT.java @@ -0,0 +1,12 @@ +package com.consol.citrus.integration; + +import com.consol.citrus.annotations.CitrusXmlTest; +import com.consol.citrus.testng.spring.TestNGCitrusSpringSupport; +import org.testng.annotations.Test; + +public class XpathSegmentVariableExtractorIT extends TestNGCitrusSpringSupport { + + @Test + @CitrusXmlTest + public void XpathSegmentVariableExtractorIT() {} +} diff --git a/validation/citrus-validation-xml/src/test/java/com/consol/citrus/xml/XmlPathSegmentVariableExtractorTest.java b/validation/citrus-validation-xml/src/test/java/com/consol/citrus/xml/XmlPathSegmentVariableExtractorTest.java new file mode 100644 index 0000000000..982f9f1102 --- /dev/null +++ b/validation/citrus-validation-xml/src/test/java/com/consol/citrus/xml/XmlPathSegmentVariableExtractorTest.java @@ -0,0 +1,71 @@ +package com.consol.citrus.xml; + +import com.consol.citrus.UnitTestSupport; +import com.consol.citrus.variable.VariableExpressionSegmentMatcher; +import org.testng.Assert; +import org.testng.annotations.Test; +import org.w3c.dom.Document; + +public class XmlPathSegmentVariableExtractorTest extends UnitTestSupport { + + private static final String XML_FIXTURE = "Peter"; + + private final XpathSegmentVariableExtractor unitUnderTest = new XpathSegmentVariableExtractor(); + + @Test + public void testExtractFromXml() { + + String xpath = "//person/name"; + VariableExpressionSegmentMatcher matcher = matchSegmentExpressionMatcher(xpath); + + Assert.assertTrue(unitUnderTest.canExtract(context, XML_FIXTURE, matcher)); + Assert.assertEquals(unitUnderTest.extractValue(context, XML_FIXTURE, matcher), "Peter"); + + // Assert that xml document was cached + Object cachedXmlDocument = context.getVariableObject( + XpathSegmentVariableExtractor.createDocumentCacheKey(matcher.getVariableExpression())); + Assert.assertTrue(cachedXmlDocument instanceof Document); + + // Assert that another match can be matched + matcher = matchSegmentExpressionMatcher(xpath); + Assert.assertTrue(unitUnderTest.canExtract(context, XML_FIXTURE, matcher)); + Assert.assertEquals(unitUnderTest.extractValue(context, XML_FIXTURE, matcher), "Peter"); + + // Assert that a XML document can be matched + matcher = matchSegmentExpressionMatcher(xpath); + Assert.assertTrue(unitUnderTest.canExtract(context, cachedXmlDocument, matcher)); + Assert.assertEquals(unitUnderTest.extractValue(context, cachedXmlDocument, matcher), "Peter"); + + } + + @Test + public void testExtractFromInvalidXpathExpression() { + + String invalidXpathPath = "name"; + VariableExpressionSegmentMatcher matcher = matchSegmentExpressionMatcher(invalidXpathPath); + + Assert.assertFalse(unitUnderTest.canExtract(context, XML_FIXTURE, matcher)); + } + + @Test + public void testExtractFromXmlExpressionFailure() { + + String invalidXpath = "//$$$"; + VariableExpressionSegmentMatcher matcher = matchSegmentExpressionMatcher(invalidXpath); + + Assert.assertTrue(unitUnderTest.canExtract(context, XML_FIXTURE, matcher)); + Assert.assertThrows(() -> unitUnderTest.extractValue(context, XML_FIXTURE, matcher)); + } + + /** + * Create a variable expression xpath matcher and match the first xpath + * @param xpath + * @return + */ + private VariableExpressionSegmentMatcher matchSegmentExpressionMatcher(String xpath) { + String variableExpression = String.format("xpath(%s)", xpath); + VariableExpressionSegmentMatcher matcher = new VariableExpressionSegmentMatcher(variableExpression); + Assert.assertTrue(matcher.nextMatch()); + return matcher; + } +} diff --git a/validation/citrus-validation-xml/src/test/resources/com/consol/citrus/integration/XpathSegmentVariableExtractorIT.xml b/validation/citrus-validation-xml/src/test/resources/com/consol/citrus/integration/XpathSegmentVariableExtractorIT.xml new file mode 100644 index 0000000000..41664b2337 --- /dev/null +++ b/validation/citrus-validation-xml/src/test/resources/com/consol/citrus/integration/XpathSegmentVariableExtractorIT.xml @@ -0,0 +1,44 @@ + + + + + Thorsten Schlathoelter + 2021-12-16 + FINAL + Thorsten Schlathoelter + 2021-12-16T00:00:00 + + + Extract value from a xml stored as variable and send it with a different payload. + + + + + + + + + + ${xmlVar.xpath(//Person/Name)} + ]]> + + + + + + + + Peter + ]]> + + + + + +