From 11fe4afc2daaa6f68bb11807b018a4cac36dc79b Mon Sep 17 00:00:00 2001 From: jakubs <jakubs> Date: Mon, 18 Apr 2016 14:02:44 +0000 Subject: [PATCH] SSDM-3510 - make it possible to use jython 2.7 everywhere where we use python SVN: 36200 --- .../common/jython/evaluator/Evaluator.java | 488 +--------------- .../jython/evaluator/IJythonEvaluator.java | 124 ++++ .../evaluator/IJythonEvaluatorFactory.java | 63 +++ .../JythonEvaluatorSpringComponent.java | 44 ++ .../cisd/common/jython/v25/Evaluator25.java | 532 ++++++++++++++++++ .../jython/v25/Jython25EvaluatorFactory.java | 55 ++ .../cisd/common/jython/v27/Evaluator27.java | 48 +- .../jython/v27/Jython27EvaluatorFactory.java | 55 ++ .../common/evaluator/Evaluator25Test.java | 42 ++ .../common/evaluator/Evaluator27Test.java | 43 ++ .../cisd/common/evaluator/EvaluatorTest.java | 146 +++-- 11 files changed, 1099 insertions(+), 541 deletions(-) create mode 100644 common/source/java/ch/systemsx/cisd/common/jython/evaluator/IJythonEvaluator.java create mode 100644 common/source/java/ch/systemsx/cisd/common/jython/evaluator/IJythonEvaluatorFactory.java create mode 100644 common/source/java/ch/systemsx/cisd/common/jython/evaluator/JythonEvaluatorSpringComponent.java create mode 100644 common/source/java/ch/systemsx/cisd/common/jython/v25/Evaluator25.java create mode 100644 common/source/java/ch/systemsx/cisd/common/jython/v25/Jython25EvaluatorFactory.java create mode 100644 common/source/java/ch/systemsx/cisd/common/jython/v27/Jython27EvaluatorFactory.java create mode 100644 common/sourceTest/java/ch/systemsx/cisd/common/evaluator/Evaluator25Test.java create mode 100644 common/sourceTest/java/ch/systemsx/cisd/common/evaluator/Evaluator27Test.java diff --git a/common/source/java/ch/systemsx/cisd/common/jython/evaluator/Evaluator.java b/common/source/java/ch/systemsx/cisd/common/jython/evaluator/Evaluator.java index 0fda16012fd..595997174c9 100644 --- a/common/source/java/ch/systemsx/cisd/common/jython/evaluator/Evaluator.java +++ b/common/source/java/ch/systemsx/cisd/common/jython/evaluator/Evaluator.java @@ -1,5 +1,5 @@ /* - * Copyright 2009 ETH Zuerich, CISD + * Copyright 2016 ETH Zuerich, CISD * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,67 +16,11 @@ package ch.systemsx.cisd.common.jython.evaluator; -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.apache.commons.lang.StringUtils; -import org.python.core.CompileMode; -import org.python.core.CompilerFlags; -import org.python.core.Py; -import org.python.core.PyBoolean; -import org.python.core.PyCode; -import org.python.core.PyException; -import org.python.core.PyFloat; -import org.python.core.PyFunction; -import org.python.core.PyInteger; -import org.python.core.PyList; -import org.python.core.PyLong; -import org.python.core.PyNone; -import org.python.core.PyObject; -import org.python.core.PyString; -import org.python.core.PyStringMap; -import org.python.core.PySystemState; -import org.python.core.PyTraceback; - -import ch.systemsx.cisd.common.jython.PythonInterpreter; -import ch.systemsx.cisd.common.shared.basic.string.CommaSeparatedListBuilder; - /** - * A class for evaluating expressions, based on Jython. - * <p> - * This class is optimized for evaluating the same expression with a different set of variables repeatedly and efficient. The mode of usage of this - * class is: - * <ol> - * <li>Construct an {@link Evaluator} with an appropriate expression.</li> - * <li>Set all variables needed for evaluation via {@link #set(String, Object)}</li> - * <li>Call one of {@link #getType()}, {@link #evalAsString}, {@link #evalToBoolean()}, {@link #evalToInt()}, {@link #evalToBigInt()}, - * {@link #evalToDouble()}.</li> - * <li>Repeat from step 2</li> - * </ol> - * - * @author Bernd Rinn + * @author Jakub Straszewski */ -public final class Evaluator +public class Evaluator { - private final PythonInterpreter interpreter; - - private final String expression; - - private final PyCode compiledExpression; - - /** - * Call once before using the object to initialize. - */ - public static void initialize() - { - PySystemState.initialize(); - } - /** * The return type of this expression. */ @@ -85,434 +29,26 @@ public final class Evaluator BOOLEAN, INTEGER, BIGINT, DOUBLE, STRING, OTHER } - /** - * Creates a new {@link Evaluator} with file system access blocked. - * - * @param expression The expression to evaluate. - */ - public Evaluator(String expression) throws EvaluatorException - { - this(expression, null, null); - } - - /** - * Creates a new {@link Evaluator} with file system access blocked. - * - * @param expression The expression to evaluate. - * @param supportFunctionsOrNull If not <code>null</code>, all public static methods of the given class will be available to the evaluator as - * "supporting functions". - * @param initialScriptOrNull If not <code>null</code>, this has to be a valid (Python) script which is evaluated initially, e.g. to define some - * new functions. Note: this script is trusted, so don't run any unvalidated code here! - */ - public Evaluator(String expression, Class<?> supportFunctionsOrNull, String initialScriptOrNull) - throws EvaluatorException - { - this(expression, null, supportFunctionsOrNull, initialScriptOrNull, true); - } - - /** - * Creates a new {@link Evaluator}. - * - * @param expression The expression to evaluate. - * @param supportFunctionsOrNull If not <code>null</code>, all public static methods of the given class will be available to the evaluator as - * "supporting functions". - * @param initialScriptOrNull If not <code>null</code>, this has to be a valid (Python) script which is evaluated initially, e.g. to define some - * new functions. Note: this script is trusted, so don't run any unvalidated code here! - * @param blockFileAccess If <code>true</code> the script will not be able to open files. - */ - public Evaluator(String expression, String[] pythonPath, Class<?> supportFunctionsOrNull, - String initialScriptOrNull, boolean blockFileAccess) throws EvaluatorException - { - if (isMultiline(expression)) - { - throw new EvaluatorException("Expression '" + expression + "' contains line breaks"); - } - - this.interpreter = PythonInterpreter.createIsolatedPythonInterpreter(); - this.interpreter.addToPath(pythonPath); - - // Security: do not allow file access. - try - { - if (blockFileAccess) - { - interpreter.exec("def open():\n pass"); - } - if (supportFunctionsOrNull != null) - { - interpreter.exec("from " + supportFunctionsOrNull.getCanonicalName() + " import *"); - } - if (initialScriptOrNull != null) - { - interpreter.exec(initialScriptOrNull); - } - this.expression = expression; - - this.compiledExpression = doCompile(expression); - } catch (PyException ex) - { - throw toEvaluatorException(ex); - } - } - - /** - * Returns <code>true</code> if specified function is defined in the script. - */ - public boolean hasFunction(String functionName) - { - PyObject pyObject = interpreter.get(functionName); - return pyObject instanceof PyFunction; - } - - /** - * Evaluates specified function with specified arguments. The arguments are turned into Python Strings if they are Java {@link String} objects. - * The return value of the function is returned as a Java object or <code>null</code>. - * - * @throws EvaluatorException if evaluation fails. - */ - public Object evalFunction(String functionName, Object... args) - { - try - { - PyObject pyObject = interpreter.get(functionName); - if (pyObject == null) - { - throw new PyException(new PyString("Unknown function"), functionName); - } - if (pyObject instanceof PyFunction == false) - { - throw new PyException(new PyString("Not a function"), "'" + functionName - + "' is of type " + pyObject.getType().getName() + "."); - } - PyFunction func = (PyFunction) pyObject; - PyObject[] pyArgs = new PyObject[args.length]; - for (int i = 0; i < args.length; i++) - { - pyArgs[i] = translateToPython(args[i]); - } - PyObject result = func.__call__(pyArgs); - return translateToJava(result); - } catch (PyException ex) - { - CommaSeparatedListBuilder builder = new CommaSeparatedListBuilder(); - for (Object argument : args) - { - builder.append(argument); - } - throw toEvaluatorException(ex, functionName + "(" + builder + ")"); - } - } - - private PyObject translateToPython(Object javaObject) - { - if (javaObject instanceof String) - { - return new PyString((String) javaObject); - } - return Py.java2py(javaObject); - } + private static IJythonEvaluatorFactory factory; - /** - * @return compiled <var>expression</var> - * @throws EvaluatorException if compilation fails - */ - private static PyCode doCompile(String expression) throws EvaluatorException + static void setFactory(IJythonEvaluatorFactory factory) { - try - { - return Py.compile_flags("__result__=(" + expression + ")", "expression: " + expression, - CompileMode.exec, new CompilerFlags()); - } catch (PyException ex) - { - throw toEvaluatorException(ex, expression); - } + Evaluator.factory = factory; } - /** - * Sets the variable <var>name</var> to <var>value</var> in the evaluator's name space. - */ - public void set(String name, Object value) - { - interpreter.set(name, value); - } - - public Object get(String name) + public static IJythonEvaluatorFactory getFactory() { - return interpreter.get(name); - } - - /** - * Deletes the variable <var>name</var> from the evaluator's name space. - */ - public void delete(String name) - { - interpreter.getLocals().__delitem__(name); - } - - /** - * Returns <code>true</code> if and only if the variable <var>name</var> exists in the evaluator's name space. - */ - public boolean has(String name) - { - return ((PyStringMap) interpreter.getLocals()).has_key(new PyString(name)); - } - - /** - * Returns the {@link ReturnType} of the expression of this evaluator. - */ - public ReturnType getType() - { - doEval(); - final Object obj = getInterpreterResult(); - if (obj instanceof PyBoolean) - { - return ReturnType.BOOLEAN; - } else if (obj instanceof PyInteger) - { - return ReturnType.INTEGER; - } else if (obj instanceof PyLong) - { - return ReturnType.BIGINT; - } else if (obj instanceof PyFloat) + if (factory == null) { - return ReturnType.DOUBLE; - } else if (obj instanceof PyString) - { - return ReturnType.STRING; - } else - { - return ReturnType.OTHER; + // we should make sure that the initialization hapens before first call to this method + throw new IllegalStateException( + "Jython evaluator component not initialized. Application context is not initialized properly - JythonEvaluatorSpringComponent must be initialized before jython evaluators are used."); } - } - - /** - * Evaluates the expression of this evaluator and returns the result. Use this method if you do not know what will be the result type. - * <p> - * <i>This is a legacy function to mimic the old Jython 2.2 Evaluator's behavior which will only return Long, Double or String and doesn't know - * boolean.</i> - * - * @return evaluation result which can be of Long, Double or String type. All other types are converted to String representation except - * {@link PyNone} that represents null value and will be converted to <code>null</code>. - */ - public Object evalLegacy2_2() - { - doEval(); - final PyObject obj = getInterpreterResult(); - Object result = translateToJavaLegacy(obj); - if (result != null && result instanceof Long == false && result instanceof Double == false - && result instanceof String == false) - { - return result.toString(); - } - return result; - } - - /** - * Evaluates the expression of this evaluator and returns the result. Use this method if you do not know what will be the result type. - * - * @return evaluation result as translated by the Jython interpreter.. - */ - public Object eval() - { - doEval(); - return translateToJava(getInterpreterResult()); - } - - private Object translateToJavaLegacy(final PyObject obj) - { - if (obj instanceof PyInteger) - { - return new Long(((PyInteger) obj).getValue()); - } else if (obj instanceof PyLong) - { - return new Long(((PyLong) obj).getValue().longValue()); - } else if (obj instanceof PyFloat) - { - return new Double(((PyFloat) obj).getValue()); - } else if (obj instanceof PyNone) - { - return null; - } else if (obj instanceof PyList) - { - PyList pyList = (PyList) obj; - PyObject[] array = pyList.getArray(); - List<Object> list = new ArrayList<Object>(); - for (int i = 0, n = pyList.size(); i < n; i++) - { - list.add(translateToJavaLegacy(array[i])); - } - return list; - } else - { - return translateToJava(obj); - } - } - - private Object translateToJava(final PyObject obj) - { - return (obj == null) ? null : obj.__tojava__(Object.class); - } - - private PyObject getInterpreterResult() - { - return interpreter.get("__result__"); - } - - /** - * Evaluates the expression of this evaluator and returns the result, assuming that the expression has a boolean return type. - */ - public boolean evalToBoolean() throws EvaluatorException - { - doEval(); - try - { - return ((PyBoolean) getInterpreterResult()).getBooleanValue(); - } catch (ClassCastException ex) - { - final ReturnType type = getType(); - throw new EvaluatorException("Expected a result of type " + ReturnType.BOOLEAN - + ", found " + type); - } - } - - /** - * Evaluates the expression of this evaluator and returns the result, assuming that the expression has an integer return type. - */ - public int evalToInt() throws EvaluatorException - { - doEval(); - try - { - return ((PyInteger) getInterpreterResult()).getValue(); - } catch (ClassCastException ex) - { - final ReturnType type = getType(); - throw new EvaluatorException("Expected a result of type " + ReturnType.INTEGER - + ", found " + type); - } - } - - /** - * Evaluates the expression of this evaluator and returns the result, assuming that the expression has a big integer return type. - */ - public BigInteger evalToBigInt() throws EvaluatorException - { - doEval(); - try - { - return ((PyLong) getInterpreterResult()).getValue(); - } catch (ClassCastException ex) - { - final ReturnType type = getType(); - throw new EvaluatorException("Expected a result of type " + ReturnType.BIGINT - + ", found " + type); - } - } - - /** - * Evaluates the expression of this evaluator and returns the result, assuming that the expression has a floating point (double) return type. - */ - public double evalToDouble() throws EvaluatorException - { - doEval(); - try - { - return ((PyFloat) getInterpreterResult()).getValue(); - } catch (ClassCastException ex) - { - final ReturnType type = getType(); - throw new EvaluatorException("Expected a result of type " + ReturnType.DOUBLE - + ", found " + type); - } - } - - /** - * Evaluates the expression of this evaluator and returns the result as a String. This method can always be called. - * <p> - * <i>This is a legacy function to mimic the old Jython 2.2 Evaluator's behavior which first translates to Long and Double and doesn't know - * boolean.</i> - * <p> - * NOTE: null will be returned if expression results in {@link PyNone} - */ - public String evalAsStringLegacy2_2() throws EvaluatorException - { - Object result = evalLegacy2_2(); - return result == null ? null : result.toString(); - } - - /** - * Evaluates the expression of this evaluator and returns the result as a String. This method can always be called. - * <p> - * NOTE: null will be returned if expression results in {@link PyNone} - */ - public String evalAsString() throws EvaluatorException - { - Object result = eval(); - return result == null ? null : result.toString(); - } - - private void doEval() throws EvaluatorException - { - try - { - interpreter.exec(compiledExpression); - } catch (PyException ex) - { - throw toEvaluatorException(ex); - } - } - - private EvaluatorException toEvaluatorException(PyException ex) - { - return toEvaluatorException(ex, expression); - } - - private static EvaluatorException toEvaluatorException(PyException ex, String expressionOrNull) - { - Throwable exception = ex; - PyObject value = ex.value; - Object object = value.__tojava__(Object.class); - if (object instanceof Throwable) - { - exception = (Throwable) object; - } - String msg = extractExceptionMessage(ex); - if (expressionOrNull != null) - { - PyTraceback traceback = ex.traceback; - String details = - traceback == null ? "" : "occurred in line " + traceback.tb_lineno - + " of the script when "; - msg = "Error " + details + "evaluating '" + expressionOrNull + "': " + msg; - } - return new EvaluatorException(msg, exception); - } - - private static String extractExceptionMessage(PyException ex) - { - final String[] description = StringUtils.split(ex.toString(), '\n'); - return description[description.length - 1]; + return factory; } public static boolean isMultiline(String expression) { return expression.indexOf('\n') >= 0; } - - public void releaseResources() - { - interpreter.releaseResources(); - } - - public Collection<String> getGlobalVariables() - { - Set<String> results = new HashSet<String>(); - PyStringMap locals = (PyStringMap) interpreter.getLocals(); - for (Object key : locals.keys()) - { - results.add(key.toString()); - } - return Collections.unmodifiableCollection(results); - } - } diff --git a/common/source/java/ch/systemsx/cisd/common/jython/evaluator/IJythonEvaluator.java b/common/source/java/ch/systemsx/cisd/common/jython/evaluator/IJythonEvaluator.java new file mode 100644 index 00000000000..56b2e2f45ce --- /dev/null +++ b/common/source/java/ch/systemsx/cisd/common/jython/evaluator/IJythonEvaluator.java @@ -0,0 +1,124 @@ +/* + * Copyright 2016 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.common.jython.evaluator; + +import java.math.BigInteger; +import java.util.Collection; + +import org.python.core.PyNone; + +import ch.systemsx.cisd.common.jython.evaluator.Evaluator.ReturnType; + +/** + * @author Jakub Straszewski + */ +public interface IJythonEvaluator +{ + /** + * Returns <code>true</code> if specified function is defined in the script. + */ + public boolean hasFunction(String functionName); + + /** + * Evaluates specified function with specified arguments. The arguments are turned into Python Strings if they are Java {@link String} objects. + * The return value of the function is returned as a Java object or <code>null</code>. + * + * @throws EvaluatorException if evaluation fails. + */ + public Object evalFunction(String functionName, Object... args); + + /** + * Sets the variable <var>name</var> to <var>value</var> in the evaluator's name space. + */ + public void set(String name, Object value); + + public Object get(String name); + + /** + * Deletes the variable <var>name</var> from the evaluator's name space. + */ + public void delete(String name); + + /** + * Returns <code>true</code> if and only if the variable <var>name</var> exists in the evaluator's name space. + */ + public boolean has(String name); + + /** + * Returns the {@link ReturnType} of the expression of this evaluator. + */ + public ReturnType getType(); + + /** + * Evaluates the expression of this evaluator and returns the result. Use this method if you do not know what will be the result type. + * <p> + * <i>This is a legacy function to mimic the old Jython 2.2 Evaluator's behavior which will only return Long, Double or String and doesn't know + * boolean.</i> + * + * @return evaluation result which can be of Long, Double or String type. All other types are converted to String representation except + * {@link PyNone} that represents null value and will be converted to <code>null</code>. + */ + public Object evalLegacy2_2(); + + /** + * Evaluates the expression of this evaluator and returns the result. Use this method if you do not know what will be the result type. + * + * @return evaluation result as translated by the Jython interpreter.. + */ + public Object eval(); + + /** + * Evaluates the expression of this evaluator and returns the result, assuming that the expression has a boolean return type. + */ + public boolean evalToBoolean(); + + /** + * Evaluates the expression of this evaluator and returns the result, assuming that the expression has an integer return type. + */ + public int evalToInt(); + + /** + * Evaluates the expression of this evaluator and returns the result, assuming that the expression has a big integer return type. + */ + public BigInteger evalToBigInt(); + + /** + * Evaluates the expression of this evaluator and returns the result, assuming that the expression has a floating point (double) return type. + */ + public double evalToDouble(); + + /** + * Evaluates the expression of this evaluator and returns the result as a String. This method can always be called. + * <p> + * <i>This is a legacy function to mimic the old Jython 2.2 Evaluator's behavior which first translates to Long and Double and doesn't know + * boolean.</i> + * <p> + * NOTE: null will be returned if expression results in {@link PyNone} + */ + public String evalAsStringLegacy2_2(); + + /** + * Evaluates the expression of this evaluator and returns the result as a String. This method can always be called. + * <p> + * NOTE: null will be returned if expression results in {@link PyNone} + */ + public String evalAsString(); + + public void releaseResources(); + + public Collection<String> getGlobalVariables(); +} diff --git a/common/source/java/ch/systemsx/cisd/common/jython/evaluator/IJythonEvaluatorFactory.java b/common/source/java/ch/systemsx/cisd/common/jython/evaluator/IJythonEvaluatorFactory.java new file mode 100644 index 00000000000..1a52ea462ea --- /dev/null +++ b/common/source/java/ch/systemsx/cisd/common/jython/evaluator/IJythonEvaluatorFactory.java @@ -0,0 +1,63 @@ +/* + * Copyright 2016 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.common.jython.evaluator; + +import ch.systemsx.cisd.common.jython.v25.Evaluator25; + +/** + * @author Jakub Straszewski + */ +public interface IJythonEvaluatorFactory +{ + /** + * Call once before using the object to initialize. + */ + public void initialize(); + + /** + * Creates a new {@link Evaluator25} with file system access blocked. + * + * @param expression The expression to evaluate. + */ + public IJythonEvaluator create(String expression) throws EvaluatorException; + + /** + * Creates a new {@link IJythonEvaluator} with file system access blocked. + * + * @param expression The expression to evaluate. + * @param supportFunctionsOrNull If not <code>null</code>, all public static methods of the given class will be available to the evaluator as + * "supporting functions". + * @param initialScriptOrNull If not <code>null</code>, this has to be a valid (Python) script which is evaluated initially, e.g. to define some + * new functions. Note: this script is trusted, so don't run any unvalidated code here! + */ + public IJythonEvaluator create(String expression, Class<?> supportFunctionsOrNull, String initialScriptOrNull) + throws EvaluatorException; + + /** + * Creates a new {@link Evaluator25}. + * + * @param expression The expression to evaluate. + * @param supportFunctionsOrNull If not <code>null</code>, all public static methods of the given class will be available to the evaluator as + * "supporting functions". + * @param initialScriptOrNull If not <code>null</code>, this has to be a valid (Python) script which is evaluated initially, e.g. to define some + * new functions. Note: this script is trusted, so don't run any unvalidated code here! + * @param blockFileAccess If <code>true</code> the script will not be able to open files. + */ + public IJythonEvaluator create(String expression, String[] pythonPath, String scriptPath, Class<?> supportFunctionsOrNull, + String initialScriptOrNull, boolean blockFileAccess) throws EvaluatorException; + +} diff --git a/common/source/java/ch/systemsx/cisd/common/jython/evaluator/JythonEvaluatorSpringComponent.java b/common/source/java/ch/systemsx/cisd/common/jython/evaluator/JythonEvaluatorSpringComponent.java new file mode 100644 index 00000000000..17e6bbdc1bf --- /dev/null +++ b/common/source/java/ch/systemsx/cisd/common/jython/evaluator/JythonEvaluatorSpringComponent.java @@ -0,0 +1,44 @@ +/* + * Copyright 2016 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.common.jython.evaluator; + +import org.springframework.beans.factory.BeanInitializationException; + +import ch.systemsx.cisd.common.jython.v25.Jython25EvaluatorFactory; +import ch.systemsx.cisd.common.jython.v27.Jython27EvaluatorFactory; +import ch.systemsx.cisd.common.spring.ExposablePropertyPlaceholderConfigurer; + +/** + * @author Jakub Straszewski + */ +public class JythonEvaluatorSpringComponent +{ + public JythonEvaluatorSpringComponent(ExposablePropertyPlaceholderConfigurer propertyConfigurer) + { + String jythonVersion = propertyConfigurer.getResolvedProps().getProperty("jython-version"); + if ("2.7".equals(jythonVersion)) + { + Evaluator.setFactory(new Jython27EvaluatorFactory()); + } else if ("2.5".equals(jythonVersion)) + { + Evaluator.setFactory(new Jython25EvaluatorFactory()); + } else + { + throw new BeanInitializationException("The jython-version property must be specified. Since version 16.04 "); + } + } +} diff --git a/common/source/java/ch/systemsx/cisd/common/jython/v25/Evaluator25.java b/common/source/java/ch/systemsx/cisd/common/jython/v25/Evaluator25.java new file mode 100644 index 00000000000..f5f91cd5690 --- /dev/null +++ b/common/source/java/ch/systemsx/cisd/common/jython/v25/Evaluator25.java @@ -0,0 +1,532 @@ +/* + * Copyright 2009 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.common.jython.v25; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang.StringUtils; +import org.python.core.CompileMode; +import org.python.core.CompilerFlags; +import org.python.core.Py; +import org.python.core.PyBoolean; +import org.python.core.PyCode; +import org.python.core.PyException; +import org.python.core.PyFloat; +import org.python.core.PyFunction; +import org.python.core.PyInteger; +import org.python.core.PyList; +import org.python.core.PyLong; +import org.python.core.PyNone; +import org.python.core.PyObject; +import org.python.core.PyString; +import org.python.core.PyStringMap; +import org.python.core.PySystemState; +import org.python.core.PyTraceback; + +import ch.systemsx.cisd.common.jython.PythonInterpreter; +import ch.systemsx.cisd.common.jython.evaluator.Evaluator; +import ch.systemsx.cisd.common.jython.evaluator.Evaluator.ReturnType; +import ch.systemsx.cisd.common.jython.evaluator.EvaluatorException; +import ch.systemsx.cisd.common.jython.evaluator.IJythonEvaluator; +import ch.systemsx.cisd.common.shared.basic.string.CommaSeparatedListBuilder; + +/** + * A class for evaluating expressions, based on Jython 2.5. + * <p> + * This class is optimized for evaluating the same expression with a different set of variables repeatedly and efficient. The mode of usage of this + * class is: + * <ol> + * <li>Construct an {@link Evaluator25} with an appropriate expression.</li> + * <li>Set all variables needed for evaluation via {@link #set(String, Object)}</li> + * <li>Call one of {@link #getType()}, {@link #evalAsString}, {@link #evalToBoolean()}, {@link #evalToInt()}, {@link #evalToBigInt()}, + * {@link #evalToDouble()}.</li> + * <li>Repeat from step 2</li> + * </ol> + * + * @author Bernd Rinn + */ +public final class Evaluator25 implements IJythonEvaluator +{ + private final PythonInterpreter interpreter; + + private final String expression; + + private final PyCode compiledExpression; + + /** + * Call once before using the object to initialize. + */ + public static void initialize() + { + PySystemState.initialize(); + } + + /** + * Creates a new {@link Evaluator25} with file system access blocked. + * + * @param expression The expression to evaluate. + */ + public Evaluator25(String expression) throws EvaluatorException + { + this(expression, null, null); + } + + /** + * Creates a new {@link Evaluator25} with file system access blocked. + * + * @param expression The expression to evaluate. + * @param supportFunctionsOrNull If not <code>null</code>, all public static methods of the given class will be available to the evaluator as + * "supporting functions". + * @param initialScriptOrNull If not <code>null</code>, this has to be a valid (Python) script which is evaluated initially, e.g. to define some + * new functions. Note: this script is trusted, so don't run any unvalidated code here! + */ + public Evaluator25(String expression, Class<?> supportFunctionsOrNull, String initialScriptOrNull) + throws EvaluatorException + { + this(expression, null, null, supportFunctionsOrNull, initialScriptOrNull, true); + } + + /** + * Creates a new {@link Evaluator25}. + * + * @param expression The expression to evaluate. + * @param supportFunctionsOrNull If not <code>null</code>, all public static methods of the given class will be available to the evaluator as + * "supporting functions". + * @param initialScriptOrNull If not <code>null</code>, this has to be a valid (Python) script which is evaluated initially, e.g. to define some + * new functions. Note: this script is trusted, so don't run any unvalidated code here! + * @param blockFileAccess If <code>true</code> the script will not be able to open files. + */ + public Evaluator25(String expression, String[] pythonPath, String scriptPath, Class<?> supportFunctionsOrNull, + String initialScriptOrNull, boolean blockFileAccess) throws EvaluatorException + { + if (Evaluator.isMultiline(expression)) + { + throw new EvaluatorException("Expression '" + expression + "' contains line breaks"); + } + + this.interpreter = PythonInterpreter.createIsolatedPythonInterpreter(); + this.interpreter.addToPath(pythonPath); + + // Security: do not allow file access. + try + { + if (blockFileAccess) + { + interpreter.exec("def open():\n pass"); + } + if (supportFunctionsOrNull != null) + { + interpreter.exec("from " + supportFunctionsOrNull.getCanonicalName() + " import *"); + } + if (initialScriptOrNull != null) + { + // we need to inject the __file__ variable to import third party python libraries + // that are added to the system path inside the python script + if (scriptPath != null) + { + interpreter.exec("__file__='" + scriptPath + "'"); + } + interpreter.exec(initialScriptOrNull); + } + this.expression = expression; + + this.compiledExpression = doCompile(expression); + } catch (PyException ex) + { + throw toEvaluatorException(ex); + } + } + + /** + * Returns <code>true</code> if specified function is defined in the script. + */ + @Override + public boolean hasFunction(String functionName) + { + PyObject pyObject = interpreter.get(functionName); + return pyObject instanceof PyFunction; + } + + /** + * Evaluates specified function with specified arguments. The arguments are turned into Python Strings if they are Java {@link String} objects. + * The return value of the function is returned as a Java object or <code>null</code>. + * + * @throws EvaluatorException if evaluation fails. + */ + @Override + public Object evalFunction(String functionName, Object... args) + { + try + { + PyObject pyObject = interpreter.get(functionName); + if (pyObject == null) + { + throw new PyException(new PyString("Unknown function"), functionName); + } + if (pyObject instanceof PyFunction == false) + { + throw new PyException(new PyString("Not a function"), "'" + functionName + + "' is of type " + pyObject.getType().getName() + "."); + } + PyFunction func = (PyFunction) pyObject; + PyObject[] pyArgs = new PyObject[args.length]; + for (int i = 0; i < args.length; i++) + { + pyArgs[i] = translateToPython(args[i]); + } + PyObject result = func.__call__(pyArgs); + return translateToJava(result); + } catch (PyException ex) + { + CommaSeparatedListBuilder builder = new CommaSeparatedListBuilder(); + for (Object argument : args) + { + builder.append(argument); + } + throw toEvaluatorException(ex, functionName + "(" + builder + ")"); + } + } + + private PyObject translateToPython(Object javaObject) + { + if (javaObject instanceof String) + { + return new PyString((String) javaObject); + } + return Py.java2py(javaObject); + } + + /** + * @return compiled <var>expression</var> + * @throws EvaluatorException if compilation fails + */ + private static PyCode doCompile(String expression) throws EvaluatorException + { + try + { + return Py.compile_flags("__result__=(" + expression + ")", "expression: " + expression, + CompileMode.exec, new CompilerFlags()); + } catch (PyException ex) + { + throw toEvaluatorException(ex, expression); + } + } + + /** + * Sets the variable <var>name</var> to <var>value</var> in the evaluator's name space. + */ + @Override + public void set(String name, Object value) + { + interpreter.set(name, value); + } + + @Override + public Object get(String name) + { + return interpreter.get(name); + } + + /** + * Deletes the variable <var>name</var> from the evaluator's name space. + */ + @Override + public void delete(String name) + { + interpreter.getLocals().__delitem__(name); + } + + /** + * Returns <code>true</code> if and only if the variable <var>name</var> exists in the evaluator's name space. + */ + @Override + public boolean has(String name) + { + return ((PyStringMap) interpreter.getLocals()).has_key(new PyString(name)); + } + + /** + * Returns the {@link ReturnType} of the expression of this evaluator. + */ + @Override + public ReturnType getType() + { + doEval(); + final Object obj = getInterpreterResult(); + if (obj instanceof PyBoolean) + { + return ReturnType.BOOLEAN; + } else if (obj instanceof PyInteger) + { + return ReturnType.INTEGER; + } else if (obj instanceof PyLong) + { + return ReturnType.BIGINT; + } else if (obj instanceof PyFloat) + { + return ReturnType.DOUBLE; + } else if (obj instanceof PyString) + { + return ReturnType.STRING; + } else + { + return ReturnType.OTHER; + } + } + + /** + * Evaluates the expression of this evaluator and returns the result. Use this method if you do not know what will be the result type. + * <p> + * <i>This is a legacy function to mimic the old Jython 2.2 Evaluator's behavior which will only return Long, Double or String and doesn't know + * boolean.</i> + * + * @return evaluation result which can be of Long, Double or String type. All other types are converted to String representation except + * {@link PyNone} that represents null value and will be converted to <code>null</code>. + */ + @Override + public Object evalLegacy2_2() + { + doEval(); + final PyObject obj = getInterpreterResult(); + Object result = translateToJavaLegacy(obj); + if (result != null && result instanceof Long == false && result instanceof Double == false + && result instanceof String == false) + { + return result.toString(); + } + return result; + } + + /** + * Evaluates the expression of this evaluator and returns the result. Use this method if you do not know what will be the result type. + * + * @return evaluation result as translated by the Jython interpreter.. + */ + @Override + public Object eval() + { + doEval(); + return translateToJava(getInterpreterResult()); + } + + private Object translateToJavaLegacy(final PyObject obj) + { + if (obj instanceof PyInteger) + { + return new Long(((PyInteger) obj).getValue()); + } else if (obj instanceof PyLong) + { + return new Long(((PyLong) obj).getValue().longValue()); + } else if (obj instanceof PyFloat) + { + return new Double(((PyFloat) obj).getValue()); + } else if (obj instanceof PyNone) + { + return null; + } else if (obj instanceof PyList) + { + PyList pyList = (PyList) obj; + PyObject[] array = pyList.getArray(); + List<Object> list = new ArrayList<Object>(); + for (int i = 0, n = pyList.size(); i < n; i++) + { + list.add(translateToJavaLegacy(array[i])); + } + return list; + } else + { + return translateToJava(obj); + } + } + + private Object translateToJava(final PyObject obj) + { + return (obj == null) ? null : obj.__tojava__(Object.class); + } + + private PyObject getInterpreterResult() + { + return interpreter.get("__result__"); + } + + /** + * Evaluates the expression of this evaluator and returns the result, assuming that the expression has a boolean return type. + */ + @Override + public boolean evalToBoolean() throws EvaluatorException + { + doEval(); + try + { + return ((PyBoolean) getInterpreterResult()).getBooleanValue(); + } catch (ClassCastException ex) + { + final ReturnType type = getType(); + throw new EvaluatorException("Expected a result of type " + ReturnType.BOOLEAN + + ", found " + type); + } + } + + /** + * Evaluates the expression of this evaluator and returns the result, assuming that the expression has an integer return type. + */ + @Override + public int evalToInt() throws EvaluatorException + { + doEval(); + try + { + return ((PyInteger) getInterpreterResult()).getValue(); + } catch (ClassCastException ex) + { + final ReturnType type = getType(); + throw new EvaluatorException("Expected a result of type " + ReturnType.INTEGER + + ", found " + type); + } + } + + /** + * Evaluates the expression of this evaluator and returns the result, assuming that the expression has a big integer return type. + */ + @Override + public BigInteger evalToBigInt() throws EvaluatorException + { + doEval(); + try + { + return ((PyLong) getInterpreterResult()).getValue(); + } catch (ClassCastException ex) + { + final ReturnType type = getType(); + throw new EvaluatorException("Expected a result of type " + ReturnType.BIGINT + + ", found " + type); + } + } + + /** + * Evaluates the expression of this evaluator and returns the result, assuming that the expression has a floating point (double) return type. + */ + @Override + public double evalToDouble() throws EvaluatorException + { + doEval(); + try + { + return ((PyFloat) getInterpreterResult()).getValue(); + } catch (ClassCastException ex) + { + final ReturnType type = getType(); + throw new EvaluatorException("Expected a result of type " + ReturnType.DOUBLE + + ", found " + type); + } + } + + /** + * Evaluates the expression of this evaluator and returns the result as a String. This method can always be called. + * <p> + * <i>This is a legacy function to mimic the old Jython 2.2 Evaluator's behavior which first translates to Long and Double and doesn't know + * boolean.</i> + * <p> + * NOTE: null will be returned if expression results in {@link PyNone} + */ + @Override + public String evalAsStringLegacy2_2() throws EvaluatorException + { + Object result = evalLegacy2_2(); + return result == null ? null : result.toString(); + } + + /** + * Evaluates the expression of this evaluator and returns the result as a String. This method can always be called. + * <p> + * NOTE: null will be returned if expression results in {@link PyNone} + */ + @Override + public String evalAsString() throws EvaluatorException + { + Object result = eval(); + return result == null ? null : result.toString(); + } + + private void doEval() throws EvaluatorException + { + try + { + interpreter.exec(compiledExpression); + } catch (PyException ex) + { + throw toEvaluatorException(ex); + } + } + + private EvaluatorException toEvaluatorException(PyException ex) + { + return toEvaluatorException(ex, expression); + } + + private static EvaluatorException toEvaluatorException(PyException ex, String expressionOrNull) + { + Throwable exception = ex; + PyObject value = ex.value; + Object object = value.__tojava__(Object.class); + if (object instanceof Throwable) + { + exception = (Throwable) object; + } + String msg = extractExceptionMessage(ex); + if (expressionOrNull != null) + { + PyTraceback traceback = ex.traceback; + String details = + traceback == null ? "" : "occurred in line " + traceback.tb_lineno + + " of the script when "; + msg = "Error " + details + "evaluating '" + expressionOrNull + "': " + msg; + } + return new EvaluatorException(msg, exception); + } + + private static String extractExceptionMessage(PyException ex) + { + final String[] description = StringUtils.split(ex.toString(), '\n'); + return description[description.length - 1]; + } + + @Override + public void releaseResources() + { + interpreter.releaseResources(); + } + + @Override + public Collection<String> getGlobalVariables() + { + Set<String> results = new HashSet<String>(); + PyStringMap locals = (PyStringMap) interpreter.getLocals(); + for (Object key : locals.keys()) + { + results.add(key.toString()); + } + return Collections.unmodifiableCollection(results); + } + +} diff --git a/common/source/java/ch/systemsx/cisd/common/jython/v25/Jython25EvaluatorFactory.java b/common/source/java/ch/systemsx/cisd/common/jython/v25/Jython25EvaluatorFactory.java new file mode 100644 index 00000000000..34a4391e3ea --- /dev/null +++ b/common/source/java/ch/systemsx/cisd/common/jython/v25/Jython25EvaluatorFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.common.jython.v25; + +import ch.systemsx.cisd.common.jython.evaluator.EvaluatorException; +import ch.systemsx.cisd.common.jython.evaluator.IJythonEvaluator; +import ch.systemsx.cisd.common.jython.evaluator.IJythonEvaluatorFactory; + +/** + * @author Jakub Straszewski + */ +public class Jython25EvaluatorFactory implements IJythonEvaluatorFactory +{ + + @Override + public void initialize() + { + Evaluator25.initialize(); + } + + @Override + public IJythonEvaluator create(String expression) throws EvaluatorException + { + return new Evaluator25(expression); + } + + @Override + public IJythonEvaluator create(String expression, Class<?> supportFunctionsOrNull, String initialScriptOrNull) throws EvaluatorException + { + return new Evaluator25(expression, supportFunctionsOrNull, initialScriptOrNull); + } + + @Override + public IJythonEvaluator create(String expression, String[] pythonPath, String scriptPath, Class<?> supportFunctionsOrNull, + String initialScriptOrNull, + boolean blockFileAccess) throws EvaluatorException + { + return new Evaluator25(expression, pythonPath, scriptPath, supportFunctionsOrNull, initialScriptOrNull, blockFileAccess); + } + +} diff --git a/common/source/java/ch/systemsx/cisd/common/jython/v27/Evaluator27.java b/common/source/java/ch/systemsx/cisd/common/jython/v27/Evaluator27.java index 6fdd4204b3f..8d527f6971c 100644 --- a/common/source/java/ch/systemsx/cisd/common/jython/v27/Evaluator27.java +++ b/common/source/java/ch/systemsx/cisd/common/jython/v27/Evaluator27.java @@ -25,7 +25,6 @@ import java.util.List; import java.util.Set; import org.apache.commons.lang.StringUtils; -import org.python.core.PySystemState; import org.python27.core.CompileMode; import org.python27.core.CompilerFlags; import org.python27.core.Py; @@ -41,9 +40,13 @@ import org.python27.core.PyNone; import org.python27.core.PyObject; import org.python27.core.PyString; import org.python27.core.PyStringMap; +import org.python27.core.PySystemState; import org.python27.core.PyTraceback; +import ch.systemsx.cisd.common.jython.evaluator.Evaluator; +import ch.systemsx.cisd.common.jython.evaluator.Evaluator.ReturnType; import ch.systemsx.cisd.common.jython.evaluator.EvaluatorException; +import ch.systemsx.cisd.common.jython.evaluator.IJythonEvaluator; import ch.systemsx.cisd.common.shared.basic.string.CommaSeparatedListBuilder; /** @@ -56,7 +59,7 @@ import ch.systemsx.cisd.common.shared.basic.string.CommaSeparatedListBuilder; * * @author Ganime Akin */ -public final class Evaluator27 +public final class Evaluator27 implements IJythonEvaluator { private final PythonInterpreter27 interpreter; @@ -72,14 +75,6 @@ public final class Evaluator27 PySystemState.initialize(); } - /** - * The return type of this expression. - */ - public enum ReturnType - { - BOOLEAN, INTEGER, BIGINT, DOUBLE, STRING, OTHER - } - /** * Creates a new {@link Evaluator27} with file system access blocked. * @@ -109,8 +104,8 @@ public final class Evaluator27 * Creates a new {@link Evaluator27}. * * @param expression The expression to evaluate. - * @param scriptPath This is passed in so that we can set the __file__ variable to a fixed path to be able to use third party libraries - * copied to the same the folder as the Python script + * @param scriptPath This is passed in so that we can set the __file__ variable to a fixed path to be able to use third party libraries copied to + * the same the folder as the Python script * @param supportFunctionsOrNull If not <code>null</code>, all public static methods of the given class will be available to the evaluator as * "supporting functions". * @param initialScriptOrNull If not <code>null</code>, this has to be a valid (Python) script which is evaluated initially, e.g. to define some @@ -120,7 +115,7 @@ public final class Evaluator27 public Evaluator27(String expression, String[] pythonPath, String scriptPath, Class<?> supportFunctionsOrNull, String initialScriptOrNull, boolean blockFileAccess) throws EvaluatorException { - if (isMultiline(expression)) + if (Evaluator.isMultiline(expression)) { throw new EvaluatorException("Expression '" + expression + "' contains line breaks"); } @@ -143,7 +138,10 @@ public final class Evaluator27 { // we need to inject the __file__ variable to import third party python libraries // that are added to the system path inside the python script - interpreter.exec("__file__='" + scriptPath + "'"); + if (scriptPath != null) + { + interpreter.exec("__file__='" + scriptPath + "'"); + } interpreter.exec(initialScriptOrNull); } this.expression = expression; @@ -158,6 +156,7 @@ public final class Evaluator27 /** * Returns <code>true</code> if specified function is defined in the script. */ + @Override public boolean hasFunction(String functionName) { PyObject pyObject = interpreter.get(functionName); @@ -170,6 +169,7 @@ public final class Evaluator27 * * @throws EvaluatorException if evaluation fails. */ + @Override public Object evalFunction(String functionName, Object... args) { try @@ -231,11 +231,13 @@ public final class Evaluator27 /** * Sets the variable <var>name</var> to <var>value</var> in the evaluator's name space. */ + @Override public void set(String name, Object value) { interpreter.set(name, value); } + @Override public Object get(String name) { return interpreter.get(name); @@ -244,6 +246,7 @@ public final class Evaluator27 /** * Deletes the variable <var>name</var> from the evaluator's name space. */ + @Override public void delete(String name) { interpreter.getLocals().__delitem__(name); @@ -252,6 +255,7 @@ public final class Evaluator27 /** * Returns <code>true</code> if and only if the variable <var>name</var> exists in the evaluator's name space. */ + @Override public boolean has(String name) { return ((PyStringMap) interpreter.getLocals()).has_key(new PyString(name)); @@ -260,6 +264,7 @@ public final class Evaluator27 /** * Returns the {@link ReturnType} of the expression of this evaluator. */ + @Override public ReturnType getType() { doEval(); @@ -294,6 +299,7 @@ public final class Evaluator27 * @return evaluation result which can be of Long, Double or String type. All other types are converted to String representation except * {@link PyNone} that represents null value and will be converted to <code>null</code>. */ + @Override public Object evalLegacy2_2() { doEval(); @@ -312,6 +318,7 @@ public final class Evaluator27 * * @return evaluation result as translated by the Jython interpreter.. */ + @Override public Object eval() { doEval(); @@ -361,6 +368,7 @@ public final class Evaluator27 /** * Evaluates the expression of this evaluator and returns the result, assuming that the expression has a boolean return type. */ + @Override public boolean evalToBoolean() throws EvaluatorException { doEval(); @@ -378,6 +386,7 @@ public final class Evaluator27 /** * Evaluates the expression of this evaluator and returns the result, assuming that the expression has an integer return type. */ + @Override public int evalToInt() throws EvaluatorException { doEval(); @@ -395,6 +404,7 @@ public final class Evaluator27 /** * Evaluates the expression of this evaluator and returns the result, assuming that the expression has a big integer return type. */ + @Override public BigInteger evalToBigInt() throws EvaluatorException { doEval(); @@ -412,6 +422,7 @@ public final class Evaluator27 /** * Evaluates the expression of this evaluator and returns the result, assuming that the expression has a floating point (double) return type. */ + @Override public double evalToDouble() throws EvaluatorException { doEval(); @@ -434,6 +445,7 @@ public final class Evaluator27 * <p> * NOTE: null will be returned if expression results in {@link PyNone} */ + @Override public String evalAsStringLegacy2_2() throws EvaluatorException { Object result = evalLegacy2_2(); @@ -445,6 +457,7 @@ public final class Evaluator27 * <p> * NOTE: null will be returned if expression results in {@link PyNone} */ + @Override public String evalAsString() throws EvaluatorException { Object result = eval(); @@ -494,16 +507,13 @@ public final class Evaluator27 return description[description.length - 1]; } - public static boolean isMultiline(String expression) - { - return expression.indexOf('\n') >= 0; - } - + @Override public void releaseResources() { interpreter.releaseResources(); } + @Override public Collection<String> getGlobalVariables() { Set<String> results = new HashSet<String>(); diff --git a/common/source/java/ch/systemsx/cisd/common/jython/v27/Jython27EvaluatorFactory.java b/common/source/java/ch/systemsx/cisd/common/jython/v27/Jython27EvaluatorFactory.java new file mode 100644 index 00000000000..b6dc37a3f97 --- /dev/null +++ b/common/source/java/ch/systemsx/cisd/common/jython/v27/Jython27EvaluatorFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.common.jython.v27; + +import ch.systemsx.cisd.common.jython.evaluator.EvaluatorException; +import ch.systemsx.cisd.common.jython.evaluator.IJythonEvaluator; +import ch.systemsx.cisd.common.jython.evaluator.IJythonEvaluatorFactory; + +/** + * @author Jakub Straszewski + */ +public class Jython27EvaluatorFactory implements IJythonEvaluatorFactory +{ + + @Override + public void initialize() + { + Evaluator27.initialize(); + } + + @Override + public IJythonEvaluator create(String expression) throws EvaluatorException + { + return new Evaluator27(expression); + } + + @Override + public IJythonEvaluator create(String expression, Class<?> supportFunctionsOrNull, String initialScriptOrNull) throws EvaluatorException + { + return new Evaluator27(expression, supportFunctionsOrNull, initialScriptOrNull); + } + + @Override + public IJythonEvaluator create(String expression, String[] pythonPath, String scriptPath, Class<?> supportFunctionsOrNull, + String initialScriptOrNull, + boolean blockFileAccess) throws EvaluatorException + { + return new Evaluator27(expression, pythonPath, scriptPath, supportFunctionsOrNull, initialScriptOrNull, blockFileAccess); + } + +} \ No newline at end of file diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/evaluator/Evaluator25Test.java b/common/sourceTest/java/ch/systemsx/cisd/common/evaluator/Evaluator25Test.java new file mode 100644 index 00000000000..97812ee3362 --- /dev/null +++ b/common/sourceTest/java/ch/systemsx/cisd/common/evaluator/Evaluator25Test.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.common.evaluator; + +import org.testng.annotations.Test; + +import ch.systemsx.cisd.common.jython.evaluator.IJythonEvaluatorFactory; +import ch.systemsx.cisd.common.jython.v25.Jython25EvaluatorFactory; + +/** + * @author Jakub Straszewski + */ +@Test +public class Evaluator25Test extends EvaluatorTest +{ + private IJythonEvaluatorFactory factory; + + @Override + public IJythonEvaluatorFactory getFactory() + { + if (factory == null) + { + factory = new Jython25EvaluatorFactory(); + } + return factory; + } + +} diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/evaluator/Evaluator27Test.java b/common/sourceTest/java/ch/systemsx/cisd/common/evaluator/Evaluator27Test.java new file mode 100644 index 00000000000..5ff86bab2c9 --- /dev/null +++ b/common/sourceTest/java/ch/systemsx/cisd/common/evaluator/Evaluator27Test.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.common.evaluator; + +import org.testng.annotations.Test; + +import ch.systemsx.cisd.common.jython.evaluator.IJythonEvaluatorFactory; +import ch.systemsx.cisd.common.jython.v27.Jython27EvaluatorFactory; + +/** + * @author Jakub Straszewski + */ +@Test +public class Evaluator27Test extends EvaluatorTest +{ + + private IJythonEvaluatorFactory factory; + + @Override + public IJythonEvaluatorFactory getFactory() + { + if (factory == null) + { + factory = new Jython27EvaluatorFactory(); + } + return factory; + } + +} diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/evaluator/EvaluatorTest.java b/common/sourceTest/java/ch/systemsx/cisd/common/evaluator/EvaluatorTest.java index 0dde2535f5c..be25380d03b 100644 --- a/common/sourceTest/java/ch/systemsx/cisd/common/evaluator/EvaluatorTest.java +++ b/common/sourceTest/java/ch/systemsx/cisd/common/evaluator/EvaluatorTest.java @@ -17,36 +17,43 @@ package ch.systemsx.cisd.common.evaluator; import java.io.File; +import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.apache.commons.io.FileUtils; import org.testng.AssertJUnit; import org.testng.annotations.BeforeTest; import org.testng.annotations.Test; +import ch.systemsx.cisd.common.jython.JythonUtils; import ch.systemsx.cisd.common.jython.evaluator.Evaluator; import ch.systemsx.cisd.common.jython.evaluator.Evaluator.ReturnType; import ch.systemsx.cisd.common.jython.evaluator.EvaluatorException; +import ch.systemsx.cisd.common.jython.evaluator.IJythonEvaluator; +import ch.systemsx.cisd.common.jython.evaluator.IJythonEvaluatorFactory; /** * Tests of the {@link Evaluator}. * * @author Bernd Rinn */ -public class EvaluatorTest extends AssertJUnit +public abstract class EvaluatorTest extends AssertJUnit { + public abstract IJythonEvaluatorFactory getFactory(); + @BeforeTest public void init() { - Evaluator.initialize(); + getFactory().initialize(); } @Test public void testEvalWithNull() { - final Evaluator eval = new Evaluator("None"); + final IJythonEvaluator eval = getFactory().create("None"); assertEquals(null, eval.eval()); assertEquals(null, eval.evalAsString()); } @@ -54,14 +61,14 @@ public class EvaluatorTest extends AssertJUnit @Test public void testSimpleExpression() { - final Evaluator eval = new Evaluator("1+1"); + final IJythonEvaluator eval = getFactory().create("1+1"); assertEquals(2, eval.evalToInt()); } @Test public void testExpressionWithVariables() { - final Evaluator eval = new Evaluator("a*b"); + final IJythonEvaluator eval = getFactory().create("a*b"); eval.set("a", 2); eval.set("b", 3.1); assertEquals(6.2, eval.evalToDouble(), 1e-15); @@ -70,7 +77,7 @@ public class EvaluatorTest extends AssertJUnit @Test public void testExpressionWithVariablesMultipleEvaluations() { - final Evaluator eval = new Evaluator("a*b"); + final IJythonEvaluator eval = getFactory().create("a*b"); eval.set("a", 2); eval.set("b", 3.1); assertEquals(6.2, eval.evalToDouble(), 1e-15); @@ -82,7 +89,7 @@ public class EvaluatorTest extends AssertJUnit @Test public void testVariables() { - final Evaluator eval = new Evaluator("a"); + final IJythonEvaluator eval = getFactory().create("a"); eval.set("a", 2); assertTrue(eval.has("a")); assertFalse(eval.has("b")); @@ -93,9 +100,9 @@ public class EvaluatorTest extends AssertJUnit @Test public void testBuiltInFunctionEval() { - final Evaluator eval = new Evaluator("min(1,2)"); + final IJythonEvaluator eval = getFactory().create("min(1,2)"); assertEquals(1, eval.evalToInt()); - final Evaluator eval2 = new Evaluator("max(1,2)"); + final IJythonEvaluator eval2 = getFactory().create("max(1,2)"); assertEquals(2, eval2.evalToInt()); } @@ -129,28 +136,26 @@ public class EvaluatorTest extends AssertJUnit @Test public void testFunctionEval() { - Evaluator eval = new Evaluator("MinInt(1,2)", Functions.class, null); + IJythonEvaluator eval = getFactory().create("MinInt(1,2)", Functions.class, null); assertEquals(1, eval.evalToInt()); - eval = new Evaluator("MinDbl([1,2,0.1])", Functions.class, null); + eval = getFactory().create("MinDbl([1,2,0.1])", Functions.class, null); assertEquals(0.1, eval.evalToDouble(), 1e-15); - eval = new Evaluator("MinDbl(v)", Functions.class, null); - eval.set("v", new double[] - { 1, 2, -99.9, 3 }); + eval = getFactory().create("MinDbl(v)", Functions.class, null); + eval.set("v", new double[] { 1, 2, -99.9, 3 }); assertEquals(-99.9, eval.evalToDouble(), 1e-15); } @Test public void testInitialScript() { - final Evaluator eval = - new Evaluator("hello()", null, "def hello():\n return 'Hello World'"); + final IJythonEvaluator eval = getFactory().create("hello()", null, "def hello():\n return 'Hello World'"); assertEquals("Hello World", eval.evalAsString()); } @Test public void testGetTypeChanging() { - final Evaluator eval = new Evaluator("a"); + final IJythonEvaluator eval = getFactory().create("a"); eval.set("a", 2); assertEquals(ReturnType.INTEGER, eval.getType()); eval.set("a", "2"); @@ -160,26 +165,26 @@ public class EvaluatorTest extends AssertJUnit @Test(expectedExceptions = EvaluatorException.class) public void testInvalidExpression() { - new Evaluator("pass"); + getFactory().create("pass"); } @Test(expectedExceptions = EvaluatorException.class) public void testInvalidExpressionWithLineBreaks() { - new Evaluator("1\nimport os\nos.remove('/etc/passwd')"); + getFactory().create("1\nimport os\nos.remove('/etc/passwd')"); } @Test(expectedExceptions = EvaluatorException.class) public void testMissingVariable() { - final Evaluator eval = new Evaluator("a"); + final IJythonEvaluator eval = getFactory().create("a"); eval.evalAsString(); } @Test public void testInvalidReturnValue() { - final Evaluator eval = new Evaluator("a"); + final IJythonEvaluator eval = getFactory().create("a"); eval.set("a", true); assertEquals(ReturnType.BOOLEAN, eval.getType()); assertEquals("true", eval.evalAsString()); @@ -199,8 +204,7 @@ public class EvaluatorTest extends AssertJUnit { final File tagFile = new File("targets/newfile"); tagFile.delete(); - final Evaluator eval = - new Evaluator("open('targets/newfile', 'w').write('Should not work')"); + final IJythonEvaluator eval = getFactory().create("open('targets/newfile', 'w').write('Should not work')"); try { eval.evalAsString(); @@ -217,25 +221,78 @@ public class EvaluatorTest extends AssertJUnit { final File tagFile = new File("targets/newfile"); tagFile.delete(); - final Evaluator eval = - new Evaluator("open('targets/newfile', 'w').write('Should work')", null, null, null, - false); + final IJythonEvaluator eval = getFactory().create("open('targets/newfile', 'w').write('Should work')", null, null, null, null, + false); eval.evalAsString(); assertTrue(tagFile.exists()); } + @Test + void testImportExternalPlugin() throws IOException + { + final File dir = new File("targets/testModule"); + dir.mkdirs(); + final File importedScriptFile = new File("targets/test/testmodule.py"); + FileUtils.writeStringToFile(importedScriptFile, "def test_hello_world(): return \"hello world\""); + + String script = + "import os.path\n" + + "import sys\n" + + "print __file__\n" + + "def package_folder(module):\n" + + " return os.path.dirname(os.path.abspath(module))\n" + + "\n" + + "\n" + + "def inject_local_package(name):\n" + + " \"\"\"patches sys.path to prefer / find packages provided next to this script\"\"\"\n" + + " here = package_folder(__file__)\n" + + " sys.path.insert(0, os.path.join(here, name))\n" + + "\n" + + "\n" + + "def import_and_check_location(package_name):\n" + + " \"\"\"checks if given package was imported from right place\"\"\"\n" + + "\n" + + " print \"import %s: \" % package_name,\n" + + " package = __import__(package_name)\n" + + " print \"succeeded\" \n" + + "\n" + + " print \"check location: \",\n" + + " found_location = package_folder(package.__file__)\n" + + " here = package_folder(__file__)\n" + + " if found_location.startswith(here):\n" + + " print \"ok\"\n" + + " else:\n" + + " print \"failed: package is imported from %s\" % found_location\n" + + "\n" + + "\n" + + "inject_local_package(\"testmodule\")\n" + + "import_and_check_location(\"testmodule\")\n" + + "import testmodule\n" + + "def test_from_module():\n" + + " return testmodule.test_hello_world()\n" + + ""; + + File scriptFile = new File("targets/test/file.py"); + String scriptPath = scriptFile.getAbsolutePath(); + FileUtils.writeStringToFile(scriptFile, script); + + final IJythonEvaluator eval = + getFactory().create("test_from_module()", JythonUtils.getScriptDirectoryPythonPath(scriptPath), scriptPath, null, script, false); + + assertEquals("hello world", eval.evalFunction("test_from_module").toString()); + } + @Test public void testEvalFunctionWithStringArgument() { - Evaluator evaluator = new Evaluator("", null, "def hello(name):\n return 'hello ' + name"); - assertEquals("hello world", evaluator.evalFunction("hello", "world").toString()); + IJythonEvaluator eval = getFactory().create("", null, "def hello(name):\n return 'hello ' + name"); + assertEquals("hello world", eval.evalFunction("hello", "world").toString()); } @Test public void testEvalFunctionWithTwoArguments() { - Evaluator evaluator = - new Evaluator("", null, "def get(map, key):\n return map.get(key)\n"); + IJythonEvaluator evaluator = getFactory().create("", null, "def get(map, key):\n return map.get(key)\n"); Map<String, List<String>> map = new HashMap<String, List<String>>(); map.put("physicists", Arrays.asList("Newton", "Einstein")); Object result = evaluator.evalFunction("get", map, "physicists"); @@ -246,8 +303,8 @@ public class EvaluatorTest extends AssertJUnit @Test public void testEvalFunctionWhichReturnsAList() { - Evaluator evaluator = new Evaluator("", null, "def get():\n return ['a','b']"); - Object result = evaluator.evalFunction("get"); + IJythonEvaluator eval = getFactory().create("", null, "def get():\n return ['a','b']"); + Object result = eval.evalFunction("get"); assertEquals("Result " + result.getClass(), true, result instanceof List); assertEquals("['a', 'b']", result.toString()); } @@ -255,10 +312,10 @@ public class EvaluatorTest extends AssertJUnit @Test public void testEvalFunctionWithScriptError() { - Evaluator evaluator = new Evaluator("", null, "def hello(name):\n return unknown\n"); + IJythonEvaluator eval = getFactory().create("", null, "def hello(name):\n return unknown\n"); try { - evaluator.evalFunction("hello", "world"); + eval.evalFunction("hello", "world"); fail("EvaluatorException expected"); } catch (EvaluatorException ex) { @@ -270,9 +327,8 @@ public class EvaluatorTest extends AssertJUnit @Test public void testHasFunction() { - Evaluator evaluator = - new Evaluator("", null, "text = 'hi'\n" - + "def hello(name):\n return 'hello ' + name"); + IJythonEvaluator evaluator = getFactory().create("", null, "text = 'hi'\n" + + "def hello(name):\n return 'hello ' + name"); assertEquals(true, evaluator.hasFunction("hello")); assertEquals(false, evaluator.hasFunction("text")); @@ -282,7 +338,7 @@ public class EvaluatorTest extends AssertJUnit @Test public void testEvalUnkownFunction() { - Evaluator evaluator = new Evaluator("", null, "def hello(name):\n return 'hello ' + name"); + IJythonEvaluator evaluator = getFactory().create("", null, "def hello(name):\n return 'hello ' + name"); try { evaluator.evalFunction("func", "world"); @@ -296,12 +352,11 @@ public class EvaluatorTest extends AssertJUnit @Test public void testEvalFunctionFunctionNameIsVariableName() { - Evaluator evaluator = - new Evaluator("", null, "text = 'universe'\n" - + "def hello(name):\n return 'hello ' + name"); + IJythonEvaluator eval = getFactory().create("", null, "text = 'universe'\n" + + "def hello(name):\n return 'hello ' + name"); try { - evaluator.evalFunction("text", "world"); + eval.evalFunction("text", "world"); fail("EvaluatorException expected"); } catch (EvaluatorException ex) { @@ -313,7 +368,7 @@ public class EvaluatorTest extends AssertJUnit @Test public void testEvalFunctionWithNotEnoughArguments() { - Evaluator evaluator = new Evaluator("", null, "def hello(name):\n return 'hello ' + name"); + IJythonEvaluator evaluator = getFactory().create("", null, "def hello(name):\n return 'hello ' + name"); try { evaluator.evalFunction("hello"); @@ -328,7 +383,7 @@ public class EvaluatorTest extends AssertJUnit @Test public void testEvalFunctionWithToManyArguments() { - Evaluator evaluator = new Evaluator("", null, "def hello(name):\n return 'hello ' + name"); + IJythonEvaluator evaluator = getFactory().create("", null, "def hello(name):\n return 'hello ' + name"); try { evaluator.evalFunction("hello", "world", "universe"); @@ -343,8 +398,7 @@ public class EvaluatorTest extends AssertJUnit @Test public void testEvalFunctionWithWrongArgument() { - Evaluator evaluator = - new Evaluator("", null, "def get(map, key):\n return map.get(key)\n"); + IJythonEvaluator evaluator = getFactory().create("", null, "def get(map, key):\n return map.get(key)\n"); try { evaluator.evalFunction("get", "world", "universe"); -- GitLab