package org.freeplane.plugin.script; import groovy.lang.Binding; import groovy.lang.MetaClass; import groovy.lang.MissingMethodException; import groovy.lang.MissingPropertyException; import groovy.lang.Script; import java.net.URI; import java.util.Date; import java.util.Map; import java.util.Properties; import java.util.ResourceBundle; import java.util.regex.Pattern; import org.codehaus.groovy.runtime.DefaultGroovyMethods; import org.codehaus.groovy.runtime.InvokerHelper; import org.freeplane.core.resources.ResourceController; import org.freeplane.core.ui.components.UITools; import org.freeplane.core.util.FreeplaneVersion; import org.freeplane.core.util.HtmlUtils; import org.freeplane.core.util.LogUtils; import org.freeplane.core.util.MenuUtils; import org.freeplane.core.util.TextUtils; import org.freeplane.features.format.FormatController; import org.freeplane.features.format.IFormattedObject; import org.freeplane.features.format.ScannerController; import org.freeplane.features.link.LinkController; import org.freeplane.plugin.script.proxy.Convertible; import org.freeplane.plugin.script.proxy.Proxy; /** All methods of this class are available as "global" methods in every script. * Only documented methods are meant to be used in scripts. * <p>The following global objects are provided as shortcuts by the binding of this class: * <ul> * <li><b>ui:</b> see {@link UITools}</li> * <li><b>logger:</b> see {@link LogUtils}</li> * <li><b>htmlUtils:</b> see {@link HtmlUtils}</li> * <li><b>textUtils:</b> see {@link TextUtils}</li> * <li><b>menuUtils:</b> see {@link MenuUtils}</li> * <li><b>config:</b> see {@link ConfigProperties}</li> * </ul> * The following classes may also be useful in scripting: * <ul> * <li>{@link FreeplaneVersion}</li> * </ul> */ public abstract class FreeplaneScriptBaseClass extends Script { /** * Accessor for Freeplane's configuration: In scripts available * as "global variable" <code>config</code>. */ public static class ConfigProperties { public boolean getBooleanProperty(final String name) { return ResourceController.getResourceController().getBooleanProperty(name); } public double getDoubleProperty(final String name, final double defaultValue) { return ResourceController.getResourceController().getDoubleProperty(name, defaultValue); } public int getIntProperty(final String name) { return ResourceController.getResourceController().getIntProperty(name); } public int getIntProperty(final String name, final int defaultValue) { return ResourceController.getResourceController().getIntProperty(name, defaultValue); } public long getLongProperty(final String name, final int defaultValue) { return ResourceController.getResourceController().getLongProperty(name, defaultValue); } public String getProperty(final String name) { return ResourceController.getResourceController().getProperty(name); } public String getProperty(final String name, final String defaultValue) { return ResourceController.getResourceController().getProperty(name, defaultValue); } public Properties getProperties() { return ResourceController.getResourceController().getProperties(); } /** support config['key'] from Groovy. */ public String getAt(final String name) { return getProperty(name); } public ResourceBundle getResources() { return ResourceController.getResourceController().getResources(); } public String getFreeplaneUserDirectory() { return ResourceController.getResourceController().getFreeplaneUserDirectory(); } } private final Pattern nodeIdPattern = Pattern.compile("ID_\\d+"); private final MetaClass nodeMetaClass; private Map<Object, Object> boundVariables; private Proxy.NodeRO node; private Proxy.ControllerRO controller; public FreeplaneScriptBaseClass() { super(); nodeMetaClass = InvokerHelper.getMetaClass(Proxy.NodeRO.class); // Groovy rocks! DefaultGroovyMethods.mixin(Number.class, NodeArithmeticsCategory.class); initBinding(); } @SuppressWarnings("unchecked") public void initBinding() { boundVariables = super.getBinding().getVariables(); // this is important: we need this reference no matter if "node" is overridden later by the user node = (Proxy.NodeRO) boundVariables.get("node"); controller = (Proxy.ControllerRO) boundVariables.get("c"); } @Override public void setBinding(Binding binding) { super.setBinding(addStaticBindings(binding)); initBinding(); } private Binding addStaticBindings(Binding binding) { binding.setProperty("logger", new LogUtils()); binding.setProperty("ui", new UITools()); binding.setProperty("htmlUtils", HtmlUtils.getInstance()); binding.setProperty("textUtils", new TextUtils()); binding.setProperty("menuUtils", new MenuUtils()); binding.setProperty("config", new ConfigProperties()); return binding; } /* <ul> * <li> translate raw node ids to nodes. * <li> "imports" node's methods into the script's namespace * </ul> */ public Object getProperty(String property) { // shortcuts for the most usual cases if (property.equals("node")) { return node; } if (property.equals("c")) { return controller; } if (nodeIdPattern.matcher(property).matches()) { return N(property); } else { final Object boundValue = boundVariables.get(property); if (boundValue != null) { return boundValue; } else { try { return nodeMetaClass.getProperty(node, property); } catch (MissingPropertyException e) { return super.getProperty(property); } } } } /* * extends super class version by node instance methods. */ public Object invokeMethod(String methodName, Object args) { try { return super.invokeMethod(methodName, args); } catch (MissingMethodException mme) { try { return nodeMetaClass.invokeMethod(node, methodName, args); } catch (MissingMethodException e) { throw e; } } } /** Shortcut for node.map.node(id) - necessary for ids to other maps. */ public Proxy.NodeRO N(String id) { final Proxy.NodeRO node = (Proxy.NodeRO) getBinding().getVariable("node"); return node.getMap().node(id); } /** Shortcut for node.map.node(id).text. */ public String T(String id) { final Proxy.NodeRO n = N(id); return n == null ? null : n.getText(); } /** Shortcut for node.map.node(id).value. */ public Object V(String id) { final Proxy.NodeRO n = N(id); try { return n == null ? null : n.getValue(); } catch (ExecuteScriptException e) { return null; } } /** returns valueIfNull if value is null and value otherwise. */ public Object ifNull(Object value, Object valueIfNull) { return value == null ? valueIfNull : value; } /** rounds a number to integral type. */ public Long round(final Double d) { if (d == null) return null; return Math.round(d); } /** round to the given number of decimal places: <code>round(0.1234, 2) -> 0.12</code> */ public Double round(final Double d, final int precision) { if (d == null) return d; double factor = 1; for (int i = 0; i < precision; i++) { factor *= 10.; } return Math.round(d * factor) / factor; } /** parses text to the proper data type, if possible, setting format to the standard. Parsing is configured via * config file scanner.xml * <pre> * assert parse('2012-11-30') instanceof Date * assert parse('1.22') instanceof Number * // if parsing fails the original string is returned * assert parse('2012XX11-30') == '2012XX11-30' * * def d = parse('2012-10-30') * c.statusInfo = "${d} is ${new Date() - d} days ago" * </pre> */ public Object parse(final String text) { return ScannerController.getController().parse(text); } /** uses formatString to return a FormattedObject. * <p><em>Note:</em> If you want to format the node core better use the format node attribute instead: * <pre> * node.object = new Date() * node.format = 'dd/MM/yy' * </pre> * @return {@link IFormattedObject} if object is formattable and the unchanged object otherwise. */ public Object format(final Object object, final String formatString) { return FormatController.format(object, formatString); } /** Applies default date-time format for dates or default number format for numbers. All other objects are left unchanged. * @return {@link IFormattedObject} if object is formattable and the unchanged object otherwise. */ public Object format(final Object object) { return FormatController.formatUsingDefault(object); } /** Applies default date format (instead of standard date-time) format on the given date. * @return {@link IFormattedObject} if object is formattable and the unchanged object otherwise. */ public Object formatDate(final Date date) { final String format = FormatController.getController().getDefaultDateFormat().toPattern(); return FormatController.format(date, format); } /** formats according to the internal standard, that is the conversion will be reversible * for types that are handled special by the scripting api namely Dates and Numbers. * @see Convertible#toString(Object) */ public String toString(final Object o) { return Convertible.toString(o); } /** opens a {@link URI} */ public void loadUri(final URI uri) { LinkController.getController().loadURI(uri); } // /** Shortcut for new {@link org.freeplane.plugin.script.proxy.Convertible}. */ // public Convertible convertible(String string) { // return new Convertible(FormulaUtils.eval string, node.get); // } }