com.mnxfst.testing.handler.exec.PTestPlanExecutionContext.java Source code

Java tutorial

Introduction

Here is the source code for com.mnxfst.testing.handler.exec.PTestPlanExecutionContext.java

Source

/*
 *  The ptest framework provides you with a performance test utility
 *  Copyright (C) 2012  Christian Kreutzfeldt <mnxfst@googlemail.com>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package com.mnxfst.testing.handler.exec;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.builder.ToStringBuilder;

import com.mnxfst.testing.handler.exec.cfg.PTestPlan;
import com.mnxfst.testing.handler.exec.exception.ContextVariableEvaluationFailedException;

/**
 * Contains all required information for a single {@link PTestPlan} execution run. Since the context is being operated
 * only by one action at a time, there is no need to make this implementation thread-safe at the moment. 
 *
 * @author mnxfst
 * @since 11.04.2012
 */
public class PTestPlanExecutionContext implements Serializable {

    private static final long serialVersionUID = -8720239271745632501L;

    private static final int REPLACEMENT_PATTERN_PREFIX_LENGTH = 2; // ${ == 2 characters
    private static final String[] EMPTY_STRING_ARRAY = new String[0];

    /** 
     * Numerical identifier being assigned to each run by the producer to uniquely identify each entity
     */
    private long runId = 0;

    /** 
     * Provides a transient store for exchanging pairs of keys and values between actions operating 
     * within a single execution run. Either the last consumer or the entity producer must ensure that
     * the map is completely cleaned up and all contained objects disconnected before reusing the entity.
     */
    private Map<String, Serializable> contextVariables = new HashMap<String, Serializable>();

    /**
     * Holds a mapping between a replacement pattern and its already evaluated pattern object to be used for faster access  
     */
    private Map<String, PTestPlanExecutionContextReplacementPattern> replacementPatternMapping = new HashMap<String, PTestPlanExecutionContextReplacementPattern>();

    /**
     * Names the current state or step within a test plan
     */
    private String currentActivityId = null;

    /**
     * Names the next state or step to execute within a test plan. 
     */
    private String nextActivityId = null;

    /**
     * Timestamp of entity initialization
     */
    private long initTimestamp = 0;

    /**
     * Timestamp of final processing step
     */
    private long finalTimestamp = 0;

    /**
     * Counts the number of actions executed
     */
    private int actionCalls = 0;

    // TODO HISTOGRAM

    /**
     * Default constructor
     */
    public PTestPlanExecutionContext() {
    }

    /**
     * Adds a new context variable
     * @param key
     * @param value
     */
    public void addContextVariable(String key, Serializable value) {
        this.contextVariables.put(key, value);
    }

    /**
     * Returns the associated value from the context
     * @param key
     * @return
     */
    public Serializable getContextVariable(String key) {
        return this.contextVariables.get(key);
    }

    /**
     * Cleans up the context
     */
    public void cleanUp() {
        this.runId = 0;
        this.currentActivityId = null;
        this.finalTimestamp = 0;
        this.initTimestamp = 0;
        this.nextActivityId = null;
        this.contextVariables.clear();
    }

