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

org.scijava.script.ScriptInfo Maven / Gradle / Ivy

Go to download

SciJava Common is a shared library for SciJava software. It provides a plugin framework, with an extensible mechanism for service discovery, backed by its own annotation processor, so that plugins can be loaded dynamically. It is used by downstream projects in the SciJava ecosystem, such as ImageJ and SCIFIO.

There is a newer version: 2.99.0
Show newest version
/*
 * #%L
 * SciJava Common shared library for SciJava software.
 * %%
 * Copyright (C) 2009 - 2016 Board of Regents of the University of
 * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
 * Institute of Molecular Cell Biology and Genetics.
 * %%
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * #L%
 */

package org.scijava.script;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.script.ScriptException;

import org.scijava.Context;
import org.scijava.Contextual;
import org.scijava.ItemIO;
import org.scijava.ItemVisibility;
import org.scijava.NullContextException;
import org.scijava.command.Command;
import org.scijava.convert.ConvertService;
import org.scijava.log.LogService;
import org.scijava.module.AbstractModuleInfo;
import org.scijava.module.DefaultMutableModuleItem;
import org.scijava.module.ModuleException;
import org.scijava.plugin.Parameter;
import org.scijava.util.DigestUtils;
import org.scijava.util.FileUtils;

/**
 * Metadata about a script.
 * 

* This class is responsible for parsing the script for parameters. See * {@link #parseParameters()} for details. *

* * @author Curtis Rueden * @author Johannes Schindelin */ public class ScriptInfo extends AbstractModuleInfo implements Contextual { private static final int PARAM_CHAR_MAX = 640 * 1024; // should be enough ;-) private final String path; private final String script; @Parameter private Context context; @Parameter private LogService log; @Parameter private ScriptService scriptService; @Parameter private ConvertService convertService; /** True iff the return value is explicitly declared as an output. */ private boolean returnValueDeclared; /** * Creates a script metadata object which describes the given script file. * * @param context The SciJava application context to use when populating * service inputs. * @param file The script file. */ public ScriptInfo(final Context context, final File file) { this(context, file.getPath()); } /** * Creates a script metadata object which describes the given script file. * * @param context The SciJava application context to use when populating * service inputs. * @param path Path to the script file. */ public ScriptInfo(final Context context, final String path) { this(context, path, null); } /** * Creates a script metadata object which describes a script provided by the * given {@link Reader}. * * @param context The SciJava application context to use when populating * service inputs. * @param path Pseudo-path to the script file. This file does not actually * need to exist, but rather provides a name for the script with file * extension. * @param reader Reader which provides the script itself (i.e., its contents). */ public ScriptInfo(final Context context, final String path, final Reader reader) { setContext(context); this.path = path; String script = null; if (reader != null) { try { script = getReaderContentsAsString(reader); } catch (final IOException exc) { log.error("Error reading script: " + path, exc); } } this.script = script; } // -- ScriptInfo methods -- /** * Gets the path to the script on disk. *

* If the path doesn't actually exist on disk, then this is a pseudo-path * merely for the purpose of naming the script with a file extension, and the * actual script content is delivered by the {@link BufferedReader} given by * {@link #getReader()}. *

*/ public String getPath() { return path; } /** * Gets a reader which delivers the script's content. *

* This might be null, in which case the content is stored in a file on disk * given by {@link #getPath()}. *

*/ public BufferedReader getReader() { if (script == null) { return null; } return new BufferedReader(new StringReader(script), PARAM_CHAR_MAX); } /** * Parses the script's input and output parameters from the script header. *

* This method is called automatically the first time any parameter accessor * method is called ({@link #getInput}, {@link #getOutput}, {@link #inputs()}, * {@link #outputs()}, etc.). Subsequent calls will reparse the parameters. *

* SciJava's scripting framework supports specifying @{@link Parameter}-style * inputs and outputs in a preamble. The format is a simplified version of the * Java @{@link Parameter} annotation syntax. The following syntaxes are * supported: *

*
    *
  • {@code // @ }
  • *
  • {@code // @(=, ..., =) }
  • *
  • {@code // @ }
  • *
  • * {@code // @(=, ..., =) } *
  • *
*

* Where: *

*
    *
  • {@code //} = the comment style of the scripting language, so that the * parameter line is ignored by the script engine itself.
  • *
  • {@code } = one of {@code INPUT}, {@code OUTPUT}, or * {@code BOTH}.
  • *
  • {@code } = the name of the input or output variable.
  • *
  • {@code } = the Java {@link Class} of the variable.
  • *
  • {@code } = an attribute key.
  • *
  • {@code } = an attribute value.
  • *
*

* See the @{@link Parameter} annotation for a list of valid attributes. *

*

* Here are a few examples: *

*
    *
  • {@code // @Dataset dataset}
  • *
  • {@code // @double(type=OUTPUT) result}
  • *
  • {@code // @BOTH ImageDisplay display}
  • *
  • {@code // @INPUT(persist=false, visibility=INVISIBLE) boolean verbose}
  • * parameters will be parsed and filled just like @{@link Parameter} * -annotated fields in {@link Command}s. *
*/ // NB: Widened visibility from AbstractModuleInfo. @Override public void parseParameters() { clearParameters(); returnValueDeclared = false; try { final BufferedReader in; if (script == null) { in = new BufferedReader(new FileReader(getPath())); } else { in = getReader(); } while (true) { final String line = in.readLine(); if (line == null) break; // NB: Scan for lines containing an '@' with no prior alphameric // characters. This assumes that only non-alphanumeric characters can // be used as comment line markers. if (line.matches("^[^\\w]*@.*")) { final int at = line.indexOf('@'); parseParam(line.substring(at + 1)); } else if (line.matches(".*\\w.*")) break; } in.close(); if (!returnValueDeclared) addReturnValue(); } catch (final IOException exc) { log.error("Error reading script: " + path, exc); } catch (final ScriptException exc) { log.error("Invalid parameter syntax for script: " + path, exc); } } /** Gets whether the return value is explicitly declared as an output. */ public boolean isReturnValueDeclared() { return returnValueDeclared; } // -- ModuleInfo methods -- @Override public String getDelegateClassName() { return ScriptModule.class.getName(); } @Override public Class loadDelegateClass() { return ScriptModule.class; } @Override public ScriptModule createModule() throws ModuleException { return new ScriptModule(this); } // -- Contextual methods -- @Override public Context context() { if (context == null) throw new NullContextException(); return context; } @Override public Context getContext() { return context; } @Override public void setContext(final Context context) { context.inject(this); } // -- Identifiable methods -- @Override public String getIdentifier() { return "script:" + path; } // -- Locatable methods -- @Override public String getLocation() { return new File(path).toURI().normalize().toString(); } @Override public String getVersion() { final File file = new File(path); if (!file.exists()) return null; // no version for non-existent script final Date lastModified = FileUtils.getModifiedTime(file); final String datestamp = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss").format(lastModified); try { final String hash = DigestUtils.bestHex(FileUtils.readFile(file)); return datestamp + "-" + hash; } catch (final IOException exc) { log.error(exc); } return datestamp; } // -- Helper methods -- private void parseParam(final String param) throws ScriptException { final int lParen = param.indexOf("("); final int rParen = param.lastIndexOf(")"); if (rParen < lParen) { throw new ScriptException("Invalid parameter: " + param); } if (lParen < 0) parseParam(param, parseAttrs("")); else { final String cutParam = param.substring(0, lParen) + param.substring(rParen + 1); final String attrs = param.substring(lParen + 1, rParen); parseParam(cutParam, parseAttrs(attrs)); } } private void parseParam(final String param, final HashMap attrs) throws ScriptException { final String[] tokens = param.trim().split("[ \t\n]+"); checkValid(tokens.length >= 1, param); final String typeName, varName; if (isIOType(tokens[0])) { // assume syntax: checkValid(tokens.length >= 3, param); attrs.put("type", tokens[0]); typeName = tokens[1]; varName = tokens[2]; } else { // assume syntax: checkValid(tokens.length >= 2, param); typeName = tokens[0]; varName = tokens[1]; } final Class type = scriptService.lookupClass(typeName); addItem(varName, type, attrs); if (ScriptModule.RETURN_VALUE.equals(varName)) returnValueDeclared = true; } /** Parses a comma-delimited list of {@code key=value} pairs into a map. */ private HashMap parseAttrs(final String attrs) throws ScriptException { // TODO: We probably want to use a real CSV parser. final HashMap attrsMap = new HashMap(); for (final String token : attrs.split(",")) { if (token.isEmpty()) continue; final int equals = token.indexOf("="); if (equals < 0) throw new ScriptException("Invalid attribute: " + token); final String key = token.substring(0, equals).trim(); String value = token.substring(equals + 1).trim(); if (value.startsWith("\"") && value.endsWith("\"")) { value = value.substring(1, value.length() - 1); } attrsMap.put(key, value); } return attrsMap; } private boolean isIOType(final String token) { return convertService.convert(token, ItemIO.class) != null; } private void checkValid(final boolean valid, final String param) throws ScriptException { if (!valid) throw new ScriptException("Invalid parameter: " + param); } /** Adds an output for the value returned by the script itself. */ private void addReturnValue() throws ScriptException { final HashMap attrs = new HashMap(); attrs.put("type", "OUTPUT"); addItem(ScriptModule.RETURN_VALUE, Object.class, attrs); } private void addItem(final String name, final Class type, final Map attrs) throws ScriptException { final DefaultMutableModuleItem item = new DefaultMutableModuleItem(this, name, type); for (final String key : attrs.keySet()) { final String value = attrs.get(key); assignAttribute(item, key, value); } if (item.isInput()) registerInput(item); if (item.isOutput()) registerOutput(item); } private void assignAttribute(final DefaultMutableModuleItem item, final String key, final String value) throws ScriptException { // CTR: There must be an easier way to do this. // Just compile the thing using javac? Or parse via javascript, maybe? if ("callback".equalsIgnoreCase(key)) { item.setCallback(value); } else if ("choices".equalsIgnoreCase(key)) { // FIXME: Regex above won't handle {a,b,c} syntax. // item.setChoices(choices); } else if ("columns".equalsIgnoreCase(key)) { item.setColumnCount(convertService.convert(value, int.class)); } else if ("description".equalsIgnoreCase(key)) { item.setDescription(value); } else if ("initializer".equalsIgnoreCase(key)) { item.setInitializer(value); } else if ("type".equalsIgnoreCase(key)) { item.setIOType(convertService.convert(value, ItemIO.class)); } else if ("label".equalsIgnoreCase(key)) { item.setLabel(value); } else if ("max".equalsIgnoreCase(key)) { item.setMaximumValue(convertService.convert(value, item.getType())); } else if ("min".equalsIgnoreCase(key)) { item.setMinimumValue(convertService.convert(value, item.getType())); } else if ("name".equalsIgnoreCase(key)) { item.setName(value); } else if ("persist".equalsIgnoreCase(key)) { item.setPersisted(convertService.convert(value, boolean.class)); } else if ("persistKey".equalsIgnoreCase(key)) { item.setPersistKey(value); } else if ("required".equalsIgnoreCase(key)) { item.setRequired(convertService.convert(value, boolean.class)); } else if ("softMax".equalsIgnoreCase(key)) { item.setSoftMaximum(convertService.convert(value, item.getType())); } else if ("softMin".equalsIgnoreCase(key)) { item.setSoftMinimum(convertService.convert(value, item.getType())); } else if ("stepSize".equalsIgnoreCase(key)) { try { final double stepSize = Double.parseDouble(value); item.setStepSize(stepSize); } catch (final NumberFormatException exc) { log.warn("Script parameter " + item.getName() + " has an invalid stepSize: " + value); } } else if ("style".equalsIgnoreCase(key)) { item.setWidgetStyle(value); } else if ("visibility".equalsIgnoreCase(key)) { item.setVisibility(convertService.convert(value, ItemVisibility.class)); } else if ("value".equalsIgnoreCase(key)) { item.setDefaultValue(convertService.convert(value, item.getType())); } else { throw new ScriptException("Invalid attribute name: " + key); } } /** * Read entire contents of a Reader and return as String. * * @param reader {@link Reader} whose contents should be returned as String. * Expected to never be null. * @return contents of reader as String. * @throws IOException If an I/O error occurs * @throws NullPointerException If reader is null */ private static String getReaderContentsAsString(final Reader reader) throws IOException, NullPointerException { final char[] buffer = new char[8192]; final StringBuilder builder = new StringBuilder(); int read; while ((read = reader.read(buffer)) != -1) { builder.append(buffer, 0, read); } return builder.toString(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy