package org.freeplane.plugin.script.proxy; import groovy.lang.GroovyObjectSupport; import groovy.lang.MissingMethodException; import java.net.URI; import java.net.URISyntaxException; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import org.apache.commons.lang.NotImplementedException; import org.codehaus.groovy.runtime.InvokerHelper; import org.freeplane.core.util.HtmlUtils; import org.freeplane.core.util.TextUtils; import org.freeplane.features.format.FormattedDate; /** Utility class that is used to convert node texts to different types. * It's especially important for Formulas. */ // 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 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 { return TextUtils.toNumber(text); } catch (NumberFormatException e) { throw new ConversionException("not a number: '" + text + "'", e); } } /** * "Safe" variant of getNum(): returns a Long or a Double if text is convertible to it or 0 otherwise * (even if text is null). * * @throws nothing - on any error (long) 0 is returned. */ public Number getNum0() { try { final Number result = getNum(); return result == null ? 0L : result; } catch (Exception 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 { final Date date = FormattedDate.toDateISO(text); if(date != null) 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; } public URI getUri() throws ConversionException { if (text == null) return null; try { if (TextUtils.matchUriPattern(text)) return new URI(text); } catch (URISyntaxException e) { // throw below } throw new ConversionException("not an uri: " + text); } /** * Uses the following priority ranking to determine the type of the text: * <ol> * <li>null * <li>Long * <li>Double * <li>Date * <li>String * </ol> * @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) { try { return getUri(); } catch (ConversionException e3) { return text; } } } } /** Allow statements like this: <code>node['attr_name'].to.num</code>. */ public Convertible getTo() { return this; } /** returns true if the text is convertible to number. */ public boolean isNum() { // handles null -> false return TextUtils.isNumber(text); } /** returns true if the text is convertible to date. */ public boolean isDate() { return FormattedDate.isDate(text); } /** 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(); if (property.equals("uri")) return getUri(); 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 * <ul> * <li>Date and Calendar are converted by * org.apache.commons.lang.time.DateFormatUtils.format(date, "yyyy-MM-dd'T'HH:mm:ss.SSSZ"), i.e. to * GMT timestamps, e.g.: "2010-08-16T22:31:55.123+0000". * <li>null is "converted" to null * </ul> * 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 FormattedDate.toStringISO(((Date) value)); else if (value instanceof Calendar) return FormattedDate.toStringISO(((Calendar) value).getTime()); else return value.toString(); } // 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 text == null; if (obj.getClass() == String.class && text != null) return text.equals(obj); if (!(obj instanceof Convertible)) 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; property to be changed: " + property); } }