/*
* #!
* Ontopia Engine
* #-
* Copyright (C) 2001 - 2013 The Ontopia Project
* #-
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* !#
*/
package net.ontopia.persistence.proxy;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import net.ontopia.utils.OntopiaRuntimeException;
import net.ontopia.utils.StringUtils;
import net.ontopia.xml.DefaultXMLReaderFactory;
import net.ontopia.xml.Slf4jSaxErrorHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
/**
* INTERNAL: The generic object relational mapping definition class.
*/
public class ObjectRelationalMapping {
// Define a logging category.
static Logger log = LoggerFactory.getLogger(ObjectRelationalMapping.class.getName());
class MappingHandler extends DefaultHandler {
protected ObjectRelationalMapping mapping;
protected ClassDescriptor cdesc;
MappingHandler(ObjectRelationalMapping mapping) {
this.mapping = mapping;
}
/**
* INTERNAL: Looks up a class object by its name.
*/
protected Class<?> getClassByName(String class_name) {
try {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
return Class.forName(class_name, true, classLoader);
} catch (ClassNotFoundException e) {
log.error("Cannot find class " + e.getMessage());
throw new OntopiaRuntimeException(e);
}
}
@Override
public void startElement (String uri, String name, String qName, Attributes atts) throws SAXException {
if (name.equals("class")) {
// Get descriptor class
Class<?> klass = getClassByName(atts.getValue("name"));
Class<?> klass_immutable = getClassByName(atts.getValue("immutable"));
// Create new class descriptor
cdesc = new ClassDescriptor(klass, klass_immutable, mapping);
// Set abstract flag (default: concrete)
String isabstract = atts.getValue("abstract");
if (isabstract != null && isabstract.equals("yes")) {
cdesc.setAbstract(true);
}
else {
cdesc.setAbstract(false);
}
// Set class descriptor type (default: identifiable)
String type = atts.getValue("type");
if (type == null || type.equals("identifiable")) {
cdesc.setType(ClassInfoIF.TYPE_IDENTIFIABLE);
}
else if (type.equals("aggregate")) {
cdesc.setType(ClassInfoIF.TYPE_AGGREGATE);
//! }
//! else if (type.equals("primitive")) {
//! cdesc.setType(ClassInfoIF.TYPE_PRIMITIVE);
} else {
throw new OntopiaRuntimeException("class.type contains invalid value: " + type);
}
if (cdesc.getType() != ClassInfoIF.TYPE_AGGREGATE) {
// Set table name
String table = atts.getValue("table");
if (table == null)
throw new OntopiaRuntimeException("class.table must be specified: " + cdesc.getName());
cdesc.setMasterTable(table);
// Set identity field
String identity = atts.getValue("identity");
if (identity == null)
throw new OntopiaRuntimeException("class.identity must be specified: " + cdesc.getName());
cdesc.setIdentityFieldNames(new String[] {identity});
// Set class structure (default: object)
String structure = atts.getValue("structure");
if (structure == null || structure.equals("object")) {
cdesc.setStructure(ClassInfoIF.STRUCTURE_OBJECT);
}
else if (structure.equals("collection")) {
cdesc.setStructure(ClassInfoIF.STRUCTURE_COLLECTION);
//! }
//! else if (structure.equals("map")) {
//! cdesc.setStructure(ClassInfoIF.STRUCTURE_MAP);
} else {
throw new OntopiaRuntimeException("class.structure contains invalid value: " + structure);
}
}
// Extends
String _extends = atts.getValue("extends");
if (_extends != null) {
String[] _class_names = StringUtils.split(_extends, " ");
Class<?>[] _classes = new Class<?>[_class_names.length];
for (int i=0; i < _class_names.length; i++) {
_classes[i] = getClassByName(_class_names[i]);
}
cdesc.setExtends(_classes);
}
// Interfaces
String _interfaces = atts.getValue("interfaces");
if (_interfaces != null) {
String[] _class_names = StringUtils.split(_interfaces, " ");
Class<?>[] _classes = new Class<?>[_class_names.length];
for (int i=0; i < _class_names.length; i++) {
_classes[i] = getClassByName(_class_names[i]);
}
cdesc.setInterfaces(_classes);
}
// Register class descriptor with mapping
mapping.addClass(cdesc);
}
else if (name.equals("field")) {
if (cdesc == null)
throw new OntopiaRuntimeException("No parent class descriptor for field: " + atts.getValue("name"));
// Create new field descriptor
name = atts.getValue("name");
if (name == null)
throw new OntopiaRuntimeException("field.name must be specified: " + cdesc.getName());
FieldDescriptor fdesc = new FieldDescriptor(name, cdesc);
String klass = atts.getValue("class");
if (klass == null)
throw new OntopiaRuntimeException("field.class must be specified: " + fdesc.getName());
// FIXME: should add more primitive types
if (klass.equals("string")) {
fdesc.setValueClass(java.lang.String.class);
}
else if (klass.equals("integer")) {
fdesc.setValueClass(java.lang.Integer.class);
}
else if (klass.equals("long")) {
fdesc.setValueClass(java.lang.Long.class);
}
else if (klass.equals("float")) {
fdesc.setValueClass(java.lang.Float.class);
}
else if (klass.equals("clob")) {
fdesc.setValueClass(java.io.Reader.class);
}
else {
fdesc.setValueClass(getClassByName(klass));
}
// Required
String required = atts.getValue("required");
if (required != null && (required.equals("yes") || required.equals("true")))
fdesc.setRequired(true);
else
fdesc.setRequired(false);
// Readonly
String readonly = atts.getValue("readonly");
if (readonly != null && (readonly.equals("yes") || readonly.equals("true")))
fdesc.setReadOnly(true);
else
fdesc.setReadOnly(false);
// Relationship type
String type = atts.getValue("type");
if (type == null) {
throw new OntopiaRuntimeException("field.type must be specified: " + fdesc.getName());
}
else if (type.equals("1:1")) {
fdesc.setCardinality(FieldDescriptor.ONE_TO_ONE);
// Columns
String columns = atts.getValue("columns");
if (columns != null)
//throw new OntopiaRuntimeException("field.columns must be specified: " + fdesc.getName());
fdesc.setColumns(StringUtils.split(columns, " "));
}
else if (type.equals("1:M")) {
fdesc.setCardinality(FieldDescriptor.ONE_TO_MANY);
}
else if (type.equals("M:M")) {
fdesc.setCardinality(FieldDescriptor.MANY_TO_MANY);
// Many key
String manykey = atts.getValue("many-keys");
if (manykey == null)
throw new OntopiaRuntimeException("field.many-keys must be specified: " + fdesc.getName());
fdesc.setManyKeys(StringUtils.split(manykey, " "));
}
else {
throw new OntopiaRuntimeException("field.type contains invalid value: " + type);
}
// Join table
String jointable = atts.getValue("join-table");
if (jointable == null) {
if (!fdesc.isOneToOne()) {
throw new OntopiaRuntimeException("field.join-table must be specified: " + fdesc.getName());
}
} else {
fdesc.setJoinTable(jointable);
}
// Join keys
String joinkeys = atts.getValue("join-keys");
if (joinkeys == null) {
if (!fdesc.isOneToOne()) {
throw new OntopiaRuntimeException("field.join-keys must be specified: " + fdesc.getName());
}
} else {
fdesc.setJoinKeys(StringUtils.split(joinkeys, " "));
}
// Collection
if (fdesc.isCollectionField()) {
String collection = atts.getValue("collection");
// FIXME: should possibly also support 'map'.
if (collection == null || collection.equals("list"))
fdesc.setCollectionClass(java.util.ArrayList.class);
else if (collection.equals("set")) {
fdesc.setCollectionClass(java.util.HashSet.class);
} else {
fdesc.setCollectionClass(getClassByName(collection));
}
}
// Getter method
String getter = atts.getValue("get-method");
if (getter != null)
fdesc.setGetter(getter);
// Setter method
String setter = atts.getValue("set-method");
if (setter != null)
fdesc.setSetter(setter);
// Register field descriptor with class descriptor
cdesc.addField(fdesc);
}
}
@Override
public void endElement (String uri, String name, String qName) throws SAXException {
if (name.equals("class")) {
// Reset class descriptor field
cdesc = null;
}
}
}
protected Map<Class<?>, ClassDescriptor> cdescs = new HashMap<Class<?>, ClassDescriptor>();
/**
* INTERNAL: Creates an object relational mapping instance that is
* to read its definition from the specified mapping file. The
* mapping file input stream should be an XML file.
*/
public ObjectRelationalMapping(InputStream istream) {
loadMapping(new InputSource(istream));
}
/**
* INTERNAL: Read a mapping description from the specified file.
*/
protected void loadMapping(InputSource isource) {
// Read mapping file.
ContentHandler handler = new MappingHandler(this);
try {
XMLReader parser = DefaultXMLReaderFactory.createXMLReader();
parser.setContentHandler(handler);
parser.setErrorHandler(new Slf4jSaxErrorHandler(log));
parser.parse(isource);
} catch (IOException e) {
throw new OntopiaRuntimeException(e);
} catch (SAXException e) {
throw new OntopiaRuntimeException(e);
}
}
/**
* INTERNAL: Utility method that converts a collection of class
* descriptors to an array of class descriptors.
*/
protected ClassDescriptor[] toClassDescriptorArray(Collection<ClassDescriptor> cdescs) {
ClassDescriptor[] _cdescs = new ClassDescriptor[cdescs.size()];
cdescs.toArray(_cdescs);
return _cdescs;
}
/**
* INTERNAL: Gets all the class descriptors in the mapping.
*/
public ClassDescriptor[] getClassDescriptors() {
return toClassDescriptorArray(cdescs.values());
}
/**
* INTERNAL: Gets the class descriptor by object type.
*/
public ClassDescriptor getDescriptorByClass(Class<?> type) {
return cdescs.get(type);
}
/**
* INTERNAL: Adds the class descriptor to the mapping.
*/
public void addClass(ClassDescriptor cdesc) {
cdescs.put(cdesc.getDescriptorClass(), cdesc);
}
}