Difference between revisions of "Scripting API (Preview)"

From Freeplane - free mind mapping and knowledge management software
m (Add Categories)
Line 1,040: Line 1,040:
 
}
 
}
 
</groovy>
 
</groovy>
 +
 +
[[Category:Scripting]][[Category:Advanced_Users]]

Revision as of 22:30, 19 October 2010

This page describes a development version which will only work with preview/alpha/beta/test versions of Freeplane. If you are seeking for the stable released version see Scripting API.

Overview over the changes

The development is currently driven by the implementation of the Formula feature that provides mindmappers with features you know from spreadsheet processors like Excel. This implies

  • The syntax should allow more concise statements. For instance to convert a node text you can now write to.num instead of Double.parse(node.text). Attributes are available as node['name'] in addition to the old node.attributes.get('name').
  • The API should provide the additional functionality that is needed for formulas.
  • The API has to be re-organized to provide a read-only API for formulas.

A very important usability improvement, not only for formula writers, is the introduction of the class Convertible that is returned now by some new methods/properties:

  • node.to (or node.getTo())
  • node['attr_name'] (or node.getAt('attr_name'))
  • node.note (or node.getNote())

Many "setters" on the other hand, like node.setText() have been extended to accept not only Strings but Objects. Much effort was spent to ensure that this conversion matches the conversions that Convertible performs for Strings. For example node.text = new Date() is converted to 2010-10-05T22:11:03.243+0000 which Convertible knows how to convert back to date (try node.to.date).

For formulas it's important that the formulas itself don't change the state of the map. Currently only the first step is made: All subinterfaces Xyz have a base interface XyzRO that includes only the methods that are suitable for formulas. The Proxy implementations implement the full interfaces currently and the constraint is not enforced.

Some controller methods were introduced mainly for testing:

  • Controller.newMap()
  • Controller.undo()
  • Controller.redo()

Example Maps

Entry Points

Each script is given two variables:

<groovy> final Proxy.Node node; final Proxy.Controller c; </groovy>

New: Methods and properties of the current node are directly available so you can write children.size() instead of node.children.size().

Interface

<groovy> package org.freeplane.plugin.script.proxy;

import groovy.lang.Closure;

import java.awt.Color; import java.io.File; import java.net.URL; import java.util.Collection; import java.util.Date; import java.util.List;

import javax.swing.Icon;

import org.freeplane.core.util.FreeplaneIconUtils; import org.freeplane.core.util.FreeplaneVersion; import org.freeplane.features.common.edge.EdgeStyle; import org.freeplane.features.common.filter.condition.ICondition; import org.freeplane.features.common.link.ArrowType; import org.freeplane.features.common.styles.IStyle;

public interface Proxy {

   interface AttributesRO {
   	/** alias for getFirst(int).
   	 * @deprecated before 1.1 - use get(int), getFirst(int) or getAll(String) instead. */
   	@Deprecated
   	String get(final String name);
   	/** returns the first value of an attribute with the given name or null otherwise. @since 1.2 */
   	String getFirst(final String name);
   	/** returns all values for the attribute name. */
   	List<String> getAll(final String name);
   	/** returns all attribute names in the proper sequence. The number of names returned
   	 * is equal to the number of attributes.

*

    	 *   // rename attribute
    	 *   int i = 0;
    	 *   for (String name : attributes.getAttributeNames()) {
    	 *       if (name.equals("xy"))
    	 *           attributes.set(i, "xyz", attributes.get(i));
    	 *       ++i;
    	 *   }
    	 * 

*/

   	List<String> getAttributeNames();
   	/** returns the attribute value at the given index.
   	 * @throws IndexOutOfBoundsException if index is out of range (index
   	 *         < 0 || index >= size()).*/
   	String get(final int index);
   	/** @deprecated since 1.2 - use findFirst(String) instead. */
   	int findAttribute(final String name);
   	/** returns the index of the first attribute with the given name if one exists or -1 otherwise.
   	 * For searches for all attributes with a given name getAttributeNames()
   	 * must be used. @since 1.2*/
   	int findFirst(final String name);
   	
