blob: 45f3f62e59bdebe92c52169290d50324e1a3b9fb [file] [log] [blame]
/*****************************************************************************
* Copyright (c) 2018, 2020 Dirk Fauth.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Dirk Fauth <dirk.fauth@googlemail.com> - Initial API and implementation
*****************************************************************************/
package org.eclipse.nebula.widgets.nattable.hierarchical;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
/**
* Helper class to de-normalize a collection of objects with hierarchical object
* relations into a simple collection.
*
* @see HierarchicalWrapper
*
* @since 1.6
*/
public final class HierarchicalHelper {
private static final Object lockObj = new Object();
private HierarchicalHelper() {
// empty default constructor for helper class
}
/**
* The regular expression that is used to separate properties for accessing
* fields on different levels of a {@link HierarchicalWrapper}.
*/
public static final String PROPERTY_SEPARATOR_REGEX = "\\."; //$NON-NLS-1$
/**
* De-normalizes a given list of objects with a hierarchical object graph to
* a flattened list. The flattened list therefore contains redundant data
* for each entry that has the same parent objects. It will created
* {@link HierarchicalWrapper} objects for this, which contain references to
* the original objects. Therefore property changes will be directly
* reflected in the hierarchical object graph.
* <p>
* To identify the nested objects, the property names are inspected if they
* contain dots for hierarchical object paths. For example
* <code>parent.children.name</code> says that the parent has a property
* that is a collection (children) where each entry in the children
* collection is an object that has a property name. In this example there
* will be an entry in the flattened list for each child, where the parent
* is repeated in each wrapper object.
* </p>
*
* @param input
* The collection of objects with a hierarchical object graph.
* @param addParentObject
* <code>true</code> to add row objects for parent objects that
* do no have object references for deeper levels,
* <code>false</code> to have no dedicated row object for parent
* objects.
* @param propertyNames
* The property names to access the properties in the object
* graph.
* @return The flattened list of the hierarchical object graph.
*/
public static List<HierarchicalWrapper> deNormalize(List<?> input, boolean addParentObject, String... propertyNames) {
return deNormalize(input, addParentObject, Arrays.asList(propertyNames));
}
/**
* De-normalizes a given list of objects with a hierarchical object graph to
* a flattened list. The flattened list therefore contains redundant data
* for each entry that has the same parent objects. It will created
* {@link HierarchicalWrapper} objects for this, which contain references to
* the original objects. Therefore property changes will be directly
* reflected in the hierarchical object graph.
* <p>
* To identify the nested objects, the property names are inspected if they
* contain dots for hierarchical object paths. For example
* <code>parent.children.name</code> says that the parent has a property
* that is a collection (children) where each entry in the children
* collection is an object that has a property name. In this example there
* will be an entry in the flattened list for each child, where the parent
* is repeated in each wrapper object.
* </p>
*
* @param input
* The collection of objects with a hierarchical object graph.
* @param addParentObject
* <code>true</code> to add row objects for parent objects that
* do no have object references for deeper levels,
* <code>false</code> to have no dedicated row object for parent
* objects.
* @param propertyNames
* The property names to access the properties in the object
* graph.
* @return The flattened list of the hierarchical object graph.
*/
public static List<HierarchicalWrapper> deNormalize(List<?> input, boolean addParentObject, List<String> propertyNames) {
ArrayList<HierarchicalWrapper> result = new ArrayList<>();
if (input != null) {
LinkedHashSet<String> nested = new LinkedHashSet<>();
for (String name : propertyNames) {
String[] prop = name.split(PROPERTY_SEPARATOR_REGEX);
if (prop.length > 1) {
nested.add(prop[prop.length - 2]);
}
}
String[] nestedArray = nested.toArray(new String[] {});
for (Object root : input) {
if (root != null) {
// length + 1 because of root
HierarchicalWrapper rootWrapper = new HierarchicalWrapper(nestedArray.length + 1);
rootWrapper.setObject(0, root);
result.addAll(deNormalizeWithDirectChildren(rootWrapper, 0, nestedArray,
new HashMap<>(), addParentObject));
}
}
}
return result;
}
/**
* De-normalizes the next level in the object graph.
*
* @param parent
* The already available {@link HierarchicalWrapper} with the
* parent levels resolved. Will be cloned for de-normalizing
* deeper levels.
* @param level
* The current level of de-normalization.
* @param nested
* The properties of the collections that specify the object
* graph without the parent properties.
* @param propertyDescriptorMap
* The map of already identified {@link PropertyDescriptor}s, to
* avoid multiple lookups via reflection.
* @param addParentObject
* <code>true</code> if the already available
* {@link HierarchicalWrapper} with the resolved parent levels
* should be added to the result, <code>false</code> if not.
* @return The flattened list of the object at the given level with regards
* to the nested objects, or a collection with only the given parent
* object if that object has not children.
*/
private static List<HierarchicalWrapper> deNormalizeWithDirectChildren(HierarchicalWrapper parent, int level,
String[] nested, Map<Class<?>, Map<String, PropertyDescriptor>> propertyDescriptorMap, boolean addParentObject) {
if (level < nested.length) {
Object child = getDataValue(parent.getObject(level), nested[level], propertyDescriptorMap);
if (child instanceof Collection<?>) {
ArrayList<HierarchicalWrapper> result = new ArrayList<>();
Collection<?> children = (Collection<?>) child;
if (addParentObject) {
result.add(parent);
}
for (Object root : children) {
HierarchicalWrapper rootWrapper = new HierarchicalWrapper(parent);
rootWrapper.setObject(level + 1, root);
result.addAll(deNormalizeWithDirectChildren(rootWrapper, level + 1, nested, propertyDescriptorMap, addParentObject));
}
return result;
}
}
return Arrays.asList(parent);
}
/**
* Get the data value for the given property name out of the given object
* via reflection.
*
* @param rowObj
* The row object from which the data value should be read.
* @param propertyName
* The name of the property that should be read.
* @param propertyDescriptorMap
* The map of already identified {@link PropertyDescriptor}s, to
* avoid multiple lookups via reflection.
* @return The data value for the given property name out of the given
* object.
*/
private static Object getDataValue(Object rowObj, String propertyName,
Map<Class<?>, Map<String, PropertyDescriptor>> propertyDescriptorMap) {
try {
PropertyDescriptor propertyDesc = getPropertyDescriptor(rowObj, propertyName, propertyDescriptorMap);
Method readMethod = propertyDesc.getReadMethod();
return readMethod.invoke(rowObj);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
/**
* Retrieves the {@link PropertyDescriptor} for the given object and
* property name.
*
* @param rowObj
* The object for which the {@link PropertyDescriptor} is
* requested.
* @param propertyName
* The name of the property for which the
* {@link PropertyDescriptor} is requested.
* @param propertyDescriptorMap
* The map of already identified {@link PropertyDescriptor}s, to
* avoid multiple lookups via reflection.
* @return The {@link PropertyDescriptor} for the property with the given
* name in the given object.
* @throws IntrospectionException
* if an exception occurs during introspection
*/
private static PropertyDescriptor getPropertyDescriptor(Object rowObj, String propertyName,
Map<Class<?>, Map<String, PropertyDescriptor>> propertyDescriptorMap) throws IntrospectionException {
synchronized (lockObj) {
Map<String, PropertyDescriptor> descriptorMap = propertyDescriptorMap.get(rowObj.getClass());
if (descriptorMap == null) {
PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(rowObj.getClass())
.getPropertyDescriptors();
Map<String, PropertyDescriptor> propertiesByAttribute = new HashMap<>();
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
propertiesByAttribute.put(propertyDescriptor.getName(), propertyDescriptor);
}
descriptorMap = propertiesByAttribute;
propertyDescriptorMap.put(rowObj.getClass(), propertiesByAttribute);
}
return descriptorMap.get(propertyName);
}
}
/**
* Returns the level object from the given {@link HierarchicalWrapper}
* object by the given property name.
*
* @param wrapper
* The {@link HierarchicalWrapper} from which the level object
* should be retrieved.
* @param propertyName
* The name of the property to identify the level object from.
* @return The level object from the {@link HierarchicalWrapper} according
* to the given property name.
*/
public static Object getLevelObjectByProperty(HierarchicalWrapper wrapper, String propertyName) {
String[] split = propertyName.split(PROPERTY_SEPARATOR_REGEX);
return wrapper.getObject(split.length - 1);
}
/**
* Calculates the mapping of hierarchy levels to column indexes belonging to
* the level.
*
* @param propertyNames
* The property names to access the properties in the object
* graph.
* @return The mapping of the level to the list of all column indexes
* belonging to a level.
*/
public static Map<Integer, List<Integer>> getLevelIndexMapping(String[] propertyNames) {
LinkedHashMap<Integer, List<Integer>> levelIndexMapping = new LinkedHashMap<>();
if (propertyNames.length > 0) {
int currentLevel = 1;
ArrayList<Integer> columns = new ArrayList<>();
columns.add(0);
levelIndexMapping.put(0, columns);
for (int col = 1; col < propertyNames.length; col++) {
String[] split = propertyNames[col].split(PROPERTY_SEPARATOR_REGEX);
if (split.length == currentLevel) {
columns.add(col);
} else if (split.length > currentLevel) {
columns = new ArrayList<>();
columns.add(col);
levelIndexMapping.put(currentLevel, columns);
currentLevel++;
}
}
}
return levelIndexMapping;
}
}