org.dellroad.stuff.vaadin8.GridColumnScanner Maven / Gradle / Ivy
Show all versions of dellroad-stuff-vaadin8 Show documentation
/*
* Copyright (C) 2022 Archie L. Cobbs. All rights reserved.
*/
package org.dellroad.stuff.vaadin8;
import com.vaadin.data.PropertySet;
import com.vaadin.data.ValueProvider;
import com.vaadin.ui.Grid;
import com.vaadin.ui.renderers.Renderer;
import com.vaadin.util.ReflectTools;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import org.dellroad.stuff.java.AnnotationUtil;
import org.dellroad.stuff.java.MethodAnnotationScanner;
import org.dellroad.stuff.java.ReflectUtil;
/**
* Scans a Java class hierarchy for {@link GridColumn @GridColumn} annotated getter methods, and auto-generates and
* configures corresponding {@link Grid.Column}s on a {@link Grid} instance.
*
*
* This class will also introspect for {@link FieldBuilder} annotations and
* {@linkplain Grid.Column#setEditorBinding(Binder.Binding) configure editor bindings} accordingly via
* {@link FieldBuilder#setEditorBindings}.
*
*
* See {@link GridColumn} for an example of usage.
*
* @param Java class to be introspected
* @see GridColumn @GridColumn
* @see FieldBuilder#setEditorBindings
*/
public class GridColumnScanner {
private final Class type;
private final LinkedHashMap.MethodInfo> columnMap = new LinkedHashMap<>();
/**
* Constructor.
*
* @param type Java class to be introspected
* @throws IllegalArgumentException if {@code type} is null
* @throws IllegalArgumentException if an annotated method with no {@linkplain GridColumn#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 GridColumn @GridColumn}-annotated
* fields or methods with the same {@linkplain GridColumn#value property name}
*/
public GridColumnScanner(Class type) {
// Sanity check
if (type == null)
throw new IllegalArgumentException("null type");
this.type = type;
// Scan for @GridColumn annotations
final Set.MethodInfo> gridColumnMethods
= new MethodAnnotationScanner(this.type, GridColumn.class).findAnnotatedMethods();
// Check for duplicate @GridColumn names
final Comparator methodComparator = Comparator.comparing(Method::getDeclaringClass,
ReflectUtil.getClassComparator(false));
final HashMap.MethodInfo> unorderedColumnMap = new HashMap<>();
for (MethodAnnotationScanner.MethodInfo methodInfo : gridColumnMethods) {
final String propertyName = this.getPropertyName(methodInfo);
// Check for name conflict
final MethodAnnotationScanner.MethodInfo previousInfo = unorderedColumnMap.get(propertyName);
if (previousInfo == null) {
unorderedColumnMap.put(propertyName, methodInfo);
continue;
}
// If there is a name conflict, the sub-type method declaration wins
switch (methodComparator.compare(previousInfo.getMethod(), methodInfo.getMethod())) {
case 0:
throw new IllegalArgumentException("duplicate @" + GridColumn.class.getSimpleName()
+ " declaration for property `" + propertyName + "' on method " + previousInfo.getMethod()
+ " and " + methodInfo.getMethod() + " declared in the same class");
case 1:
unorderedColumnMap.put(propertyName, methodInfo);
break;
default:
break;
}
}
// Order columns
final ArrayList.MethodInfo>> columnList
= new ArrayList<>(unorderedColumnMap.entrySet());
Collections.sort(columnList,
Comparator..MethodInfo>>comparingDouble(
entry -> entry.getValue().getAnnotation().order())
.thenComparing(Map.Entry::getKey));
for (Map.Entry.MethodInfo> entry : columnList)
this.columnMap.put(entry.getKey(), entry.getValue());
}
/**
* Build a {@link Grid} with columns auto-generated from introspected {@link GridColumn @GridColumn} annotations.
*
*
* The implementation in {@link GridColumnScanner} simply invokes {@link #buildGrid() this.buildGrid(Grid::withPropertySet)}.
*
* @return new {@link Grid}
*/
public Grid buildGrid() {
return this.buildGrid(Grid::withPropertySet);
}
/**
* Build a {@link Grid} with columns auto-generated from introspected {@link GridColumn @GridColumn} annotations,
* using the given function to instantiate the {@link Grid}.
*
* @param creator function that creates a new {@link Grid} instance given a {@link PropertySet}
* @param {@link Grid} type
* @return new {@link Grid}
* @throws IllegalArgumentException if {@code creator} is null
*/
public > G buildGrid(Function super PropertySet, G> creator) {
// Sanity check
if (creator == null)
throw new IllegalArgumentException("null creator");
// Build property set
final SimplePropertySet propertySet = new SimplePropertySet<>(this.type);
for (Map.Entry.MethodInfo> e : this.columnMap.entrySet()) {
final String propertyName = e.getKey();
final MethodAnnotationScanner.MethodInfo methodInfo = e.getValue();
final Method getter = methodInfo.getMethod();
final Method setter = methodInfo.getSetter();
propertySet.add(ReflectTools.convertPrimitiveType(getter.getReturnType()),
propertyName, methodInfo.getAnnotation().caption(), getter, setter);
}
// Create grid
final G grid = creator.apply(propertySet);
// Set field editors
new FieldBuilder<>(this.type).buildAndBind().setEditorBindings(grid);
// Get default annotation
final GridColumn defaults = GridColumnScanner.getDefaults();
// Modify columns
for (Map.Entry.MethodInfo> e : this.columnMap.entrySet()) {
final String propertyName = e.getKey();
final MethodAnnotationScanner.MethodInfo methodInfo = e.getValue();
final GridColumn annotation = methodInfo.getAnnotation();
// Get column
final Grid.Column column = grid.getColumn(propertyName);
// Apply annotation values
AnnotationUtil.applyAnnotationValues(column, "set", annotation,
defaults, (methodList, name) -> name, ReflectUtil::instantiate);
// Special handling for setRenderer() method with two parameters
if (annotation.renderer() != defaults.renderer() && annotation.valueProvider() != defaults.valueProvider())
this.setRenderer(column, annotation.renderer(), annotation.valueProvider());
}
// Order columns
grid.setColumns(this.columnMap.keySet().toArray(new String[this.columnMap.size()]));
// Done
return grid;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private void setRenderer(
Grid.Column column, Class rendererType, Class valueProviderType) {
final Renderer renderer = (Renderer
)ReflectUtil.instantiate(rendererType);
final ValueProvider valueProvider = (ValueProvider)ReflectUtil.instantiate(valueProviderType);
column.setRenderer(valueProvider, renderer);
}
@GridColumn
private static GridColumn getDefaults() {
return AnnotationUtil.getAnnotation(GridColumn.class, GridColumnScanner.class, "getDefaults");
}
/**
* Get the property name from the annotation.
*
* @param methodInfo method info
* @return property name
*/
protected String getPropertyName(MethodAnnotationScanner.MethodInfo methodInfo) {
return methodInfo.getAnnotation().value().length() > 0 ?
methodInfo.getAnnotation().value() : methodInfo.getMethodPropertyName();
}
/**
* Get the type associated with this instance.
*
* @return backing object type
*/
public Class getType() {
return this.type;
}
/**
* Get the annotations found through introspection keyed by property name.
*
* @return columns keyed by property name and sorted based on {@link GridColumn#order}, then property name
*/
public Map.MethodInfo> getColumnMap() {
return Collections.unmodifiableMap(this.columnMap);
}
}