edu.tum.cs.conqat.quamoco.qiesl.QIESLEngine.java Source code

Java tutorial

Introduction

Here is the source code for edu.tum.cs.conqat.quamoco.qiesl.QIESLEngine.java

Source

/*-------------------------------------------------------------------------+
|                                                                          |
| Copyright 2012 Technische Universitaet Muenchen and                      |
| Fraunhofer-Institut fuer Experimentelles Software Engineering (IESE)     |
|                                                                          |
| 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 edu.tum.cs.conqat.quamoco.qiesl;

import java.io.StringReader;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.jexl2.JexlContext;
import org.apache.commons.jexl2.JexlEngine;
import org.apache.commons.jexl2.JexlException;
import org.apache.commons.jexl2.MapContext;
import org.apache.commons.jexl2.Script;
import org.apache.commons.jexl2.introspection.UberspectImpl;
import org.apache.commons.jexl2.parser.ASTAssignment;
import org.apache.commons.jexl2.parser.ASTIdentifier;
import org.apache.commons.jexl2.parser.ASTJexlScript;
import org.apache.commons.jexl2.parser.ASTMethodNode;
import org.apache.commons.jexl2.parser.ASTReference;
import org.apache.commons.jexl2.parser.ASTSizeFunction;
import org.apache.commons.jexl2.parser.Node;
import org.apache.commons.jexl2.parser.ParseException;
import org.apache.commons.jexl2.parser.Parser;
import org.apache.commons.jexl2.parser.TokenMgrError;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.conqat.lib.commons.assertion.CCSMAssert;
import org.conqat.lib.commons.assertion.CCSMPre;
import org.conqat.lib.commons.io.NullOutputStream;
import org.conqat.lib.commons.logging.ILogger;
import org.conqat.lib.commons.logging.SimpleLogger;
import org.conqat.lib.commons.string.StringUtils;

import de.quamoco.qm.Type;
import edu.tum.cs.conqat.quamoco.FindingCollection;
import edu.tum.cs.conqat.quamoco.IFileRangeResolver;
import edu.tum.cs.conqat.quamoco.IFunctionRangeResolver;
import edu.tum.cs.conqat.quamoco.NullFileRangeResolver;
import edu.tum.cs.conqat.quamoco.NullFunctionRangeResolver;

/**
 * Engine for interpreting QIESL expression.
 * 
 * It must be used in the following way: 1) initialize(String expression, Type
 * expectedReturnType) 2) parse() 3) Set all variables, that were returned by
 * parse() 4) evaluate()
 * 
 * @version $Rev: 4974 $
 * @levd.rating RED Hash: 89E50A2CB0F7657BC91D2AF857229FC4
 */
public class QIESLEngine {

    /** Literal for all impacts and refinements */
    public static final String ALL_IMPACTS_AND_REFINEMENTS_LITERAL = "allImpactsAndRefinements";
    /** The predefined result variable */
    private static final String RESULT_VARIABLE_NAME = "result";

    /** Delimiters for measure names in QIESL formulas. */
    public static final String MEASURE_NAME_DELIMITER = "%%";

    /** Pattern to substitute measure names */
    private static final Pattern PATTERN_FOR_MEASURE_NAMES = Pattern.compile("%%([^%]*)%%");

    /** Literal for veto QPoint value */
    private static final String VETO_LITERAL = "veto";
    /** Literal for unknown findings value */
    private static final String UNKNOWN_FINDINGS_LITERAL = "unknownFindings";
    /** Literal for unknown points */
    private static final String UNKNOWN_POINTS_LITERAL = "unknownPoints";
    /** Literal for unknown number */
    private static final String UNKNOWN_NUMBER_LITERAL = "unknownNumber";

    /** The JexlEngine */
    private final JexlEngine jexlEngine;
    /** The JexlContext */
    private JexlContext jexlContext;

