/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.jmeter.control; import java.io.Serializable; import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.SimpleScriptContext; import org.apache.jmeter.samplers.Sampler; import org.apache.jmeter.testelement.ThreadListener; import org.apache.jmeter.testelement.property.StringProperty; import org.apache.jmeter.util.JMeterUtils; import org.mozilla.javascript.Context; import org.mozilla.javascript.Scriptable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * * This is a Conditional Controller; it will execute the set of statements * (samplers/controllers, etc) while the 'condition' is true. * <p> * In a programming world - this is equivalent of : * <pre> * if (condition) { * statements .... * } * </pre> * In JMeter you may have : * <pre> * Thread-Group (set to loop a number of times or indefinitely, * ... Samplers ... (e.g. Counter ) * ... Other Controllers .... * ... IfController ( condition set to something like - ${counter} < 10) * ... statements to perform if condition is true * ... * ... Other Controllers /Samplers } * </pre> */ // for unit test code @see TestIfController public class IfController extends GenericController implements Serializable, ThreadListener { private static final Logger log = LoggerFactory.getLogger(IfController.class); private static final long serialVersionUID = 242L; private static final String NASHORN_ENGINE_NAME = "nashorn"; //$NON-NLS-1$ private static final String CONDITION = "IfController.condition"; //$NON-NLS-1$ private static final String EVALUATE_ALL = "IfController.evaluateAll"; //$NON-NLS-1$ private static final String USE_EXPRESSION = "IfController.useExpression"; //$NON-NLS-1$ private static final String USE_RHINO_ENGINE_PROPERTY = "javascript.use_rhino"; //$NON-NLS-1$ private static final boolean USE_RHINO_ENGINE = JMeterUtils.getPropDefault(USE_RHINO_ENGINE_PROPERTY, false) || getInstance().getEngineByName(NASHORN_ENGINE_NAME) == null; private static final ThreadLocal<ScriptEngine> NASHORN_ENGINE = new ThreadLocal<ScriptEngine>() { @Override protected ScriptEngine initialValue() { return getInstance().getEngineByName("nashorn");//$NON-NLS-N$ } }; private interface JsEvaluator { boolean evaluate(String testElementName, String condition); } private static class RhinoJsEngine implements JsEvaluator { @Override public boolean evaluate(String testElementName, String condition) { boolean result = false; // now evaluate the condition using JavaScript Context cx = Context.enter(); try { Scriptable scope = cx.initStandardObjects(null); Object cxResultObject = cx.evaluateString(scope, condition /** * conditionString ** */ , "<cmd>", 1, null); result = computeResultFromString(condition, Context.toString(cxResultObject)); } catch (Exception e) { log.error("{}: error while processing "+ "[{}]", testElementName, condition, e); } finally { Context.exit(); } return result; } } private static class NashornJsEngine implements JsEvaluator { @Override public boolean evaluate(String testElementName, String condition) { try { ScriptContext newContext = new SimpleScriptContext(); newContext.setBindings(NASHORN_ENGINE.get().createBindings(), ScriptContext.ENGINE_SCOPE); Object o = NASHORN_ENGINE.get().eval(condition, newContext); return computeResultFromString(condition, o.toString()); } catch (Exception ex) { log.error("{}: error while processing [{}]", testElementName, condition, ex); } return false; } } private static JsEvaluator JAVASCRIPT_EVALUATOR = USE_RHINO_ENGINE ? new RhinoJsEngine() : new NashornJsEngine(); /** * Initialization On Demand Holder pattern */ private static class LazyHolder { public static final ScriptEngineManager INSTANCE = new ScriptEngineManager(); } /** * @return ScriptEngineManager singleton */ private static ScriptEngineManager getInstance() { return LazyHolder.INSTANCE; } /** * constructor */ public IfController() { super(); } /** * constructor * @param condition The condition for this controller */ public IfController(String condition) { super(); this.setCondition(condition); } /** * Condition Accessor - this is gonna be like <code>${count} < 10</code> * @param condition The condition for this controller */ public void setCondition(String condition) { setProperty(new StringProperty(CONDITION, condition)); } /** * Condition Accessor - this is gonna be like <code>${count} < 10</code> * @return the condition associated with this controller */ public String getCondition() { return getPropertyAsString(CONDITION); } /** * evaluate the condition clause log error if bad condition */ private boolean evaluateCondition(String cond) { log.debug(" getCondition() : [{}]", cond); return JAVASCRIPT_EVALUATOR.evaluate(getName(), cond); } /** * @param condition * @param resultStr * @return boolean * @throws Exception */ private static boolean computeResultFromString( String condition, String resultStr) throws Exception { boolean result; switch(resultStr) { case "false": result = false; break; case "true": result = true; break; default: throw new Exception(" BAD CONDITION :: " + condition + " :: expected true or false"); } log.debug(" >> evaluate Condition - [{}] results is [{}]", condition, result); return result; } private static boolean evaluateExpression(String cond) { return cond.equalsIgnoreCase("true"); // $NON-NLS-1$ } @Override public boolean isDone() { // bug 26672 : the isDone result should always be false and not based on the expession evaluation // if an IfController ever gets evaluated to false it gets removed from the test tree. // The problem is that the condition might get evaluated to true the next iteration, // which we don't get the opportunity for return false; } /** * @see org.apache.jmeter.control.Controller#next() */ @Override public Sampler next() { // We should only evalute the condition if it is the first // time ( first "iteration" ) we are called. // For subsequent calls, we are inside the IfControllerGroup, // so then we just pass the control to the next item inside the if control boolean result = true; if(isEvaluateAll() || isFirst()) { result = isUseExpression() ? evaluateExpression(getCondition()) : evaluateCondition(getCondition()); } if (result) { return super.next(); } // If-test is false, need to re-initialize indexes try { initializeSubControllers(); return nextIsNull(); } catch (NextIsNullException e1) { return null; } } /** * {@inheritDoc} */ @Override public void triggerEndOfLoop() { super.initializeSubControllers(); super.triggerEndOfLoop(); } public boolean isEvaluateAll() { return getPropertyAsBoolean(EVALUATE_ALL,false); } public void setEvaluateAll(boolean b) { setProperty(EVALUATE_ALL,b); } public boolean isUseExpression() { return getPropertyAsBoolean(USE_EXPRESSION, false); } public void setUseExpression(boolean selected) { setProperty(USE_EXPRESSION, selected, false); } @Override public void threadStarted() {} @Override public void threadFinished() { NASHORN_ENGINE.remove(); } }