    /**
     * The provided pattern names a context variable and an access path along getter methods that
     * need to be evaluated to fetch the final value. Eg.: ${user.address.street} executes user#getAddress#getStreet
     * @param replacementPattern
     * @return
     * @throws ContextVariableEvaluationFailedException
     */
    public Object evaluate(String replacementPattern) throws ContextVariableEvaluationFailedException {

        // validate the replacement pattern
        if (replacementPattern == null || replacementPattern.trim().isEmpty())
            throw new ContextVariableEvaluationFailedException("No replacement pattern provided");

        // find replacement pattern in map of previously computed pattern and apply access methods
        PTestPlanExecutionContextReplacementPattern patternInstance = replacementPatternMapping
                .get(replacementPattern);
        if (patternInstance != null) {
            Serializable ctxVariable = getContextVariable(patternInstance.getName());
            return evaluateObject(ctxVariable, patternInstance.getAccessMethods());
        }

        // ensure that the replacement pattern starts with '${' and ends with '}'
        if (replacementPattern.startsWith("${") && replacementPattern.endsWith("}")) {

            // extract context variable name
            String ctxVariableName = extractContextVariableName(replacementPattern);
            Serializable ctxVariable = getContextVariable(ctxVariableName);
            if (ctxVariable == null)
                return null;

            // if the variable has been found, try to extract the getter method names along the path described. if there 
            // are not getters to be called, just return the variable value
            String[] getterMethodNames = extractGetterMethodNames(replacementPattern);
            if (getterMethodNames == null || getterMethodNames.length < 1)
                return ctxVariable;

            // convert the getter names into method representations
            List<Method> getterMethods = new ArrayList<Method>();
            extractGetterMethods(ctxVariable.getClass(), getterMethodNames, getterMethods);

            // create entity for previously mentioned association map for pattern information and insert it
            PTestPlanExecutionContextReplacementPattern patternMapping = new PTestPlanExecutionContextReplacementPattern(
                    replacementPattern, ctxVariableName);
            for (Method m : getterMethods)
                patternMapping.addAccessMethod(m);
            replacementPatternMapping.put(replacementPattern, patternMapping);

            // evaluate the getter methods against the variable value
            return evaluateObject(ctxVariable, getterMethods);
        }

        throw new ContextVariableEvaluationFailedException(
                "Invalid replacement pattern: " + replacementPattern + ". Expected prefix: ${variable}");
    }

    /**
     * Evaluates the provided list of methods on the given object and the results of the getter recursively 
     * @param input
     * @param getterMethods
     * @return
     * @throws TSVariableEvaluationFailedException
     */
    protected Object evaluateObject(Object input, List<Method> getterMethods)
            throws ContextVariableEvaluationFailedException {

        if (input == null)
            return null;

        if (getterMethods != null && !getterMethods.isEmpty()) {

            Method nextGetterMethod = getterMethods.get(0);
            Object result = null;

            try {
                result = nextGetterMethod.invoke(input);
            } catch (IllegalArgumentException e) {
                throw new ContextVariableEvaluationFailedException(
                        "Failed to evaluate method '" + nextGetterMethod.getName() + "' on entity of type "
                                + input.getClass().getName() + ". Error: " + e.getMessage());
            } catch (IllegalAccessException e) {
                throw new ContextVariableEvaluationFailedException(
                        "Failed to evaluate method '" + nextGetterMethod.getName() + "' on entity of type "
                                + input.getClass().getName() + ". Error: " + e.getMessage());
            } catch (InvocationTargetException e) {
                throw new ContextVariableEvaluationFailedException(
                        "Failed to evaluate method '" + nextGetterMethod.getName() + "' on entity of type "
                                + input.getClass().getName() + ". Error: " + e.getMessage());
            }

            if (getterMethods.size() > 1 && result != null)
                return evaluateObject(result, getterMethods.subList(1, getterMethods.size()));

            return result;
        }

        return input;

    }

    /**
     * Extracts the context variable name from the given replacement pattern. The provided prefix helps to speed up the
     * stripping and must be contained in the replacement pattern. The replacement pattern is expected to have a prefix 
     * holding the characters ${ and a postfix of char }
     * @param replacementPattern 
     * @return
     */
    protected String extractContextVariableName(String replacementPattern) {

        String tmp = replacementPattern.substring(REPLACEMENT_PATTERN_PREFIX_LENGTH);
        if (tmp.indexOf(".") == -1) {
            return tmp.substring(0, tmp.length() - 1);
        }
        return tmp.substring(0, tmp.indexOf('.'));
    }

