/*
* 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);
}
}
}