/*
$Header: /cvsroot/xorm/xorm/src/org/xorm/ModelMapping.java,v 1.57 2004/05/04 18:57:09 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.PropertyDescriptor;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.logging.Logger;
import javax.jdo.JDOFatalException;
import javax.jdo.JDOFatalUserException;
import javax.jdo.JDOUserException;
import javax.jdo.spi.JDOImplHelper;
import javax.jdo.spi.PersistenceCapable;
import javax.sql.DataSource;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xorm.datastore.Column;
import org.xorm.datastore.ConnectionInfo;
import org.xorm.datastore.Table;
import org.xorm.datastore.sql.SQLConnectionInfo;
import org.xorm.datastore.sql.SQLType;
import org.xorm.util.FieldDescriptor;
import org.xorm.util.jdoxml.*;
/**
* Represents the full set of mappings for an object model.
*/
public class ModelMapping implements Configurable, I15d {
protected static Logger logger = Logger.getLogger("org.xorm.ModelMapping");
public static final String XORM_VENDOR_NAME = "XORM";
public static final String ATTR_SOURCE = "source";
public static final String ATTR_TARGET = "target";
public static final String ATTR_ORDER_BY = "order-by";
public static final String ATTR_INDEX = "index";
public static final String ATTR_FILTER = "filter";
public static final String ATTR_PARAMETERS = "parameters";
public static final String ATTR_VARIABLES = "variables";
public static final String ATTR_ORDERING = "ordering";
public static final String ATTR_IMPORTS = "imports";
public static final String DATABASE_XML = "org.xorm.datastore.database";
public static final String DATABASE_DTD = "/org/xorm/datastore/database.dtd";
public static final String OPTION_VALIDATE_XML = "org.xorm.option.ValidateXML";
public static final String OPTION_BOOTSTRAP_JDO = "org.xorm.option.BootstrapJDO";
public static final String OPTION_DEFAULT_MAPPING = "org.xorm.option.DefaultMapping";
public static final String OPTION_ONLY_CONFIGURED_PROPERTIES = "org.xorm.option.onlyConfiguredProperties";
private HashMap classToMapping = new HashMap();
private HashMap nameToTable = new HashMap();
private Properties properties;
private InterfaceManagerFactory factory;
private boolean validateXML = true;
private boolean useDefaultMapping = false;
private boolean registerClasses = false;
private boolean usingDatabaseMetaData = false;
public void setFactory(InterfaceManagerFactory factory) {
this.factory = factory;
}
public void addClassMapping(ClassMapping mapping) {
Class clazz = mapping.getMappedClass();
// System.out.println("Registering class: " + clazz.getName());
classToMapping.put(clazz, mapping);
if (registerClasses) {
// Register XORM-specific classes' JDO Metadata
if (!PersistenceCapable.class.isAssignableFrom(clazz)) {
InterfaceInvocationHandler handler = new InterfaceInvocationHandler(factory, mapping, null);
PersistenceCapable pc = (PersistenceCapable) handler.newProxy();
Collection managedFields = mapping.getManagedFields();
int len = managedFields.size();
String[] fieldNames = new String[len];
Class[] fieldTypes = new Class[len];
byte[] fieldFlags = new byte[len];
int i = 0;
Iterator it = managedFields.iterator();
while (it.hasNext()) {
FieldDescriptor fd = mapping.getFieldDescriptor
((String) it.next());
fieldNames[i] = fd.name;
fieldTypes[i] = fd.type;
fieldFlags[i] = (byte) (pc.CHECK_READ | pc.CHECK_WRITE);
i++;
}
// Register it with JDOImplHelper
JDOImplHelper.registerClass(clazz,
fieldNames,
fieldTypes,
fieldFlags,
null, // PC superclasses not supported
pc);
}
}
}
/**
* Retrieves a ClassMapping for the specified class. If the class
* itself is not mapped, but a superclass or superinterface is mapped,
* that mapping is cloned and used for the specified class.
* @exception JDOUserException if no applicable mapping is found
*/
public ClassMapping getClassMapping(Class clazz) {
ClassMapping mapping = getClassMappingImpl(clazz);
if (mapping == null) {
throw new JDOUserException(I18N.msg("E_no_class_mapping", clazz.getName()));
}
return mapping;
}
private ClassMapping getClassMappingImpl(Class clazz) {
// First check the cache of previously loaded mappings
if (classToMapping.containsKey(clazz)) {
return (ClassMapping) classToMapping.get(clazz);
}
ClassMapping mapping;
if ((mapping = loadClassMapping(clazz)) == null) {
boolean found = false;
Class[] interfaces = clazz.getInterfaces();
for (int i = 0; i < interfaces.length; i++) {
if ((mapping = getClassMappingImpl(interfaces[i])) != null) {
found = true;
break;
}
}
if (!found) {
/*
if(!clazz.isInterface()){
//try superclass
*/
Class superclass = clazz.getSuperclass();
if ((superclass != null) && ((mapping = getClassMappingImpl(superclass)) != null)) {
found = true;
}
/*
} else {
//try to find class implementing this iterface, first found will be used
logger.info("loking for implementation for interface "+clazz);
Iterator classesIter = classToMapping.keySet().iterator();
while (classesIter.hasNext()) {
Class nextClass = (Class) classesIter.next();
if(clazz.isAssignableFrom(nextClass)){
logger.info("found matching class "+nextClass);
mapping = (ClassMapping)classToMapping.get(nextClass);
found = true;
break;
}
}
}
*/
}
if (found && !mapping.getMappedClass().equals(clazz)) {
// Simulate the mapping
//System.out.println("Cloning ClassMapping for " + mapping.getMappedClass() + " to " + clazz);
mapping = (ClassMapping) mapping.clone();
mapping.clazz = clazz;
addClassMapping(mapping);
//classToMapping.put(clazz, mapping);
}
}
return mapping;
}
/**
* Loads the class mapping information by reading the corresponding
* JDO metadata files.
*
* @param clazz the interface or abstract class class to be loaded
* @return a ClassMapping object representing the mappings, or
* null if no mapping exists.
*/
private synchronized ClassMapping loadClassMapping(Class clazz) {
// First request for this class; read appropriate JDO files.
String name = clazz.getName();
StringTokenizer toke = new StringTokenizer(name, ".");
StringBuffer path = new StringBuffer("/");
while (toke.hasMoreTokens()) {
path.append(toke.nextToken());
if (toke.hasMoreTokens()) {
path.append("/");
} else {
path.append(".jdo");
}
}
InputStream jdo = getClass().getResourceAsStream(path.toString());
if (jdo == null) {
// Try package file
int pos = path.toString().lastIndexOf("/");
if (pos > 0) {
path.delete(pos, path.length());
path.append(".jdo");
}
jdo = getClass().getResourceAsStream(path.toString());
}
if (jdo == null) {
// No mapping exists
classToMapping.put(clazz, null);
return null;
} else {
initJDO(jdo);
return (ClassMapping) classToMapping.get(clazz);
}
}
public Table getTable(String name) {
return (Table) nameToTable.get(name);
}
/**
* Initializes an empty ModelMapping that will be populated
* on demand by reading JDO files, or can be hand-populated
* by the user.
* @param props the Properties to use
*/
public void setProperties(Properties props) {
this.properties = props;
validateXML = !"false".equalsIgnoreCase(properties.getProperty(OPTION_VALIDATE_XML));
useDefaultMapping = "true".equalsIgnoreCase(properties.getProperty(OPTION_DEFAULT_MAPPING));
String databaseXML = properties.getProperty(DATABASE_XML);
if (databaseXML != null) {
SAXBuilder biff = new SAXBuilder(validateXML);
biff.setEntityResolver(new EntityResolver() {
public InputSource resolveEntity(String publicId, String systemId) {
if ("database.dtd".equals(systemId)) {
return new InputSource(getClass().getResourceAsStream(DATABASE_DTD));
}
return null;
}
});
InputStream testStream = getClass().getResourceAsStream(databaseXML);
if(testStream == null){
testStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(databaseXML);
}
if(testStream == null) {
throw new JDOFatalUserException(I18N.msg("E_locate_db_xml", databaseXML));
}
InputSource inputSource = new InputSource(testStream);
// Fake out Crimson
inputSource.setSystemId(getClass().getResource(DATABASE_DTD).toString());
try {
Document doc = biff.build(inputSource);
Element root = doc.getRootElement();
parseTables(root);
} catch (JDOMException e) {
throw new JDOFatalException(I18N.msg("E_parse_db_xml", databaseXML), e);
} catch (IOException e) {
throw new JDOFatalException(I18N.msg("E_parse_db_xml", databaseXML), e);
}
}
else {
// DATABASE_XML was not defined.
usingDatabaseMetaData = true;
logger.info(DATABASE_XML +
" not defined, will dynamically use DatabaseMetaData");
}
// Check for bootstrap loading of JDO files
String bootstrap = properties.getProperty(OPTION_BOOTSTRAP_JDO);
if (bootstrap != null) {
String[] parts = bootstrap.split(",");
registerClasses = true;
for (int i = 0; i < parts.length; i++) {
InputStream jdo = getClass().getResourceAsStream(parts[i]);
initJDO(jdo);
}
registerClasses = false;
}
}
/**
* Returns true if the interface type passed in has been configured
* via the mapping file to be persisted.
* Note that this is NOT the same thing as checking if an instance
* that implements the interface is managed, and in particular, a call to
* mgr.isManagedType(persistentInstance.getClass())
* will NOT return true.
*/
public boolean isManagedType(Class type) {
return (getClassMappingImpl(type) != null);
}
/** Used as a utility method from init. */
private String getPackagedClassName(String className, String defaultPackage) {
if (defaultPackage == null || className.indexOf('.') != -1) {
return className;
}
return defaultPackage + '.' + className;
}
/**
* Uses JDOM to load configuration from an XML .jdo file.
*/
private void initJDO(InputStream mappingStream) {
try {
JDOPackage jdoPackage = JDOXML.read(mappingStream, validateXML);
String defaultPackage = jdoPackage.getName();
if ("".equals(defaultPackage)) {
defaultPackage = null;
}
if (usingDatabaseMetaData) {
// The user didn't specify a DATABASE_XML file, so we need
// to load all tables referenced in the .jdo file from the
// database.
loadReferencedTablesFromMetaData(jdoPackage);
}
// Look at the classes twice: first create ClassMapping
// instances with tables only, then fill in the details.
// This is done so that different ClassMappings can use
// each other's default fetch group settings, possibly
// circularly.
Iterator i = jdoPackage.getClasses().iterator();
while (i.hasNext()) {
JDOClass jdoClass = (JDOClass) i.next();
String className = getPackagedClassName(jdoClass.getName(), defaultPackage);
Class c = Class.forName(className);
ClassMapping classMapping;
if("true".equalsIgnoreCase(properties.getProperty(OPTION_ONLY_CONFIGURED_PROPERTIES))) {
classMapping = new ClassMapping(this, c, jdoClass);
} else {
classMapping = new ClassMapping(this, c);
}
Iterator exts = jdoClass.getExtensions().iterator();
JDOExtension extension;
Class dsi = null;
Table t = null;
while (exts.hasNext()) {
extension = (JDOExtension) exts.next();
if (XORM_VENDOR_NAME.equals(extension.getVendorName())) {
String key = extension.getKey();
if ("datastore-identity-type".equals(key)) {
dsi = Class.forName(extension.getValue());
} else if ("table".equals(key)) {
String tableName = extension.getValue();
t = getTable(tableName);
if (t == null) {
throw new JDOFatalUserException(I18N.msg("E_no_table_definition", tableName));
}
}
}
}
if (t == null && !useDefaultMapping) {
throw new JDOFatalUserException(I18N.msg("E_no_table", c.getName()));
}
classMapping.setTable(t);
classMapping.setDatastoreIdentityType(dsi);
addClassMapping(classMapping);
}
i = jdoPackage.getClasses().iterator();
while (i.hasNext()) {
JDOClass jdoClass = (JDOClass) i.next();
parseClass(jdoClass, defaultPackage);
}
} catch (IOException e) {
throw new JDOFatalException(I18N.msg("E_parse_jdo_xml"), e);
} catch (ClassNotFoundException e) {
throw new JDOFatalException(I18N.msg("E_jdo_xml_no_class", e.getMessage()));
}
}
/**
* Parses a <class> element passed in as the first parameter.
*/
private void parseClass(JDOClass jdoClass, String defaultPackage) throws ClassNotFoundException {
String className = getPackagedClassName(jdoClass.getName(), defaultPackage);
Class c = Class.forName(className);
ClassMapping classMapping = getClassMapping(c);
Table t = classMapping.getTable();
Iterator exts;
JDOExtension extension;
// read fields
Iterator j = jdoClass.getFields().iterator();
while (j.hasNext()) {
JDOField field = (JDOField) j.next();
exts = field.getExtensions().iterator();
while (exts.hasNext()) {
extension = (JDOExtension) exts.next();
if (XORM_VENDOR_NAME.equals(extension.getVendorName())) {
String key = extension.getKey();
if ("column".equals(key)) {
Column c2 = t.getColumnByName(extension.getValue());
if (c2 == null) {
throw new JDOFatalUserException(I18N.msg("E_no_column", extension.getValue(), t.getName(), jdoClass.getName()));
}
classMapping.setColumn(field.getName(), c2, field.isDefaultFetchGroup());
if (field.getNullValue().equals(JDONullValue.EXCEPTION)) {
// TODO: should this be done here?
c2.setNonNull(true);
}
} else if ("inverse".equals(key)) {
classMapping.setInverse(field.getName(), extension.getValue());
}
} // XORM is vendor-name
} // end extension elements
// See if field is a collection
JDOCollection collection = field.getCollection();
if (collection != null) {
RelationshipMapping rm = parseRelationship(collection, c, defaultPackage, field, jdoClass);
classMapping.setRelationship(field.getName(), rm);
}
}
}
private RelationshipMapping parseRelationship(JDOCollection root, Class ownerClass, String defaultPackage, JDOField field, JDOClass jdoClass) throws ClassNotFoundException {
RelationshipMapping relationship = new RelationshipMapping();
String elementType = root.getElementType();
elementType = getPackagedClassName(elementType, defaultPackage);
Class elementClass = Class.forName(elementType);
RelationshipMapping.Endpoint source = new RelationshipMapping.Endpoint();
source.setCollectionType(source.SET);
source.setElementClass(elementClass);
relationship.setSource(source);
RelationshipMapping.Endpoint target = new RelationshipMapping.Endpoint();
target.setCollectionType(target.SET);
relationship.setTarget(target);
Table table = null;
Iterator i = root.getExtensions().iterator();
String sourceStr = null;
String targetStr = null;
String indexStr = null;
// Added support for filtered collections (Dan Checkoway, 6/26/03)
String filterStr = null;
String parametersStr = null;
String variablesStr = null;
String importsStr = null;
while (i.hasNext()) {
JDOExtension element = (JDOExtension) i.next();
if (XORM_VENDOR_NAME.equals(element.getVendorName())) {
String key = element.getKey();
String value = element.getValue();
boolean redefined = false;
if ("table".equals(key)) {
redefined = table != null;
table = getTable(value);
if (table == null) {
throw new JDOFatalUserException(I18N.msg("E_unknown_table", value));
}
} else if (ATTR_SOURCE.equals(key)) {
redefined = sourceStr != null;
sourceStr = value;
} else if (ATTR_TARGET.equals(key)) {
redefined = targetStr != null;
targetStr = value;
} else if (ATTR_ORDER_BY.equals(key)) {
redefined = relationship.getOrderBy() != null;
relationship.setOrderBy(value);
} else if (ATTR_INDEX.equals(key)) {
redefined = indexStr != null;
indexStr = value;
} else if (ATTR_FILTER.equals(key)) {
// Filtered collection query
redefined = filterStr != null;
filterStr = value;
} else if (ATTR_PARAMETERS.equals(key)) {
// Filtered collection query parameters
redefined = parametersStr != null;
parametersStr = value;
} else if (ATTR_VARIABLES.equals(key)) {
// Filtered collection query variables
redefined = variablesStr != null;
variablesStr = value;
} else if (ATTR_ORDERING.equals(key)) {
// JDO-style ordering
redefined = relationship.getOrdering() != null;
relationship.setOrdering(value);
} else if (ATTR_IMPORTS.equals(key)) {
redefined = importsStr != null;
importsStr = value;
}
if (redefined) {
throw new JDOFatalUserException(I18N.msg("E_collection_redefinition", key, field.getName(), jdoClass.getName()));
}
}
} // for each "extension" element
if (sourceStr == null && filterStr == null) {
if (useDefaultMapping) {
sourceStr = "source_" + ownerClass.getName();
} else {
// You have to specify either a source or a filter or both.
throw new JDOFatalUserException(I18N.msg("E_collection_no_source", field.getName(), jdoClass.getName()));
}
}
if (table == null) {
// The table wasn't explicitly specified. We can safely
// assume that if a collection is specified without an explicit
// table, then the table is the collection element type's table.
ClassMapping mapping = getClassMapping(elementClass);
if ((table = mapping.getTable()) == null) {
// Not much we can do about this. I don't think it will ever
// happen, but just in case...
throw new JDOFatalUserException(I18N.msg("E_collection_no_table", field.getName(), jdoClass.getName()));
}
logger.fine("No table specified for collection field \"" +
field.getName() +
"\"...assuming element type table: " +
table.getName());
}
if (relationship.getOrderBy() != null &&
relationship.getOrdering() != null) {
// Don't even bother trying to interpret precedence
throw new JDOFatalUserException(I18N.msg("E_collection_order_by_and_ordering"));
}
if (relationship.getOrdering() != null && filterStr == null) {
throw new JDOFatalUserException(I18N.msg("E_collection_ordering_without_filter", field.getName(), jdoClass.getName()));
}
if (sourceStr != null) {
source.setColumn(table.getColumnByName(sourceStr));
}
if (targetStr == null) {
target.setColumn(table.getPrimaryKey());
} else {
// It's many-to-many. We used to set the target's elementClass
// to signify this, but that seemed like a hack to me. And since
// the target's elementClass was set to the owner class, which
// didn't seem to apply (if anything the target element class
// would be the collection's element class, not the owner's class),
// I added this boolean setter/getter instead.
relationship.setMToN(true);
target.setColumn(table.getColumnByName(targetStr));
}
if (indexStr != null) {
Column c = table.getColumnByName(indexStr);
// Set the column to managed mode; we don't want
// indices to be included in Row.equals() operations
c.setManaged(true);
relationship.setIndexColumn(c);
}
if (filterStr != null && !filterStr.equals("")) {
relationship.setFilter(filterStr);
// Only set parameters & variables if the filter is specified.
// Otherwise it would make no sense.
if (parametersStr != null && !parametersStr.equals("")) {
relationship.setParameters(parametersStr);
}
if (variablesStr != null && !variablesStr.equals("")) {
relationship.setVariables(variablesStr);
}
if (importsStr != null && !importsStr.equals("")) {
relationship.setImports(importsStr);
}
}
return relationship;
}
private void parseTables(Element root) {
List tables = root.getChildren("table");
Iterator i = tables.iterator();
while (i.hasNext()) {
Element element = (Element) i.next();
String tableName = element.getAttributeValue("name");
Table table = new Table(tableName);
nameToTable.put(tableName, table);
List columns = element.getChildren("column");
Iterator j = columns.iterator();
while (j.hasNext()) {
Element colElement = (Element) j.next();
Column column = new Column(table, colElement.getAttributeValue("name"));
if ("true".equalsIgnoreCase(colElement.getAttributeValue("primary-key"))) {
table.setPrimaryKey(column);
}
if ("true".equalsIgnoreCase(colElement.getAttributeValue("read-only"))) {
column.setReadOnly(true);
}
if ("true".equalsIgnoreCase(colElement.getAttributeValue("non-null"))) {
column.setNonNull(true);
}
// Is it a sequenced column?
if (colElement.getAttributeValue("sequence") != null) {
column.setSequence(colElement.getAttributeValue("sequence"));
}
// Is it an autoincremented column?
if ("true".equalsIgnoreCase(colElement.getAttributeValue("auto"))) {
column.setAutoIncremented(true);
}
// Is it manually typed?
column.setType(colElement.getAttributeValue("type"));
column.setFormat(colElement.getAttributeValue("format"));
} // columns iterator
// map the table for later reference
} // tables iterator
}
/**
* Generates a list of table names extracted from the JDO file
* and passes each one to ConnectionInfo.describeTable(String name).
*/
private void loadReferencedTablesFromMetaData(JDOPackage jdoPackage)
{
HashSet tableNames = new HashSet();
for (Iterator classIter = jdoPackage.getClasses().iterator();
classIter.hasNext(); ) {
JDOClass jdoClass = (JDOClass)classIter.next();
// Look at the class extensions for the "table"
for (Iterator extIter = jdoClass.getExtensions().iterator();
extIter.hasNext(); ) {
JDOExtension extension = (JDOExtension)extIter.next();
if ("table".equals(extension.getKey())) {
String tableName = extension.getValue();
if (tableNames.add(tableName)) {
logger.fine("Discovered table name: " + tableName);
}
}
}
// Look at the class fields and everything under there
for (Iterator fieldIter = jdoClass.getFields().iterator();
fieldIter.hasNext(); ) {
JDOField field = (JDOField)fieldIter.next();
// Look at the field's extensions
for (Iterator extIter = field.getExtensions().iterator();
extIter.hasNext(); ) {
JDOExtension extension = (JDOExtension)extIter.next();
if ("table".equals(extension.getKey())) {
String tableName = extension.getValue();
if (tableNames.add(tableName)) {
logger.fine("Discovered table name: " + tableName);
}
}
}
// Look at the field's collection, if there is one
JDOCollection collection = field.getCollection();
if (collection != null) {
for (Iterator extIter = collection.getExtensions().iterator();
extIter.hasNext(); ) {
JDOExtension extension = (JDOExtension)extIter.next();
if ("table".equals(extension.getKey())) {
String tableName = extension.getValue();
if (tableNames.add(tableName)) {
logger.fine("Discovered table name: " + tableName);
}
}
}
}
}
}
logger.fine("Discovered " + tableNames.size() +
" table names...loading them now");
ConnectionInfo ci = factory.getConnectionInfo();
for (Iterator iter = tableNames.iterator(); iter.hasNext(); ) {
String tableName = (String)iter.next();
// Load it from db metadata
nameToTable.put(tableName, ci.describeTable(tableName));
}
}
}