    /**
     * Extracts the getter method names that needs to be executed along the expression path for evaluate
     * an objects value. The provided prefix helps to speed-up stripping down the storage dependent prefix, eg. ${global.
     * @param replacementPattern the provided input is assumed to be not null and must contain the provided prefix
     * @param storageDependentPrefix the provided input is assumed to be not null and not empty
     * @return
     */
    protected String[] extractGetterMethodNames(String replacementPattern) {

        if (replacementPattern == null || replacementPattern.trim().isEmpty())
            return EMPTY_STRING_ARRAY;

        // strip out the named prefix and the closing brackts
        String[] splittedPath = replacementPattern
                .substring(REPLACEMENT_PATTERN_PREFIX_LENGTH, replacementPattern.length() - 1).split("\\.");

        if (splittedPath != null && splittedPath.length > 1) {

            List<String> result = new ArrayList<String>();

            // iterate through path elements starting with the 2nd element since the first names the variable whereas the second references the first attribute
            // to be accessed via an assigned getter
            for (int i = 1; i < splittedPath.length; i++) {
                String attrName = splittedPath[i];
                if (attrName != null && !attrName.isEmpty()) {
                    result.add("get" + attrName.substring(0, 1).toUpperCase() + attrName.substring(1));
                }
            }

            return (String[]) result.toArray(EMPTY_STRING_ARRAY);
        }

        return EMPTY_STRING_ARRAY;
    }

    /**
     * Extracts the  {@link Method method representations} for the given path of getter methods 
     * @param varType
     * @param getterMethodNames
     * @param result
     * @throws ContextVariableEvaluationFailedException
     */
    protected void extractGetterMethods(Class<?> varType, String[] getterMethodNames, List<Method> result)
            throws ContextVariableEvaluationFailedException {

        if (varType != null && getterMethodNames != null && getterMethodNames.length > 0) {

            String nextGetter = getterMethodNames[0];
            try {
                Method getterMethod = varType.getMethod(nextGetter);
                result.add(getterMethod);
                extractGetterMethods(getterMethod.getReturnType(),
                        (String[]) ArrayUtils.subarray(getterMethodNames, 1, getterMethodNames.length), result);
            } catch (NoSuchMethodException e) {
                throw new ContextVariableEvaluationFailedException(
                        "No such getter '" + nextGetter + "' for class " + varType.getName());
            }

        }

    }

    /**
     * @return the runId
     */
    public long getRunId() {
        return runId;
    }

    /**
     * @param runId the runId to set
     */
    public void setRunId(long runId) {
        this.runId = runId;
    }

    /**
     * @return the contextVariables
     */
    public Map<String, Serializable> getContextVariables() {
        return contextVariables;
    }

    /**
     * @param contextVariables the contextVariables to set
     */
    public void setContextVariables(Map<String, Serializable> contextVariables) {
        this.contextVariables = contextVariables;
    }

    /**
     * @return the currentActivityId
     */
    public String getCurrentActivityId() {
        return currentActivityId;
    }

    /**
     * @param currentActivityId the currentActivityId to set
     */
    public void setCurrentActivityId(String currentActivityId) {
        this.currentActivityId = currentActivityId;
    }

    /**
     * @return the nextActivityId
     */
    public String getNextActivityId() {
        return nextActivityId;
    }

    /**
     * @param nextActivityId the nextActivityId to set
     */
    public void setNextActivityId(String nextActivityId) {
        this.nextActivityId = nextActivityId;
    }

    /**
     * @return the initTimestamp
     */
    public long getInitTimestamp() {
        return initTimestamp;
    }

    /**
     * @param initTimestamp the initTimestamp to set
     */
    public void setInitTimestamp(long initTimestamp) {
        this.initTimestamp = initTimestamp;
    }

    /**
     * @return the finalTimestamp
     */
    public long getFinalTimestamp() {
        return finalTimestamp;
    }

    /**
     * @param finalTimestamp the finalTimestamp to set
     */
    public void setFinalTimestamp(long finalTimestamp) {
        this.finalTimestamp = finalTimestamp;
    }

    /**
     * @return the actionCalls
     */
    public int getActionCalls() {
        return actionCalls;
    }

    /**
     * @param actionCalls the actionCalls to set
     */
    public void setActionCalls(int actionCalls) {
        this.actionCalls = actionCalls;
    }

    /**
     * @see java.lang.Object#toString()
     */
    public String toString() {
        return new ToStringBuilder(this).append("runId", this.runId).append("initTimestamp", this.initTimestamp)
                .append("finalTimestamp", this.finalTimestamp).append("currentActivityId", this.currentActivityId)
                .append("nextActivityId", this.nextActivityId).append("actionCalls", this.actionCalls)
                .append("contextVariables", this.contextVariables).toString();
    }

}