    /** Constructs a new engine with the given resolvers. */
    public QIESLEngine(IFunctionRangeResolver functionRangeResolver, IFileRangeResolver fileRangeResolver,
            IFileRangeResolver classRangeResolver, ILogger logger) {
        jexlEngine = createJexlEngine(
                new QIESLFunctions(functionRangeResolver, fileRangeResolver, classRangeResolver, logger));
    }

    /**
     * Constructs a new engine with Null resolvers, i.e. <b>file range resolving
     * and function range resolving will not produce correct results</b>. The
     * logger used is a NullLogger.
     */
    public QIESLEngine() {
        jexlEngine = createJexlEngine(
                new QIESLFunctions(new NullFunctionRangeResolver(), new NullFileRangeResolver(),
                        new NullFileRangeResolver(), new SimpleLogger(new NullOutputStream())));
    }

    /** Creates a new {@link JexlEngine} using the given function object */
    public static JexlEngine createJexlEngine(Object functions) {
        Log logger = LogFactory.getLog(QIESLEngine.class);
        Map<String, Object> allFunctions = new HashMap<String, Object>();
        allFunctions.put(null, functions);
        JexlEngine jexlEngine = new JexlEngine(new UberspectImpl(logger), new QIESLArithmetic(), allFunctions,
                logger);
        jexlEngine.setSilent(false);
        jexlEngine.setLenient(false);
        return jexlEngine;
    }

    /** Evaluate a QIESL specification. */
    public Object evaluate(String expression, IQIESLEvalVariables variables, Type expectedReturnType)
            throws QIESLException {

        Map<String, Object> optionalVariables = variables.getOptionalVariables();
        Map<String, Object> mandatoryVariables = variables.getMandatoryVariables();

        checkOverlappingVariables(optionalVariables.keySet(), mandatoryVariables.keySet());

        initialize();

        Map<String, Object> modelVariables = new HashMap<String, Object>();
        modelVariables.putAll(mandatoryVariables);
        modelVariables.putAll(optionalVariables);
        modelVariables.put(ALL_IMPACTS_AND_REFINEMENTS_LITERAL, variables.getAllImpactsAndRefinements());
        modelVariables.put(VETO_LITERAL, QPoints.VETO);
        modelVariables.put(UNKNOWN_FINDINGS_LITERAL, QIESLFunctions.UNKNOWN_FINDING_COLLECTION);
        modelVariables.put(UNKNOWN_POINTS_LITERAL, QPoints.valueOf(0, 1));
        modelVariables.put(UNKNOWN_NUMBER_LITERAL, QIESLFunctions.UNKNOWN_DOUBLE);

        Map<String, String> nameMapping = createNameMapping(modelVariables);
        String technicalExpression = toTechnicalExpression(expression, nameMapping);

        HashSet<String> usedTechnicalNames = validate(technicalExpression, nameMapping.keySet());

        checkMandatoryVariables(expression, mandatoryVariables, nameMapping, usedTechnicalNames);

        prepareVariables(usedTechnicalNames, nameMapping, modelVariables);
        Object result = evaluate(technicalExpression, expectedReturnType);

        return result;

    }

    /** Checks if the sets contain no overlapping variables */
    private void checkOverlappingVariables(Set<String> set1, Set<String> set2) throws QIESLException {
        Set<String> intersection = new HashSet<String>(set1);
        intersection.retainAll(set2);
        if (!intersection.isEmpty()) {
            throw new QIESLException("Overlapping variables: " + StringUtils.concat(intersection, ", "));
        }
    }

    /**
     * @param expression
     * @param mandatoryVariables
     * @param nameMapping
     * @param usedTechnicalNames
     * @throws QIESLException
     */
    private void checkMandatoryVariables(String expression, Map<String, Object> mandatoryVariables,
            Map<String, String> nameMapping, HashSet<String> usedTechnicalNames) throws QIESLException {
        HashSet<String> unusedModelVariables = new HashSet<String>(mandatoryVariables.keySet());
        for (String technicalName : usedTechnicalNames) {
            unusedModelVariables.remove(nameMapping.get(technicalName));
        }

        if (!unusedModelVariables.isEmpty() && !usedTechnicalNames.contains(ALL_IMPACTS_AND_REFINEMENTS_LITERAL)) {
            throw new QIESLException("Expression " + expression + " does not use mandatory variables: "
                    + StringUtils.concat(unusedModelVariables, ", "));
        }
    }

