/*
$Header: /cvsroot/xorm/xorm/src/org/xorm/ClassMapping.java,v 1.43 2004/05/30 08:45:52 wbiggs Exp $
This file is part of XORM.
XORM is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
XORM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with XORM; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.xorm;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import javax.jdo.JDOFatalUserException;
import javax.jdo.JDOUserException;
import javax.jdo.spi.JDOImplHelper;
import org.xorm.datastore.Table;
import org.xorm.datastore.Column;
import org.xorm.datastore.DataFetchGroup;
import org.xorm.util.FieldDescriptor;
import org.xorm.util.jdoxml.JDOClass;
import org.xorm.util.jdoxml.JDOField;
/**
* A ClassMapping provides the mapping from a Java type (class or interface)
* to a datastore type. Each field of a Class can map to a Column in
* a Table, or may be mapped as a RelationshipMapping.
*
* A ClassMapping is also the repository for default fetch group
* information.
*/
public class ClassMapping implements I15d, Cloneable {
private ModelMapping modelMapping;
Class clazz;
private Table table;
private Class datastoreIdentityType;
private Map fieldToColumn = new HashMap();
private Map methodToProperty = new HashMap();
private Map relationships = new HashMap();
private Map inverses = new HashMap();
private Set managedFields = new HashSet(); // contains String field names
private Collection fields; // contains FieldDescriptors
private Set mappedFields = new HashSet();
private DataFetchGroup defaultFetchGroup = new DataFetchGroup();
/** Creates a shallow clone. */
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
// ignored
}
return null;
}
/**
* Returns true if the class parameter is not a primitive type,
* a wrapper class for a primitive type, or java.util.Date,
* java.lang.String, java.util.Locale, java.math.BigDecimal,
* or java.math.BigInteger.
*/
public static boolean isUserType(Class clazz) {
if (clazz.isArray()) {
clazz = clazz.getComponentType();
}
return (!clazz.isPrimitive()
&& !clazz.equals(Boolean.class)
&& !clazz.equals(Character.class)
&& !clazz.equals(Double.class)
&& !clazz.equals(Float.class)
&& !clazz.equals(Integer.class)
&& !clazz.equals(Long.class)
&& !clazz.equals(Short.class)
&& !clazz.equals(String.class)
&& !clazz.equals(BigDecimal.class)
&& !clazz.equals(BigInteger.class)
&& !clazz.equals(Date.class)
&& !clazz.equals(Timestamp.class)
&& !clazz.equals(Time.class)
&& !clazz.equals(Locale.class)
);
}
/**
* Creates a new ClassMapping that maps the given Class. The
* class will be introspected and assigned relationships for all
* properties that are of a user-defined (non-collection) type.
*/
public ClassMapping(ModelMapping modelMapping, Class clazz) {
this.clazz = clazz;
this.modelMapping = modelMapping;
if (clazz.isInterface() || ((clazz.getModifiers() & Modifier.ABSTRACT) != 0)) {
fields = FieldDescriptor.getFieldDescriptors(clazz);
} else {
JDOImplHelper helper = JDOImplHelper.getInstance();
String[] names = helper.getFieldNames(clazz);
Class[] types = helper.getFieldTypes(clazz);
fields = new ArrayList();
for (int i = 0; i < names.length; i++) {
fields.add(new FieldDescriptor(names[i], types[i]));
}
}
init();
}
/**
*@see #ClassMapping(ModelMapping, Class)
*/
public ClassMapping(ModelMapping modelMapping, Class clazz, JDOClass jdoClass) {
this.clazz = clazz;
this.modelMapping = modelMapping;
fields = loadFieldDescriptors(clazz, jdoClass);
init();
}
/**
* Common code for both constructors.
*/
private void init(){
defaultFetchGroup = new DataFetchGroup();
Iterator i = fields.iterator();
while (i.hasNext()) {
FieldDescriptor fd = (FieldDescriptor) i.next();
// This is a kluge for now, maybe require explicit config on this?
if (isUserType(fd.type)) {
RelationshipMapping implicit = new RelationshipMapping();
RelationshipMapping.Endpoint end1 = new RelationshipMapping.Endpoint();
end1.setElementClass(fd.type);
implicit.setSource(end1);
relationships.put(fd.name, implicit);
}
Method read = fd.readMethod;
if (read != null) {
methodToProperty.put(read.getName(), fd);
}
Method write = fd.writeMethod;
if (write != null) {
methodToProperty.put(write.getName(), fd);
}
}
}
/**
* Defines the table that will be used to map the class.
* If table is null, defaults will be applied to create a mapping for
* the table. This will include a datastore identity column called
* "xorm_pk"; other column names will be the same as the field name.
*/
public void setTable(Table table) {
boolean fakeTable = false;
Column pk;
if (table == null) {
fakeTable = true;
table = new Table(clazz.getName());
pk = new Column(table, "xorm_pk");
table.addColumn(pk);
table.setPrimaryKey(pk);
Iterator i = fields.iterator();
while (i.hasNext()) {
FieldDescriptor fd = (FieldDescriptor) i.next();
Column c = new Column(table, fd.name);
table.addColumn(c);
fieldToColumn.put(fd.name, c);
}
} else {
pk = table.getPrimaryKey();
if (pk == null) {
throw new JDOFatalUserException(I18N.msg("E_mapping_no_pk", table.getName()));
}
}
defaultFetchGroup.addColumn(pk);
this.table = table;
}
/**
* Get the map of relationships. The keys are Strings and the values
* are RelationshipMapping objects. A relationship is defined as a
* -to-one or -to-many relationship to another first class user object.
*/
public Map getRelationships() {
return relationships;
}
public DataFetchGroup getDefaultFetchGroup() {
return defaultFetchGroup;
}
/**
* Gets the set of FieldDescriptors that have column mappings defined
* in the JDO metadata file.
*/
public Set getMappedFieldDescriptors() {
return mappedFields;
}
/**
* Gets the field descriptors found by introspection on the class
* or interface. This includes primitive types, user types, and
* collection references. The returned items are instances of
* org.xorm.FieldDescriptor. Not all fields found by introspection
* are required to be mapped.
*/
public Collection getFieldDescriptors() {
return fields;
}
/**
* Returns the set of managed field names. This set is the union of
* fields that are mapped and fields that are defined as relationships.
*/
public Set getManagedFields() {
return managedFields;
}
public String getInverse(String field) {
return (String) inverses.get(field);
}
public void setInverse(String localField, String inverseField) {
inverses.put(localField, inverseField);
}
/**
* Retrieves the field associated with a get or set method.
* Returns null if there is no mapping.
*/
public String getFieldForMethod(Method method) {
String methodName = method.getName();
FieldDescriptor fd = (FieldDescriptor) methodToProperty.get(methodName);
if (fd == null) return null;
return fd.name;
}
/**
* Convenience method that gets the Column mapped to a particular
* get or set method. Returns null for unmapped methods.
*/
public Column getColumnForMethod(Method method) {
return getColumn(getFieldForMethod(method));
}
/** Get the table the class is mapped to. */
public Table getTable() {
return table;
}
/** Get the class that this mapping references. */
public Class getMappedClass() {
return clazz;
}
/** Get the Column mapped to the given field. */
public Column getColumn(String field) {
if ("this".equals(field)) {
return table.getPrimaryKey();
}
return (Column) fieldToColumn.get(field);
}
/**
* Set the Column mapped to the given field.
*
* @exception JDOUserException if the field does not exist, or if
* the field has a set() method but the column is marked as read-only.
*/
public void setColumn(String field, Column column, Boolean inDefaultFetchGroup) {
FieldDescriptor fd = getFieldDescriptor(field);
if (column.isReadOnly() && (fd.writeMethod != null)) {
throw new JDOUserException(I18N.msg("E_setter_ro_column", new Object[] {
column.getName(), clazz.getName(), field }));
}
mappedFields.add(fd);
fieldToColumn.put(field, column);
managedFields.add(field);
// The column selections for the default fetch group follow
// the JDO defaults -- all non-user-defined types are included
// unless specifically set to default-fetch-group="false".
// For user-defined types (First-Class Objects), the column itself
// is included by default (the foreign key, that is), but the
// target object is not resolved unless default-fetch-group="true"
if (!Boolean.FALSE.equals(inDefaultFetchGroup)) {
defaultFetchGroup.addColumn(column);
if (isUserType(fd.type) && Boolean.TRUE.equals(inDefaultFetchGroup)) {
ClassMapping target = modelMapping.getClassMapping(fd.type);
defaultFetchGroup.addSubgroup(column, target.getDefaultFetchGroup());
}
}
}
/**
* Define a to-many relationship on a field.
*
* @exception JDOUserException if the field does not exist
*/
public void setRelationship(String field, RelationshipMapping relation) {
FieldDescriptor fd = getFieldDescriptor(field); // assertion
relationships.put(field, relation);
managedFields.add(field);
}
/**
* Retrieves the FieldDescriptor associated with the given field
* name by iterating through the collection of all FieldDescriptors
* for this class.
*
* @exception JDOUserException if the named field does not exist.
*/
public FieldDescriptor getFieldDescriptor(String field) {
Iterator i = fields.iterator();
while (i.hasNext()) {
FieldDescriptor fd = (FieldDescriptor) i.next();
if (fd.name.equals(field)) return fd;
}
throw new JDOUserException(I18N.msg("E_no_field", new Object[] { field, clazz.getName() }));
}
/**
* Get the relationship mapped to a field. If no relationship
* exists, returns null.
*/
public RelationshipMapping getRelationship(String field) {
return (RelationshipMapping) relationships.get(field);
}
/**
* Convenience method to go from a get/set method to the
* associated relationship mapping.
*/
public RelationshipMapping getRelationshipForMethod(Method method) {
String methodName = method.getName();
FieldDescriptor fd = (FieldDescriptor) methodToProperty.get(methodName);
if (fd == null) return null;
return (RelationshipMapping) relationships.get(fd.name);
}
/** Returns the Java type of the field. */
public Class getFieldType(String field) {
return getRelationship(field).getSource().getElementClass();
}
/**
* Returns the Class specified as the identity-type for a class mapping,
* or null if none was specified.
*/
public Class getDatastoreIdentityType() {
return datastoreIdentityType;
}
public void setDatastoreIdentityType(Class datastoreIdentityType) {
this.datastoreIdentityType = datastoreIdentityType;
}
/**
* Returns a Collection of all FieldDescriptors that are configured as
* persistent. The Class argument may be an interface or class. Declared
* interfaces will be used too.
* Only fields configured in JDO mapping file are taken into account.
* @author radek radzimir@polbox.com - 12.08.2003
* @return a Collection<FieldDescriptor>
* @see findLocalFieldDescriptors(Class, Map)
*/
private Collection loadFieldDescriptors(Class clazz, JDOClass jdoClass) {
//save properties to a map
Map propMap = new HashMap();
try {
//analyse the class or interface
BeanInfo bi;
if(clazz.isInterface()) {
bi = Introspector.getBeanInfo(clazz);
} else {
bi = Introspector.getBeanInfo(clazz, Object.class);
}
PropertyDescriptor [] pd = bi.getPropertyDescriptors();
for (int i = 0; i < pd.length; i++) {
PropertyDescriptor descriptor = pd[i];
propMap.put(descriptor.getName(), descriptor);
}
//analyse interfaces
Class [] interfaces = clazz.getInterfaces();
for(int k=0; k<interfaces.length; k++){
bi = Introspector.getBeanInfo(interfaces[k]);
pd = bi.getPropertyDescriptors();
for (int i = 0; i < pd.length; i++) {
PropertyDescriptor descriptor = pd[i];
String key = descriptor.getName();
if( !propMap.containsKey(key) ){
propMap.put(key, descriptor);
}
}
}
} catch (IntrospectionException ex){
throw new JDOFatalUserException("Error while introspecting class "+clazz.getName(), ex);
}
//iterate over configured properties
Collection descriptorsColl = new Vector();
Iterator fieldsIter = jdoClass.getFields().iterator();
while (fieldsIter.hasNext()){
JDOField jdoField = (JDOField)fieldsIter.next();
String name = jdoField.getName();
//find getter abd setter
PropertyDescriptor descriptor = (PropertyDescriptor)propMap.get(name);
if(descriptor == null){
throw new JDOFatalUserException(I18N.msg("E_unknown_property", name, clazz.getName()));
}
FieldDescriptor fd = new FieldDescriptor(descriptor);
descriptorsColl.add(fd);
}
return descriptorsColl;
}
}