package edu.ualberta.med.biobank.common.wrappers; import java.io.Serializable; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.regex.Pattern; import org.apache.commons.lang.StringUtils; import edu.ualberta.med.biobank.common.util.TypeReference; import edu.ualberta.med.biobank.common.wrappers.property.GetterInterceptor; import edu.ualberta.med.biobank.common.wrappers.property.PropertyLink; /** * * @author jferland * * @param <P> the type of this {@link Property} * @param <M> the type of the model that has this {@link Property} */ public class Property<P, M> implements Serializable { private static final long serialVersionUID = 1L; private static final Pattern NAME_SPLITTER = Pattern.compile("\\."); //$NON-NLS-1$ private final String name; private final List<String> splitNames; private final String propertyChangeName; private final Class<M> modelClass; private final TypeInfo typeInfo; private final Accessor<P, M> accessor; private final PropertyLink<P, ?, M> link; private Property(String name, Class<M> modelClass, TypeReference<P> typeReference, Accessor<P, M> accessor) { this.name = name; this.splitNames = Arrays.asList(NAME_SPLITTER.split(name)); this.propertyChangeName = name; this.modelClass = modelClass; this.typeInfo = new TypeInfo(typeReference); this.link = null; this.accessor = accessor; } private Property(String name, String propertyChangeName, Class<M> modelClass, TypeInfo typeInfo, PropertyLink<P, ?, M> link) { this.name = name; this.splitNames = Arrays.asList(NAME_SPLITTER.split(name)); this.propertyChangeName = propertyChangeName; this.modelClass = modelClass; this.typeInfo = typeInfo; this.link = link; this.accessor = link; } public String getName() { return name; } private static <P, A, M> P get(PropertyLink<P, A, M> link, M model, GetterInterceptor getter) { P value = null; Property<A, M> fromProperty = link.getFrom(); A from = fromProperty.get(model, getter); if (from != null) { Property<P, ? super A> toProperty = link.getTo(); value = toProperty.get(from, getter); } return value; } public P get(M model, GetterInterceptor getter) { P value = null; if (link != null) { value = get(link, model, getter); } else { value = getter.get(this, model); } return value; } private static <P, A, M> void set(PropertyLink<P, A, M> link, M model, P value, GetterInterceptor getter) { Property<A, M> fromProperty = link.getFrom(); A from = fromProperty.get(model, getter); Property<P, ? super A> toProperty = link.getTo(); toProperty.set(from, value, getter); } public void set(M model, P value, GetterInterceptor getter) { if (link != null) { set(link, model, value, getter); } else { set(model, value); } } public String getPropertyChangeName() { return propertyChangeName; } /** * @return the {@link Class} of the elements in the {@link Collection} * returned by this class, otherwise the {@link Class} itself. */ public Class<?> getElementClass() { return typeInfo.elementClass; } public boolean isCollection() { return typeInfo.isCollection; } public Class<M> getModelClass() { return modelClass; } public P get(M model) { return accessor.get(model); } public void set(M model, P value) { accessor.set(model, value); } /** * An alias for the {@code wrap()} method. Behaves exactly the same, but * with a shorter method name for use such as * * <pre> * Property cThroughA = A.to(B.to(C)) * C c = cThroughA.get(a); * </pre> * * @param <T2> * @param property * @returnAssociationAccessor */ public <T2> Property<T2, M> to(final Property<T2, ? super P> property) { return wrap(property.name, property); } // TODO: write an "ofEach" method that could return a PropertyCollection // with get() and set() methods that return and take lists, respectively? // Could also make a PropertyName object that provides methods to construct // an at-compile-time checked list of properties?? COOOOOOOL! ;-) e.g. // PropertyName.start(SpecimenPeer.ID).ofEach(SpecimenPeer.CHILD_SPECIMEN_COLLECTION)).get(); public <T2> Property<T2, M> wrap(final Property<T2, ? super P> property) { return wrap(property.name, property); } /** * Creates a new {@link Property} by treating the {@link Property} of an * association as a direct property. * * @param <A> * @param propertyChangeName a new name to use for the property when firing * a change event (should correspond to the {@link ModelWrapper} * 's method name) * @param property * @return */ public <A> Property<A, M> wrap(String propertyChangeName, final Property<A, ? super P> property) { PropertyLink<A, ?, M> link = new PropertyLink<A, P, M>(this, property); return new Property<A, M>(concatNames(this, property), propertyChangeName, modelClass, property.typeInfo, link); } public Collection<Property<?, M>> wrap( Collection<Property<?, ? super P>> properties) { List<Property<?, M>> wrappedProperties = new ArrayList<Property<?, M>>(); for (Property<?, ? super P> property : properties) { Property<?, M> wrappedProperty = wrap(property.name, property); wrappedProperties.add(wrappedProperty); } return wrappedProperties; } /** * Returns a copy of a {@link List} of the component names that make up the * name of this {@link Property}. For example, if a {@link Property} has a * name of "specimen" then this method would return ("specimen"). However, * if a {@link Property} has a name of "specimen.container.id" then this * method would return the list ("specimen", "container", "id"). * * @return */ public List<String> getNames() { return new ArrayList<String>(splitNames); } public static String concatNames(Property<?, ?>... props) { String[] propNames = new String[props.length]; int count = 0; for (Property<?, ?> prop : props) { propNames[count++] = prop.getName(); } return StringUtils.join(propNames, '.'); } public static <P, M> Property<P, M> create(String name, Class<M> modelClass, TypeReference<P> type, Accessor<P, M> accessor) { return new Property<P, M>(name, modelClass, type, accessor); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } // TODO: include model class or type info for equality check? @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Property<?, ?> other = (Property<?, ?>) obj; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; } @Override public String toString() { return name + "(" + typeInfo.toString + ")"; //$NON-NLS-1$ //$NON-NLS-2$ } public interface Accessor<P, M> extends Serializable { public P get(M model); public void set(M model, P value); }; /** * Because {@link TypeReference} is not necessarily {@link Serializable}, * this internal class is used to extract all the necessary information, * encapsulate it, and all it to be serialized. * * @author jferland * */ private static final class TypeInfo implements Serializable { // TODO: we don't need a TypeReference class to get this information, it // can be generated by another file that analyzes the uml and inserted // into the definitions of the peer classes private static final long serialVersionUID = 1L; private final Class<?> elementClass; private final boolean isCollection; private final String toString; public TypeInfo(TypeReference<?> typeReference) { Type type = typeReference.getType(); this.elementClass = getElementClass(type); this.isCollection = isCollection(type); this.toString = type.toString(); } private static Class<?> getElementClass(Type type) { Class<?> klazz = null; if (type instanceof Class<?>) { klazz = (Class<?>) type; } else if (type instanceof ParameterizedType) { ParameterizedType pType = (ParameterizedType) type; Type[] elementTypes = pType.getActualTypeArguments(); if (elementTypes.length > 0) { Type elementType = elementTypes[0]; klazz = getElementClass(elementType); } } return klazz; } private static boolean isCollection(Type type) { boolean isCollection = false; if (type instanceof ParameterizedType) { ParameterizedType pType = (ParameterizedType) type; Type rawType = pType.getRawType(); if (rawType instanceof Class) { Class<?> rawTypeClass = (Class<?>) rawType; if (rawTypeClass.isAssignableFrom(Collection.class)) { isCollection = true; } } } return isCollection; } } }