    /** Checks if the variable is the predefined result variable */
    private boolean isResultVariable(String name) {
        return name.equals("result");
    }

    /** Prepares the variables */
    private void prepareVariables(Set<String> usedTechnicalNames, Map<String, String> nameMapping,
            Map<String, Object> modelVariables) throws QIESLException {

        for (String technicalName : usedTechnicalNames) {
            if (isResultVariable(technicalName)) {
                continue;
            }
            String modelName = nameMapping.get(technicalName);
            CCSMAssert.isNotNull(modelName, "Null-reference should be sorted out in toTechnicalExpression");

            Object value = modelVariables.get(modelName);

            CCSMAssert.isNotNull(value, "Value of model variable " + modelName + " was null");

            if (value instanceof Exception) {
                throw new QIESLException("not evaluated because sub-value is missing.");
            }

            jexlContext.set(technicalName, value);

        }
    }

    /** Validates the expression */
    private HashSet<String> validate(String expressionString, Set<String> knownTechnicalNames)
            throws QIESLException {
        Parser parser = new Parser(new StringReader(";"));
        ASTJexlScript result;
        try {
            result = parser.parse(new StringReader(expressionString), null);
        } catch (ParseException e) {
            throw new QIESLException("Syntax error: " + e.getMessage(), e);
        } catch (TokenMgrError e) {
            throw new QIESLException("Error: " + e.getMessage(), e);
        } catch (Error e) {
            throw new QIESLException("Error: " + e.getMessage(), e);
        }

        HashSet<String> identifiers = getReferencedVariables(result);
        HashSet<String> declaredVariables = getDeclaredVariables(result);
        identifiers.removeAll(declaredVariables);

        Set<String> unknownTechnicalNames = difference(identifiers, knownTechnicalNames);

        for (String unknownName : unknownTechnicalNames) {
            if (!isResultVariable(unknownName)) {
                throw new QIESLException("Unknown variable: " + unknownName);
            }
        }

        return identifiers;
    }

    // TODO (FD): Move to commons
    /** Computes the set difference */
    public static <T> Set<T> difference(Set<T> set1, Set<T> set2) {
        HashSet<T> result = new HashSet<T>(set1);
        result.removeAll(set2);
        return result;
    }

    /** Returns the referenced variables in the AST */
    private HashSet<String> getReferencedVariables(Node node) throws QIESLException {
        HashSet<String> identifiers = new HashSet<String>();
        if (node instanceof ASTSizeFunction) {
            throw new QIESLException("Function size does not exist. Use extent instead");
        }
        if (node instanceof ASTIdentifier) {
            if (node.jjtGetParent() instanceof ASTMethodNode) {
                // TODO(FD): Validate functions
            } else if (!isLeftHandSideOfAssignment((ASTIdentifier) node)) {
                identifiers.add(((ASTIdentifier) node).image);
            }
        }
        for (int i = 0; i < node.jjtGetNumChildren(); i++) {
            identifiers.addAll(getReferencedVariables(node.jjtGetChild(i)));
        }
        return identifiers;
    }

    /** Returns the declared variables in the AST */
    private HashSet<String> getDeclaredVariables(Node node) throws QIESLException {
        HashSet<String> identifiers = new HashSet<String>();
        if (node instanceof ASTIdentifier) {
            if (node.jjtGetParent() instanceof ASTMethodNode) {
                // ignore
            } else if (isLeftHandSideOfAssignment((ASTIdentifier) node)) {
                identifiers.add(((ASTIdentifier) node).image);
            }
        }
        for (int i = 0; i < node.jjtGetNumChildren(); i++) {
            identifiers.addAll(getDeclaredVariables(node.jjtGetChild(i)));
        }
        return identifiers;
    }

