package com.c2kernel.scripting; import java.io.StringReader; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.w3c.dom.Text; import org.xml.sax.InputSource; import com.c2kernel.common.ObjectNotFoundException; import com.c2kernel.entity.agent.Job; import com.c2kernel.entity.proxy.AgentProxy; import com.c2kernel.entity.proxy.ItemProxy; import com.c2kernel.utils.LocalObjectLoader; import com.c2kernel.utils.Logger; import com.ibm.bsf.BSFException; import com.ibm.bsf.BSFManager; /************************************************************************** * * $Revision: 1.25 $ * $Date: 2005/10/05 07:39:37 $ * * Copyright (C) 2003 CERN - European Organization for Nuclear Research * All rights reserved. **************************************************************************/ public class Script { Class mOutputClass; String mOutputName; String mScript = ""; String mName; String mVersion; String mLang; HashMap mInputParams = new HashMap(); HashMap mAllInputParams = new HashMap(); ArrayList mIncludes = new ArrayList(); BSFManager scriptManager; /** * Loads script xml and parses it for script source, parameters and output specifications. * First tries to load the script from resource path /scriptFiles/scriptName_scriptVersion.xml * If not found tries to find item at /desc/ScriptDesc/scriptName and load Viewpoint scriptVersion from it. * * For the specification of script xml, see the Script schema from resources. * * @param scriptName - name of the script * @param scriptVersion - named version of the script (must be numbered viewpoint) * @throws ScriptParsingException - when script not found (ScriptLoadingException) or xml is invalid (ScriptParsingException) */ public Script(String scriptName, int scriptVersion) throws ScriptingEngineException { this(scriptName, scriptVersion, new BSFManager()); } public Script(String scriptName, int scriptVersion, BSFManager scriptManager) throws ScriptingEngineException { mName = scriptName; mVersion = String.valueOf(scriptVersion); if (scriptName.equals("")) return; setScriptEnv(scriptManager); setScript(mName, mVersion); } /** * Creates a script executor for the supplied expression, bypassing the xml parsing bit * Output class is forced to an object. */ public Script(String lang, String expr, BSFManager scriptManager) throws ScriptingEngineException { setScriptEnv(scriptManager); mName = ""; mLang = lang; mVersion = ""; mOutputClass = Object.class; mScript = expr; } public Script(String lang, String expr) throws ScriptingEngineException { this(lang, expr, new BSFManager()); } public Script(ItemProxy object, AgentProxy subject, Job job) throws ScriptingEngineException { this(job.getActPropString("ScriptName"), job.getActPropString("ScriptVersion") == null ? -1 : Integer.parseInt(job.getActPropString("ScriptVersion"))); // set enviroment - this needs to be well documented for script developers addInputParam("item", "com.c2kernel.entity.proxy.ItemProxy"); setInputParamValue("item", object); addInputParam("agent", "com.c2kernel.entity.proxy.AgentProxy"); setInputParamValue("agent", subject); addInputParam("job", "com.c2kernel.entity.agent.Job"); setInputParamValue("job", job); setOutput("errors", "com.c2kernel.scripting.ErrorInfo"); } public void setScript(String scriptName, String scriptVersion) throws ScriptingEngineException { try { mName = scriptName; mVersion = scriptVersion; parseScriptXML(LocalObjectLoader.getScript(scriptName, scriptVersion)); } catch (ObjectNotFoundException e) { throw new ScriptingEngineException("Script "+scriptName+" not found"); } } public void setScriptEnv(BSFManager manager) { this.scriptManager = manager; } /** * Extracts script data from script xml. * * @param scriptXML * @throws ScriptParsingException - when script is invalid */ private void parseScriptXML(String scriptXML) throws ScriptParsingException, ParameterException { Document scriptDoc = null; // get the DOM document from the XML DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); try { DocumentBuilder domBuilder = factory.newDocumentBuilder(); scriptDoc = domBuilder.parse(new InputSource(new StringReader(scriptXML))); } catch (Exception ex) { throw new ScriptParsingException("Error parsing Script XML : " + ex.toString()); } Element root = scriptDoc.getDocumentElement(); NodeList scriptNodes = root.getChildNodes(); for (int i = 0; i < scriptNodes.getLength(); i++) { Element currentParam; String paramName; try { currentParam = (Element) scriptNodes.item(i); } catch (ClassCastException ex) { // not an element, skip continue; } paramName = currentParam.getTagName(); Logger.msg(9, "Script.parseScriptXML() - Found element " + paramName); // Process script parameters // New input parameter if (paramName.equals("param")) { if (!(currentParam.hasAttribute("name") && currentParam.hasAttribute("type"))) throw new ScriptParsingException("Script Input Param incomplete, must have name and type"); addInputParam(currentParam.getAttribute("name"), currentParam.getAttribute("type")); } //load output type else if (paramName.equals("output")) { if (!currentParam.hasAttribute("type")) throw new ScriptParsingException("Script Output declaration incomplete, must have type"); setOutput(currentParam.getAttribute("name"), currentParam.getAttribute("type")); } //load any included scripts else if (paramName.equals("include")) { if (!(currentParam.hasAttribute("name") && currentParam.hasAttribute("version"))) throw new ScriptParsingException("Script include declaration incomplete, must have name and version"); String includeName = currentParam.getAttribute("name"); String includeVersion = currentParam.getAttribute("version"); try { Script includedScript = new Script(includeName, Integer.parseInt(includeVersion), scriptManager); mIncludes.add(includedScript); for (Iterator iter = includedScript.getInputParams().values().iterator(); iter.hasNext();) { Parameter includeParam = (Parameter) iter.next(); addIncludedInputParam(includeParam.getName(), includeParam.getType()); } } catch (NumberFormatException e) { throw new ScriptParsingException("Invalid version in imported script "+includeName+"_"+includeVersion); } catch (ScriptingEngineException e) { throw new ScriptParsingException("Error parsing imported script "+includeName+"_"+includeVersion+": "+e.getMessage()); } } //load Script else if (paramName.equals("script")) { if (!currentParam.hasAttribute("language")) throw new ScriptParsingException("Script data incomplete, must have language"); Logger.msg(6, "Script.parseScriptXML() - Script Language: " + currentParam.getAttribute("language")); mLang = currentParam.getAttribute("language"); // get script source NodeList scriptChildNodes = currentParam.getChildNodes(); if (scriptChildNodes.getLength() != 1) throw new ScriptParsingException("More than one child element found under script tag. Script characters may need escaping - suggest convert to CDATA section"); if (scriptChildNodes.item(0) instanceof Text) mScript = ((Text) scriptChildNodes.item(0)).getData(); else throw new ScriptParsingException("Child element of script tag was not text"); Logger.msg(6, "Script.parseScriptXML() - script:" + mScript); } } } protected void addInputParam(String name, String type) throws ParameterException { Parameter inputParam = new Parameter(name); try { inputParam.setType(Class.forName(type)); } catch (ClassNotFoundException ex) { throw new ParameterException("Input parameter " + inputParam.getName() + " specifies class " + type + " which was not found."); } Logger.msg(6, "ScriptExecutor.parseScriptXML() - declared parameter " + name + " (" + type + ")"); //add parameter to hashtable mInputParams.put(inputParam.getName(), inputParam); mAllInputParams.put(inputParam.getName(), inputParam); } protected void addIncludedInputParam(String name, Class type) throws ParameterException { // check if we already have it if (mAllInputParams.containsKey(name)) { Parameter existingParam = (Parameter)mAllInputParams.get(name); // check the types match if (existingParam.getType() == type) return; // matches else // error throw new ParameterException("Parameter conflict. Parameter'"+name+"' is declared as " +existingParam.getType().getName()+" is declared in another script as "+type.getName()); } Parameter inputParam = new Parameter(name); inputParam.setType(type); //add parameter to hashtable mAllInputParams.put(inputParam.getName(), inputParam); } protected void setOutput(String name, String type) throws ScriptParsingException { mOutputName = name; // set name to null if empty if (mOutputName != null && mOutputName.equals("")) mOutputName = null; try { Logger.msg(6, "Script.setOutput() - Output: " + name + " (" + type + ")"); mOutputClass = Class.forName(type); } catch (ClassNotFoundException ex) { throw new ScriptParsingException("Output class "+type+" not found"); } // set up the output object if named // we declare this now so imported scripts using the same object will not overwrite each other during execution if (mOutputName!=null) try { Logger.msg(8, "Script.setOutput() - Initialising output bean '" + mOutputName + "'"); Object emptyObject = mOutputClass.newInstance(); scriptManager.declareBean(mOutputName, emptyObject, mOutputClass); } catch (Exception ex) { Logger.error(ex); throw new ScriptParsingException("Error initialising output bean: "+ex.getMessage()); } } /** * Gets all declared parameters * @return HashMap of String (name), com.c2kernel.scripting.Parameter (param) * @see com.c2kernel.scripting.Parameter */ public HashMap getInputParams() { return mInputParams; } /** * Gets all declared parameters, including those of imported scripts * @return HashMap of String (name), com.c2kernel.scripting.Parameter (param) * @see com.c2kernel.scripting.Parameter */ public HashMap getAllInputParams() { return mAllInputParams; } /** * Submits an input parameter to the script. Must be declared by name and type in the script XML. * * @param name - input parameter name from the script xml * @param value - object to use for this parameter * @throws ParameterException - name not found or wrong type */ public void setInputParamValue(String name, Object value) throws ParameterException { Parameter param = (Parameter) mInputParams.get(name); if (!mAllInputParams.containsKey(name)) throw new ParameterException("Parameter " + name + " not found in parameter list"); if (param != null) { // param is in this script if (value.getClass() != param.getType()) throw new ParameterException( "Parameter " + name + " is wrong type \n" + "Required: " + param.getType().toString() + "\n" + "Supplied: " + value.getClass().toString()); try { scriptManager.declareBean(name, value, param.getType()); Logger.msg(7, "Script.setInputParamValue() - " + name + ": " + value.toString()); param.setInitialised(true); } catch (BSFException ex) { throw new ParameterException("Error initialising parameter '"+name+"' - "+ex.getMessage()); } } // pass param down to child scripts for (Iterator iter = mIncludes.iterator(); iter.hasNext();) { Script importScript = (Script) iter.next(); importScript.setInputParamValue(name, value); } } /** * Executes the script with the submitted parameters. All declared input parametes should have been set first. * * @return The return value depends on the way the output type was declared in the script xml. * * @throws ScriptingEngineException - input parameters weren't set, there was an error executing the script, or the output was invalid */ public Object execute() throws ScriptingEngineException { Object returnValue = null; Object outputValue = null; // check input params StringBuffer missingParams = new StringBuffer(); for (Iterator iter = mInputParams.values().iterator(); iter.hasNext();) { Parameter thisParam = (Parameter) iter.next(); if (!thisParam.getInitialised()) missingParams.append(thisParam.getName()).append("\n"); } // croak if any missing if (missingParams.length() > 0) throw new ScriptingEngineException("Execution aborted, the following declared parameters were not set: \n" + missingParams.toString()); // execute the child scripts for (Iterator iter = mIncludes.iterator(); iter.hasNext();) { Script importScript = (Script) iter.next(); importScript.execute(); } // run the script try { Logger.msg(7, "Script.execute() - Executing script"); scriptManager.setDebug(Logger.doLog(8)); returnValue = scriptManager.eval(mLang, mName, 0, 0, mScript); Logger.msg(8, "Script.execute() - script returned \"" + returnValue + "\""); if (mOutputName != null) { // retrieve the value from the registered output bean outputValue = scriptManager.lookupBean(mOutputName); Logger.msg(8, "Script.execute() - output bean value: \"" + outputValue + "\""); } } catch (Exception ex) { throw new ScriptingEngineException("Error executing script: " + ex.getMessage()); } // if no output class specified, return null if (mOutputClass == null) return null; // if output name not specified, then check the return value against the output class if (mOutputName == null && returnValue != null && !(mOutputClass.isInstance(returnValue))) throw new ScriptingEngineException( "Script return value was not null or instance of " + mOutputClass.getName() + ", it was " + returnValue.getClass().getName()); // return the output, or the return value if output name isn't defined if (mOutputName != null) return outputValue; else return returnValue; } /** * Resets the scripting enviroment, clearing all state and parameters for another execution. */ public void reset() { for (Iterator iter = mInputParams.values().iterator(); iter.hasNext();) ((Parameter) iter.next()).setInitialised(false); scriptManager = new BSFManager(); } }