/*******************************************************************************
* 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.callbacks;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.inject.Inject;
import com.google.inject.Provider;
import de.gebit.integrity.classloading.IntegrityClassLoader;
import de.gebit.integrity.dsl.Call;
import de.gebit.integrity.dsl.MethodReference;
import de.gebit.integrity.dsl.SuiteStatementWithResult;
import de.gebit.integrity.dsl.TableTest;
import de.gebit.integrity.dsl.TableTestRow;
import de.gebit.integrity.dsl.Test;
import de.gebit.integrity.dsl.VariableAssignment;
import de.gebit.integrity.exceptions.MethodNotFoundException;
import de.gebit.integrity.fixtures.FixtureMethod;
import de.gebit.integrity.operations.UnexecutableException;
import de.gebit.integrity.parameter.conversion.ConversionContext;
import de.gebit.integrity.parameter.conversion.UnresolvableVariable;
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.parameter.resolving.TableTestParameterResolveMethod;
import de.gebit.integrity.utils.IntegrityDSLUtil;
/**
* The {@link TestFormatter} is responsible for creating human-readable strings out of various test-related entities.
* This is usually done by using the descriptions attached to fixtures.
*
*
* @author Rene Schneider - initial API and implementation
*
*/
public class TestFormatter {
/**
* Escape pattern for parameters in descriptions.
*/
private static final Pattern PARAMETER_PATTERN = Pattern.compile("^(.*)\\$(.*)\\$(.*)$");
/**
* The classloader to use.
*/
@Inject
private IntegrityClassLoader classLoader;
/**
* The value converter to use.
*/
@Inject
private ValueConverter valueConverter;
/**
* The parameter resolver to use.
*/
@Inject
private ParameterResolver parameterResolver;
/**
* The conversion context provider.
*/
@Inject
protected Provider<ConversionContext> conversionContextProvider;
/**
* Creates a new instance.
*
*/
public TestFormatter() {
super();
}
/**
* Creates a human-readable string for a test.
*
* @param aTest
* the test
* @return the human-readable test description
* @throws ClassNotFoundException
* @throws InstantiationException
* @throws UnexecutableException
* @throws MethodNotFoundException
*/
public String testToHumanReadableString(Test aTest, ConversionContext aConversionContext)
throws ClassNotFoundException, UnexecutableException, InstantiationException, MethodNotFoundException {
ConversionContext tempConversionContext = safeguardConversionContext(aConversionContext);
return fixtureMethodToHumanReadableString(aTest.getDefinition().getFixtureMethod(), aTest, parameterResolver
.createParameterMap(aTest, true, tempConversionContext.getUnresolvableVariableHandlingPolicy()),
tempConversionContext);
}
/**
* Creates a human-readable string for a tabletest.
*
* @param aTest
* the test
* @param aRow
* the row (may be null if the string shall be for the whole test)
* @return the human-readable description
* @throws ClassNotFoundException
* @throws InstantiationException
* @throws UnexecutableException
* @throws MethodNotFoundException
*/
public String tableTestRowToHumanReadableString(TableTest aTest, TableTestRow aRow,
ConversionContext aConversionContext) throws ClassNotFoundException, UnexecutableException,
InstantiationException, MethodNotFoundException {
ConversionContext tempConversionContext = safeguardConversionContext(aConversionContext);
return fixtureMethodToHumanReadableString(aTest.getDefinition().getFixtureMethod(), aTest,
parameterResolver.createParameterMap(aTest, aRow, TableTestParameterResolveMethod.COMBINED, true,
tempConversionContext.getUnresolvableVariableHandlingPolicy()),
tempConversionContext);
}
/**
* Creates a human-readable string for a tabletest.
*
* @param aTest
* the test
* @return the human-readable string
* @throws ClassNotFoundException
* @throws InstantiationException
* @throws UnexecutableException
* @throws MethodNotFoundException
*/
public String tableTestToHumanReadableString(TableTest aTest, ConversionContext aConversionContext)
throws ClassNotFoundException, UnexecutableException, InstantiationException, MethodNotFoundException {
ConversionContext tempConversionContext = safeguardConversionContext(aConversionContext);
return fixtureMethodToHumanReadableString(aTest.getDefinition().getFixtureMethod(), aTest,
parameterResolver.createParameterMap(aTest.getParameters(), true,
tempConversionContext.getUnresolvableVariableHandlingPolicy()),
tempConversionContext);
}
/**
* Creates a human-readable string for a call.
*
* @param aCall
* the call
* @return the human-readable string
* @throws ClassNotFoundException
* @throws InstantiationException
* @throws UnexecutableException
* @throws MethodNotFoundException
*/
public String callToHumanReadableString(Call aCall, ConversionContext aConversionContext)
throws ClassNotFoundException, UnexecutableException, InstantiationException, MethodNotFoundException {
ConversionContext tempConversionContext = safeguardConversionContext(aConversionContext);
return fixtureMethodToHumanReadableString(aCall.getDefinition().getFixtureMethod(), aCall, parameterResolver
.createParameterMap(aCall, true, tempConversionContext.getUnresolvableVariableHandlingPolicy()),
tempConversionContext);
}
/**
* Creates a human-readable string for a variable assignment.
*
* @param anAssignment
* the assignment
* @param aConversionContext
* @return the human-readable string
*/
public String variableAssignmentToHumanReadableString(VariableAssignment anAssignment,
ConversionContext aConversionContext) {
ConversionContext tempConversionContext = safeguardConversionContext(aConversionContext);
String tempValueString = valueConverter.convertValueToString(anAssignment.getValue(), false,
tempConversionContext);
return "Assign new value '" + tempValueString + "' to variable '"
+ IntegrityDSLUtil.getQualifiedVariableEntityName(anAssignment.getTarget().getName(), false) + "'";
}
/**
* Creates a human-readable string for a fixture method.
*
* @param aFixtureMethod
* the fixture method
* @param aStatement
* the suite statement currently being executed, if known
* @param someParameters
* a map of parameters used for the test
* @param aConversionContext
* the conversion context to use for conversion of parameters, if necessary
* @return the human-readable string
* @throws ClassNotFoundException
* @throws MethodNotFoundException
*/
public String fixtureMethodToHumanReadableString(MethodReference aFixtureMethod,
SuiteStatementWithResult aStatement, Map<String, Object> someParameters,
ConversionContext aConversionContext) throws ClassNotFoundException, MethodNotFoundException {
ConversionContext tempConversionContext = safeguardConversionContext(aConversionContext);
Method tempMethod = classLoader.loadMethod(aFixtureMethod);
FixtureMethod tempAnnotation = tempMethod.getAnnotation(FixtureMethod.class);
if (tempAnnotation == null) {
return null;
}
String tempText = null;
// Prefer specific texts, if not possible prefer generic text, if not possible use fixture method name
if (aStatement != null) {
if ((aStatement instanceof Test) || (aStatement instanceof TableTest)) {
tempText = tempAnnotation.descriptionTest();
} else if (aStatement instanceof Call) {
tempText = tempAnnotation.descriptionCall();
}
}
if (tempText != null && tempText.length() == 0) {
tempText = tempAnnotation.description();
}
if (tempText != null && tempText.length() == 0) {
tempText = aFixtureMethod.getMethod().getSimpleName();
}
tempText = replaceConditionalTextBlocks(tempText, someParameters);
tempText = replaceParameters(tempText, someParameters, tempConversionContext);
return tempText;
}
/**
* Replaces all blocks of conditional text. The syntax for these looks like this:<<br>
* <br>
* {someParameterName?some conditional text} or {^someParameterName?some conditional text}<br>
* <br>
* The first statement prints 'some conditional text' if the parameter 'someParameterName' is set to a value. The
* latter prints the text if the parameter is NOT set to a value (negation of the first example).<br>
* <br>
* Parameters may be used in the text itself according to the {@link #PARAMETER_PATTERN}. Conditional statements may
* be nested.
*
* @param anInput
* the text to start with
* @param someParameters
* the parameters
* @return the resulting text
*/
protected String replaceConditionalTextBlocks(String anInput, Map<String, Object> someParameters) {
int tempStart = -1;
int tempEnd = -1;
int tempNestingCount = 0;
for (int i = 0; i < anInput.length(); i++) {
char tempChar = anInput.charAt(i);
if (tempChar == '{') {
if (tempStart == -1) {
tempStart = i;
} else {
tempNestingCount++;
}
} else if (tempChar == '}') {
if (tempNestingCount == 0) {
tempEnd = i;
break;
} else {
tempNestingCount--;
}
}
}
if (tempStart >= 0) {
if (tempEnd >= 0) {
String tempBlock = anInput.substring(tempStart + 1, tempEnd);
int tempDivider = tempBlock.indexOf('?');
if (tempDivider >= 0) {
String tempParameterName = tempBlock.substring(0, tempDivider);
String tempBlockText = tempBlock.substring(tempDivider + 1);
boolean tempInversion = false;
if (tempParameterName.startsWith("^")) {
tempParameterName = tempParameterName.substring(1);
tempInversion = true;
}
String tempPrefix = anInput.substring(0, tempStart);
String tempSuffix = anInput.substring(tempEnd + 1);
// Fix for issue #41: Conditional fixture description parts are not correctly chosen in some
// situations
boolean tempParameterConsideredPresent = false;
if (someParameters.containsKey(tempParameterName)) {
Object tempParameterValue = someParameters.get(tempParameterName);
tempParameterConsideredPresent = tempParameterValue != null
&& tempParameterValue != UnresolvableVariable.getInstance();
}
if ((!tempInversion && tempParameterConsideredPresent)
|| (tempInversion && !tempParameterConsideredPresent)) {
return replaceConditionalTextBlocks(tempPrefix + tempBlockText + tempSuffix, someParameters);
} else {
return replaceConditionalTextBlocks(tempPrefix + tempSuffix, someParameters);
}
}
}
}
return anInput;
}
/**
* Replaces all parameters according to the {@link #PARAMETER_PATTERN} with their respective value.
*
* @param anInput
* the text to start with
* @param someParameters
* the parameters
* @param aConversionContext
* the conversion context to use for conversion of parameters, if necessary
* @return the resulting text
*/
protected String replaceParameters(String anInput, Map<String, Object> someParameters,
ConversionContext aConversionContext) {
String tempString = anInput;
Matcher tempMatcher = PARAMETER_PATTERN.matcher(tempString);
while (tempMatcher.matches()) {
// classloader and variable maps are not supplied here because the parameters are already expected to be
// resolved
Object tempValueBeforeConversion = someParameters.get(tempMatcher.group(2));
String tempValue = null;
if (tempValueBeforeConversion == null && aConversionContext
.getUnresolvableVariableHandlingPolicy() == UnresolvableVariableHandling.RESOLVE_TO_UNRESOLVABLE_OBJECT) {
// If the unresolvable variable handling policy requires question marks as a replacement, we'll assume
// that's required for unresolvable parameters as well; this is typically required for tabletests.
tempValue = UnresolvableVariable.getInstance().toString();
} else {
tempValue = valueConverter.convertValueToString(tempValueBeforeConversion, false, aConversionContext);
}
tempString = tempMatcher.group(1) + tempValue + tempMatcher.group(3);
tempMatcher = PARAMETER_PATTERN.matcher(tempString);
}
return tempString;
}
/**
* This method creates a default conversion context in case none is provided, and returns the provided context
* otherwise.
*
* @param aContext
* the context to safeguard
* @return a context (guaranteed not to return null)
*/
public ConversionContext safeguardConversionContext(ConversionContext aContext) {
if (aContext == null) {
return conversionContextProvider.get();
} else {
return aContext;
}
}
}