   	/** the number of attributes. It is size() == getAttributeNames().size(). */
   	int size();
   }
   /** Attributes are name - value pairs assigned to a node. A node may have multiple attributes
    * with the same name. */
   interface Attributes extends AttributesRO {
   	/** sets the value of the attribute at an index. This method will not create new attributes.
   	 * @throws IndexOutOfBoundsException if index is out of range (index
   	 *         < 0 || index >= size()). */
   	void set(final int index, final String value);
   	/** sets name and value of the attribute at the given index. This method will not create new attributes.
   	 * @throws IndexOutOfBoundsException if index is out of range (index
   	 *         < 0 || index >= size()). */
   	void set(final int index, final String name, final String value);
   	/** removes the first attribute with this name.
   	 * @returns true on removal of an existing attribute and false otherwise.
   	 * @deprecated before 1.1 - use remove(int) or removeAll(String) instead. */
   	@Deprecated
   	boolean remove(final String name);
   	/** removes all attributes with this name.
   	 * @returns true on removal of an existing attribute and false otherwise. */
   	boolean removeAll(final String name);
   	/** removes the attribute at the given index.
   	 * @throws IndexOutOfBoundsException if index is out of range (index
   	 *         < 0 || index >= size()). */
   	void remove(final int index);
   	/** adds an attribute if there is no attribute with the given name or changes
   	 * the value of the first attribute with the given name. */
   	void set(final String name, final String value);
   	/** adds an attribute no matter if an attribute with the given name already exists. */
   	void add(final String name, final String value);
   	/** removes all attributes. @since 1.2 */
   	void clear();
   }
   interface ConnectorRO {
   	Color getColor();
   	ArrowType getEndArrow();
   	String getMiddleLabel();
   	Node getSource();
   	String getSourceLabel();
   	ArrowType getStartArrow();
   	Node getTarget();
   	String getTargetLabel();
   	boolean simulatesEdge();
   }
   interface Connector extends ConnectorRO {
   	void setColor(Color color);
   	void setEndArrow(ArrowType arrowType);
   	void setMiddleLabel(String label);
   	void setSimulatesEdge(boolean simulatesEdge);
   	void setSourceLabel(String label);
   	void setStartArrow(ArrowType arrowType);
   	void setTargetLabel(String label);
   }
   interface ControllerRO {
   	/** if multiple nodes are selected returns one (arbitrarily chosen)
   	 * selected node or the selected node for a single node selection. */
   	Node getSelected();
   	List<Node> getSelecteds();
   	/** returns List<Node> of Node objects sorted on Y
   	 *
   	 * @param differentSubtrees if true
   	 *   children/grandchildren/grandgrandchildren/... nodes of selected
   	 *   parent nodes are excluded from the result. */
   	List<Node> getSortedSelection(boolean differentSubtrees);
   	/**
   	 * returns Freeplane version.
   	 * Use it like this:

*

    	 *   import org.freeplane.core.util.FreeplaneVersion
    	 *   import org.freeplane.core.ui.components.UITools
    	 * 
    	 *   def required = FreeplaneVersion.getVersion("1.1.2");
    	 *   if (c.freeplaneVersion < required)
    	 *       UITools.errorMessage("Freeplane version " + c.freeplaneVersion
    	 *           + " not supported - update to at least " + required);
    	 * 
   	 */
   	FreeplaneVersion getFreeplaneVersion();
   	/** Starting from the root node, recursively searches for nodes for which
   	 * condition.checkNode(node) returns true.
   	 * @see Node.find(ICondition) for searches on subtrees
   	 * @deprecated since 1.2 use find(Closure) instead. */
   	List<Node> find(ICondition condition);
   	/**
   	 * Starting from the root node, recursively searches for nodes for which closure.call(node)
   	 * returns true.

*

* A find method that uses a Groovy closure ("block") for simple custom searches. As this closure * will be called with a node as an argument (to be referenced by it) the search can * evaluate every node property, like attributes, icons, node text or notes. *

* Examples: *

    	 *    def nodesWithNotes = c.find{ it.noteText != null }
    	 *    
    	 *    def matchingNodes = c.find{ it.text.matches(".*\\d.*") }
    	 *    def texts = matchingNodes.collect{ it.text }
    	 *    print "node texts containing numbers:\n " + texts.join("\n ")
    	 * 
   	 * @param closure a Groovy closure that returns a boolean value. The closure will receive
   	 *        a NodeModel as an argument which can be tested for a match.
   	 * @return all nodes for which closure.call(NodeModel) returns true.
   	 * @see Node.find(Closure) for searches on subtrees
   	 */
   	List<Node> find(Closure closure);
   }
   interface Controller extends ControllerRO {
   	void centerOnNode(Node center);
   	void select(Node toSelect);
   	/** selects branchRoot and all children */
   	void selectBranch(Node branchRoot);
   	/** toSelect is a List<Node> of Node objects */
   	void selectMultipleNodes(List<Node> toSelect);
   	/** reset undo / redo lists and deactivate Undo for current script */
   	void deactivateUndo();
   	/** invokes undo once - for testing purposes mainly. @since 1.2 */
   	void undo();
   	/** invokes redo once - for testing purposes mainly. @since 1.2 */
   	void redo();
   	/** The main info for the status line with key="standard", use null to remove. Removes icon if there is one. */
   	void setStatusInfo(String info);
   	/** Info for status line, null to remove. Removes icon if there is one.
   	 * @see setStatusInfo(String, String, String) */
   	void setStatusInfo(String infoPanelKey, String info);
   	/** Info for status line - text and icon - null stands for "remove" (text or icon)
   	 * @param infoPanelKey "standard" is the left most standard info panel. If a panel with
   	 *        this name doesn't exist it will be created.
   	 * @param info Info text
   	 * @param iconKey key as those that are used for nodes (see {@link Icons#addIcon(String)}).

*

    	 *   println("all available icon keys: " + FreeplaneIconUtils.listStandardIconKeys())
    	 *   c.setStatusInfo("standard", "hi there!", "button_ok");
    	 * 
   	 * @see FreeplaneIconUtils
   	 * @since 1.2 */
   	void setStatusInfo(String infoPanelKey, String info, String iconKey);
   	
