/******************************************************************************* * Copyright (c) 2013 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.runner.comparator; import java.lang.reflect.Array; import java.util.Arrays; import java.util.Date; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import com.google.inject.Inject; import de.gebit.integrity.comparator.ComparisonResult; import de.gebit.integrity.comparator.MapComparisonResult; import de.gebit.integrity.comparator.SimpleComparisonResult; import de.gebit.integrity.dsl.CustomOperation; import de.gebit.integrity.dsl.DateValue; import de.gebit.integrity.dsl.MethodReference; import de.gebit.integrity.dsl.NestedObject; import de.gebit.integrity.dsl.NullValue; import de.gebit.integrity.dsl.TimeValue; import de.gebit.integrity.dsl.TypedNestedObject; import de.gebit.integrity.dsl.ValueOrEnumValueOrOperation; import de.gebit.integrity.dsl.ValueOrEnumValueOrOperationCollection; import de.gebit.integrity.dsl.Variable; import de.gebit.integrity.dsl.VariableOrConstantEntity; import de.gebit.integrity.fixtures.FixtureWrapper; import de.gebit.integrity.operations.UnexecutableException; import de.gebit.integrity.parameter.conversion.UnresolvableVariableHandling; import de.gebit.integrity.parameter.conversion.ValueConverter; import de.gebit.integrity.parameter.resolving.ParameterResolver; import de.gebit.integrity.utils.DateUtil; import de.gebit.integrity.utils.IntegrityDSLUtil; import de.gebit.integrity.utils.ParameterUtil.UnresolvableVariableException; /** * The standard result comparator component. * * @author Rene Schneider - initial API and implementation * */ public class DefaultResultComparator implements ResultComparator { /** * The value converter to use. */ @Inject protected ValueConverter valueConverter; /** * The parameter resolver to use. */ @Inject protected ParameterResolver parameterResolver; @Override public ComparisonResult compareResult(Object aFixtureResult, ValueOrEnumValueOrOperationCollection anExpectedResult, FixtureWrapper<?> aFixtureInstance, MethodReference aFixtureMethod, String aPropertyName) throws ClassNotFoundException, UnexecutableException, InstantiationException { if (anExpectedResult != null) { if (aFixtureResult == null) { if (anExpectedResult.getMoreValues().size() > 0) { // if there's more than one value expected, this can never equal a single null value return SimpleComparisonResult.NOT_EQUAL; } else { boolean tempIsNull = false; // This is only true if the expected result is also null. That could be directly... if (anExpectedResult.getValue() instanceof NullValue) { tempIsNull = true; } else { // ...or indirectly by the value being a variable/constant that resolves to a null value VariableOrConstantEntity tempEntity = IntegrityDSLUtil .extractVariableOrConstantEntity(anExpectedResult.getValue()); if (tempEntity != null) { Object tempResult = parameterResolver.resolveParameterValue(anExpectedResult, UnresolvableVariableHandling.RESOLVE_TO_NULL_VALUE); tempIsNull = (tempResult == null || (tempResult instanceof NullValue)); } } return SimpleComparisonResult.valueOf(tempIsNull); } } else { if (aFixtureInstance.isCustomComparatorFixture()) { // Custom comparators will get whole arrays at once if arrays are used String tempMethodName = aFixtureMethod.getMethod().getSimpleName(); Object tempConvertedResult = null; Class<?> tempConversionTargetType = null; if (aFixtureInstance.isCustomComparatorAndConversionFixture()) { tempConversionTargetType = aFixtureInstance.determineCustomConversionTargetType(aFixtureResult, tempMethodName, aPropertyName); } else { 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); for (int i = 0; i < Array.getLength(tempConvertedResult); i++) { ValueOrEnumValueOrOperation tempSingleExpectedResult = (i == 0 ? anExpectedResult.getValue() : anExpectedResult.getMoreValues().get(i - 1)); Array.set(tempConvertedResult, i, valueConverter.convertValue(tempConversionTargetType, tempSingleExpectedResult, null)); } } else { tempConvertedResult = valueConverter.convertValue(tempConversionTargetType, anExpectedResult.getValue(), null); } return aFixtureInstance.performCustomComparation(tempConvertedResult, aFixtureResult, tempMethodName, aPropertyName); } else { // Standard comparation compares each value for itself in case of arrays if (anExpectedResult.getMoreValues().size() > 0) { // multiple result values were given -> fixture result must be an array of same size if (!(aFixtureResult.getClass().isArray() && Array.getLength(aFixtureResult) == anExpectedResult.getMoreValues().size() + 1)) { return SimpleComparisonResult.NOT_EQUAL; } // now compare all values for (int i = 0; i < Array.getLength(aFixtureResult); i++) { Object tempSingleFixtureResult = Array.get(aFixtureResult, i); ValueOrEnumValueOrOperation tempSingleExpectedResult = (i == 0 ? anExpectedResult.getValue() : anExpectedResult.getMoreValues().get(i - 1)); if (tempSingleFixtureResult == null) { // The fixture returned a null, we need to expect a null if (!(tempSingleExpectedResult instanceof NullValue)) { return SimpleComparisonResult.NOT_EQUAL; } } else { if (!convertAndPerformEqualityCheck(tempSingleFixtureResult, tempSingleExpectedResult, tempSingleFixtureResult.getClass()).isSuccessful()) { return SimpleComparisonResult.NOT_EQUAL; } } } return SimpleComparisonResult.EQUAL; } else { // If we arrive here, the expected result is a simple, single value. ValueOrEnumValueOrOperation tempSingleExpectedResult = anExpectedResult.getValue(); Object tempSingleFixtureResult = aFixtureResult; // First see if we have the special case of byte arrays (issue #66). Those must be handled // separately. if (tempSingleFixtureResult instanceof byte[]) { byte[] tempConvertedExpectedResult = (byte[]) valueConverter.convertValue(byte[].class, tempSingleExpectedResult, null); return SimpleComparisonResult.valueOf( Arrays.equals((byte[]) tempSingleFixtureResult, tempConvertedExpectedResult)); } else if (tempSingleFixtureResult instanceof Byte[]) { Byte[] tempConvertedExpectedResult = (Byte[]) valueConverter.convertValue(Byte[].class, tempSingleExpectedResult, null); return SimpleComparisonResult.valueOf( Arrays.equals((Byte[]) tempSingleFixtureResult, tempConvertedExpectedResult)); } // The fixture might still have returned an array. // If the expected type is an array, we don't want to convert to that array, but to the // component type, of course Class<?> tempConversionTargetType = tempSingleFixtureResult.getClass(); if (tempSingleFixtureResult.getClass().isArray()) { tempConversionTargetType = tempSingleFixtureResult.getClass().getComponentType(); if (tempConversionTargetType == Object.class) { // Object arrays are bad target types; in this case we try to deduct a target type from // the values within the array tempConversionTargetType = null; for (int i = 0; i < Array.getLength(tempSingleFixtureResult); i++) { Object tempArrayValue = Array.get(tempSingleFixtureResult, i); if (tempArrayValue != null) { if (tempConversionTargetType == null) { tempConversionTargetType = tempArrayValue.getClass(); } else { if (tempConversionTargetType.isAssignableFrom(tempArrayValue.getClass())) { // current value type is a subtype of the current target type -> good! } else { // the types in the array don't match at all -> bad! Use standard // conversion. tempConversionTargetType = null; break; } } } } } } return convertAndPerformEqualityCheck(tempSingleFixtureResult, tempSingleExpectedResult, tempConversionTargetType); } } } } else { if (aFixtureInstance.isCustomComparatorFixture()) { return aFixtureInstance.performCustomComparation(null, aFixtureResult, aFixtureMethod.getMethod().getSimpleName(), aPropertyName); } else { if (aFixtureResult instanceof Boolean) { return SimpleComparisonResult.valueOf((Boolean) aFixtureResult); } else { throw new IllegalArgumentException( "If no expected test result is given and the fixture is not a CustomComparatorFixture, " + "the test fixture must return a boolean result!"); } } } } /** * Converts a fixture result and an expected result value for comparison (usually by converting the expected result * to match the fixture result, but nested objects are handled differently and converted to maps for comparison). * The final results are then compared. * * @param aSingleFixtureResult * the fixture result * @param aSingleExpectedResult * the expected result * @param aConversionTargetType * the target type for conversion * @return true if both values are considered equal, false if not * @throws UnresolvableVariableException * @throws UnexecutableException */ protected ComparisonResult convertAndPerformEqualityCheck(Object aSingleFixtureResult, ValueOrEnumValueOrOperation aSingleExpectedResult, Class<?> aConversionTargetType) throws UnresolvableVariableException, UnexecutableException { Object tempConvertedExpectedResult; Object tempConvertedFixtureResult = aSingleFixtureResult; if (((aSingleExpectedResult instanceof NestedObject) || (aSingleExpectedResult instanceof TypedNestedObject)) && !(aSingleFixtureResult instanceof Map)) { // if the expected result is a (typed) nested object, and the fixture has NOT returned a // map, we assume the fixture result to be a bean class/instance. We'll convert both to maps // for comparison! NestedObject tempNestedObject; if (aSingleExpectedResult instanceof TypedNestedObject) { tempNestedObject = ((TypedNestedObject) aSingleExpectedResult).getNestedObject(); } else { tempNestedObject = (NestedObject) aSingleExpectedResult; } tempConvertedFixtureResult = valueConverter.convertValue(Map.class, aSingleFixtureResult, null); tempConvertedExpectedResult = valueConverter.convertValue(Map.class, tempNestedObject, null); } else { // Two special bean-related cases still may apply: expected result may be a map, or a variable or custom // operation which results in a map when resolving. We now check for those. Object tempPossibleMapAsSingleExpectedResult = aSingleExpectedResult; if ((aSingleExpectedResult instanceof Variable) || (aSingleExpectedResult instanceof CustomOperation)) { try { tempPossibleMapAsSingleExpectedResult = parameterResolver.resolveSingleParameterValue( aSingleExpectedResult, UnresolvableVariableHandling.RESOLVE_TO_NULL_VALUE); } catch (InstantiationException exc) { throw new UnexecutableException("Failed to resolve an operation", exc); } catch (ClassNotFoundException exc) { throw new UnexecutableException("Failed to resolve an operation", exc); } } if (tempPossibleMapAsSingleExpectedResult 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 // result to be a bean class/instance. But we only need to convert that to a map for comparison. tempConvertedFixtureResult = valueConverter.convertValue(Map.class, aSingleFixtureResult, null); tempConvertedExpectedResult = tempPossibleMapAsSingleExpectedResult; } else { // no special bean-related cases apply: convert the expected result to match the given fixture result tempConvertedExpectedResult = valueConverter.convertValue(aConversionTargetType, aSingleExpectedResult, null); } } return performEqualityCheck(tempConvertedFixtureResult, tempConvertedExpectedResult, aSingleExpectedResult); } /** * Perform the actual equality check between a real result returned from a fixture and a converted result gathered * from the test scripts. A few special cases are handled here, but if no special case applies, this just runs a * standard equals() comparison. * * @param aConvertedResult * the actual result * @param aConvertedExpectedResult * the expected result from the scripts, converted to the same type as the actual result * @param aRawExpectedResult * the raw expected result object from the scripts * @return true if equal, false otherwise */ protected ComparisonResult performEqualityCheck(Object aConvertedResult, Object aConvertedExpectedResult, ValueOrEnumValueOrOperation aRawExpectedResult) { if (aConvertedResult == null) { return SimpleComparisonResult.valueOf(aConvertedExpectedResult == null || (aConvertedExpectedResult.getClass().isArray() && Array.getLength(aConvertedExpectedResult) == 1 && Array.get(aConvertedExpectedResult, 0) == null)); } else { if (aConvertedResult instanceof Date && aConvertedExpectedResult instanceof Date) { return performEqualityCheckForDates((Date) aConvertedResult, (Date) aConvertedExpectedResult, aRawExpectedResult); } else if (aConvertedResult instanceof Map && aConvertedExpectedResult instanceof Map) { return performEqualityCheckForMaps((Map<?, ?>) aConvertedResult, (Map<?, ?>) aConvertedExpectedResult, aRawExpectedResult); } else if (aConvertedResult.getClass().isArray()) { if (aConvertedExpectedResult == null) { // the fixture may still be returning an array that has to be unpacked if (Array.getLength(aConvertedResult) != 1) { return SimpleComparisonResult.NOT_EQUAL; } return SimpleComparisonResult.valueOf(Array.get(aConvertedResult, 0) == null); } else { if (!aConvertedExpectedResult.getClass().isArray()) { // the fixture may be returning an array that has to be unpacked if (Array.getLength(aConvertedResult) != 1) { return SimpleComparisonResult.NOT_EQUAL; } return performEqualityCheck(Array.get(aConvertedResult, 0), aConvertedExpectedResult, aRawExpectedResult); } else { if (Array.getLength(aConvertedResult) != Array.getLength(aConvertedExpectedResult)) { return SimpleComparisonResult.NOT_EQUAL; } // both are converted arrays -> compare all values! for (int i = 0; i < Array.getLength(aConvertedResult); i++) { ComparisonResult tempResult = performEqualityCheck(Array.get(aConvertedResult, i), Array.get(aConvertedExpectedResult, i), aRawExpectedResult); if (!tempResult.isSuccessful()) { return SimpleComparisonResult.NOT_EQUAL; } } return SimpleComparisonResult.EQUAL; } } } else { // This is the super-simple case where we basically have only one value to compare if (aConvertedExpectedResult == null) { // we have validated convertedResult to be non-null before return SimpleComparisonResult.NOT_EQUAL; } else { if (aConvertedExpectedResult.getClass().isArray()) { // the converted result may still be an array if (Array.getLength(aConvertedExpectedResult) != 1) { return SimpleComparisonResult.EQUAL; } return performEqualityCheck(aConvertedResult, Array.get(aConvertedExpectedResult, 0), aRawExpectedResult); } else { // If no special cases apply, perform standard equals comparison return performEqualityCheckForObjects(aConvertedResult, aConvertedExpectedResult, aRawExpectedResult); } } } } } /** * 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!). * * @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 MapComparisonResult performEqualityCheckForMaps(Map<?, ?> aResult, Map<?, ?> anExpectedResult, ValueOrEnumValueOrOperation aRawExpectedResult) { boolean tempSuccess = true; Set<String> tempCombinedFailedPaths = new HashSet<String>(); for (Entry<?, ?> tempEntry : ((Map<?, ?>) anExpectedResult).entrySet()) { Object tempActualValue = ((Map<?, ?>) aResult).get(tempEntry.getKey()); Object tempReferenceValue = tempEntry.getValue(); Object tempConvertedReferenceValue = tempReferenceValue; if (!(tempActualValue instanceof Map && tempReferenceValue instanceof Map)) { // If the inner values aren't maps themselves, special handling is required. // First see if they are arrays (maybe of maps, even). This stuff fixes issue #124! if ((tempActualValue != null && tempActualValue.getClass().isArray()) || (tempReferenceValue != null && tempReferenceValue.getClass().isArray())) { // If one or both values is an array, things get more complicated... if (!(tempActualValue != null && tempActualValue.getClass().isArray()) || !(tempReferenceValue != null && tempReferenceValue.getClass().isArray())) { // If just one is an array, we automatically fail, since we have a different number of elements tempCombinedFailedPaths.add(tempEntry.getKey().toString()); tempSuccess = false; } else { // Both are arrays -> check if length is equal, then check each entry if (Array.getLength(tempActualValue) != Array.getLength(tempReferenceValue)) { tempSuccess = false; tempCombinedFailedPaths.add(tempEntry.getKey().toString()); } else { for (int i = 0; i < Array.getLength(tempActualValue); i++) { ComparisonResult tempInnerResult = performEqualityCheck(Array.get(tempActualValue, i), Array.get(tempReferenceValue, i), aRawExpectedResult); if (!tempInnerResult.isSuccessful()) { tempSuccess = false; // In case the sub-result is of a map comparison, we just add the failed paths to // ours, prepending them with the necessary prefix in the process if (tempInnerResult instanceof MapComparisonResult) { for (String tempSubPath : ((MapComparisonResult) tempInnerResult) .getFailedPaths()) { tempCombinedFailedPaths.add(tempEntry.getKey() + "." + tempSubPath); } } else { tempCombinedFailedPaths.add(tempEntry.getKey().toString()); } break; } } continue; } } } // Okay, not arrays. In this case we still have to ensure both values are of equal type first, // since even though both outer values are maps, their inner values have not been necessarily converted // to the same types. try { tempConvertedReferenceValue = (tempActualValue != null) ? valueConverter.convertValue(tempActualValue.getClass(), tempReferenceValue, null) : tempReferenceValue; } catch (UnresolvableVariableException exc) { exc.printStackTrace(); } catch (UnexecutableException exc) { exc.printStackTrace(); } } ComparisonResult tempInnerResult = performEqualityCheck(tempActualValue, tempConvertedReferenceValue, (tempReferenceValue instanceof ValueOrEnumValueOrOperation) ? (ValueOrEnumValueOrOperation) tempReferenceValue : null); if (!tempInnerResult.isSuccessful()) { tempSuccess = false; // In case the sub-result is of a map comparison, we just add the failed paths to ours, prepending them // with the necessary prefix in the process if (tempInnerResult instanceof MapComparisonResult) { for (String tempSubPath : ((MapComparisonResult) tempInnerResult).getFailedPaths()) { tempCombinedFailedPaths.add(tempEntry.getKey() + "." + tempSubPath); } } else { tempCombinedFailedPaths.add(tempEntry.getKey().toString()); } } } return new MapComparisonResult(tempSuccess, tempCombinedFailedPaths); } /** * Compare two {@link Date}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 performEqualityCheckForDates(Date aResult, Date anExpectedResult, Object aRawExpectedResult) { if (aRawExpectedResult instanceof DateValue) { // compare only the date part return SimpleComparisonResult.valueOf(DateUtil.stripTimeFromDate((Date) anExpectedResult) .equals(DateUtil.stripTimeFromDate((Date) aResult))); } else if (aRawExpectedResult instanceof TimeValue) { // compare only the time part return SimpleComparisonResult.valueOf(DateUtil.stripDateFromTime((Date) anExpectedResult) .equals(DateUtil.stripDateFromTime((Date) aResult))); } else { // compare both parts return SimpleComparisonResult.valueOf(anExpectedResult.equals(aResult)); } } /** * Compare two objects. At this point it is expected that the previous stages have done all conversion work, * iteration through arrays etc. * * @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 performEqualityCheckForObjects(Object aResult, Object anExpectedResult, Object aRawExpectedResult) { return SimpleComparisonResult.valueOf(anExpectedResult.equals(aResult)); } }