All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.adchilds.jython.JythonScript Maven / Gradle / Ivy

Go to download

A simple Jython wrapper to allow JVM-based projects to execute or evaluate Python scripts at runtime with ease.

There is a newer version: 2.0.1
Show newest version
package com.github.adchilds.jython;

import com.github.adchilds.jython.exception.JythonResultNotFoundException;
import com.github.adchilds.jython.exception.JythonScriptException;
import com.github.adchilds.jython.exception.JythonScriptNotFoundException;
import com.github.adchilds.util.FileUtil;
import com.github.adchilds.util.StringUtil;
import org.python.core.*;
import org.python.util.PythonInterpreter;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * {@link JythonScript} provides an easy to use wrapper for executing and/or evaluating Python expressions or scripts
 * for JVM-based languages. JythonScript packages the Jython standalone JAR as part of it's distribution, so no further
 * dependencies should be required.
 *
 * JythonScript operates on a couple of select criteria:
 * 
    *
  1. A Jython or Python script accessible by the JVM via a {@link String} file path, {@link File}, or {@link InputStream}
  2. *
  3. Optional arguments to be passed to the given script
  4. *
* * To use this utility class, you must follow a few strict rules in creating Jython scripts: *
    *
  • Each script passed to an {@code #evaluate(...)} or {@code #execute(...)} function, such as {@link * JythonScript#evaluate(String, Object...)} or {@link JythonScript#execute(String, Object...)} MUST be setup as a * standard Python main module. This means that the following code must exist: *
     *     {@code if __name__ == '__main__':
     *          ...
     *          ...
     *     }
     *     
    *
  • *
  • If running an evaluation via any of the {@code #evaluate(...)} functions, and expecting a valid result to be * returned, you MUST push the resulting value to a local Python variable named 'result', otherwise JythonScript * will not be able to locate this value within the Python interpreter.
  • *
* * When consuming arguments passed to a Jython script via the 'Object... args' parameter of an execute or evaluate * method, the arguments will be available via Python's sys.argv list. It's important to note that the first argument * (index 0 [sys.argv[0]]) is reserved. Therefore, Jython scripts should always access these arguments beginning with * the second index of sys.argv (i.e. sys.argv[1]). * *
*
* * JythonScript provides {@code #compile(...)} functions that compile the given scripts into {@link PyCode} objects. For * speed increases at runtime, it's better to pre-compile Jython scripts with these functions, maintain the PyCode * objects via an in-memory cache or local variable, and execute or evaluate against the compiled scripts. JythonScript * provides the necessary {@link #evaluate(PyCode, Object...)} and {@link #execute(PyCode, Object...)} methods to * foster these speed increases. * *
*
* * Note: JythonScript does not dictate any other strict code structure. You are free to use any of the features provided * by Java, Python and Jython, including, but not limited to: *
    *
  • OOP
  • *
  • Python Standard Library
  • *
  • Java Standard Library
  • *
  • etc.
  • *
* * For more information, follow the respective links to documentation: *
    *
  • JythonScript - https://github.com/adchilds/JythonScript
  • *
  • Python - https://www.python.org
  • *
  • Jython - http://www.jython.org
  • *
* * @author Adam Childs * @since 0.1 */ public class JythonScript { private static PythonInterpreter INTERPRETER = new PythonInterpreter(); private static final String EVALUATION_RESULT_LOCAL_VARIABLE = "result"; /** * Compiles the Jython script at the given {@code filePath} into a {@link PyCode} object. * * @param filePath the absolute path of the Jython file to compile * @return a compiled Jython script * @throws JythonScriptException when the given file path is empty or null */ public static PyCode compile(String filePath) throws JythonScriptException { // Make sure the file path is is not null or empty if (StringUtil.isBlank(filePath)) { throw new JythonScriptException("Given path is not a file. path=[" + filePath + "]"); } // Compile the script return compile(new File(filePath)); } /** * Compiles the given Jython script into a {@link PyCode} object. * * @param file the Jython file to compile * @return a compiled Jython script * @throws JythonScriptException when the given file is null or is not a file (i.e. a directory) */ public static PyCode compile(File file) throws JythonScriptException { if (file == null) { throw new JythonScriptException("Given file is null; cannot be compiled into PyCode."); } // Make sure the file is actually a valid file if (!file.isFile()) { throw new JythonScriptException("Given file object is not a file. Is it a directory? file=[" + file.getAbsolutePath() + "]"); } // Compile the file, returning the associated PyCode object try { return INTERPRETER.compile(FileUtil.readFully(file)); } catch (IOException e) { throw new JythonScriptException("Could not compile the given file. file=[" + file.getAbsolutePath() + "]", e); } } /** * Evaluates the Jython script at the given {@code scriptPath}, returning the result as it's equivalent Java type. * Accepts optional arguments to be passed to the script at runtime. * * @param scriptPath the fully qualified path of the Jython script to execute * @param args arguments to be passed to the script * @return the result from executing the given script * @throws JythonScriptException when the given file path is null, a directory, or cannot be found */ public static Object evaluate(String scriptPath, Object... args) throws JythonScriptException { // Ensure that the scriptRelativePath is not null or empty if (StringUtil.isBlank(scriptPath)) { throw new JythonScriptNotFoundException("File not found at path=[" + scriptPath + "]"); } // Open an InputStream for the file InputStream inputStream; try { inputStream = FileUtil.getFileInputStream(scriptPath); } catch (Exception e) { throw new JythonScriptNotFoundException("Could not open Jython script from location=[" + scriptPath + "]", e); } return evaluate(inputStream, args); } /** * Evaluates the given Jython script, returning the result as it's equivalent Java type. Accepts optional arguments * to be passed to the script at runtime. * * @param scriptFile the Jython script to execute * @param args arguments to be passed to the script * @return the result from executing the given script * @throws JythonScriptException when the given file is null, a directory, or cannot be found */ public static Object evaluate(File scriptFile, Object... args) throws JythonScriptException { // Ensure that the script is not null if (scriptFile == null) { throw new JythonScriptNotFoundException("Could not open Jython script, the file was null."); } // Open an InputStream for the file InputStream inputStream; try { inputStream = FileUtil.getFileInputStream(scriptFile); } catch (Exception e) { throw new JythonScriptNotFoundException("Could not open Jython script from location=[" + scriptFile.getAbsolutePath() + "]", e); } // Execute the script return evaluate(inputStream, args); } /** * Evaluates the given Jython script, returning the result as it's equivalent Java type. Accepts optional arguments * to be passed to the script at runtime. * * @param inputStream the {@link InputStream} that represents the Jython script to be executed * @param args arguments to be passed to the script * @return the result from executing the given script * @throws JythonScriptException when a script execution error occurs or when a local Python variable named 'result' is not found */ public static Object evaluate(InputStream inputStream, Object... args) throws JythonScriptException { // Execute the script execute(inputStream, args); // Obtain the value of a local variable named 'result' from the executed script PyObject result = INTERPRETER.get(EVALUATION_RESULT_LOCAL_VARIABLE); if (result == null) { throw new JythonResultNotFoundException("Local variable 'result' not found during script execution."); } return parseResult(result); } /** * Evaluates the given Jython script, returning the result as it's equivalent Java type. Accepts optional arguments * to be passed to the script at runtime. * * @param pyCode the compiled Jython script to evaluate * @param args arguments to be passed to the script * @return the result from executing the given script * @throws JythonScriptException when a script execution error occurs or when a local Python variable named 'result' is not found */ public static Object evaluate(PyCode pyCode, Object... args) throws JythonScriptException { // Execute the script execute(pyCode, args); // Obtain the value of a local variable named 'result' from the executed script PyObject result = INTERPRETER.get(EVALUATION_RESULT_LOCAL_VARIABLE); if (result == null) { throw new JythonResultNotFoundException("Local variable 'result' not found during script execution."); } return parseResult(result); } /** * Executes the Jython script at the given {@code scriptPath} with optional arguments passed to the script at * runtime. * * @param scriptPath the fully qualified path of the Jython script to execute * @param args arguments to be passed to the script * @throws JythonScriptException when the given file path is null, a directory, or cannot be found */ public static void execute(String scriptPath, Object... args) throws JythonScriptException { // Ensure that the scriptRelativePath is not null or empty if (StringUtil.isBlank(scriptPath)) { throw new JythonScriptNotFoundException("File not found at path=[" + scriptPath + "]"); } // Open an InputStream for the file InputStream inputStream; try { inputStream = FileUtil.getFileInputStream(scriptPath); } catch (Exception e) { throw new JythonScriptNotFoundException("Could not open Jython script from location=[" + scriptPath + "]", e); } // Execute the script execute(inputStream, args); } /** * Executes the given Jython script with optional arguments passed to the script at runtime. * * @param scriptFile the Jython script to execute * @param args arguments to be passed to the script * @throws JythonScriptException when the given file is null, a directory, or cannot be found */ public static void execute(File scriptFile, Object... args) throws JythonScriptException { // Ensure that the script is not null if (scriptFile == null) { throw new JythonScriptNotFoundException("Could not open Jython script, the file was null."); } // Open an InputStream for the file InputStream inputStream; try { inputStream = FileUtil.getFileInputStream(scriptFile); } catch (Exception e) { throw new JythonScriptNotFoundException("Could not open Jython script from location=[" + scriptFile.getAbsolutePath() + "]", e); } // Execute the script execute(inputStream, args); } /** * Executes the given Jython script with optional arguments passed to the script at runtime. * * @param inputStream the {@link InputStream} that represents the Jython script to be executed * @param args arguments to be passed to the script * @throws JythonScriptException when the given inputstream is null or a script execution error occurs */ public static void execute(InputStream inputStream, Object... args) throws JythonScriptException { if (inputStream == null) { throw new JythonScriptException("Cannot execute a Jython script that doesn't exist! InputStream is null."); } // Set the arguments on the Python System State updateInterpreterState(args); try { // Execute the script INTERPRETER.execfile(inputStream); } catch (Exception e) { throw new JythonScriptException("An error occurred during script execution. cause=[\n\t" + e.toString() + "]"); } } /** * Executes the given Jython script with optional arguments passed to the script at runtime. * * @param pyCode the compiled Jython script to evaluate * @param args arguments to be passed to the script * @throws JythonScriptException when the given PyCode is null or a script execution error occurs */ public static void execute(PyCode pyCode, Object... args) throws JythonScriptException { if (pyCode == null) { throw new JythonScriptException("Cannot execute a Jython script that doesn't exist! InputStream is null."); } // Set the arguments on the Python System State updateInterpreterState(args); try { // Execute the script INTERPRETER.exec(pyCode); } catch (Exception e) { throw new JythonScriptException("An error occurred during script execution. cause=[\n\t" + e.toString() + "]"); } } /** * Updates the {@link PythonInterpreter}s {@link PySystemState} by adding the given {@code args}. These arguments * may be accessed from within Jython scripts via the 'sys.argv' parameters, beginning at the second index (i.e. * sys.argv[1]). Note: the first index is reserved. * * @param args the arguments to set on the {@link PySystemState} for the current {@link PythonInterpreter} */ private static void updateInterpreterState(Object... args) { // Setup the Python System State by appending the given arguments PySystemState state = parseArguments(args); // Add the arguments to the PythonInterpreter INTERPRETER = new PythonInterpreter(null, state); } /** * Given an arbitrary set of arguments, parses each individually into it's correct Jython type, before appending the * value to the set of arguments to be passed to the executing script. * * @param args the arguments to parse before being sent to a Python script */ private static PySystemState parseArguments(Object... args) { PySystemState systemState = new PySystemState(); for (Object arg : args) { systemState.argv.append(Py.java2py(arg)); } return systemState; } /** * Given a {@link PyObject} attempts to convert the object to it's Java representation. If an equivalent java type * cannot be found, the original PyObject is returned. * * Current supported type conversions: *
    *
  • {@link PyBoolean} to boolean
  • *
  • {@link PyInteger} to int
  • *
  • {@link PyString} to {@link String}
  • *
  • {@link PyFloat} to float
  • *
  • {@link PyLong} to long
  • *
  • {@link PyList} to an array of {@link Object}s
  • *
  • {@link PyDictionary} to a {@link Map} of {@link Object}s
  • *
  • {@link PySet} to a {@link Set} of {@link Object}s
  • *
* * @param object the object to convert to it's equivalent Java type, if supported; otherwise, returns the unconverted {@link PyObject} * @return the Java type representation of the given {@link PyObject} */ private static Object parseResult(PyObject object) { if (object == null) { // We should never get here since evaluate provides this check; but, just incase. return null; } if (object instanceof PyBoolean) { return Py.py2boolean(object); } else if (object instanceof PyInteger) { return Py.py2int(object); } else if (object instanceof PyString) { return ((PyString) object).getString(); } else if (object instanceof PyFloat) { return Py.py2float(object); } else if (object instanceof PyLong) { return Py.py2long(object); } else if (object instanceof PyList) { return parsePyObjectList(((PyList) object).getArray()); } else if (object instanceof PyDictionary) { return parsePyObjectDict(((PyDictionary) object).getMap()); } else if (object instanceof PySet) { return parsePyObjectSet(((PySet) object).getSet()); } return object; } /** * Converts the given array of {@link PyObject}s to an array of the corresponding Java types for each value in the array. * * @param pyObjects an array of {@link PyObject}s to parse * @return a new array of {@link Object}s */ private static Object[] parsePyObjectList(PyObject[] pyObjects) { Object[] objects = new Object[pyObjects.length]; int index = 0; for (PyObject pyObject : pyObjects) { objects[index] = parseResult(pyObject); index++; } return objects; } /** * Converts the given {@link Map} of {@link PyObject}s to a Map of the corresponding Java types. * * @param pyDict the dictionary to parse * @return a new {@link Map} of {@link Object}s */ private static Map parsePyObjectDict(Map pyDict) { Map objects = new HashMap<>(); for (Map.Entry entry : pyDict.entrySet()) { Object key = parseResult((PyObject) entry.getKey()); Object value = parseResult((PyObject) entry.getValue()); objects.put(key, value); } return objects; } /** * Converts the given {@link Set} of {@link PyObject}s to a Set of the corresponding Java types. * * @param pySet the set to parse * @return a new {@link Set} of {@link Object}s */ private static Set parsePyObjectSet(Set pySet) { Set objects = new HashSet<>(); for (PyObject pyObject : pySet) { objects.add(parseResult(pyObject)); } return objects; } // Don't allow this class to be instantiated private JythonScript() { } }