   	/** @deprecated since 1.2 - use setStatusInfo(String, String, String) */
   	void setStatusInfo(String infoPanelKey, Icon icon);
   	/** opens a new map with a default name in the foreground. @since 1.2 */
   	Map newMap();
   	/** opens a new map for url in the foreground if it isn't opened already. @since 1.2 */
   	Map newMap(URL url);
   }
   interface EdgeRO {
   	Color getColor();
   	EdgeStyle getType();
   	int getWidth();
   }
   interface Edge extends EdgeRO {
   	void setColor(Color color);
   	void setType(EdgeStyle type);
   	/** can be -1 for default, 0 for thin, >0 */
   	void setWidth(int width);
   }
   interface ExternalObjectRO {
   	/** empty string means that there's no external object */
   	String getURI();
   	float getZoom();
   }
   interface ExternalObject extends ExternalObjectRO {
   	/** setting empty String uri means remove external object (as for Links); */
   	void setURI(String uri);
   	void setZoom(float zoom);
   }
   interface FontRO {
   	String getName();
   	int getSize();
   	boolean isBold();
   	boolean isBoldSet();
   	boolean isItalic();
   	boolean isItalicSet();
   	boolean isNameSet();
   	boolean isSizeSet();
   }
   interface Font extends FontRO {
   	void resetBold();
   	void resetItalic();
   	void resetName();
   	void resetSize();
   	void setBold(boolean bold);
   	void setItalic(boolean italic);
   	void setName(String name);
   	void setSize(int size);
   }
   interface IconsRO {
   	/** returns List<Node> of Strings (corresponding to iconID above);
   	 * iconID is one of "Idea","Question","Important", etc. */
   	List<String> getIcons();
   }
   interface Icons extends IconsRO {
   	/**
   	 * adds an icon to a node if an icon for the given key can be found. The same icon can be added multiple
   	 * times.

*

    	 *   println("all available icon keys: " + FreeplaneIconUtils.listStandardIconKeys())
    	 *   node.icons.addIcon("button_ok")
    	 * 
   	 * @see FreeplaneIconUtils */
   	void addIcon(String name);
   	/** deletes first occurence of icon with the given name, returns true if
   	 * success (icon existed); */
   	boolean removeIcon(String name);
   }
   interface LinkRO {
   	String get();
   }
   interface Link extends LinkRO {
   	/** target is a URI.
   	 * An empty String will remove the link.
   	 * To get a local link (i.e. to another node) target should be: "#" + nodeId */
   	boolean set(String target);
   }
   interface MapRO {
   	/** @since 1.2 */
   	Node getRoot();
   	/** @deprecated since 1.2 - use getRoot() instead. */
   	Node getRootNode();
   	/** returns the node if the map contains it or null otherwise. */
   	Node node(String id);
   	/** returns the physical location of the map if available or null otherwise. */
   	File getFile();
   	/** returns the title of the MapView. @since 1.2 */
   	String getName();
   }
   interface Map extends MapRO {
   	/**
   	 * closes a map. Note that there is no undo for this method.
   	 * @param close map even if there are unsaved changes.
   	 * @param allowInteraction if (allowInteraction && ! force) a saveAs dialog will be opened if there are
   	 *        unsaved changes.
   	 * @return false if the saveAs was cancelled by the user and true otherwise.
   	 * @throws RuntimeException if the map contains changes and parameter force is false.
   	 * @since 1.2
   	 */
   	boolean close(boolean force, boolean allowInteraction);
   	/**
   	 * saves the map to disk. Note that there is no undo for this method.
   	 * @param allowInteraction if a saveAs dialog should be opened if the map has no assigned URL so far.
   	 * @return false if the saveAs was cancelled by the user and true otherwise.
   	 * @throws RuntimeException if the map has no assigned URL and parameter allowInteraction is false.
   	 * @since 1.2
   	 */
   	boolean save(boolean allowInteraction);
   }
   interface NodeRO {
   	Attributes getAttributes();
   	/** allows to access attribute values like array elements. Note that the returned type is a
   	 * {@link Convertible}, not a String. Nevertheless it behaves like a String in almost all respects,
   	 * that is, in Groovy scripts it understands all String methods like lenght(), matches() etc.

*

    	 *   // standard way
    	 *   node.attributes.set("attribute name", "12")
    	 *   // implicitely use getAt()
    	 *   def val = node["attribute name"]
    	 *   // use all conversions that Convertible provides (num, date, string, ...)
    	 *   assert val.num == new Long(12)
    	 *   // or use it just like a string
    	 *   assert val.startsWith("1")
    	 * 
   	 * @since 1.2
   	 */
   	Convertible getAt(String attributeName);
   	/** returns the index (0..) of this node in the (by Y coordinate sorted)
   	 * list of this node's children. Returns -1 if childNode is not a child
   	 * of this node. */
   	int getChildPosition(Node childNode);
   	/** returns the children of this node ordered by Y coordinate. */
   	List<Node> getChildren();
   	Collection<Connector> getConnectorsIn();
   	Collection<Connector> getConnectorsOut();
   	ExternalObject getExternalObject();
   	Icons getIcons();
   	Link getLink();
   	/** the map this node belongs to. */
   	Map getMap();
   	/** @deprecated since 1.2 - use Node.getId() instead. */
   	String getNodeID();
   	/** @since 1.2 */
   	String getId();
   	/** if countHidden is false then only nodes that are matched by the
   	 * current filter are counted. */
   	int getNodeLevel(boolean countHidden);
   	/**
   	 * Returns a Convertible object for the plain not text. Convertibles behave like Strings in most respects.
   	 * Additionally String methods are overridden to handle Convertible arguments as if the argument were the
   	 * result of Convertible.getText().
   	 * @return Convertible getString(), getText() and toString() will return plain text instead of the HTML.
   	 *         Use getNoteText() to get the HTML text.
   	 * @since 1.2
   	 */
   	Convertible getNote();
   	
