org.dellroad.stuff.vaadin7.ProvidesPropertyScanner Maven / Gradle / Ivy
Show all versions of dellroad-stuff-vaadin7 Show documentation
/*
* Copyright (C) 2011 Archie L. Cobbs. All rights reserved.
*/
package org.dellroad.stuff.vaadin7;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.dellroad.stuff.java.MethodAnnotationScanner;
import org.dellroad.stuff.java.Primitive;
/**
* Scans a Java class hierarchy for {@link ProvidesProperty @ProvidesProperty} and
* {@link ProvidesPropertySort @ProvidesPropertySort} annotated getter methods and creates
* a corresponding set of {@link PropertyDef} property definitions and a {@link SortingPropertyExtractor}
* that will extract the properties from instances of the given class and sort them accordingly.
*
* @param Java class to be introspected
* @see ProvidesProperty @ProvidesProperty
* @see ProvidesPropertySort @ProvidesPropertySort
*/
public class ProvidesPropertyScanner {
private final ArrayList> propertyList = new ArrayList<>();
private final SortingPropertyExtractor propertyExtractor;
/**
* Constructor.
*
* @param type Java class to be introspected
* @throws IllegalArgumentException if {@code type} is null
* @throws IllegalArgumentException if an annotated method with no {@linkplain ProvidesProperty#value property name specified}
* has a name which cannot be interpreted as a bean property "getter" method
* @throws IllegalArgumentException if {@code type} has two {@link ProvidesProperty @ProvidesProperty}-annotated
* fields or methods with the same {@linkplain ProvidesProperty#value property name}
*/
public ProvidesPropertyScanner(Class type) {
// Sanity check
if (type == null)
throw new IllegalArgumentException("null type");
// Scan for @ProvidesProperty and @ProvidesPropertySort annotations
final Set.MethodInfo> providesPropertyMethods
= new MethodAnnotationScanner(type, ProvidesProperty.class).findAnnotatedMethods();
final Set.MethodInfo> providesPropertySortMethods
= new MethodAnnotationScanner(type, ProvidesPropertySort.class).findAnnotatedMethods();
// Check for duplicate @ProvidesProperty names
final HashMap.MethodInfo> providesPropertyNameMap = new HashMap<>();
for (MethodAnnotationScanner.MethodInfo methodInfo : providesPropertyMethods) {
final String propertyName = this.getPropertyName(methodInfo);
// Check for name conflict
final MethodAnnotationScanner.MethodInfo previousInfo = providesPropertyNameMap.get(propertyName);
if (previousInfo == null) {
providesPropertyNameMap.put(propertyName, methodInfo);
continue;
}
// If there is a name conflict, the sub-type method declaration wins
switch (this.compareDeclaringClass(previousInfo.getMethod(), methodInfo.getMethod())) {
case 0:
throw new IllegalArgumentException("duplicate @" + ProvidesProperty.class.getSimpleName()
+ " declaration for property `" + propertyName + "' on method " + previousInfo.getMethod()
+ " and " + methodInfo.getMethod() + " declared in the same class");
case 1:
providesPropertyNameMap.put(propertyName, methodInfo);
break;
default:
break;
}
}
// Check for duplicate @ProvidesPropertySort names, etc.
final HashMap.MethodInfo> providesPropertySortNameMap
= new HashMap<>();
for (MethodAnnotationScanner.MethodInfo methodInfo : providesPropertySortMethods) {
final String propertyName = this.getSortPropertyName(methodInfo);
// Verify the method's return type (or wrapper type if primitive) implements Comparable or Comparator
Class> methodType = methodInfo.getMethod().getReturnType();
if (methodType.isPrimitive())
methodType = Primitive.get(methodType).getWrapperType();
if (!Comparable.class.isAssignableFrom(methodType) && !Comparator.class.isAssignableFrom(methodType)) {
throw new IllegalArgumentException("invalid @" + ProvidesPropertySort.class.getSimpleName()
+ " declaration for property `" + propertyName + "': method " + methodInfo.getMethod()
+ " return type " + methodType.getName() + " implements neither " + Comparable.class.getName()
+ " nor " + Comparator.class.getName());
}
// Check for name conflict
final MethodAnnotationScanner.MethodInfo previousInfo
= providesPropertySortNameMap.get(propertyName);
if (previousInfo == null) {
providesPropertySortNameMap.put(propertyName, methodInfo);
continue;
}
// If there is a name conflict, the sub-type method declaration wins
switch (this.compareDeclaringClass(previousInfo.getMethod(), methodInfo.getMethod())) {
case 0:
throw new IllegalArgumentException("duplicate @" + ProvidesPropertySort.class.getSimpleName()
+ " declaration for property `" + propertyName + "' on method " + previousInfo.getMethod()
+ " and " + methodInfo.getMethod() + " declared in the same class");
case 1:
providesPropertySortNameMap.put(propertyName, methodInfo);
break;
default:
break;
}
}
// Build PropertyDef list
for (Map.Entry.MethodInfo> e : providesPropertyNameMap.entrySet()) {
final String propertyName = e.getKey();
final MethodAnnotationScanner.MethodInfo methodInfo = e.getValue();
// Get property type
Class> propertyType = methodInfo.getMethod().getReturnType();
// Get property default value
Object defaultValue = null;
if (propertyType.isPrimitive()) {
final Primitive> primitiveType = Primitive.get(propertyType);
defaultValue = primitiveType.getDefaultValue();
propertyType = primitiveType.getWrapperType();
}
// Get sort property (if any)
final MethodAnnotationScanner.MethodInfo sortMethodInfo
= providesPropertySortNameMap.get(propertyName);
// Create property definition
this.propertyList.add(this.createAnnotationPropertyDef(
propertyName, propertyType, defaultValue, methodInfo, sortMethodInfo));
}
// Build PropertyExtractor
this.propertyExtractor = new SortingPropertyExtractor() {
@Override
@SuppressWarnings("unchecked")
public V getPropertyValue(T obj, PropertyDef propertyDef) {
if (!(propertyDef instanceof AnnotationPropertyDef))
throw new IllegalArgumentException("unknown property " + propertyDef);
final AnnotationPropertyDef annotationPropertyDef = (AnnotationPropertyDef)propertyDef;
return propertyDef.getType().cast(annotationPropertyDef.getMethodInfo().invoke(obj));
}
@Override
@SuppressWarnings("unchecked")
public boolean canSort(PropertyDef> propertyDef) {
if (!(propertyDef instanceof AnnotationPropertyDef))
return false;
final AnnotationPropertyDef> annotationPropertyDef = (AnnotationPropertyDef)propertyDef;
return annotationPropertyDef.getSortMethodInfo() != null;
}
@Override
@SuppressWarnings("unchecked")
public int sort(PropertyDef> propertyDef, T obj1, T obj2) {
if (!(propertyDef instanceof AnnotationPropertyDef))
throw new UnsupportedOperationException("unknown property " + propertyDef);
final AnnotationPropertyDef> annotationPropertyDef = (AnnotationPropertyDef>)propertyDef;
return annotationPropertyDef.getComparator().compare(obj1, obj2);
}
};
}
/**
* Get the list of {@link PropertyDef}s generated from the annotated methods.
*
*
* All of the properties in the returned list can be extracted from instances of this reader's configured
* class by the {@link PropertyExtractor} returned by {@link #getPropertyExtractor}.
*
* @return unmodifiable list of properties
* @see #getPropertyExtractor
*/
public List> getPropertyDefs() {
return Collections.unmodifiableList(this.propertyList);
}
/**
* Get the {@link PropertyExtractor} that extracts Vaadin {@link com.vaadin.data.Property} values
* from instances of the annotated class when given one of the {@link PropertyDef}s returned by
* {@link #getPropertyDefs}.
*
* @see #getPropertyDefs
*/
public SortingPropertyExtractor getPropertyExtractor() {
return this.propertyExtractor;
}
private String getPropertyName(MethodAnnotationScanner.MethodInfo methodInfo) {
return methodInfo.getAnnotation().value().length() > 0 ?
methodInfo.getAnnotation().value() : methodInfo.getMethodPropertyName();
}
private String getSortPropertyName(MethodAnnotationScanner.MethodInfo methodInfo) {
return methodInfo.getAnnotation().value().length() > 0 ?
methodInfo.getAnnotation().value() : methodInfo.getMethodPropertyName();
}
// This method exists solely to bind the generic type
private AnnotationPropertyDef createAnnotationPropertyDef(String propertyName, Class propertyType,
Object defaultValue, MethodAnnotationScanner.MethodInfo methodInfo,
MethodAnnotationScanner.MethodInfo sortMethodInfo) {
return new AnnotationPropertyDef(propertyName, propertyType,
propertyType.cast(defaultValue), methodInfo, sortMethodInfo);
}
// Compare two methods to determine which one has the declaring class that is a sub-type of the other's
private int compareDeclaringClass(Method method1, Method method2) {
final Class> class1 = method1.getDeclaringClass();
final Class> class2 = method2.getDeclaringClass();
if (class1 == class2)
return 0;
if (class1.isAssignableFrom(class2))
return 1;
if (class2.isAssignableFrom(class1))
return -1;
throw new RuntimeException("internal error: incomparable classes " + class1.getName() + " and " + class2.getName());
}
private class AnnotationPropertyDef extends PropertyDef {
private static final long serialVersionUID = 4983663265225248971L;
private final MethodAnnotationScanner.MethodInfo methodInfo;
private final MethodAnnotationScanner.MethodInfo sortMethodInfo;
private Comparator comparator;
AnnotationPropertyDef(String name, Class type, V defaultValue,
MethodAnnotationScanner.MethodInfo methodInfo,
MethodAnnotationScanner.MethodInfo sortMethodInfo) {
super(name, type, defaultValue);
this.methodInfo = methodInfo;
this.sortMethodInfo = sortMethodInfo;
}
public MethodAnnotationScanner.MethodInfo getMethodInfo() {
return this.methodInfo;
}
public MethodAnnotationScanner.MethodInfo getSortMethodInfo() {
return this.sortMethodInfo;
}
public Comparator getComparator() {
// Sanity check
if (this.sortMethodInfo == null)
throw new UnsupportedOperationException("can't sort property " + this);
// Already created?
if (this.comparator != null)
return this.comparator;
// Handle Comparator case
if (Comparator.class.isAssignableFrom(this.sortMethodInfo.getMethod().getReturnType())) {
this.comparator = new Comparator() {
private Comparator comparator;
@Override
@SuppressWarnings("unchecked")
public int compare(T obj1, T obj2) {
if (this.comparator == null)
this.comparator = (Comparator)AnnotationPropertyDef.this.sortMethodInfo.invoke(obj1);
return this.comparator.compare(obj1, obj2);
}
};
return this.comparator;
}
// Handle Comparable case
this.comparator = new Comparator() {
@Override
@SuppressWarnings("unchecked")
public int compare(T obj1, T obj2) {
final Comparable