Skip to content

Commit

Permalink
extended support for inexistent results (issue #170) to nested objects
Browse files Browse the repository at this point in the history
  • Loading branch information
S1artie committed Feb 21, 2019
1 parent ba22cbd commit 4e0ee4b
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import de.gebit.integrity.dsl.ConstantDefinition;
import de.gebit.integrity.dsl.ConstantValue;
import de.gebit.integrity.dsl.CustomOperation;
import de.gebit.integrity.dsl.InexistentValue;
import de.gebit.integrity.dsl.StandardOperation;
import de.gebit.integrity.dsl.StringValue;
import de.gebit.integrity.dsl.ValueOrEnumValueOrOperation;
Expand Down Expand Up @@ -99,23 +100,27 @@ public abstract class AbstractModularValueConverter implements ValueConverter {
/**
* All known conversions.
*/
private final Map<ConversionKey, Class<? extends Conversion<?, ?>>> conversions = new HashMap<ConversionKey, Class<? extends Conversion<?, ?>>>();
private final Map<ConversionKey, Class<? extends Conversion<?, ?>>> conversions
= new HashMap<ConversionKey, Class<? extends Conversion<?, ?>>>();

/**
* Conversions derived from the directly added conversions by searching superclasses of the target type.
*/
private final Map<ConversionKey, List<Class<? extends Conversion<?, ?>>>> derivedConversions = new HashMap<ConversionKey, List<Class<? extends Conversion<?, ?>>>>();
private final Map<ConversionKey, List<Class<? extends Conversion<?, ?>>>> derivedConversions
= new HashMap<ConversionKey, List<Class<? extends Conversion<?, ?>>>>();

/**
* Reverse index of all known conversions.
*/
private final Map<Class<? extends Conversion<?, ?>>, ConversionKey> conversionToKey = new HashMap<Class<? extends Conversion<?, ?>>, ConversionKey>();
private final Map<Class<? extends Conversion<?, ?>>, ConversionKey> conversionToKey
= new HashMap<Class<? extends Conversion<?, ?>>, ConversionKey>();

/**
* The default conversions for all known source types. These are the conversions with the highest priority from
* their respective source types' conversion pool.
*/
private final Map<Class<?>, Class<? extends Conversion<?, ?>>> defaultConversions = new HashMap<Class<?>, Class<? extends Conversion<?, ?>>>();
private final Map<Class<?>, Class<? extends Conversion<?, ?>>> defaultConversions
= new HashMap<Class<?>, Class<? extends Conversion<?, ?>>>();

/**
* The current defaults' priority. Used to fill the {@link #defaultConversions} map.
Expand Down Expand Up @@ -368,6 +373,12 @@ private Object convertSingleValueToTargetType(Class<?> aTargetType, Class<?> aPa
return null;
}

if (aValue instanceof InexistentValue
&& aConversionContext.getInexistentValueHandlingPolicy() == InexistentValueHandling.KEEP_AS_IS) {
// In case of an Inexistent value, we may have to keep it as-is, if required by conversion context policy
return aValue;
}

// It is possible that we arrive here with a single value, but still a target type that's an array. In that case
// we need to create the requested array, but it will only have one element.
if (aTargetType != null && aTargetType.isArray()) {
Expand Down Expand Up @@ -500,8 +511,8 @@ protected Object convertEncapsulatedValueToTargetType(Class<?> aTargetType, Clas
// cannot execute operations without the ability to load them
return null;
} else {
CustomOperationWrapper tempWrapper = wrapperFactory
.newCustomOperationWrapper((CustomOperation) aValue);
CustomOperationWrapper tempWrapper
= wrapperFactory.newCustomOperationWrapper((CustomOperation) aValue);
Object tempResult = tempWrapper.executeOperation();
return convertPlainValueToTargetType(aTargetType, aParameterizedType, tempResult,
aConversionContext, someVisitedValues);
Expand Down Expand Up @@ -671,8 +682,8 @@ protected Object convertEncapsulatedValueCollectionToTargetType(Class<?> aTarget
// that property by the position within the array, and reset it later to the original plain value. This
// is necessary to distinguish between array elements when converting to formatted strings (there's
// highlighting being added based on these paths). In all other conversion cases it at least doesn't hurt.
String tempBaseObjectPath = (String) aConversionContext
.getProperty(AbstractNestedObjectToString.NESTEDOBJECT_PATH_PROPERTY);
String tempBaseObjectPath
= (String) aConversionContext.getProperty(AbstractNestedObjectToString.NESTEDOBJECT_PATH_PROPERTY);

Object tempResultArray = Array.newInstance(tempTargetArrayType, aCollection.getMoreValues().size() + 1);
for (int i = 0; i < aCollection.getMoreValues().size() + 1; i++) {
Expand All @@ -681,8 +692,8 @@ protected Object convertEncapsulatedValueCollectionToTargetType(Class<?> aTarget
tempBaseObjectPath + "#" + i);
}

ValueOrEnumValueOrOperation tempValue = (i == 0 ? aCollection.getValue()
: aCollection.getMoreValues().get(i - 1));
ValueOrEnumValueOrOperation tempValue
= (i == 0 ? aCollection.getValue() : aCollection.getMoreValues().get(i - 1));
Object tempResultValue = convertEncapsulatedValueToTargetType(tempTargetType, aParameterizedType,
tempValue, aConversionContext, someVisitedValues);
Array.set(tempResultArray, i, tempResultValue);
Expand Down Expand Up @@ -810,8 +821,8 @@ public FormattedString convertValueToFormattedString(Object aValue, boolean aFor

@Override
public String[] convertValueToStringArray(Object aValue, ConversionContext aConversionContext) {
FormattedString[] tempFormattedStrings = convertValueToStringArray(aValue, aConversionContext,
new HashSet<Object>());
FormattedString[] tempFormattedStrings
= convertValueToStringArray(aValue, aConversionContext, new HashSet<Object>());

String[] tempStrings = new String[tempFormattedStrings.length];
for (int i = 0; i < tempFormattedStrings.length; i++) {
Expand Down Expand Up @@ -1080,8 +1091,8 @@ public String toString() {
protected Conversion<?, ?> findAndInstantiateConversion(Class<?> aSourceType, Class<?> aTargetType,
Set<Object> someVisitedValues, ConversionContext aConversionContext)
throws InstantiationException, IllegalAccessException {
Class<? extends Conversion<?, ?>> tempConversionClass = findConversion(aSourceType, aTargetType,
someVisitedValues, aConversionContext);
Class<? extends Conversion<?, ?>> tempConversionClass
= findConversion(aSourceType, aTargetType, someVisitedValues, aConversionContext);

return createConversionInstance(tempConversionClass, someVisitedValues);
}
Expand All @@ -1100,8 +1111,8 @@ public String toString() {
*/
protected Class<? extends Conversion<?, ?>> findConversion(Class<?> aSourceType, Class<?> aTargetType,
Set<Object> someVisitedValues, ConversionContext aConversionContext) {
Class<? extends Conversion<?, ?>> tempConversion = findConversionRecursive(aSourceType, aTargetType,
aConversionContext);
Class<? extends Conversion<?, ?>> tempConversion
= findConversionRecursive(aSourceType, aTargetType, aConversionContext);

if (tempConversion != null) {
return tempConversion;
Expand Down Expand Up @@ -1154,8 +1165,8 @@ public String toString() {
* @return a conversion class, or null if none was found
*/
protected Class<? extends Conversion<?, ?>> searchDerivedConversionMap(Class<?> aSourceType, Class<?> aTargetType) {
List<Class<? extends Conversion<?, ?>>> tempList = derivedConversions
.get(new ConversionKey(aSourceType, aTargetType));
List<Class<? extends Conversion<?, ?>>> tempList
= derivedConversions.get(new ConversionKey(aSourceType, aTargetType));
if (tempList != null && !tempList.isEmpty()) {
return tempList.get(0);
}
Expand Down Expand Up @@ -1221,15 +1232,15 @@ public String toString() {
for (Class<?> tempSourceInterface : tempSourceTypeInFocus.getInterfaces()) {
if (aTargetType == null || aTargetType == Object.class) {
// This is the default target type case
Class<? extends Conversion<?, ?>> tempConversion = findConversionRecursive(tempSourceInterface,
null, aConversionContext);
Class<? extends Conversion<?, ?>> tempConversion
= findConversionRecursive(tempSourceInterface, null, aConversionContext);
if (tempConversion != null) {
return tempConversion;
}
} else {
// We actually have a target type
Class<? extends Conversion<?, ?>> tempConversion = findConversionRecursive(tempSourceInterface,
aTargetType, aConversionContext);
Class<? extends Conversion<?, ?>> tempConversion
= findConversionRecursive(tempSourceInterface, aTargetType, aConversionContext);
if (tempConversion != null) {
return tempConversion;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.Map;

import de.gebit.integrity.comparator.ComparisonResult;
import de.gebit.integrity.dsl.InexistentValue;

/**
* A conversion context is a container for contextual information required to perform a value conversion.<br>
Expand All @@ -33,7 +34,13 @@ public class ConversionContext implements Cloneable {
/**
* The way in which unresolvable variables shall be treated.
*/
protected UnresolvableVariableHandling unresolvableVariableHandlingPolicy = UnresolvableVariableHandling.RESOLVE_TO_NULL_VALUE;
protected UnresolvableVariableHandling unresolvableVariableHandlingPolicy
= UnresolvableVariableHandling.RESOLVE_TO_NULL_VALUE;

/**
* The way in which {@link InexistentValue}s are to be treated.
*/
protected InexistentValueHandling inexistentValueHandlingPolicy = InexistentValueHandling.CONVERT;

/**
* In case of a value being converted which belongs to a comparison that has been executed, the result of said
Expand Down Expand Up @@ -76,6 +83,17 @@ public ConversionContext withUnresolvableVariableHandlingPolicy(UnresolvableVari
return this;
}

/**
* Enables a certain {@link InexistentValueHandling} policy instead of the default.
*
* @param aPolicy
* the policy to use
*/
public ConversionContext withInexistentValueHandling(InexistentValueHandling aPolicy) {
inexistentValueHandlingPolicy = aPolicy;
return this;
}

/**
* Adds the provided comparison result.
*
Expand Down Expand Up @@ -108,6 +126,10 @@ public UnresolvableVariableHandling getUnresolvableVariableHandlingPolicy() {
return unresolvableVariableHandlingPolicy;
}

public InexistentValueHandling getInexistentValueHandlingPolicy() {
return inexistentValueHandlingPolicy;
}

public ComparisonResult getComparisonResult() {
return comparisonResult;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*******************************************************************************
* Copyright (c) 2019 Rene Schneider, GEBIT Solutions GmbH and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
package de.gebit.integrity.parameter.conversion;

import de.gebit.integrity.dsl.InexistentValue;
import de.gebit.integrity.utils.ParameterUtil;

/**
* This enum offers various ways in which inexistent values ({@link InexistentValue}) may be handled during conversion.
*
* @author Rene Schneider - initial API and implementation
*
*/
public enum InexistentValueHandling {

/**
* Inexistent values should be converted normally - which most likely means they will end up as a {@link String}.
* However, the String won't be just any object, but the specific, reserved instance
* {@link ParameterUtil#INEXISTENT_VALUE} (so even if converted, one could perform an instance comparison to
* specifically detect inexistent values - however, this is discouraged and should only be used if it is not
* possible to simply turn off conversion of inexistent values via {@link #KEEP_AS_IS}).
* <p>
* This is the default if not specified otherwise in a {@link ConversionContext}.
*/
CONVERT,

/**
* Inexistent values will not be converted.
*/
KEEP_AS_IS;

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import de.gebit.integrity.comparator.SimpleComparisonResult;
import de.gebit.integrity.dsl.CustomOperation;
import de.gebit.integrity.dsl.DateValue;
import de.gebit.integrity.dsl.InexistentValue;
import de.gebit.integrity.dsl.MethodReference;
import de.gebit.integrity.dsl.NestedObject;
import de.gebit.integrity.dsl.NullValue;
Expand All @@ -40,6 +41,8 @@
import de.gebit.integrity.dsl.VariableOrConstantEntity;
import de.gebit.integrity.fixtures.FixtureWrapper;
import de.gebit.integrity.operations.UnexecutableException;
import de.gebit.integrity.parameter.conversion.ConversionContext;
import de.gebit.integrity.parameter.conversion.InexistentValueHandling;
import de.gebit.integrity.parameter.conversion.UnresolvableVariableHandling;
import de.gebit.integrity.parameter.conversion.ValueConverter;
import de.gebit.integrity.parameter.resolving.ParameterResolver;
Expand Down Expand Up @@ -83,8 +86,8 @@ public ComparisonResult compareResult(Object aFixtureResult, ValueOrEnumValueOrO
tempIsNull = true;
} else {
// ...or indirectly by the value being a variable/constant that resolves to a null value
VariableOrConstantEntity tempEntity = IntegrityDSLUtil
.extractVariableOrConstantEntity(anExpectedResult.getValue());
VariableOrConstantEntity tempEntity
= IntegrityDSLUtil.extractVariableOrConstantEntity(anExpectedResult.getValue());
if (tempEntity != null) {
Object tempResult = parameterResolver.resolveParameterValue(anExpectedResult,
UnresolvableVariableHandling.RESOLVE_TO_NULL_VALUE);
Expand All @@ -105,18 +108,18 @@ public ComparisonResult compareResult(Object aFixtureResult, ValueOrEnumValueOrO
tempConversionTargetType = aFixtureInstance.determineCustomConversionTargetType(aFixtureResult,
tempMethodName, aPropertyName);
} else {
tempConversionTargetType = aFixtureResult.getClass().isArray()
? aFixtureResult.getClass().getComponentType()
: aFixtureResult.getClass();
tempConversionTargetType
= aFixtureResult.getClass().isArray() ? aFixtureResult.getClass().getComponentType()
: aFixtureResult.getClass();
}

if (anExpectedResult.getMoreValues().size() > 0) {
// multiple result values given -> we're going to put them into an array of the same type
// as the fixture result
Class<?> tempArrayType = (tempConversionTargetType == null) ? Object.class
: tempConversionTargetType;
tempConvertedResult = Array.newInstance(tempArrayType,
anExpectedResult.getMoreValues().size() + 1);
Class<?> tempArrayType
= (tempConversionTargetType == null) ? Object.class : tempConversionTargetType;
tempConvertedResult
= Array.newInstance(tempArrayType, anExpectedResult.getMoreValues().size() + 1);
for (int i = 0; i < Array.getLength(tempConvertedResult); i++) {
ValueOrEnumValueOrOperation tempSingleExpectedResult = (i == 0 ? anExpectedResult.getValue()
: anExpectedResult.getMoreValues().get(i - 1));
Expand Down Expand Up @@ -274,7 +277,10 @@ protected ComparisonResult convertAndPerformEqualityCheck(Object aSingleFixtureR
}

tempConvertedFixtureResult = valueConverter.convertValue(Map.class, aSingleFixtureResult, null);
tempConvertedExpectedResult = valueConverter.convertValue(Map.class, tempNestedObject, null);
// Keeping Inexistent values as they are (= the InexistentValue model class instance) is necessary so that
// we can check for them later in map comparison. Otherwise they would be converted to strings.
tempConvertedExpectedResult = valueConverter.convertValue(Map.class, tempNestedObject,
new ConversionContext().withInexistentValueHandling(InexistentValueHandling.KEEP_AS_IS));
} else {
if (tempSingleExpectedResult instanceof Map && !(aSingleFixtureResult instanceof Map)) {
// if the expected result is a map, and the fixture has NOT returned a map, we also assume the fixture
Expand All @@ -283,8 +289,8 @@ protected ComparisonResult convertAndPerformEqualityCheck(Object aSingleFixtureR
tempConvertedExpectedResult = tempSingleExpectedResult;
} else {
// no special bean-related cases apply: convert the expected result to match the given fixture result
tempConvertedExpectedResult = valueConverter.convertValue(aConversionTargetType,
tempSingleExpectedResult, null);
tempConvertedExpectedResult
= valueConverter.convertValue(aConversionTargetType, tempSingleExpectedResult, null);
}
}

Expand Down Expand Up @@ -375,7 +381,8 @@ protected ComparisonResult performEqualityCheck(Object aConvertedResult, Object

/**
* Compare two {@link Map}s for equality. Maps are considered equal if all the values in the expected result are
* found in the actual result (there may well be more keys in the actual result than expected!).
* found in the actual result (there may well be more keys in the actual result than expected, except if one of the
* maps declares one of these "inexistent", in which case it may NOT exist in the other!).
*
* @param aResult
* the result returned by the fixture
Expand All @@ -394,6 +401,18 @@ protected MapComparisonResult performEqualityCheckForMaps(Map<?, ?> aResult, Map
Object tempActualValue = ((Map<?, ?>) aResult).get(tempEntry.getKey());
Object tempReferenceValue = tempEntry.getValue();

if (tempReferenceValue instanceof InexistentValue) {
if (tempActualValue != null || ((Map<?, ?>) aResult).containsKey(tempEntry.getKey())) {
// Reference values requires inexistence of key in actual map, but it contains it -> failure
tempSuccess = false;
tempCombinedFailedPaths.add(tempEntry.getKey().toString());
continue;
} else {
// Inexistence of value confirmed -> prevent further comparison (which would fail)
continue;
}
}

Object tempConvertedReferenceValue = tempReferenceValue;
if (!(tempActualValue instanceof Map && tempReferenceValue instanceof Map)) {
// If the inner values aren't maps themselves, special handling is required.
Expand Down Expand Up @@ -511,6 +530,17 @@ protected ComparisonResult performEqualityCheckForDates(Date aResult, Date anExp
}
}

/**
* Compare two {@link Temporal}s for equality.
*
* @param aResult
* the result returned by the fixture
* @param anExpectedResult
* the expected result as in the script, converted for comparison
* @param aRawExpectedResult
* the raw expected result as in the script, before conversion
* @return true if equal, false otherwise
*/
protected ComparisonResult performEqualityCheckForJava8Dates(Temporal aResult, Temporal anExpectedResult,
Object aRawExpectedResult) {
if (aRawExpectedResult instanceof DateValue) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,27 @@ packagedef integrity.basic.beans with
}

suiteend

suitedef simpleBeanWithInexistenceTest with

-- This should succeed
test createSimpleBeanUntyped = {
inventedParameter: inexistent
secondParameter: 100
thirdParameter: {
innerParameter: 1.99
}
}

-- This should fail
test createSimpleBeanUntyped = {
firstParameter: inexistent
secondParameter: 100
thirdParameter: {
innerParameter: 1.99
}
}

suiteend

packageend
Loading

0 comments on commit 4e0ee4b

Please sign in to comment.