   	/** Returns the HTML text of the node. (Notes always contain HTML text.) */
   	String getNoteText();
   	/** @since 1.2 */
   	Node getParent();
   	/** @deprecated since 1.2 - use getParent() instead. */
   	Node getParentNode();
   	NodeStyle getStyle();
   	/** use this method to remove all tags from an HTML node. @since 1.2 */
   	String getPlainText();
   	/** use this method to remove all tags from an HTML node.
   	 * @deprecated since 1.2 - use getPlainText() or getTo().getPlain() instead. */
   	String getPlainTextContent();
   	String getText();
   	/**
   	 * returns an object that performs conversions (method name is choosen to give descriptive code):

*

*
node.to.num
Long or Double, see {@link Convertible#getDate()}. *
node.to.date
Date, see {@link Convertible#getDate()}. *
node.to.string
Text, see {@link Convertible#getString()}. *
node.to.text
an alias for getString(), see {@link Convertible#getText()}. *
node.to.object
returns what fits best, see {@link Convertible#getObject()}. *
   	 * Note that parse errors result in {@link ConversionException}s.
   	 * @return ConvertibleObject
   	 * @since 1.2
   	 */
   	Convertible getTo();
   	/** an alias for getTo(). @since 1.2 */
   	Convertible getValue();
   	/** returns true if p is a parent, or grandparent, ... of this node, or if it is equal
   	 * to this node; returns false otherwise. */
   	boolean isDescendantOf(Node p);
   	boolean isFolded();
   	boolean isLeaf();
   	boolean isLeft();
   	boolean isRoot();
   	boolean isVisible();
   	/** Starting from this node, recursively searches for nodes for which
   	 * condition.checkNode(node) returns true.
   	 * @deprecated since 1.2 use find(Closure) instead. */
   	List<Node> find(ICondition condition);
   	/** Starting from this node, recursively searches for nodes for which closure.call(node)
   	 * returns true. See {@link Controller#find(Closure)} for details. */
   	List<Node> find(Closure closure);
   	Date getLastModifiedAt();
   	Date getCreatedAt();
   }
   interface Node extends NodeRO {
   	Connector addConnectorTo(Node target);
   	/** adds a new Connector object to List<Node> connectors and returns
   	 * reference for optional further editing (style); also enlists the
   	 * Connector on the target Node object. */
   	Connector addConnectorTo(String targetNodeId);
   	/** inserts *new* node as child, takes care of all construction work and
   	 * internal stuff inserts as last child. */
   	Node createChild();
   	/** inserts *new* node as child, takes care of all construction work and
   	 * internal stuff */
   	Node createChild(int position);
   	void delete();
   	/** removes connector from List<Node> connectors; does the corresponding
   	 * on the target Node object referenced by connectorToBeRemoved */
   	void moveTo(Node parentNode);
   	void moveTo(Node parentNode, int position);
   	/** as above, using String nodeId instead of Node object to establish the connector*/
   	void removeConnector(Connector connectorToBeRemoved);
   	void setFolded(boolean folded);
   	/**
   	 * Set the note text:

*

    *
  • This methods provides automatic conversion to String in a way that node.getNote().getXyz() * methods will be able to convert the string properly to the wanted type. *
  • Special conversion is provided for dates and calendars: They will be converted in a way that * node.note.date and node.note.calendar will work. All other types are converted via value.toString(). *
  • If the conversion result is not valid HTML it will be automatically converted to HTML. *

*

*

    	 *   // converts numbers and other stuff with toString()
    	 *   node.note = 1.2
    	 *   assert node.note.text == "<html><body><p>1.2"
    	 *   assert node.note.plain == "1.2"
    	 *   assert node.note.num == 1.2d
    	 *   // == dates
    	 *   // a date in some non-UTC time zone
    	 *   def date = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ").
    	 *       parse("1970-01-01 00:00:00.000-0200")
    	 *   // converts to "1970-01-01T02:00:00.000+0000" (GMT)
    	 *   // - note the shift due to the different time zone
    	 *   // - the missing end tags don't matter for rendering
    	 *   node.note = date
    	 *   assert node.note == "<html><body><p>1970-01-01T02:00:00.000+0000"
    	 *   assert node.note.plain == "1970-01-01T02:00:00.000+0000"
    	 *   assert node.note.date == date
    	 *   // == remove note
    	 *   node.note = null
    	 *   assert node.note.text == null
    	 * 
   	 * @param value An object for conversion to String. Works well for all types that {@link Convertible}
   	 *        handles, particularly {@link Convertible}s itself.
   	 * @since 1.2 (note that the old setNoteText() did not support non-String arguments.
   	 */
   	void setNote(Object value);
   	/** @deprecated since 1.2 - use setNote() instead. */
   	void setNoteText(String text);
   	/**
   	 * A node's text is String valued. This methods provides automatic conversion to String in a way that
   	 * node.to.getXyz() methods will be able to convert the string properly to the wanted type.
   	 * Special conversion is provided for dates and calendars: They will be converted in a way that
   	 * node.to.date and node.to.calendar will work. All other types are converted via value.toString():

*

    	 *   // converts non-Dates with toString()
    	 *   node.text = 1.2
    	 *   assert node.to.text == "1.2"
    	 *   assert node.to.num == 1.2d
    	 *   // == dates
    	 *   // a date in some non-GMT time zone
    	 *   def date = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ").
    	 *       parse("1970-01-01 00:00:00.000-0200")
    	 *   // converts to "1970-01-01T02:00:00.000+0000" (GMT)
    	 *   // - note the shift due to the different time zone
    	 *   node.text = date
    	 *   assert node.to.text == "1970-01-01T02:00:00.000+0000"
    	 *   assert node.to.date == date
    	 * 
   	 * @param value A not-null object for conversion to String. Works well for all types that {@link Convertible}
   	 *        handles, particularly {@link Convertible}s itself.
   	 */
   	void setText(Object value);
   	void setLastModifiedAt(Date date);
   	void setCreatedAt(Date date);
   	// Attributes
   	/**
   	 * Allows to set and to change attribute like array elements.

*

* Note that attributes are String valued. This methods provides automatic conversion to String in a way that * node["a name"].getXyz() methods will be able to convert the string properly to the wanted type. * Special conversion is provided for dates and calendars: They will be converted in a way that * node["a name"].date and node["a name"].calendar will work. All other types are converted via * value.toString(): *

    	 *   // == text
    	 *   node["attribute name"] = "a value"
    	 *   assert node["attribute name"] == "a value"
    	 *   assert node.attributes.getFirst("attribute name") == "a value" // the same
    	 *   // == numbers and others
    	 *   // converts numbers and other stuff with toString()
    	 *   node["a number"] = 1.2
    	 *   assert node["a number"].text == "1.2"
    	 *   assert node["a number"].num == 1.2d
    	 *   // == dates
    	 *   // a date in some non-GMT time zone
    	 *   def date = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ").
    	 *       parse("1970-01-01 00:00:00.000-0200")
    	 *   // converts to "1970-01-01T02:00:00.000+0000" (GMT)
    	 *   // - note the shift due to the different time zone
    	 *   node["a date"] = date
    	 *   assert node["a date"].text == "1970-01-01T02:00:00.000+0000"
    	 *   assert node["a date"].date == date
    	 *   // == remove an attribute
    	 *   node["removed attribute"] = "to be removed"
    	 *   assert node["removed attribute"] == "to be removed"
    	 *   node["removed attribute"] = null
    	 *   assert node.attributes.find("removed attribute") == -1
    	 * 
   	 * @param value An object for conversion to String. Works well for all types that {@link Convertible}
   	 *        handles, particularly {@link Convertible}s itself. Use null to unset an attribute.
   	 * @return the new value
   	 */
   	String putAt(String attributeName, Object value);
   	/** allows to set all attributes at once:

*

    	 *   node.attributes = [:] // clear the attributes
    	 *   assert node.attributes.size() == 0
    	 *   node.attributes = ["1st" : "a value", "2nd" : "another value"] // create 2 attributes 
    	 *   assert node.attributes.size() == 2
    	 *   node.attributes = ["one attrib" : new Double(1.22)] // replace all attributes
    	 *   assert node.attributes.size() == 1
    	 *   assert node.attributes.getFirst("one attrib") == "1.22" // note the type conversion
    	 *   assert node["one attrib"] == "1.22" // here we compare Convertible with String
    	 * 
   	 */
   	void setAttributes(java.util.Map<String, Object> attributes);
   }
   interface NodeStyleRO {
   	IStyle getStyle();
   	Node getStyleNode();
   	Color getBackgroundColor();
   	Edge getEdge();
   	Font getFont();
   	Color getNodeTextColor();
   }
   interface NodeStyle extends NodeStyleRO {
   	void setStyle(IStyle key);
   	void setBackgroundColor(Color color);
   	void setNodeTextColor(Color color);
   }

} </groovy>

