/* * Copyright (c) 2010, The University of Sheffield. * * This file is part of the GATE/Groovy integration layer, and is free software, * released under the terms of the GNU Lesser General Public Licence, version * 2.1 (or any later version). A copy of this licence is provided in the file * LICENCE in the distribution. * * Groovy is developed by The Codehaus, details are available from * http://groovy.codehaus.org */ package gate.groovy; import gate.AnnotationSet; import gate.Controller; import gate.Corpus; import gate.CorpusController; import gate.FeatureMap; import gate.Gate; import gate.Factory; import gate.ProcessingResource; import gate.Resource; import gate.Document; import gate.creole.AbstractLanguageAnalyser; import gate.creole.ControllerAwarePR; import gate.creole.ExecutionException; import gate.creole.ResourceInstantiationException; import gate.creole.metadata.CreoleParameter; import gate.creole.metadata.CreoleResource; import gate.creole.metadata.Optional; import gate.creole.metadata.RunTime; import gate.util.BomStrippingInputStreamReader; import gate.util.GateClassLoader; import groovy.lang.Binding; import groovy.lang.GroovyShell; import groovy.lang.MetaMethod; import groovy.lang.Script; import java.io.IOException; import java.io.Reader; import java.net.URL; import java.util.List; import org.codehaus.groovy.control.CompilationFailedException; import org.codehaus.groovy.runtime.InvokerInvocationException; /** * Groovy Script PR. * * @author Angus Roberts, Ian Roberts */ @CreoleResource(name = "Groovy scripting PR", comment = "Runs a Groovy script as a processing resource", helpURL = "http://gate.ac.uk/userguide/sec:api:groovy", icon = "/gate/groovy/script-pr") public class ScriptPR extends AbstractLanguageAnalyser implements ProcessingResource, ControllerAwarePR { private static final long serialVersionUID = 7563063357390091727L; /** * Groovy script file */ private URL scriptURL; /** * Parameters passed to the Groovy script */ private FeatureMap scriptParams; /** * The compiled Groovy script. */ private Script groovyScript; /** * the source of the loaded groovy script for use in the VR */ private String groovySrc; /** * The character encoding of the script file. */ private String encoding; /** * Name of the output annotation set */ private String outputASName; /** * Name of the input annotation set */ private String inputASName; private GateClassLoader classloader = null; /** Initialise this resource, and return it. */ public Resource init() throws ResourceInstantiationException { if(scriptURL == null) throw new ResourceInstantiationException( "You must specify a Groovy script to load"); //if we are being re-initialized then forget the class loader we used last if (classloader != null) Gate.getClassLoader().forgetClassLoader(classloader); //create a disposable classloader for the groovy shell to use as its parent classloader = Gate.getClassLoader().getDisposableClassLoader(scriptURL.toExternalForm()+System.currentTimeMillis(), ScriptPR.class.getClassLoader(), true); // Create the shell, with the GateClassLoader as its parent (so the script // will have access to plugin classes) GroovyShell groovyShell = new GroovyShell(classloader); StringBuilder scriptText = new StringBuilder(); char[] buf = new char[4096]; int charsRead = 0; try { Reader reader = new BomStrippingInputStreamReader(scriptURL.openStream(), encoding); while((charsRead = reader.read(buf)) >= 0) { scriptText.append(buf, 0, charsRead); } reader.close(); groovySrc = scriptText.toString(); // append a load of standard imports to the end of the script. We put them // at the end rather than the beginning because (in a script) imports work // anywhere, and putting them at the end means we don't mess up line // numbers in any compilation error messages. scriptText.append("\n\n\n"); scriptText.append(GroovySupport.STANDARD_IMPORTS); // determine the file name of the script String scriptFileName = scriptURL.toString(); scriptFileName = scriptFileName.substring(scriptFileName.lastIndexOf('/')); groovyScript = groovyShell.parse(scriptText.toString(), scriptFileName); } catch(IOException ioe) { throw new ResourceInstantiationException("Error loading Groovy script " + "from URL " + scriptURL, ioe); } catch(CompilationFailedException e) { throw new ResourceInstantiationException("Script compilation failed", e); } finally { fireProcessFinished(); } return this; } public void reInit() throws ResourceInstantiationException { init(); } @Override public void cleanup() { //do the normal clean up first super.cleanup(); //make sure we clean up properly before being destroyed if (classloader != null) Gate.getClassLoader().forgetClassLoader(classloader); } // ControllerAwarePR implementation public void controllerExecutionStarted(Controller c) throws ExecutionException { // ensure scriptParams are available to the callback if(scriptParams == null) { scriptParams = Factory.newFeatureMap(); } groovyScript.getBinding().setVariable("scriptParams", scriptParams); groovyScript.getBinding().setVariable("controller",c); callControllerAwareMethod("beforeCorpus", c); } public void controllerExecutionFinished(Controller c) throws ExecutionException { callControllerAwareMethod("afterCorpus", c); } public void controllerExecutionAborted(Controller c, Throwable t) throws ExecutionException { callControllerAwareMethod("aborted", c); } /** * Check whether the script declares a method with the given name that takes a * corpus parameter, and if so, call it passing the corpus from the given * controller. If the controller is not a CorpusController, do nothing. * * @throws ExecutionException * if the script method throws an ExecutionException we re-throw it */ protected void callControllerAwareMethod(String methodName, Controller c) throws ExecutionException { if(!(c instanceof CorpusController)) { return; } List<MetaMethod> metaMethods = groovyScript.getMetaClass().respondsTo(groovyScript, methodName, new Class[]{gate.Corpus.class}); if(!metaMethods.isEmpty()) { try { metaMethods.get(0).invoke(groovyScript, new Corpus[]{((CorpusController)c).getCorpus()}); } catch(InvokerInvocationException iie) { if(iie.getCause() instanceof ExecutionException) { throw (ExecutionException)iie.getCause(); } else if(iie.getCause() instanceof RuntimeException) { throw (RuntimeException)iie.getCause(); } else if(iie.getCause() instanceof Error) { throw (Error)iie.getCause(); } else { throw iie; } } } } /** * Execute method. Runs the groovy script, first passing a set of bindings * including the document, the input AnnotationSet and the output * AnnotationSet */ public void execute() throws ExecutionException { AnnotationSet outputAS = null; AnnotationSet inputAS = null; if(document != null) { if(outputASName == null || outputASName.trim().length() == 0) outputAS = document.getAnnotations(); else outputAS = document.getAnnotations(outputASName); if(inputASName == null || inputASName.trim().length() == 0) inputAS = document.getAnnotations(); else inputAS = document.getAnnotations(inputASName); fireStatusChanged("Groovy script PR running on " + document.getSourceUrl()); } else { fireStatusChanged("Groovy script PR running"); } fireProgressChanged(0); Binding binding = groovyScript.getBinding(); try { // Create the variable bindings binding.setVariable("doc", document); binding.setVariable("corpus", corpus); if(document != null) { binding.setVariable("content", document.getContent().toString()); } else { binding.setVariable("content", null); } binding.setVariable("inputAS", inputAS); binding.setVariable("outputAS", outputAS); // these should be deprecated, really, they're no longer necessary with the // imports binding.setVariable("gate", Gate.class); binding.setVariable("factory", gate.Factory.class); // The FeatureMap is passed in its entirety, making the keys available in // a bean-like way. So in a map with k=v, the script can say // assert scriptParams.k == v binding.setVariable("scriptParams", scriptParams); // Run the script engine groovyScript.run(); } catch(RuntimeException re) { throw new ExecutionException("Problem running Groovy script", re); } finally { binding.setVariable("doc", null); binding.setVariable("corpus", null); binding.setVariable("content", null); binding.setVariable("inputAS", null); binding.setVariable("outputAS", null); // TODO not sure if we should nullify scriptParams as well, but I'm // thinking it's fairly safe not to as they shouldn't be document/corpus // based } // We've done fireProgressChanged(100); fireProcessFinished(); fireStatusChanged("Groovy script PR finished"); } /** * gets name of the output annotation set * * @return */ public String getOutputASName() { return outputASName; } /** * sets name of the output annotaiton set * * @param outputAS */ @Optional @RunTime @CreoleParameter public void setOutputASName(String outputAS) { this.outputASName = outputAS; } /** * gets name of the input annotation set * * @return */ public String getInputASName() { return inputASName; } /** * sets name of the input annotaiton set * * @param inputAS */ @Optional @RunTime @CreoleParameter public void setInputASName(String inputAS) { this.inputASName = inputAS; } /** * gets URL of the Groovy script * * @return */ public URL getScriptURL() { return scriptURL; } /** * sets File of the Groovy script * * @param script */ @CreoleParameter(comment = "Location of the Groovy script that will be " + "run for each document") public void setScriptURL(URL script) { this.scriptURL = script; } /** * Get the character encoding used to load the script. */ public String getEncoding() { return encoding; } /** * Set the character encoding used to load the script. */ @CreoleParameter(defaultValue = "UTF-8", comment = "Character encoding used " + "to read the script") public void setEncoding(String encoding) { this.encoding = encoding; } /** * Get Map of parameters for the Groovy script * * @return */ public FeatureMap getScriptParams() { return scriptParams; } /** * Set Map of parameters for the Groovy script * * @return */ @Optional @RunTime @CreoleParameter(comment = "Optional additional parameters to pass to the " + "script.") public void setScriptParams(FeatureMap params) { this.scriptParams = params; } /** * Return the source of the loaded groovy script * * @return the source of the loaded groovy script */ public String getGroovySrc() { return groovySrc; } @Override @Optional @RunTime @CreoleParameter(comment = "The document to process") public void setDocument(Document doc) { document = doc; } }