/* * ome.dsl.Property * * Copyright 2006-2014 University of Dundee. All rights reserved. * Use is subject to license terms supplied in LICENSE.txt */ package ome.dsl; import java.io.IOException; import java.sql.Timestamp; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Properties; import java.util.Set; /** * represents the <b>definition</b> of a property within a SemanticType * * @author <br> * Josh Moore      <a * href="mailto:josh.moore@gmx.de"> josh.moore@gmx.de</a> * @version 3.0 * @since OMERO-3.0 */ public abstract class Property { // TODO need to define equality so that two // with the // same name isn't allowed within one type./ // FIELD identifiers public final static String REQUIRED = "required"; public final static String OPTIONAL = "optional"; public final static String ONEMANY = "onemany"; public final static String ZEROMANY = "zeromany"; public final static String MANYONE = "manyone"; public final static String MANYZERO = "manyzero"; public final static String ENTRY = "entry"; public final static String CHILD = "child"; public final static String PARENT = "parent"; public final static String TOCHILD = "to_child"; public final static String FROMPARENT = "from_parent"; public final static String MAP = "map"; public final static Set<String> FIELDS = new HashSet<String>(); static { FIELDS.add(REQUIRED); FIELDS.add(OPTIONAL); FIELDS.add(ONEMANY); FIELDS.add(ZEROMANY); FIELDS.add(MANYONE); FIELDS.add(MANYZERO); FIELDS.add(ENTRY); FIELDS.add(CHILD); FIELDS.add(PARENT); FIELDS.add(FROMPARENT); FIELDS.add(TOCHILD); FIELDS.add(MAP); } public final static Map<String, Class<? extends Property>> FIELDS2CLASSES = new HashMap<String, Class<? extends Property>>(); static { FIELDS2CLASSES.put(REQUIRED, RequiredField.class); FIELDS2CLASSES.put(OPTIONAL, OptionalField.class); FIELDS2CLASSES.put(ONEMANY, OneManyField.class); FIELDS2CLASSES.put(ZEROMANY, ZeroManyField.class); FIELDS2CLASSES.put(MANYONE, ManyOneField.class); FIELDS2CLASSES.put(MANYZERO, ManyZeroField.class); FIELDS2CLASSES.put(ENTRY, EntryField.class); FIELDS2CLASSES.put(PARENT, ParentLink.class); FIELDS2CLASSES.put(CHILD, ChildLink.class); FIELDS2CLASSES.put(FROMPARENT, LinkParent.class); FIELDS2CLASSES.put(TOCHILD, LinkChild.class); FIELDS2CLASSES.put(MAP, MapField.class); } // VALUE-Type identifiers public final static String STRING = "string"; public final static String BOOLEAN = "boolean"; public final static String INTEGER = "int"; public final static String FLOAT = "float"; public final static String DOUBLE = "double"; public final static String LONG = "long"; public final static String TIMESTAMP = "timestamp"; public final static String TEXT = "text"; public final static String BYTES = "byte[]"; public final static String DOUBLES = "double[]"; public final static String STRINGS = "string[]"; public final static String STRINGS2 = "string[][]"; public final static String INTEGERS = "int[]"; public final static String POSITIVEFLOAT = "PositiveFloat"; public final static String POSITIVEINTEGER = "PositiveInteger"; public final static String NONNEGATIVEINTEGER = "NonNegativeInteger"; public final static String PERCENTFRACTION = "PercentFraction"; public final static Map<String, String> JAVATYPES = new HashMap<String, String>(); static { JAVATYPES.put(STRING, String.class.getName()); JAVATYPES.put(BOOLEAN, Boolean.class.getName()); JAVATYPES.put(INTEGER, Integer.class.getName()); JAVATYPES.put(POSITIVEFLOAT, Double.class.getName()); JAVATYPES.put(POSITIVEINTEGER, Integer.class.getName()); JAVATYPES.put(NONNEGATIVEINTEGER, Integer.class.getName()); JAVATYPES.put(FLOAT, Float.class.getName()); JAVATYPES.put(PERCENTFRACTION, Double.class.getName()); JAVATYPES.put(DOUBLE, Double.class.getName()); JAVATYPES.put(LONG, Long.class.getName()); JAVATYPES.put(TIMESTAMP, Timestamp.class.getName()); JAVATYPES.put(TEXT, String.class.getName()); JAVATYPES.put(BYTES, BYTES); JAVATYPES.put(DOUBLES, DOUBLES); JAVATYPES.put(STRINGS, "java.util.List<String>"); JAVATYPES.put(STRINGS2, "java.util.List<String[]>"); JAVATYPES.put(INTEGERS, INTEGERS); } public final Properties DBTYPES; /** * The {@link SemanticType} instance which this property belongs to */ private SemanticType st; /** * The {@link SemanticType} instance which this property points at */ private SemanticType actualType; /** * The {@link SemanticType} instance which is the target of this property * (for collections) */ private SemanticType actualTarget; // String based values. private String name; private String type; private String defaultValue; private String foreignKey; private String tag; private String inverse; private String target; // Specialties private Boolean required; private Boolean unique; private Boolean ordered; private Boolean insert; private Boolean update; // Mappings private Boolean one2Many = Boolean.FALSE; private Boolean bidirectional; public void validate() { if (null == getName() || null == getType()) { throw new IllegalStateException( "All propeties must have a name and a type. (" + this + ")"); } } /** * creates a new property based on the element-valued key in FIELDS2CLASSES. * Used mainly by the xml reader */ public static Property makeNew(String element, SemanticType st, Properties attributes) throws IllegalArgumentException, IllegalStateException { Class<? extends Property> klass = FIELDS2CLASSES.get(element); if (null == klass) { throw new IllegalArgumentException( "FIELDS2CLASSES does not contain type " + element); } Property p; try { p = klass.getConstructor( new Class[] { SemanticType.class, Properties.class }) .newInstance(new Object[] { st, attributes }); } catch (Exception e) { throw new IllegalStateException( "Cannot instantiate class " + klass, e); } return p; } @Override public String toString() { return "Property: " + getName() + " (" + getType() + ")"; } // // Getters and Setters // public void setSt(SemanticType st) { this.st = st; } public SemanticType getSt() { return st; } public void setName(String name) { this.name = name; } public String getName() { return name; } /** * Read-only property */ public String getNameCapped() { return firstCap(name); } /** * Read-only property */ public String getNameUpper() { return name.toUpperCase(); } public void setType(String type) { this.type = type; } public String getType() { String t = JAVATYPES.get(type); if (t == null) { return type; } return t; } /** * Returns the actual type with no modification. {@link #getType()} is * probably poorly named, but changing it would require changing all the * templates extensively. */ public String _getType() { return type; } public void setActualType(SemanticType type) { this.actualType = type; } public SemanticType getActualType() { return this.actualType; } public void setActualTarget(SemanticType type) { this.actualTarget = type; } public SemanticType getActualTarget() { return this.actualTarget; } /** * Read-only variable */ public String getDbType() { String t = DBTYPES.getProperty(type); if (t == null) { return SemanticType.typeToColumn(type); } return t; } /** * Read-only variable */ public String getTypeAnnotation() { String T = "@org.hibernate.annotations.Type"; String P = ", parameters=@org.hibernate.annotations.Parameter(name=\"profile\", value=\"@PROFILE@\"))"; if (type.equals("text")) { return T + "(type=\"org.hibernate.type.TextType\")"; } else if (type.equals("string[]")) { return T + "(type=\"ome.tools.hibernate.ListAsSQLArrayUserType$STRING\"" + P; } else if (type.equals("string[][]")) { return T + "(type=\"ome.tools.hibernate.ListAsSQLArrayUserType$STRING2\"" + P; } else { return "// No @Type annotation"; } } /** * Read-only variable */ public String getShortType() { return unqualify(type); } /** * Read-only variable * * 4.2: multiple links to the same object * forces us to not use "shortType" (the unqualified * type of the link) and instead to use the * name of the link relationship. * * TODO: In later versions, we could always use this value in order to * have everything more consistent and simple. */ public String getFieldName() { SemanticType st = getActualType(); if (st != null) { for (Property p : st.getPropertyClosure()) { // Get everything if (p != this && p != null) { // but skip ourselves if (unqualify(p.getType()).equals(unqualify(type))) { return firstCap(getName()); } } } } return unqualify(type); } /** * Read-only value. Overwritten in subclasses */ public String getFieldType() { return getType(); } /** * Read-only value. Overwritten in subclasses */ public String getFieldInitializer() { return "null"; } // TODO remove public void setTag(String tag) { this.tag = tag; } public String getTag() { return tag; } public void setTarget(String target) { this.target = target; } public String getTarget() { return target; } /** * Read-only property */ public String getShortTarget() { return unqualify(target); } /** * Read-only property * * 4.2: multiple links to the same object * forces generate a name different from "shortTarget" * * @see #getShortType() */ public String getTargetName() { SemanticType st = getActualType(); for (Property p : st.getPropertyClosure()) { // Get everything if (p != this) { // but skip ourselves if (p.getTarget() != null && unqualify(p.getTarget()).equals(unqualify(target))) { String n = firstCap(getName()); // HACK ticket:2287 if (n.endsWith("Link")) { n = n.substring(0, n.length() - 4); } return n; } } } return unqualify(target); } public void setDefaultValue(String defaultValue) { this.defaultValue = defaultValue; } public String getDefaultValue() { return defaultValue; } public void setRequired(Boolean required) { this.required = required; } public Boolean getRequired() { return required; } /** * Read-only field */ public Boolean getNullable() { return !required; } public void setUnique(Boolean unique) { this.unique = unique; } public Boolean getUnique() { return unique; } public void setOrdered(Boolean ordered) { this.ordered = ordered; } public Boolean getOrdered() { return ordered; } public void setInverse(String inverse) { this.inverse = inverse; } public String getInverse() { return inverse; } /** * Read-only variable */ public String getInverseCapped() { if (inverse == null) { return null; } return inverse.substring(0, 1).toUpperCase() + inverse.substring(1, inverse.length()); } public void setInsert(Boolean insert) { this.insert = insert; } public Boolean getInsert() { return insert; } public void setUpdate(Boolean update) { this.update = update; } public Boolean getUpdate() { return update; } public void setForeignKey(String foreignKey) { this.foreignKey = foreignKey; } public String getForeignKey() { return foreignKey; } public void setOne2Many(Boolean one2Many) { this.one2Many = one2Many; } public Boolean getOne2Many() { return one2Many; } public void setBidirectional(Boolean bdir) { this.bidirectional = bdir; } public Boolean getBidirectional() { return this.bidirectional; } /** * @return the database definition for the property's type, or an empty string * @see <a href="https://trac.openmicroscopy.org/ome/ticket/803">ticket 803</a> */ public String getDef() { final String def = DBTYPES.getProperty(type); return def == null ? "" : def; } /** * Read-only property, for subclassing */ public boolean getIsLink() { return false; } /** * creates a Property and sets fields based on attributes USING DEFAULT * VALUES. Subclassees may override these values */ public Property(SemanticType st, Properties attrs) { setSt(st); setName(attrs.getProperty("name", null)); setType(attrs.getProperty("type", null)); setDefaultValue(attrs.getProperty("default", null));// TODO currently no // way to use this!! setTag(attrs.getProperty("tag", null)); setTarget(attrs.getProperty("target", null)); setInverse(attrs.getProperty("inverse", null)); // see // DslHandler.process() setBidirectional(Boolean.TRUE);// will be handle by // DslHandler.process() setRequired(Boolean.valueOf(attrs.getProperty("required", "false"))); setUnique(Boolean.valueOf(attrs.getProperty("unique", "false"))); // TODO // wanted // to // use // KEYS.put(id,field) // !! setOrdered(Boolean.valueOf(attrs.getProperty("ordered", "false"))); // TODO Mutability setInsert(Boolean.TRUE); setUpdate(Boolean.valueOf(attrs.getProperty("mutable", "true"))); // Load DBTYPES from resource file try { DBTYPES = new Properties(); String typesResource = "ome/dsl/" + st.profile + "-types.properties"; DBTYPES.load(this.getClass().getClassLoader() .getResourceAsStream(typesResource)); } catch (IOException e) { throw new RuntimeException("Error loading DB types: " + e.getMessage(), e); } if (JAVATYPES.containsKey(type)) { setForeignKey(null); } else { setForeignKey(SemanticType.typeToColumn(st.getId())); } } private static String unqualify(String str) { if (str == null) { return null; } int idx = str.lastIndexOf("."); if (idx < 0) { return str; } return str.substring(idx + 1, str.length()); } private static String firstCap(String str) { return str.substring(0, 1).toUpperCase() + str.substring(1, str.length()); } } // NOTE: For all the following be sure to check the defaults set on Property! // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~ Simple // ======== class OptionalField extends Property { public OptionalField(SemanticType st, Properties attrs) { super(st, attrs); } } class RequiredField extends OptionalField { public RequiredField(SemanticType st, Properties attrs) { super(st, attrs); setRequired(Boolean.TRUE); } } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~ 1-Many // ======== class ZeroManyField extends Property { public ZeroManyField(SemanticType st, Properties attrs) { super(st, attrs); setRequired(Boolean.FALSE); setOne2Many(Boolean.TRUE); /* see ch. 7 hibernate doc on association mappings */ if (getOrdered().booleanValue()) { setRequired(Boolean.TRUE); // FIXME here we need to change the // many2one!! } } @Override public String getFieldType() { StringBuilder sb = new StringBuilder(); if (getOrdered()) { sb.append("java.util.List<"); } else { sb.append("java.util.Set<"); } sb.append(getType()); sb.append(">"); return sb.toString(); } @Override public String getFieldInitializer() { StringBuilder sb = new StringBuilder(); if (getOrdered()) { sb.append("new java.util.ArrayList<"); } else { sb.append("new java.util.HashSet<"); } sb.append(getType()); sb.append(">()"); return sb.toString(); } @Override public void validate() { super.validate(); if (getInverse() == null) { throw new IllegalArgumentException( "\n" + this.toString() + ": invalid " + this.getClass().getName() + " property.\n" + "\n All zeromany and onemany fields must provide either the \"inverse\" " + "\n \"ordered\" or \"tag\" attribute E.g.\n" + "\n" + "<type id=...>\n" + "\t<properties>\n" + "\t\t<onemany name=\"example\" type=\"Example\" inverse=\"parent\">"); } } } class OneManyField extends ZeroManyField { public OneManyField(SemanticType st, Properties attrs) { super(st, attrs); setRequired(Boolean.TRUE); } } abstract class AbstractLink extends ZeroManyField { public AbstractLink(SemanticType st, Properties attrs) { super(st, attrs); setTarget(attrs.getProperty("target", null)); } @Override public void validate() { super.validate(); if (getTarget() == null) { throw new IllegalArgumentException( "Target must be set on all parent/child properties:" + this); } } } /** property from a child iobject to a link */ class ChildLink extends AbstractLink { public ChildLink(SemanticType st, Properties attrs) { super(st, attrs); setForeignKey("child"); setInverse("parent"); } @Override public boolean getIsLink() { return true; } } /** property from a parent iobject to a link */ class ParentLink extends AbstractLink { public ParentLink(SemanticType st, Properties attrs) { super(st, attrs); setForeignKey("parent"); setInverse("child"); } @Override public boolean getIsLink() { return true; } } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~ Many-1 // ======== class ManyZeroField extends Property { public ManyZeroField(SemanticType st, Properties attrs) { super(st, attrs); } } class ManyOneField extends ManyZeroField { public ManyOneField(SemanticType st, Properties attrs) { super(st, attrs); setRequired(Boolean.TRUE); if (getOrdered()) { setInsert(Boolean.FALSE); setUpdate(Boolean.FALSE); } } } /** property from a link to a parent iobject */ class LinkParent extends ManyOneField { public LinkParent(SemanticType st, Properties attrs) { super(st, attrs); setName("parent"); } @Override public String getFieldType() { return "IObject"; } @Override public boolean getIsLink() { return true; } } /** property from a link to a child iobject */ class LinkChild extends ManyOneField { public LinkChild(SemanticType st, Properties attrs) { super(st, attrs); setName("child"); } @Override public String getFieldType() { return "IObject"; } @Override public boolean getIsLink() { return true; } } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~ DIFFERENT SEMANTICS!!! // ======================== class EntryField extends Property { public EntryField(SemanticType st, Properties attrs) { super(st, attrs); setType("string"); setForeignKey(null); } @Override public void validate() { super.validate(); if (!"java.lang.String".equals(getType())) { throw new IllegalStateException( "Enum entries can only be of type \"java.lang.String\", not " + getType()); } } } class DetailsField extends Property { public DetailsField(SemanticType st, Properties attrs) { super(st, attrs); setName("details"); setType("ome.model.internal.Details"); } @Override public String getFieldInitializer() { return "new Details()"; } } class MapField extends Property { public MapField(SemanticType st, Properties attrs) { super(st, attrs); } @Override public String getFieldType() { StringBuilder sb = new StringBuilder(); sb.append("java.util.List<ome.model.internal.NamedValue>"); return sb.toString(); } }