Convertible

<groovy> package org.freeplane.plugin.script.proxy;

import groovy.lang.GroovyObjectSupport; import groovy.lang.MissingMethodException;

import java.text.ParsePosition; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.regex.Matcher; import java.util.regex.Pattern;

import org.apache.commons.lang.NotImplementedException; import org.apache.commons.lang.math.NumberUtils; import org.apache.commons.lang.time.DateFormatUtils; import org.codehaus.groovy.runtime.InvokerHelper; import org.freeplane.core.util.HtmlUtils;

/** Utility class that is used to convert node texts to different types.

*

* Warning: The nodeModel is used for script invocation ({@link #getValue()}), not * for access its properties. Therefore text and nodeModel are not synchronized */ // Unfortunately it seems impossible to implement Comparable<Object> since in this case // TypeTransformation.compareToWithEqualityCheck() is called and will return false for // assert new Comparable(2) == "2" // instead of just calling equals, which is correctly defined public class Convertible extends GroovyObjectSupport /*implements Comparable<Object>*/ { private static final Pattern DATE_REGEXP_PATTERN = Pattern.compile("\\d{4}(-?)\\d{2}(-?)\\d{2}" // + "(([ T])?\\d{2}(:?)\\d{2}(:?)(\\d{2})?(\\.\\d{3})?([-+]\\d{4})?)?"); private final String text; /** doesn't evaluate formulas since this would require a calculation rule or NodeModel. */ public Convertible(String text) { this.text = text; } /** same as toString(text), i.e. conversion is done properly. */ public Convertible(Object text) { this.text = toString(text); } /** * returns a Long or a Double, whatever fits best. All Java number literals are allowed as described * by {@link Long#decode(String)} * * @throws ConversionException if text is not a number. */ public Number getNum() throws ConversionException { try { try { return text == null ? null : text.length() == 0 ? 0 : Long.decode(text); } catch (NumberFormatException e) { return Double.valueOf(text); } } catch (NumberFormatException e) { throw new ConversionException("not a number: '" + text + "'", e); } } /** * "Safe" variant of getNum(): returns a Long or a Double, and (long) 0 on conversion errors. * * @throws nothing - on error (long) 0 is returned. */ public Number getNum0() { try { return getNum(); } catch (ConversionException e) { return 0L; } } public String getString() { return text; } public String getText() { return text; } public String getPlain() { return text == null ? null : HtmlUtils.htmlToPlain(text); } /** * returns a Date for the parsed text. * The valid date patterns are "yyyy-MM-dd HH:dd:ss.SSSZ" with optional '-', ':'. ' ' may be replaced by 'T'. * @throws ConversionException if the text is not convertible to a date. */ public Date getDate() throws ConversionException { return text == null ? null : parseDate(text); } private static Date parseDate(String text) throws ConversionException { // 1 2 34 5 6 7 8 9 // \\d{4}(-?)\\d{2}(-?)\\d{2}(([ T])?\\d{2}(:?)\\d{2}(:?)(\\d{2})?(\\.\\d{3})?([-+]\\d{4})?)? final Matcher matcher = DATE_REGEXP_PATTERN.matcher(text); if (matcher.matches()) { StringBuilder builder = new StringBuilder("yyyy"); builder.append(matcher.group(1)); builder.append("MM"); builder.append(matcher.group(2)); builder.append("dd"); if (matcher.group(3) != null) { if (matcher.group(4) != null) { builder.append('\); builder.append(matcher.group(4)); builder.append('\); } builder.append("HH"); builder.append(matcher.group(5)); builder.append("mm"); if (matcher.group(7) != null) { builder.append(matcher.group(6)); builder.append("ss"); } if (matcher.group(8) != null) { builder.append(".SSS"); } if (matcher.group(9) != null) { builder.append("Z"); } } SimpleDateFormat parser = new SimpleDateFormat(builder.toString()); ParsePosition pos = new ParsePosition(0); Date date = parser.parse(text, pos); if (date != null && pos.getIndex() == text.length()) { return date; } } throw new ConversionException("not a date: " + text); } /** * returns a Calendar for the parsed text. * @throws ConversionException if the text is not convertible to a date. */ public Calendar getCalendar() throws ConversionException { if (text == null) return null; final Date date = parseDate(text); final GregorianCalendar result = new GregorianCalendar(0, 0, 0); result.setTime(date); return result; } /** * Uses the following priority ranking to determine the type of the text: *

    *
  1. null *
  2. Long *
  3. Double *
  4. Date *
  5. String *
    * @return Object - the type that fits best.
    */
   public Object getObject() {
       if (text == null)
           return null;
       try {
           return getNum();
       }
       catch (ConversionException e1) {
           try {
               return getDate();
           }
           catch (ConversionException e2) {
               return text;
           }
       }
   }
   /** Allow statements like this: node['attr_name'].to.num. */
   public Convertible getTo() {
       return this;
   }
   /** returns true if the text is convertible to number. */
   public boolean isNum() {
       // handles null -> false
       return NumberUtils.isNumber(text);
   }
   /** returns true if the text is convertible to date. */
   public boolean isDate() {
       if (text == null)
           return false;
       final Matcher matcher = DATE_REGEXP_PATTERN.matcher(text);
       return matcher.matches();
   }
   /** pretend we are a String if we don't provide a property for ourselves. */
   public Object getProperty(String property) {
       // called methods should handle null values
       try {
           // disambiguate isNum()/getNum() in favor of getNum()
           if (property.equals("num"))
               return getNum();
           // same for isDate()/getDate()
           if (property.equals("date"))
               return getDate();
           return super.getProperty(property);
       }
       catch (ConversionException e) {
           throw new RuntimeException(e);
       }
       catch (Exception e) {
           return InvokerHelper.getMetaClass(String.class).getProperty(text, property);
       }
   }
   /** pretend we are a String if we don't provide a method for ourselves. */
   public Object invokeMethod(String name, Object args) {
       try {
           // called methods should handle null values
           return super.invokeMethod(name, args);
       }
       catch (MissingMethodException mme) {
           return InvokerHelper.getMetaClass(String.class).invokeMethod(text, name, args);
       }
   }
   /** has special conversions for

*

    *
  • Date and Calendar are converted by * org.apache.commons.lang.time.DateFormatUtils.formatUTC(date, "yyyy-MM-dd'T'HH:mm:ss.SSSZ"), i.e. to * GMT timestamps, e.g.: "2010-08-16 22:31:55.123+0000". *
  • null is "converted" to null *
    * All other types are converted via value.toString().
    */
   public static String toString(Object value) {
       if (value == null)
           return null;
       else if (value.getClass().equals(String.class))
           return (String) value;
       else if (value instanceof Date)
           return Convertible.dateToString(((Date) value));
       else if (value instanceof Calendar)
           return Convertible.dateToString(((Calendar) value).getTime());
       else
           return value.toString();
   }
   private static String dateToString(Date date) {
       return DateFormatUtils.formatUTC(date, "yyyy-MM-dd'T'HH:mm:ss.SSSZ");
   }
   
   // Unfortunately it seems impossible to implement Comparable<Object> since in this case
   // TypeTransformation.compareToWithEqualityCheck() is called and will return false for
   //   assert new Comparable(2) == "2"
   // instead of just calling equals, which is correctly defined
   public int compareTo(Object string) {
       if (string.getClass() == String.class)
           return text.compareTo((String) string);
       else
           return 1;
   }
   
   public int compareTo(Convertible convertible) {
       return text.compareTo(convertible.getText());
   }
   /** since equals handles Strings special we have to stick to that here too since
    * equal objects have to have the same hasCode. */
   @Override
   public int hashCode() {
       return text == null ? 0 : text.hashCode();
   }
   /** note: if obj is a String the result is true if String.equals(text). */
   @Override
   public boolean equals(Object obj) {
       if (this == obj)
           return true;
       if (obj == null)
           return false;
       if (obj.getClass() == String.class)
           return text.equals(obj);
       if (getClass() != obj.getClass())
           return false;
       Convertible other = (Convertible) obj;
       if (text == null) {
           if (other.text != null)
               return false;
       }
       else if (!text.equals(other.text))
           return false;
       return true;
   }
   @Override
   public String toString() {
       return text;
   }
   @Override
   public void setProperty(String property, Object newValue) {
       throw new NotImplementedException("Convertibles are immutable");
   }

} </groovy>