/******************************************************************************* * 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.fixtures; import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Provider; import de.gebit.integrity.classloading.IntegrityClassLoader; import de.gebit.integrity.comparator.ComparisonResult; import de.gebit.integrity.dsl.MethodReference; import de.gebit.integrity.dsl.NamedCallResult; import de.gebit.integrity.dsl.NamedResult; import de.gebit.integrity.dsl.ResultTableHeader; import de.gebit.integrity.dsl.ValueOrEnumValueOrOperationCollection; import de.gebit.integrity.dsl.VariableVariable; import de.gebit.integrity.exceptions.ModelRuntimeLinkException; import de.gebit.integrity.fixtures.ExtendedResultFixture.ExtendedResult; import de.gebit.integrity.fixtures.ExtendedResultFixture.FixtureInvocationResult; import de.gebit.integrity.modelsource.ModelSourceExplorer; import de.gebit.integrity.modelsource.ModelSourceInformationElement; import de.gebit.integrity.operations.UnexecutableException; import de.gebit.integrity.parameter.conversion.ConversionContext; import de.gebit.integrity.parameter.conversion.ValueConverter; import de.gebit.integrity.string.FormattedString; import de.gebit.integrity.utils.IntegrityDSLUtil; import de.gebit.integrity.utils.ParameterUtil.UnresolvableVariableException; /** * This wrapper is used to encapsulate fixture instances. * * @param <C> * the fixture class * @author Rene Schneider - initial API and implementation * */ public class FixtureWrapper<C extends Object> { /** * The fixture method reference. */ private MethodReference methodReference; /** * The fixture method name. */ private String methodName; /** * The fixture class. */ private Class<C> fixtureClass; /** * The wrapped instance. */ private C fixtureInstance; /** * The factory that created the fixture instance. */ private FixtureInstanceFactory<C> factory; /** * The value converter to use. */ @Inject private ValueConverter valueConverter; /** * The classloader to use. */ @Inject private IntegrityClassLoader classLoader; /** * The model source explorer. */ @Inject private ModelSourceExplorer modelSourceExplorer; /** * The conversion context provider. */ @Inject protected Provider<ConversionContext> conversionContextProvider; /** * Fixture instance factories are cached in this map. */ private static Map<Class<?>, FixtureInstanceFactory<?>> factoryCache = new HashMap<Class<?>, FixtureInstanceFactory<?>>(); /** * Creates a new instance. This also instantiates the given fixture class! * * @param aMethodReference * the fixture method reference to be wrapped * @param anInjector * The injector required to inject dependencies into fixture instances and factories. (I don't really * like to provide this explicitly here, but cannot use injection, since that happens after the * constructor. Maybe I'll refactor this some time later...) * @throws InstantiationException * @throws IllegalAccessException * @throws ClassNotFoundException */ @SuppressWarnings("unchecked") public FixtureWrapper(MethodReference aMethodReference, Injector anInjector) throws InstantiationException, IllegalAccessException, ClassNotFoundException { anInjector.injectMembers(this); methodReference = aMethodReference; if (methodReference.getMethod() == null) { throw new ModelRuntimeLinkException("Method reference missing", aMethodReference, modelSourceExplorer.determineSourceInformation(aMethodReference)); } methodName = methodReference.getMethod().getSimpleName(); fixtureClass = (Class<C>) classLoader.loadClass(methodReference); FixtureInstanceFactory<C> tempFactory = null; if (factoryCache.containsKey(fixtureClass)) { tempFactory = (FixtureInstanceFactory<C>) factoryCache.get(fixtureClass); } else { FixtureFactory tempFactoryAnnotation = fixtureClass.getAnnotation(FixtureFactory.class); if (tempFactoryAnnotation != null) { tempFactory = (FixtureInstanceFactory<C>) tempFactoryAnnotation.value().newInstance(); anInjector.injectMembers(tempFactory); } factoryCache.put(fixtureClass, tempFactory); } if (tempFactory != null) { fixtureInstance = tempFactory.retrieveInstance(); factory = tempFactory; } else { fixtureInstance = fixtureClass.newInstance(); } anInjector.injectMembers(fixtureInstance); } /** * Releases the fixture instance. */ public void release() { if (factory != null) { try { factory.releaseInstance(fixtureInstance); // SUPPRESS CHECKSTYLE IllegalCatch } catch (Throwable exc) { // Since releasing is usually happening outside of the exception catch block that spans the actual // fixture method calls, we'll catch everything coming up during fixture instance release here. We // don't want any custom code running there to fuck up the whole test execution process. Instead, we // will just push it out on the console and go on. exc.printStackTrace(); } } fixtureInstance = null; } protected Class<?> getFixtureClass() { return fixtureClass; } protected Object getFixtureInstance() { return fixtureInstance; } /** * Checks whether the wrapped fixture is a {@link CustomComparatorFixture}. * * @return true if it is */ public boolean isCustomComparatorFixture() { return (CustomComparatorFixture.class.isAssignableFrom(fixtureClass)); } /** * Checks whether the wrapped fixture is a {@link CustomComparatorAndConversionFixture}. * * @return true if it is */ public boolean isCustomComparatorAndConversionFixture() { return (CustomComparatorAndConversionFixture.class.isAssignableFrom(fixtureClass)); } /** * Checks whether the wrapped fixture is a {@link CustomStringConversionFixture}. * * @return true if it is */ public boolean isCustomStringConversionFixture() { return (CustomStringConversionFixture.class.isAssignableFrom(fixtureClass)); } /** * Checks whether the wrapped fixture is a {@link ResultAwareFixture}. * * @return true if it is */ public boolean isResultAwareFixture() { return (ResultAwareFixture.class.isAssignableFrom(fixtureClass)); } /** * Performs a custom comparation using the wrapped fixture, which must be a {@link CustomComparatorFixture}. Only * usable if {@link #isCustomComparatorFixture()} returns true. * * @param anExpectedResult * the expected result * @param aFixtureResult * the result actually returned by the fixture * @param aMethodName * the name of the fixture method * @param aPropertyName * the name of the result property to be compared (null if it's the default result) * @return true if comparation was successful, false otherwise */ public ComparisonResult performCustomComparation(Object anExpectedResult, Object aFixtureResult, String aMethodName, String aPropertyName) { return ((CustomComparatorFixture) fixtureInstance).compareResults(anExpectedResult, aFixtureResult, aMethodName, aPropertyName); } /** * Returns the type to which the expected result (the data given in the test script) that corresponds to the given * fixture result is to be converted. This can only be used for {@link CustomComparatorAndConversionFixture} * instances, which can be checked via {@link #isCustomComparatorAndConversionFixture()}. * * @param aFixtureResult * the result value returned by the fixture call * @param aMethodName * the fixture method that was called * @param aPropertyName * the property name that is to be compared (null if it's the default result) * @return the desired target type. "null" chooses the default conversion, but note that this does NOT mean "the * conversion that would have been used if the fixture was just a {@link CustomComparatorFixture}", but "the * conversion that has the highest priority for the data type found in the script". */ public Class<?> determineCustomConversionTargetType(Object aFixtureResult, String aMethodName, String aPropertyName) { return ((CustomComparatorAndConversionFixture) fixtureInstance).determineConversionTargetType(aFixtureResult, aMethodName, aPropertyName); } /** * Converts the given value to a string. This method either calls the * {@link ValueConverter#convertValueToString(Object, ConversionContext)} method or delegates the conversion to the * contained fixture instance, if it does implement the {@link CustomStringConversionFixture} interface. * * @param aValue * the value to convert * @param aFixtureMethod * the fixture method that was called to return the given value * @param aForceIntermediateMapFlag * whether the conversion should force the usage of an intermediate map (useful for bean types) * @param aConversionContext * the conversion context to use (may be null if the default shall be used) * @return the converted string */ public FormattedString performValueToFormattedStringConversion(Object aValue, String aFixtureMethod, boolean aForceIntermediateMapFlag, ConversionContext aConversionContext) { if (isCustomStringConversionFixture()) { return ((CustomStringConversionFixture) fixtureInstance).convertValueToString(aValue, aFixtureMethod); } else { return valueConverter.convertValueToFormattedString(aValue, aForceIntermediateMapFlag, aConversionContext); } } /** * Executes the fixture method, using the given set of parameters. * * @param someParameters * a map of parameters * @return the resulting object * @throws Throwable */ public Object execute(Map<String, Object> someParameters) throws Throwable { Method tempMethod = classLoader.loadMethod(methodReference); convertParameterValuesToFixtureDefinedTypes(tempMethod, someParameters, true); int tempMethodParamCount = tempMethod.getParameterTypes().length; Object[] tempParams = new Object[tempMethodParamCount]; for (int i = 0; i < tempMethodParamCount; i++) { FixtureParameter tempAnnotation = findAnnotation(FixtureParameter.class, tempMethod.getParameterAnnotations()[i]); if (tempAnnotation != null && tempAnnotation.name() != null) { tempParams[i] = someParameters.remove(tempAnnotation.name()); } else if (Map.class.equals(tempMethod.getParameterTypes()[i])) { // this gets any arbitrary parameters left over tempParams[i] = someParameters; } } try { return tempMethod.invoke(getFixtureInstance(), tempParams); } catch (IllegalAccessException exc) { ModelSourceInformationElement tempModelSourceInfo = modelSourceExplorer .determineSourceInformation(methodReference); throw new IllegalArgumentException("Caught exception when trying to invoke fixture method '" + tempModelSourceInfo.getSnippet() + "' defined at " + tempModelSourceInfo, exc); } catch (InvocationTargetException exc) { throw (Throwable) exc.getCause(); } } /** * Replaces all values in the given parameter map with converted versions that match the types that are expected by * the given fixture method. * * @param aFixtureMethod * the method * @param aParameterMap * the parameter map * @param anIncludeArbitraryParametersFlag * whether arbitrary parameters shall be included * @throws InstantiationException * @throws UnexecutableException * @throws ClassNotFoundException * @throws UnresolvableVariableException */ public void convertParameterValuesToFixtureDefinedTypes(Method aFixtureMethod, Map<String, Object> aParameterMap, boolean anIncludeArbitraryParametersFlag) throws UnresolvableVariableException, ClassNotFoundException, UnexecutableException, InstantiationException { Map<String, Object> tempClonedParameterMap = new HashMap<String, Object>(aParameterMap); int tempMethodParamCount = aFixtureMethod.getParameterTypes().length; for (int i = 0; i < tempMethodParamCount; i++) { FixtureParameter tempAnnotation = findAnnotation(FixtureParameter.class, aFixtureMethod.getParameterAnnotations()[i]); if (tempAnnotation != null && tempAnnotation.name() != null) { String tempName = tempAnnotation.name(); Object tempValue = aParameterMap.get(tempName); Class<?> tempExpectedType = aFixtureMethod.getParameterTypes()[i]; if (tempValue != null) { Object tempConvertedValue; if (tempValue instanceof Object[]) { if (!tempExpectedType.isArray()) { throw new IllegalArgumentException("The parameter '" + tempName + "' of method '" + aFixtureMethod.getName() + "' in fixture '" + fixtureClass.getName() + "' is not an array type, thus you cannot put multiple values into it!"); } Object tempConvertedValueArray = Array.newInstance(tempExpectedType.getComponentType(), ((Object[]) tempValue).length); for (int k = 0; k < ((Object[]) tempValue).length; k++) { Object tempSingleValue = ((Object[]) tempValue)[k]; Array.set(tempConvertedValueArray, k, valueConverter.convertValue( tempExpectedType.getComponentType(), tempSingleValue, null)); } tempConvertedValue = tempConvertedValueArray; } else { // 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 = tempExpectedType.isArray() ? tempExpectedType .getComponentType() : tempExpectedType; // ...except for byte arrays (issue #66), those must be treated specially! boolean tempSpecialByteArrayMode = false; if (tempExpectedType == byte[].class || tempExpectedType == Byte[].class) { tempConversionTargetType = tempExpectedType; tempSpecialByteArrayMode = true; } tempConvertedValue = valueConverter.convertValue(tempConversionTargetType, tempValue, null); if (!tempSpecialByteArrayMode && tempExpectedType.isArray()) { // ...and if the expected type is an array, now we create one Object tempNewArray = Array.newInstance(tempExpectedType.getComponentType(), 1); Array.set(tempNewArray, 0, tempConvertedValue); tempConvertedValue = tempNewArray; } } aParameterMap.put(tempName, tempConvertedValue); } tempClonedParameterMap.remove(tempName); } } if (anIncludeArbitraryParametersFlag && (getFixtureInstance() instanceof ArbitraryParameterFixture)) { for (Entry<String, Object> tempParameter : tempClonedParameterMap.entrySet()) { String tempName = tempParameter.getKey(); Object tempValue = aParameterMap.remove(tempName); if (tempValue != null) { Object tempConvertedValue; // In case of arbitrary parameters, we don't want to perform the default bean-to-map conversion, // because otherwise one couldn't put any objects into the fixture without having them converted to // maps. See also issue #52: https://github.com/integrity-tf/integrity/issues/52 tempConvertedValue = valueConverter.convertValue(null, tempValue, conversionContextProvider.get() .skipBeanToMapDefaultConversion()); aParameterMap.put(tempName, tempConvertedValue); } } } else { if (tempClonedParameterMap.size() > 0) { throw new IllegalStateException("There were " + tempClonedParameterMap.size() + " parameters left after processing the fixed params, but the fixture '" + fixtureClass.getName() + "' is not an arbitrary parameter fixture. Left-over params: " + tempClonedParameterMap.keySet()); } } } /** * Call {@link ExtendedResultFixture#provideExtendedResults()} on the fixture - if it is an extended result fixture * - and return the extended results. * * @return the extended result list, or null if the fixture does not support the protocol or didn't return anything */ public List<ExtendedResult> retrieveExtendedResults(FixtureInvocationResult anInvocationResult) { if (fixtureInstance instanceof ExtendedResultFixture) { List<ExtendedResult> tempList = ((ExtendedResultFixture) fixtureInstance) .provideExtendedResults(anInvocationResult); return (tempList != null && tempList.size() > 0) ? tempList : null; } else { return null; } } /** * Invoke the {@link ResultAwareFixture} method for the case of a 'call' type fixture invocation, if the fixture is * a {@link ResultAwareFixture}. */ public void announceCallResults(VariableVariable aDefaultTargetVariable, List<NamedCallResult> someNamedTargetVariables) { if (fixtureInstance instanceof ResultAwareFixture) { Set<String> tempNamedResultSet = new HashSet<>(); if (someNamedTargetVariables != null) { for (NamedCallResult tempResult : someNamedTargetVariables) { tempNamedResultSet.add(IntegrityDSLUtil.getExpectedResultNameStringFromTestResultName(tempResult .getName())); } } ((ResultAwareFixture) fixtureInstance).announceCheckedResults(methodName, aDefaultTargetVariable != null, tempNamedResultSet); } } /** * Invoke the {@link ResultAwareFixture} method for the case of a 'test' type fixture invocation, if the fixture is * a {@link ResultAwareFixture}. * * @param aDefaultResult * The default result as given in the test script * @param someNamedResults * A list of named results used by the test */ public void announceTestResults(ValueOrEnumValueOrOperationCollection aDefaultResult, List<NamedResult> someNamedResults) { if (fixtureInstance instanceof ResultAwareFixture) { Set<String> tempNamedResultSet = new HashSet<>(); if (someNamedResults != null) { for (NamedResult tempResult : someNamedResults) { tempNamedResultSet.add(IntegrityDSLUtil.getExpectedResultNameStringFromTestResultName(tempResult .getName())); } } announceTestResultsInternal(aDefaultResult, tempNamedResultSet); } } /** * Invoke the {@link ResultAwareFixture} method for the case of a 'tabletest' type fixture invocation. * * @param aDefaultResult * The default result as given in the test script * @param someResultHeaders * A list of named results used by the test */ public void announceTableTestResults(ValueOrEnumValueOrOperationCollection aDefaultResult, List<ResultTableHeader> someResultHeaders) { if (fixtureInstance instanceof ResultAwareFixture) { Set<String> tempNamedResultSet = new HashSet<>(); if (someResultHeaders != null) { for (ResultTableHeader tempHeader : someResultHeaders) { tempNamedResultSet.add(IntegrityDSLUtil.getExpectedResultNameStringFromTestResultName(tempHeader .getName())); } } announceTestResultsInternal(aDefaultResult, tempNamedResultSet); } } /** * Actually performs the test result announcement call. * * @param aDefaultResult * The default result as given in the test script * @param aNamedResultSet * A list of named results used by the test */ protected void announceTestResultsInternal(ValueOrEnumValueOrOperationCollection aDefaultResult, Set<String> aNamedResultSet) { // no named result and no explicit default result = implicit default result! boolean tempHasDefaultResult = aDefaultResult != null || aNamedResultSet.size() == 0; ((ResultAwareFixture) fixtureInstance) .announceCheckedResults(methodName, tempHasDefaultResult, aNamedResultSet); } @SuppressWarnings("unchecked") private static <A extends Annotation> A findAnnotation(Class<A> aClass, Annotation[] someAnnotations) { for (int i = 0; i < someAnnotations.length; i++) { if (aClass.isAssignableFrom(someAnnotations[i].getClass())) { return (A) someAnnotations[i]; } } return null; } }