
com.vaadin.addon.jpacontainer.PropertyList Maven / Gradle / Ivy
/*
JPAContainer
Copyright (C) 2009-2011 Oy Vaadin Ltd
This program is available under GNU Affero General Public License (version
3 or later at your option).
See the file licensing.txt distributed with this software for more
information about licensing.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
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 com.vaadin.addon.jpacontainer.metadata.ClassMetadata;
import com.vaadin.addon.jpacontainer.metadata.PersistentPropertyMetadata;
import com.vaadin.addon.jpacontainer.metadata.PersistentPropertyMetadata.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.
*
* 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 implements Serializable {
private static final long serialVersionUID = 372287057799712177L;
private ClassMetadata metadata;
private Set propertyNames = new HashSet();
private Set persistentPropertyNames = new HashSet();
// 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 sortablePropertyMap = new HashMap();
private Set nestedPropertyNames = new HashSet();
private Set allPropertyNames = new HashSet();
/**
* Creates a new PropertyList
for the specified metadata.
* Initially, all the properties of metadata
will be added to
* the list.
*
* @param metadata
* the class metadata (must not be null).
*/
public PropertyList(ClassMetadata 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 parentList;
/**
* Creates a new PropertyList
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 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 getClassMetadata() {
return metadata;
}
/**
* Gets the parent property list, if any.
*
* @return the parent list, or null if the list has no parent.
*/
public PropertyList getParentList() {
return parentList;
}
/**
* Configures a property to be sortable based on another property, typically
* a sub-property.
*
* For example, let's say there is a property named address
and
* that this property's type in turn has the property street
.
* Addresses are not directly sortable as they are not simple properties.
*
* If we want to be able to sort addresses based on the street property, we
* can set the sort property for address
to be
* address.street
using this method. Sort properties must be
* persistent and usable in JPQL, but need not be registered as separate
* properties in the PropertyList.
*
* 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 propertyName
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 propertyName
to the set of
* properties. An asterisk can be used as a wildcard to indicate all
* leaf-properties.
*
* For example, let's say there is a property named address
and
* that this property's type in turn has the properties street
,
* postalCode
and city
.
*
* If we want to be able to access the street property directly, we can add
* the nested property address.street
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).
*
* However, if we want to add all the address properties, we can also use
* address.*
. This will cause the nested properties
* address.street
, address.postalCode
and
* address.city
to be added to the set of properties.
*
* @param propertyName
* the nested property to add (must not be null).
* @throws IllegalArgumentException
* if propertyName
was invalid.
*/
public void addNestedProperty(String propertyName)
throws IllegalArgumentException {
assert propertyName != null : "propertyName must not be null";
if (propertyName.indexOf('.') == -1) {
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")
&& !Modifier.isStatic(m.getModifiers())
&& m.getReturnType() != Void.TYPE
&& 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 nestedPropertyMap = new HashMap();
private NestedProperty getNestedProperty(String propertyName)
throws IllegalArgumentException {
if (nestedPropertyMap.containsKey(propertyName)) {
return nestedPropertyMap.get(propertyName);
} else {
try {
if (propertyName.indexOf('.') != -1) {
// 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 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 propertyName
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 getAllAvailablePropertyNames() {
return Collections.unmodifiableSet(doGetAllAvailablePropertyNames());
}
private Set union(Set... sets) {
HashSet newSet = new HashSet();
for (Set s : sets) {
newSet.addAll(s);
}
return newSet;
}
private Map union(Map... maps) {
HashMap newMap = new HashMap();
for (Map s : maps) {
newMap.putAll(s);
}
return newMap;
}
@SuppressWarnings("unchecked")
protected Set 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 getPropertyNames() {
return Collections.unmodifiableSet(doGetPropertyNames());
}
@SuppressWarnings("unchecked")
protected Set 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 getPersistentPropertyNames() {
return Collections.unmodifiableSet(doGetPersistentPropertyNames());
}
@SuppressWarnings("unchecked")
protected Set 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 getSortablePropertyMap() {
return Collections.unmodifiableMap(doGetSortablePropertyMap());
}
@SuppressWarnings("unchecked")
protected Map 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 getNestedPropertyNames() {
return Collections.unmodifiableSet(doGetNestedPropertyNames());
}
@SuppressWarnings("unchecked")
protected Set doGetNestedPropertyNames() {
if (parentList == null) {
return nestedPropertyNames;
} else {
return union(nestedPropertyNames,
parentList.doGetNestedPropertyNames());
}
}
/**
* Gets the type of propertyName
. 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 propertyName
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 (propertyName.indexOf('.') != -1) {
return getNestedProperty(propertyName).getType();
} else {
return metadata.getProperty(propertyName).getType();
}
}
/**
* Checks if propertyName
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 propertyName
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 (propertyName.indexOf('.') != -1) {
return getNestedProperty(propertyName).isWritable();
} else {
return metadata.getProperty(propertyName).isWritable();
}
}
/**
* Gets the value of propertyName
from the instance
* object
. The property name may be nested, but must be in the
* {@link #getAllAvailablePropertyNames() } set.
*
* 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 propertyName
to propertyValue
* . 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);
}
}