    /** Checks if the identifier is the left hand side of an assignment */
    private static boolean isLeftHandSideOfAssignment(ASTIdentifier identifier) {
        if (identifier.jjtGetParent() instanceof ASTReference) {
            ASTReference reference = (ASTReference) identifier.jjtGetParent();
            return (reference.jjtGetParent() instanceof ASTAssignment)
                    && ((ASTAssignment) reference.jjtGetParent()).jjtGetChild(0) == identifier.jjtGetParent();
        }
        return false;
    }

    /**
     * @param variables
     * @throws QIESLException
     */
    private Map<String, String> createNameMapping(Map<String, Object> variables) throws QIESLException {
        HashMap<String, String> mapping = new HashMap<String, String>();
        for (String modelName : variables.keySet()) {
            String technicalName = toTechnicalName(modelName);
            if (mapping.containsKey(technicalName)) {
                throw new QIESLException("Model names " + modelName + " and " + mapping.get(technicalName)
                        + " map to same to technial name: " + technicalName);
            }
            mapping.put(technicalName, modelName);
        }
        return mapping;

    }

    /**
     * Initializes the QIESL-Engine.
     */
    private void initialize() {
        this.jexlContext = new MapContext();
    }

    /**
     * Converts expression with model names to expression with technical names.
     * If a model name cannot be resolved a {@link QIESLException} is thrown.
     */
    protected static String toTechnicalExpression(String expression, Map<String, String> nameMapping)
            throws QIESLException {
        Matcher matcher = PATTERN_FOR_MEASURE_NAMES.matcher(expression);
        StringBuffer result = new StringBuffer();
        while (matcher.find()) {
            String match = matcher.group(1);
            if (!nameMapping.containsValue(match)) {
                throw new QIESLException("Unknown variable '" + match + "' in spec;" + " available names are '"
                        + nameMapping.values() + "'");
            }
            String replacement = toTechnicalName(match);
            matcher.appendReplacement(result, StringUtils.escapeRegexReplacementString(replacement));
        }
        matcher.appendTail(result);
        return result.toString();
    }

    /**
     * Evaluates the given QUISL expression
     * 
     * @param type
     */
    private Object evaluate(String qiesl, Type type) throws QIESLException {

        try {
            Script script = jexlEngine.createScript(qiesl);
            Object directResult = script.execute(jexlContext);
            Object result = jexlContext.get(RESULT_VARIABLE_NAME);

            // if result was not set, then use the value of the expression
            if (result == null) {
                result = directResult;
            }
            checkType(result, type);

            return result;
        } catch (JexlException ex) {
            if (ex.getCause() != null) {
                throw new QIESLException(ex.getCause().getMessage(), ex.getCause());
            }
            throw new QIESLException(ex.getMessage(), ex);
        }
    }

    /**
     * Checks whether the type of o conforms to expectedReturnType
     * 
     * @param o
     * @param expectedReturnType
     */
    private void checkType(Object o, Type expectedReturnType) throws QIESLException {
        if (o == null) {
            throw QIESLException.NULL_RETURNED;
        }
        if (expectedReturnType == null) {
            return;
        }
        boolean ok = false;
        switch (expectedReturnType.getValue()) {
        case Type.NONE_VALUE:
            ok = false;
            break;
        case Type.FINDINGS_VALUE:
            ok = o instanceof FindingCollection;
            break;
        case Type.NUMBER_VALUE:
            ok = o instanceof Number;
            break;
        default:
            ok = false;
        }
        if (!ok) {
            throw new QIESLException("QIESL should return " + expectedReturnType.getLiteral() + " but it returns "
                    + o.getClass().getName());
        }
    }

    /**
     * Escapes a measure name, so that a valid java identifier results
     */
    protected static String toTechnicalName(String name) {
        CCSMPre.isFalse(StringUtils.isEmpty(name), "Name cannot be empty");
        if (!Character.isJavaIdentifierStart(name.charAt(0))) {
            name = "_" + name;
        }
        return name.replaceAll("[^\\p{javaJavaIdentifierPart}]", "_");
    }

}