/* * Copyright 2001-2008 Geert Bevin <gbevin[remove] at uwyn dot com> and * JR Boyens <gnu-jrb[remove] at gmx dot net> * Licensed under the Apache License, Version 2.0 (the "License") * $Id: GenericQueryManagerRelationalUtils.java 3918 2008-04-14 17:35:35Z gbevin $ */ package com.uwyn.rife.database.querymanagers.generic; import com.uwyn.rife.database.querymanagers.generic.exceptions.*; import java.util.*; import com.uwyn.rife.database.exceptions.DatabaseException; import com.uwyn.rife.site.Constrained; import com.uwyn.rife.site.ConstrainedProperty; import com.uwyn.rife.site.ConstrainedUtils; import com.uwyn.rife.tools.BeanPropertyProcessor; import com.uwyn.rife.tools.BeanUtils; import com.uwyn.rife.tools.ClassUtils; import com.uwyn.rife.tools.JavaSpecificationUtils; import com.uwyn.rife.tools.exceptions.BeanUtilsException; import java.beans.PropertyDescriptor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * Utility class to provide many-to-many and many-to-one relational * capabilities to generic query manager implementations. * * @author Geert Bevin (gbevin[remove] at uwyn dot com) * @version $Revision: 3918 $ * @since 1.6 */ public abstract class GenericQueryManagerRelationalUtils { /** * Restores a constrained many-to-one property value that is lazily loaded. * * @param manager the {@code GenericQueryManager} that will be used to * restore the related bean instance * @param constrained the constrained bean instance that contains the * property whose value will be restored * @param propertyName the name of the property value that will be restored * @param propertyTypeClassName the class name of the property that will be * restored * @return the value of the property, or * <p>{@code null} if the constrained property doesn't exists or if it * didn't have the {@code manyToOne} constraint * @since 1.6 */ public static Object restoreLazyManyToOneProperty(GenericQueryManager manager, Constrained constrained, String propertyName, String propertyTypeClassName) { Object result = null; ConstrainedProperty property = constrained.getConstrainedProperty(propertyName); // only lazily load the property value if a constrained property has been found if (property != null) { // only consider a constrained property with a many-to-one constraint if (property.hasManyToOne()) { // try to obtain the class for the property's type Class return_type = null; try { return_type = Class.forName(propertyTypeClassName); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } // obtain the associated class from the many-to-one constraint Class associated_class = property.getManyToOne().getAssociatedClass(); // if the associated class wasn't specified, use the property's type if (null == associated_class) { associated_class = return_type; } else { // ensure that the specified associated class is compatible with the property's type if (!return_type.isAssignableFrom(associated_class)) { throw new IncompatibleManyToOneValueTypeException(manager.getBaseClass(), propertyName, return_type, associated_class); } } // retrieve the entity from the database for this property ManyToOneDeclaration declaration = createManyToOneDeclaration(manager, property, return_type); if (!declaration.isBasic()) { String column_name = generateManyToOneJoinColumnName(property.getPropertyName(), declaration); result = restoreManyToOneProperty(manager, constrained, declaration.getAssociationManager(), column_name, associated_class); } } } return result; } public static Object restoreManyToOneProperty(GenericQueryManager manager, Object constrained, GenericQueryManager associationManager, String columnName, Class propertyType) { RestoreQuery query = associationManager.getRestoreQuery() .fields(associationManager.getTable(), propertyType) .join(manager.getTable()) .where(associationManager.getTable()+"."+associationManager.getIdentifierName()+" = "+manager.getTable()+"."+columnName) .whereAnd(manager.getTable()+"."+manager.getIdentifierName(), "=", manager.getIdentifierValue(constrained)); return associationManager.restoreFirst(query); } public static ManyToOneDeclaration createManyToOneDeclaration(GenericQueryManager manager, ConstrainedProperty property, Class propertyType) throws IncompatibleManyToOneValueTypeException { ManyToOneDeclaration declaration = null; if (property != null && property.hasManyToOne()) { ConstrainedProperty.ManyToOne many_to_one = property.getManyToOne(); declaration = new ManyToOneDeclaration() .associationType(many_to_one.getAssociatedClass()) .associationColumn(many_to_one.getColumn()); // fall back to the associated class in case the property type wasn't provided if (null == propertyType) { propertyType = declaration.getAssociationType(); } // detect whether the property is a basic type, or if its type is unknown // it will be treated as a primary key and not an object instance of data that's // stored in the associated table if (null == propertyType || ClassUtils.isBasic(propertyType)) { declaration .isBasic(true) .associationTable(many_to_one.getDerivedTable()); } else { declaration .isBasic(false) .associationTable(many_to_one.getTable()); // retrieve the class that has been associated to the property through constraints Class associated_class = declaration.getAssociationType(); // if an associated class has already been specified through constraints, // ensure that it's compatible with the type of the property if (associated_class != null) { if (!propertyType.isAssignableFrom(associated_class)) { throw new IncompatibleManyToOneValueTypeException(manager.getBaseClass(), property.getName(), propertyType, associated_class); } } // since no associated class has been specified yet, use the property type else { declaration.setAssociationType(propertyType); } // create the association query manager GenericQueryManager association_manager = manager.createNewManager(declaration.getAssociationType()); declaration.setAssociationManager(association_manager); // determine the association table if (null == declaration.getAssociationTable()) { declaration.setAssociationTable(association_manager.getTable()); } // determine the association column name if (null == declaration.getAssociationColumn()) { declaration.setAssociationColumn(association_manager.getIdentifierName()); } } } return declaration; } public static Map<String, ManyToOneDeclaration> obtainManyToOneDeclarations(final GenericQueryManager manager, final Constrained constrained, final String fixedMainProperty, final Class fixedAssocationType) { final Map<String, ManyToOneDeclaration> declarations; if (constrained != null && constrained.hasPropertyConstraint(ConstrainedProperty.MANY_TO_ONE)) { declarations = new LinkedHashMap<String, ManyToOneDeclaration>(); // collect all properties that have a many-to-one relationship final List<String> property_names = new ArrayList<String>(); if (fixedMainProperty != null) { ConstrainedProperty property = constrained.getConstrainedProperty(fixedMainProperty); if (property.hasManyToOne()) { property_names.add(property.getPropertyName()); } } else { for (ConstrainedProperty property : (Collection<ConstrainedProperty>)constrained.getConstrainedProperties()) { if (property.hasManyToOne()) { property_names.add(property.getPropertyName()); } } } // obtain the actual bean properties for the many-to-one relationships if (property_names.size() > 0) { String[] unresolvednamearray = new String[property_names.size()]; property_names.toArray(unresolvednamearray); try { BeanUtils.processProperties(manager.getBaseClass(), unresolvednamearray, null, null, new BeanPropertyProcessor() { public boolean gotProperty(String name, PropertyDescriptor descriptor) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { ConstrainedProperty property = constrained.getConstrainedProperty(name); ManyToOneDeclaration declaration = createManyToOneDeclaration(manager, property, descriptor.getReadMethod().getReturnType()); if (declaration != null) { if (null == fixedAssocationType || fixedAssocationType == declaration.getAssociationType()) { declarations.put(property.getPropertyName(), declaration); if (fixedAssocationType != null) { return false; } } } return true; } }); } catch (BeanUtilsException e) { throw new DatabaseException(e); } } } else { declarations = null; } return declarations; } public static Map<String, ManyToManyDeclaration> obtainManyToManyDeclarations(final GenericQueryManager manager, Constrained constrained, boolean includeAssociations) { final Map<String, ManyToManyDeclaration> declarations; if (constrained != null && (constrained.hasPropertyConstraint(ConstrainedProperty.MANY_TO_MANY) || includeAssociations && constrained.hasPropertyConstraint(ConstrainedProperty.MANY_TO_MANY_ASSOCIATION))) { declarations = new HashMap<String, ManyToManyDeclaration>(); // collect all properties that have a many-to-many relationship final List<String> unresolvednamelist = new ArrayList<String>(); for (ConstrainedProperty property : (Collection<ConstrainedProperty>)constrained.getConstrainedProperties()) { if (property.hasManyToMany()) { declarations.put(property.getPropertyName(), new ManyToManyDeclaration() .associationType(property.getManyToMany().getAssociatedClass())); unresolvednamelist.add(property.getPropertyName()); } else if (includeAssociations && property.hasManyToManyAssociation()) { declarations.put(property.getPropertyName(), new ManyToManyDeclaration() .reversed(true) .associationType(property.getManyToManyAssociation().getAssociatedClass())); unresolvednamelist.add(property.getPropertyName()); } } // obtain the actual bean properties for the many-to-many relationships if (unresolvednamelist.size() > 0) { String[] unresolvednamearray = new String[unresolvednamelist.size()]; unresolvednamelist.toArray(unresolvednamearray); try { BeanUtils.processProperties(manager.getBaseClass(), unresolvednamearray, null, null, new BeanPropertyProcessor() { public boolean gotProperty(String name, PropertyDescriptor descriptor) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { Method read_method = descriptor.getReadMethod(); ManyToManyDeclaration declaration = declarations.get(name); // make sure that the many-to-many property has a supported collection type Class return_type = read_method.getReturnType(); ensureSupportedManyToManyPropertyCollectionType(manager.getBaseClass(), name, return_type); declaration.setCollectionType(return_type); // check if the class of the relationship has already been set, otherwise detect it from // the generic information that's available in the collection if (null == declaration.getAssociationType()) { Class associated_class = null; if (JavaSpecificationUtils.isAtLeastJdk15()) { try { Class klass = Class.forName("com.uwyn.rife.database.querymanagers.generic.GenericTypeDetector"); Method method = klass.getDeclaredMethod("detectAssociatedClass", Method.class); associated_class = (Class)method.invoke(null, read_method); } catch (Exception e) { throw new DatabaseException(e); } } if (null == associated_class) { throw new MissingManyToManyTypeInformationException(manager.getBaseClass(), name); } declaration.setAssociationType(associated_class); } return false; } }); } catch (BeanUtilsException e) { throw new DatabaseException(e); } } } else { declarations = null; } return declarations; } public static Map<String, ManyToOneAssociationDeclaration> obtainManyToOneAssociationDeclarations(final GenericQueryManager manager, Constrained constrained) { final Map<String, ManyToOneAssociationDeclaration> declarations; if (constrained != null && constrained.hasPropertyConstraint(ConstrainedProperty.MANY_TO_ONE_ASSOCIATION)) { declarations = new HashMap<String, ManyToOneAssociationDeclaration>(); // collect all properties that have a many-to-one association relationship final List<String> unresolvednamelist = new ArrayList<String>(); for (ConstrainedProperty property : (Collection<ConstrainedProperty>)constrained.getConstrainedProperties()) { if (property.hasManyToOneAssociation()) { ConstrainedProperty.ManyToOneAssociation association = property.getManyToOneAssociation(); declarations.put(property.getPropertyName(), new ManyToOneAssociationDeclaration() .mainType(association.getMainClass()) .mainProperty(association.getMainProperty())); unresolvednamelist.add(property.getPropertyName()); } } // obtain the actual bean properties for the many-to-one association relationships if (unresolvednamelist.size() > 0) { String[] unresolvednamearray = new String[unresolvednamelist.size()]; unresolvednamelist.toArray(unresolvednamearray); try { BeanUtils.processProperties(manager.getBaseClass(), unresolvednamearray, null, null, new BeanPropertyProcessor() { public boolean gotProperty(String name, PropertyDescriptor descriptor) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { Method read_method = descriptor.getReadMethod(); ManyToOneAssociationDeclaration declaration = declarations.get(name); // make sure that the many-to-one association property has a supported collection type Class return_type = read_method.getReturnType(); ensureSupportedManyToOneAssociationPropertyCollectionType(manager.getBaseClass(), name, return_type); declaration.setCollectionType(return_type); // check if the class of the relationship has already been set, otherwise detect it from // the generic information that's available in the collection if (null == declaration.getMainType()) { Class associated_class = null; if (JavaSpecificationUtils.isAtLeastJdk15()) { try { Class klass = Class.forName("com.uwyn.rife.database.querymanagers.generic.GenericTypeDetector"); Method method = klass.getDeclaredMethod("detectAssociatedClass", Method.class); associated_class = (Class)method.invoke(null, read_method); } catch (Exception e) { throw new DatabaseException(e); } } if (null == associated_class) { throw new MissingManyToOneAssociationTypeInformationException(manager.getBaseClass(), name); } declaration.setMainType(associated_class); } // obtain the main declaration Constrained main_constrained = ConstrainedUtils.getConstrainedInstance(declaration.getMainType()); GenericQueryManager main_manager = manager.createNewManager(declaration.getMainType()); Map<String, ManyToOneDeclaration> main_declarations = obtainManyToOneDeclarations(main_manager, main_constrained, declaration.getMainProperty(), manager.getBaseClass()); if (null == main_declarations || 0 == main_declarations.size()) { throw new MissingManyToOneMainPropertyException(manager.getBaseClass(), name, declaration.getMainType()); } else { Map.Entry<String, ManyToOneDeclaration> main_entry = main_declarations.entrySet().iterator().next(); declaration .mainProperty(main_entry.getKey()) .mainDeclaration(main_entry.getValue()); } return false; } }); } catch (BeanUtilsException e) { throw new DatabaseException(e); } } } else { declarations = null; } return declarations; } public static String generateManyToManyJoinTableName(ManyToManyDeclaration assocation, GenericQueryManager manager1, GenericQueryManager manager2) { if (assocation.isReversed()) { return manager2.getTable() + "_" + manager1.getTable(); } else { return manager1.getTable() + "_" + manager2.getTable(); } } public static String generateManyToManyJoinColumnName(GenericQueryManager manager) { return manager.getTable() + "_" + manager.getIdentifierName(); } public static String generateManyToOneJoinColumnName(String mainPropertyName, ManyToOneDeclaration declaration) { return mainPropertyName + "_" + declaration.getAssociationColumn(); } public static void processManyToOneJoinColumns(GenericQueryManager manager, ManyToOneJoinColumnProcessor processor) { if (null == processor) { return; } // generate many-to-one join columns Constrained constrained = ConstrainedUtils.getConstrainedInstance(manager.getBaseClass()); if (constrained != null && constrained.hasPropertyConstraint(ConstrainedProperty.MANY_TO_ONE)) { Map<String, ManyToOneDeclaration> manytoone_declarations = obtainManyToOneDeclarations(manager, constrained, null, null); if (manytoone_declarations != null) { // iterate over all the many-to-one relationships that have associated classes for (Map.Entry<String, ManyToOneDeclaration> entry : manytoone_declarations.entrySet()) { ManyToOneDeclaration declaration = entry.getValue(); if (!declaration.isBasic()) { String column_name = generateManyToOneJoinColumnName(entry.getKey(), declaration); if (!processor.processJoinColumn(column_name, entry.getKey(), declaration)) { break; } } } } } } public static void ensureSupportedManyToManyPropertyCollectionType(Class beanClass, String propertyName, Class propertyType) throws UnsupportedManyToManyPropertyTypeException { if (!(propertyType == Collection.class || propertyType == Set.class || propertyType == List.class)) { throw new UnsupportedManyToManyPropertyTypeException(beanClass, propertyName, propertyType); } } public static void ensureSupportedManyToManyPropertyValueType(Class beanClass, String propertyName, Object propertyValue) throws UnsupportedManyToManyPropertyTypeException { if (!(propertyValue instanceof Collection)) { throw new UnsupportedManyToManyValueTypeException(beanClass, propertyName, propertyValue); } } public static void ensureSupportedManyToOneAssociationPropertyCollectionType(Class beanClass, String propertyName, Class propertyType) throws UnsupportedManyToManyPropertyTypeException { if (!(propertyType == Collection.class || propertyType == Set.class || propertyType == List.class)) { throw new UnsupportedManyToOneAssociationPropertyTypeException(beanClass, propertyName, propertyType); } } public static void ensureSupportedManyToOneAssociationPropertyValueType(Class beanClass, String propertyName, Object propertyValue) throws UnsupportedManyToManyPropertyTypeException { if (!(propertyValue instanceof Collection)) { throw new UnsupportedManyToOneValueTypeException(beanClass, propertyName, propertyValue); } } }