/** * Copyright 2009-2013 Oy Vaadin Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.vaadin.addon.jpacontainer; import java.beans.Introspector; import java.io.Serializable; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.persistence.Basic; import javax.persistence.ElementCollection; import javax.persistence.FetchType; import javax.persistence.ManyToMany; import javax.persistence.ManyToOne; import javax.persistence.OneToMany; import javax.persistence.OneToOne; import com.vaadin.addon.jpacontainer.metadata.ClassMetadata; import com.vaadin.addon.jpacontainer.metadata.PersistentPropertyMetadata; import com.vaadin.addon.jpacontainer.metadata.PropertyKind; import com.vaadin.addon.jpacontainer.metadata.PropertyMetadata; /** * Helper class to make it easier to work with nested properties. Intended to be * used by {@link JPAContainer}. This class is not part of the public API and * hence should not be used directly by client applications. * <p> * Property lists can be chained. A child property list will always include all * the properties of its parent in addition to its own. A child list cannot be * used to add or remove properties to/from its parent. * * @author Petter Holmström (Vaadin Ltd) * @since 1.0 */ final class PropertyList<T> implements Serializable { private static final long serialVersionUID = 372287057799712177L; private ClassMetadata<T> metadata; private Set<String> propertyNames = new HashSet<String>(); private Set<String> persistentPropertyNames = new HashSet<String>(); // map from property name to the name of the property to be used to sort by // that property (in a format usable in JPQL - e.g. address.street) private Map<String, String> sortablePropertyMap = new HashMap<String, String>(); private Set<String> nestedPropertyNames = new HashSet<String>(); private Set<String> allPropertyNames = new HashSet<String>(); /** * Creates a new <code>PropertyList</code> for the specified metadata. * Initially, all the properties of <code>metadata</code> will be added to * the list. * * @param metadata * the class metadata (must not be null). */ public PropertyList(ClassMetadata<T> metadata) { assert metadata != null : "metadata must not be null"; this.metadata = metadata; for (PropertyMetadata pm : metadata.getProperties()) { propertyNames.add(pm.getName()); allPropertyNames.add(pm.getName()); if (pm instanceof PersistentPropertyMetadata) { persistentPropertyNames.add(pm.getName()); if (PropertyKind.SIMPLE .equals(((PersistentPropertyMetadata) pm) .getPropertyKind())) { sortablePropertyMap.put(pm.getName(), pm.getName()); } } } } private PropertyList<T> parentList; /** * Creates a new <code>PropertyList</code> with the specified parent list. * Initially, all the properties of the parent list will be available. * * @param parentList * the parent list (must not be null). */ public PropertyList(PropertyList<T> parentList) { assert parentList != null : "parentList must not be null"; this.parentList = parentList; this.metadata = parentList.getClassMetadata(); } /** * Gets the metadata for the class from which the properties should be * fetched. * * @return the class metadata (never null). */ public ClassMetadata<T> getClassMetadata() { return metadata; } /** * Gets the parent property list, if any. * * @return the parent list, or null if the list has no parent. */ public PropertyList<T> getParentList() { return parentList; } /** * Configures a property to be sortable based on another property, typically * a sub-property. * <p> * For example, let's say there is a property named <code>address</code> and * that this property's type in turn has the property <code>street</code>. * Addresses are not directly sortable as they are not simple properties. * <p> * If we want to be able to sort addresses based on the street property, we * can set the sort property for <code>address</code> to be * <code>address.street</code> using this method. Sort properties must be * persistent and usable in JPQL, but need not be registered as separate * properties in the PropertyList. * <p> * Note that the sort property is not checked when this method is called. If * it is not a valid sort property, an exception will be thrown when trying * to sort a container. * * @param propertyName * the property for which sorting is to be customized (must not * be null). * @param sortPropertyName * the property based on which sorting should be performed - this * need not be a separate property in the container but needs to * be usable in JPQL * @throws IllegalArgumentException * if <code>propertyName</code> does not refer to a persistent * property. * @since 1.2.1 */ public void setSortProperty(String propertyName, String sortPropertyName) throws IllegalArgumentException { if (persistentPropertyNames.contains(propertyName)) { sortablePropertyMap.put(propertyName, sortPropertyName); } else { throw new IllegalArgumentException("Property " + propertyName + " cannot be sorted based on " + sortPropertyName + ": not a persistent property"); } } /** * Adds the nested property <code>propertyName</code> to the set of * properties. An asterisk can be used as a wildcard to indicate all * leaf-properties. * <p> * For example, let's say there is a property named <code>address</code> and * that this property's type in turn has the properties <code>street</code>, * <code>postalCode</code> and <code>city</code>. * <p> * If we want to be able to access the street property directly, we can add * the nested property <code>address.street</code> using this method. The * method will figure out whether the nested property is persistent (can be * used in queries) or transient (can only be used to display data). * <p> * However, if we want to add all the address properties, we can also use * <code>address.*</code>. This will cause the nested properties * <code>address.street</code>, <code>address.postalCode</code> and * <code>address.city</code> to be added to the set of properties. * * @param propertyName * the nested property to add (must not be null). * @throws IllegalArgumentException * if <code>propertyName</code> was invalid. */ public void addNestedProperty(String propertyName) throws IllegalArgumentException { assert propertyName != null : "propertyName must not be null"; if (!isNestedProperty(propertyName)) { throw new IllegalArgumentException(propertyName + " is not nested"); } if (getAllAvailablePropertyNames().contains(propertyName)) { return; // Do nothing, the property already exists. } if (propertyName.endsWith("*")) { // We add a whole bunch of properties String parentPropertyName = propertyName.substring(0, propertyName.length() - 2); NestedProperty parentProperty = getNestedProperty(parentPropertyName); if (parentProperty.getTypeMetadata() != null) { // The parent property is persistent and contains metadata for (PropertyMetadata pm : parentProperty.getTypeMetadata() .getProperties()) { String newName = parentPropertyName + "." + pm.getName(); if (!getAllAvailablePropertyNames().contains(newName)) { if (pm instanceof PersistentPropertyMetadata) { persistentPropertyNames.add(newName); if (PropertyKind.SIMPLE .equals(((PersistentPropertyMetadata) pm) .getPropertyKind())) { sortablePropertyMap.put(newName, newName); } } propertyNames.add(newName); allPropertyNames.add(newName); nestedPropertyNames.add(newName); } } } else { // The parent property is transient or is a simple property that // does not contain any nestable properties for (Method m : parentProperty.getType().getMethods()) { if (m.getName().startsWith("get") && m.getName().length() > 3 && !Modifier.isStatic(m.getModifiers()) && m.getReturnType() != Void.TYPE && m.getParameterTypes().length == 0 && m.getDeclaringClass() != Object.class) { String newName = parentPropertyName + "." + Introspector.decapitalize(m.getName() .substring(3)); if (!getAllAvailablePropertyNames().contains(newName)) { propertyNames.add(newName); nestedPropertyNames.add(newName); allPropertyNames.add(newName); } } } } } else { // We add a single property NestedProperty np = getNestedProperty(propertyName); if (np.getKind() == NestedPropertyKind.PERSISTENT) { persistentPropertyNames.add(propertyName); PropertyMetadata propertyMetadata = np.getPropertyMetadata(); if (propertyMetadata instanceof PersistentPropertyMetadata && PropertyKind.SIMPLE .equals(((PersistentPropertyMetadata) propertyMetadata) .getPropertyKind())) { sortablePropertyMap.put(propertyName, propertyName); } } // Transient property propertyNames.add(propertyName); nestedPropertyNames.add(propertyName); allPropertyNames.add(propertyName); } } /* * TODO The current way of handling nested properties was designed to also * support getting and setting values of nested properties. However, this * responsibility was later moved to ClassMetadata. Therefore, this design * may be more complex than would actually be required. In a future version * it should be cleaned up. */ private static enum NestedPropertyKind { PERSISTENT, TRANSIENT } private static class NestedProperty implements Serializable { private static final long serialVersionUID = -430502035392444897L; final NestedProperty parent; private final String name; final ClassMetadata<? extends Object> parentClassMetadata; final Method propertyGetterMethod; NestedProperty(String name, ClassMetadata<? extends Object> parentClassMetadata) { this.name = name; this.parentClassMetadata = parentClassMetadata; this.parent = null; this.propertyGetterMethod = null; } /* * NestedProperty(String name, Method propertyGetterMethod) { this.name * = name; this.parentClassMetadata = null; this.parent = null; * this.propertyGetterMethod = propertyGetterMethod; } */ NestedProperty(String name, ClassMetadata<? extends Object> parentClassMetadata, NestedProperty parent) { this.name = name; this.parentClassMetadata = parentClassMetadata; this.parent = parent; this.propertyGetterMethod = null; } NestedProperty(String name, Method propertyGetterMethod, NestedProperty parent) { this.name = name; this.parentClassMetadata = null; this.parent = parent; this.propertyGetterMethod = propertyGetterMethod; } String getName() { if (parent == null) { return name; } else { return parent.getName() + "." + name; } } Class<?> getType() { if (parentClassMetadata != null) { return parentClassMetadata.getProperty(name).getType(); } else { return propertyGetterMethod.getReturnType(); } } PropertyMetadata getPropertyMetadata() { if (parentClassMetadata != null) { return parentClassMetadata.getProperty(name); } return null; } ClassMetadata<?> getTypeMetadata() { PropertyMetadata pm = getPropertyMetadata(); if (pm instanceof PersistentPropertyMetadata) { return ((PersistentPropertyMetadata) pm).getTypeMetadata(); } return null; } NestedPropertyKind getKind() { if (parentClassMetadata != null && parentClassMetadata.getProperty(name) instanceof PersistentPropertyMetadata) { return NestedPropertyKind.PERSISTENT; } else { return NestedPropertyKind.TRANSIENT; } } boolean isWritable() { if (parentClassMetadata != null) { return parentClassMetadata.getProperty(name).isWritable(); } else { /* * There are cases when this may not work. For example, if the * setter is declared in a subclass. */ try { propertyGetterMethod.getDeclaringClass().getMethod( "s" + propertyGetterMethod.getName().substring(1), getType()); return true; } catch (NoSuchMethodException e) { return false; } } } } private Map<String, NestedProperty> nestedPropertyMap = new HashMap<String, NestedProperty>(); private NestedProperty getNestedProperty(String propertyName) throws IllegalArgumentException { if (nestedPropertyMap.containsKey(propertyName)) { return nestedPropertyMap.get(propertyName); } else { try { if (isNestedProperty(propertyName)) { // Try with the parent int offset = propertyName.lastIndexOf('.'); String parentName = propertyName.substring(0, offset); String name = propertyName.substring(offset + 1); NestedProperty parentProperty = getNestedProperty(parentName); NestedProperty property; if (parentProperty.getTypeMetadata() != null) { PropertyMetadata pm = parentProperty.getTypeMetadata() .getProperty(name); if (pm == null) { throw new IllegalArgumentException( "Invalid property name"); } else { property = new NestedProperty(pm.getName(), parentProperty.getTypeMetadata(), parentProperty); } } else { Method getter = getGetterMethod(name, parentProperty.getType()); if (getter == null) { throw new IllegalArgumentException( "Invalid property name"); } else { property = new NestedProperty(name, getter, parentProperty); } } nestedPropertyMap.put(propertyName, property); return property; } else { // There are no more parent properties PropertyMetadata pm = metadata.getProperty(propertyName); if (pm == null) { throw new IllegalArgumentException( "Invalid property name"); } else { NestedProperty property = new NestedProperty( pm.getName(), metadata); nestedPropertyMap.put(propertyName, property); return property; } } } catch (IllegalArgumentException e) { if (parentList == null) { throw e; } else { return parentList.getNestedProperty(propertyName); } } } } private boolean isNestedProperty(String propertyName) { return propertyName.indexOf('.') != -1; } private Method getGetterMethod(String prop, Class<?> parent) { String propertyName = prop.substring(0, 1).toUpperCase() + prop.substring(1); try { Method m = parent.getMethod("get" + propertyName); if (m.getReturnType() != Void.TYPE) { return m; } else { return null; } } catch (NoSuchMethodException e) { return null; } } /** * Removes <code>propertyName</code> from the set of properties. If the * property is contained in the parent list, nothing happens. * * @param propertyName * the property name to remove, must not be null. * @return true if a property was removed, false if not (i.e. it did not * exist in the first place). */ public boolean removeProperty(String propertyName) { assert propertyName != null : "propertyName must not be null"; boolean result = propertyNames.remove(propertyName); persistentPropertyNames.remove(propertyName); sortablePropertyMap.remove(propertyName); if (nestedPropertyNames.remove(propertyName)) { allPropertyNames.remove(propertyName); } // Do not remove from map of nested properties in case the property // is referenced by other nested properties. return result; } /** * Gets the set of all available property names, i.e. the union of * {@link ClassMetadata#getPropertyNames() } and * {@link #getNestedPropertyNames() }. Only nested property names can be * added to or removed from this set. * * @return an unmodifiable set of property names (never null). */ public Set<String> getAllAvailablePropertyNames() { return Collections.unmodifiableSet(doGetAllAvailablePropertyNames()); } private <E> Set<E> union(Set<E>... sets) { HashSet<E> newSet = new HashSet<E>(); for (Set<E> s : sets) { newSet.addAll(s); } return newSet; } private <K, V> Map<K, V> union(Map<K, V>... maps) { HashMap<K, V> newMap = new HashMap<K, V>(); for (Map<K, V> s : maps) { newMap.putAll(s); } return newMap; } @SuppressWarnings("unchecked") protected Set<String> doGetAllAvailablePropertyNames() { if (parentList == null) { return allPropertyNames; } else { return union(allPropertyNames, parentList.doGetAllAvailablePropertyNames()); } } /** * Gets the set of all property names. If no properties have been explicitly * removed using {@link #removeProperty(java.lang.String) }, this set is * equal to {@link #getAllAvailablePropertyNames() }. Otherwise, this set is * a subset of {@link #getAllAvailablePropertyNames()}. * * @return an unmodifiable set of property names (never null). */ public Set<String> getPropertyNames() { return Collections.unmodifiableSet(doGetPropertyNames()); } @SuppressWarnings("unchecked") protected Set<String> doGetPropertyNames() { if (parentList == null) { return propertyNames; } else { return union(propertyNames, parentList.doGetPropertyNames()); } } /** * Gets the set of persistent property names. This set is a subset of * {@link #getPropertyNames() }. * * @return an unmodifiable set of property names (never null). */ public Set<String> getPersistentPropertyNames() { return Collections.unmodifiableSet(doGetPersistentPropertyNames()); } @SuppressWarnings("unchecked") protected Set<String> doGetPersistentPropertyNames() { if (parentList == null) { return persistentPropertyNames; } else { return union(persistentPropertyNames, parentList.doGetPersistentPropertyNames()); } } /** * Gets the map of all sortable property names and their corresponding sort * properties. The keys of this map also show up in * {@link #getPropertyNames() } and {@link #getPersistentPropertyNames() }. * * @return an unmodifiable map from property names (never null) to sort * properties (not necessarily in the list). */ public Map<String, String> getSortablePropertyMap() { return Collections.unmodifiableMap(doGetSortablePropertyMap()); } @SuppressWarnings("unchecked") protected Map<String, String> doGetSortablePropertyMap() { if (parentList == null) { return sortablePropertyMap; } else { return union(sortablePropertyMap, parentList.doGetSortablePropertyMap()); } } /** * Gets the set of all nested property names. These names also show up in * {@link #getPropertyNames() } and {@link #getPersistentPropertyNames() }. * * @return an unmodifiable set of property names (never null). */ public Set<String> getNestedPropertyNames() { return Collections.unmodifiableSet(doGetNestedPropertyNames()); } @SuppressWarnings("unchecked") protected Set<String> doGetNestedPropertyNames() { if (parentList == null) { return nestedPropertyNames; } else { return union(nestedPropertyNames, parentList.doGetNestedPropertyNames()); } } /** * Gets the type of <code>propertyName</code>. Nested properties are * supported. This method works with property names in the * {@link #getAllAvailablePropertyNames() } set. * * @param propertyName * the name of the property (must not be null). * @return the type of the property (never null). * @throws IllegalArgumentException * if <code>propertyName</code> is illegal. */ public Class<?> getPropertyType(String propertyName) throws IllegalArgumentException { assert propertyName != null : "propertyName must not be null"; if (!getAllAvailablePropertyNames().contains(propertyName)) { throw new IllegalArgumentException("Illegal property name: " + propertyName); } if (isNestedProperty(propertyName)) { return getNestedProperty(propertyName).getType(); } else { return metadata.getProperty(propertyName).getType(); } } /** * Checks if <code>propertyName</code> is writable. Nested properties are * supported. This method works with property names in the * {@link #getAllAvailablePropertyNames() } set. * * @param propertyName * the name of the property (must not be null). * @return true if the property is writable, false otherwise. * @throws IllegalArgumentException * if <code>propertyName</code> is illegal. */ public boolean isPropertyWritable(String propertyName) throws IllegalArgumentException { assert propertyName != null : "propertyName must not be null"; if (!getAllAvailablePropertyNames().contains(propertyName)) { throw new IllegalArgumentException("Illegal property name: " + propertyName); } if (isNestedProperty(propertyName)) { return getNestedProperty(propertyName).isWritable(); } else { return metadata.getProperty(propertyName).isWritable(); } } /** * Gets the value of <code>propertyName</code> from the instance * <code>object</code>. The property name may be nested, but must be in the * {@link #getAllAvailablePropertyNames() } set. * <p> * When using nested properties and one of the properties in the chain is * null, this method will return null without throwing any exceptions. * * @param object * the object that the property value is fetched from (must not * be null). * @param propertyName * the property name (must not be null). * @return the property value. * @throws IllegalArgumentException * if the property name was illegal. */ public Object getPropertyValue(T object, String propertyName) throws IllegalArgumentException { assert propertyName != null : "propertyName must not be null"; assert object != null : "object must not be null"; if (!getAllAvailablePropertyNames().contains(propertyName)) { throw new IllegalArgumentException("Illegal property name: " + propertyName); } return metadata.getPropertyValue(object, propertyName); } /** * Sets the value of <code>propertyName</code> to <code>propertyValue</code> * . The property name may be nested, but must be in the * {@link #getAllAvailablePropertyNames() } set. * * @param object * the object to which the property is set (must not be null). * @param propertyName * the property name (must not be null). * @param propertyValue * the property value to set. * @throws IllegalArgumentException * if the property name was illegal. * @throws IllegalStateException * if one of the properties in the chain of nested properties * was null. */ public void setPropertyValue(T object, String propertyName, Object propertyValue) throws IllegalArgumentException, IllegalStateException { assert propertyName != null : "propertyName must not be null"; assert object != null : "object must not be null"; if (!getAllAvailablePropertyNames().contains(propertyName)) { throw new IllegalArgumentException("Illegal property name: " + propertyName); } metadata.setPropertyValue(object, propertyName, propertyValue); } public PropertyKind getPropertyKind(String propertyName) { assert propertyName != null : "propertyName must not be null"; if (!getAllAvailablePropertyNames().contains(propertyName)) { throw new IllegalArgumentException("Illegal property name: " + propertyName); } if (isNestedProperty(propertyName)) { return getNestedProperty(propertyName).getPropertyMetadata() .getPropertyKind(); } else { return metadata.getProperty(propertyName).getPropertyKind(); } } /** * Finds out whether a given property or any property in a nested "path" is * lazy loaded. * * @param propertyName * the name of the property to inspect * @return true if the property is loaded lazily */ public boolean isPropertyLazyLoaded(String propertyName) { if (isNestedProperty(propertyName)) { int dotIx = propertyName.indexOf('.'); if (isPropertyLazyLoaded(propertyName.substring(dotIx + 1))) { return true; } return getPropertyFetchType(propertyName.substring(0, dotIx)) == FetchType.LAZY; } return getPropertyFetchType(propertyName) == FetchType.LAZY; } /** * Finds the fetch type for the given property. * * @param propertyName * the name of the property * @return the {@link FetchType} or null if not applicable (e.g. not a * reference to another table on the database level) */ private FetchType getPropertyFetchType(String propertyName) { PropertyMetadata pm = metadata.getProperty(propertyName); if (pm != null) { if (pm.getAnnotation(Basic.class) != null) { return pm.getAnnotation(Basic.class).fetch(); } else if (pm.getAnnotation(ElementCollection.class) != null) { return pm.getAnnotation(ElementCollection.class).fetch(); } else if (pm.getAnnotation(ManyToMany.class) != null) { return pm.getAnnotation(ManyToMany.class).fetch(); } else if (pm.getAnnotation(OneToMany.class) != null) { return pm.getAnnotation(OneToMany.class).fetch(); } else if (pm.getAnnotation(ManyToOne.class) != null) { return pm.getAnnotation(ManyToOne.class).fetch(); } else if (pm.getAnnotation(OneToOne.class) != null) { return pm.getAnnotation(OneToOne.class).fetch(); } } return null; } }