/*
* Rapid Beans Framework: RapidBeanDeserializer.java
*
* Copyright (C) 2009 Martin Bluemel
*
* Creation Date: 11/27/2005
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by the Free Software Foundation;
* either version 3 of the License, or (at your option) any later version.
* This program 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 Lesser General Public License for more details.
* You should have received a copies of the GNU Lesser General Public License and the
* GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
package org.rapidbeans.core.common;
import java.io.File;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.StringTokenizer;
import org.rapidbeans.core.basic.Id;
import org.rapidbeans.core.basic.IdType;
import org.rapidbeans.core.basic.Property;
import org.rapidbeans.core.basic.PropertyCollection;
import org.rapidbeans.core.basic.RapidBean;
import org.rapidbeans.core.basic.RapidBeanImplParent;
import org.rapidbeans.core.basic.ThreadLocalValidationSettings;
import org.rapidbeans.core.exception.RapidBeansRuntimeException;
import org.rapidbeans.core.type.RapidBeansTypeLoader;
import org.rapidbeans.core.type.TypePropertyCollection;
import org.rapidbeans.core.type.TypeRapidBean;
import org.rapidbeans.core.util.StringHelper;
import org.rapidbeans.core.util.XmlAttribute;
import org.rapidbeans.core.util.XmlNode;
import org.rapidbeans.core.util.XmlNodeTopLevel;
import org.w3c.dom.Node;
/**
* Deserialize a Rapid Bean from file.
*
* @author Martin Bluemel
*/
public final class RapidBeanDeserializer {
/**
* Helper collection to collect beans with idtype keypropswithparenscope.
*/
private Collection<RapidBean> beansWithLateIdBinding = null;
/**
* the URL deserialized
*/
private URL url = null;
/**
* the document's character encoding.
*/
private String encoding = null;
/**
* @return the document's character encoding.
*/
public String getEncoding() {
return encoding;
}
/**
* Constructor.
*
* @param argFile
* the XML file
*/
public RapidBeanDeserializer() {
}
public static URL urlFromFile(final File file) {
URL theUrl = null;
try {
theUrl = file.toURI().toURL();
return theUrl;
} catch (MalformedURLException e) {
throw new RapidBeansRuntimeException(e);
}
}
/**
* load a bean from an XML file.
*
* @param rbType
* the rapid bean type
* @param theUrl
* the URL
*
* @return the bean
*/
public RapidBean loadBean(final TypeRapidBean rbType, final File theFile) {
return loadBean(rbType, urlFromFile(theFile));
}
/**
* load a document from an XML file.
*
* @param rbType
* the rapid bean type
* @param theUrl
* the URL
*
* @return the document's root bean
*/
public RapidBean loadBean(final TypeRapidBean rbType, final URL theUrl) {
this.url = theUrl;
final XmlNodeTopLevel bizBeanNode = XmlNode.getDocumentTopLevel(url);
return loadBean(rbType, bizBeanNode);
}
/**
* load a document from a stream.
*
* @param rbType
* the type of the root bean
* @param url
* the download URL
* @param is
* the input stream to load from
*
* @return the document's root bean
*/
public RapidBean loadBean(TypeRapidBean rbType, URL theUrl, InputStream is) {
this.url = theUrl;
final XmlNodeTopLevel bizBeanNode = XmlNode.getDocumentTopLevel(is);
return loadBean(rbType, bizBeanNode);
}
/**
* load a bean from an input stream.
*
* @param is
* the input stream
* @param rbType
* the rapid bean type
*
* @return the bean loaded
*/
public RapidBean loadBean(final TypeRapidBean rbType, final InputStream is) {
this.url = null;
return loadBean(rbType, XmlNode.getDocumentTopLevel(is));
}
/**
* load a bean from an XML node.
*
* @param is
* the input stream
* @param rbType
* the root bean type
*
* @return the bean
*/
public RapidBean loadBean(final TypeRapidBean rbType, final XmlNodeTopLevel bizBeanNode) {
this.encoding = bizBeanNode.getEncoding();
TypeRapidBean rootBeanType = rbType;
final String rootNodeName = bizBeanNode.getName();
if (rootBeanType == null) {
// if the type is not given as argument
// try to find a type via an XML root element binding
rootBeanType = RapidBeansTypeLoader.getInstance().getXmlRootElementBinding(rootNodeName);
}
if (rootBeanType == null) {
// next try is to read the root element's rb:type attribute if set
String typename = bizBeanNode.getAttributeValue("@rb:type");
if (typename == null) {
// next try is to interpret the XML name space as a package
// and the rest as a class but I do not know if this is really
// reasonable
typename = extractTypenameFromNamespacedRootElement(bizBeanNode);
}
rootBeanType = TypeRapidBean.forName(typename);
}
final RapidBean bean = RapidBeanImplParent.createInstance(rootBeanType);
loadBeanNode(0, bean, bizBeanNode);
if (this.beansWithLateIdBinding != null) {
for (RapidBean pbean : this.beansWithLateIdBinding) {
pbean.clearId();
}
this.beansWithLateIdBinding = null;
}
return bean;
}
private String extractTypenameFromNamespacedRootElement(final XmlNode rootBeanNode) {
final String rootNodeName = rootBeanNode.getName();
String typename = null;
if (!rootNodeName.contains(":")) {
throw new RapidBeansRuntimeException("can't determine typename."
+ " Neither root element attribute \"rb:type\"" + " nor a namespace scoped root element is defined");
}
final StringTokenizer st = new StringTokenizer(rootNodeName, ":");
st.nextToken();
final String rootNodePureName = st.nextToken();
final String rootNodeNsVal = rootBeanNode.getNamespaceURI();
if (rootNodeNsVal == null) {
throw new RapidBeansRuntimeException("can't determine typename."
+ " Neither root element attribute \"rb:type\"" + " nor a namespace URI is defined");
}
typename = mapNamespaceToPackage(rootNodeNsVal);
typename += "." + StringHelper.upperFirstCharacter(rootNodePureName);
return typename;
}
/**
* Map a name space of the form "http://rapidbeans.org/clubadmin/domain" to
* a fully qualified class or type name
*
* @param namespace
* the XML name space description
*
* @return the mapped class or type name
*/
private String mapNamespaceToPackage(final String namespace) {
final StringBuffer buf = new StringBuffer();
try {
final URI uri = new URI(namespace);
final List<String> host = StringHelper.split(uri.getHost(), ".");
final int hostsize = host.size();
boolean first = true;
for (int i = hostsize - 1; i >= 0; i--) {
if (!first) {
buf.append('.');
}
buf.append(host.get(i));
if (first) {
first = false;
}
}
for (String pathElement : StringHelper.split(uri.getPath(), "/")) {
if (!first) {
buf.append('.');
}
buf.append(pathElement);
if (first) {
first = false;
}
}
} catch (URISyntaxException e) {
throw new RapidBeansRuntimeException(e);
}
return buf.toString();
}
/**
* load a bean from an XML node.
*
* @param depth
* the recursion depth
* @param bean
* the bean to fill with values
* @param node
* the XML DOM node
*/
private void loadBeanNode(final int depth, final RapidBean bean, final XmlNode node) {
// load XML attribute into bean properties
Property prop;
for (XmlAttribute attr : node.getAttributes()) {
// the type attribute has a special meaning
// - top level: defines the top level bean's type
// - else: defines a bean's fine type while the
// property's type is a supertype
// (e. g. for Composites)
if (attr.getName().equals("rb:type")) {
continue;
} else if (attr.getName().equals("id")) {
if (bean.getType().getIdtype() != IdType.keyprops
&& bean.getType().getIdtype() != IdType.keypropswithparentscope
&& !bean.getType().hasDependendKeyProp()) {
bean.setId(Id.createInstance(bean, attr.getValue()));
}
} else {
prop = bean.getProperty(attr.getName());
if (prop != null) {
try {
ThreadLocalValidationSettings.validationOff();
prop.setValue(attr.getValue());
} finally {
ThreadLocalValidationSettings.remove();
}
}
}
}
// load XML sub entities
final Collection<XmlNode> subnodes = node.getSubnodes();
PropertyCollection colProp;
String colPropName;
String lastColPropName = null;
for (XmlNode subnode : subnodes) {
if (subnode.getType() == Node.ELEMENT_NODE) {
// special handling of non empty collection properties
// (= collections that have already got some default instances):
// clean all default instances if there are any
colProp = determineCollectionProperty(bean, subnode.getName());
if (colProp == null) {
prop = bean.getProperty(subnode.getName());
if (prop != null) {
try {
ThreadLocalValidationSettings.validationOff();
final String value = subnode.getValue();
prop.setValue(value);
} finally {
ThreadLocalValidationSettings.remove();
}
}
} else {
colPropName = colProp.getType().getPropName();
if ((lastColPropName == null) || (!lastColPropName.equals(colPropName))) {
if (colProp.getValue() != null) {
colProp.setValue(null);
}
lastColPropName = colPropName;
}
loadBeanSubnode(depth, bean, subnode, colProp);
}
}
}
}
/**
* load a bean from an XML subnode.
*
* @param depth
* the recursion depth
* @param bean
* the bean to fill with values
* @param subnode
* the XML node
* @param colProp
* the collection property for this sub node
*/
private void loadBeanSubnode(final int depth, final RapidBean bean, final XmlNode subnode,
final PropertyCollection colProp) {
final TypeRapidBean colPropTargetType = determineColPropTargetType(subnode, colProp);
try {
final RapidBean subnodeBean = RapidBeanImplParent.createInstance(colPropTargetType.getName());
loadBeanNode(depth + 1, subnodeBean, subnode);
colProp.addLink(subnodeBean);
if (subnodeBean.getType().getIdtype() == IdType.keypropswithparentscope
|| subnodeBean.getType().hasDependendKeyProp()) {
if (this.beansWithLateIdBinding == null) {
this.beansWithLateIdBinding = new ArrayList<RapidBean>();
}
this.beansWithLateIdBinding.add(subnodeBean);
}
} catch (RapidBeansRuntimeException e) {
if (e.getCause() instanceof InstantiationException) {
throw new RapidBeansRuntimeException("Cannot instantiate bean type \"" + colPropTargetType.getName()
+ "\"", e);
} else {
throw e;
}
}
}
/**
* determines the collection property for a certain subnode name. 1st try)
* the collection property is the one with the subnode's name 2nd try) the
* collection property is the one with the subnode's name + "s"
*
* @param bean
* the bean with the property
* @param subnodeName
* the subnode's name
*
* @return the collection property
*/
private PropertyCollection determineCollectionProperty(final RapidBean bean, final String subnodeName) {
String subnodeNameAlt = null;
PropertyCollection colProp = null;
try {
colProp = (PropertyCollection) bean.getProperty(subnodeName);
} catch (ClassCastException e) {
colProp = null;
}
if (colProp == null) {
subnodeNameAlt = subnodeName + "s";
try {
colProp = (PropertyCollection) bean.getProperty(subnodeNameAlt);
} catch (ClassCastException e) {
colProp = null;
}
}
if (colProp == null) {
colProp = bean.findAssociationPropertyWithSingularName(subnodeName);
}
if (colProp == null) {
try {
colProp = (PropertyCollection) bean.getProperty(bean.getType().mapXmlElementToPropName(subnodeName));
} catch (ClassCastException e) {
colProp = null;
}
}
return colProp;
}
/**
* Determine the target type for the given collection property.
*
* Ether the directly target type of the given collection property or the
* type specified by the type attribute if the subnode. If a type is
* specified by the subnode this must be a subtype of the collection
* property's target type.
*
* @param subnode
* the subnode
* @param colProp
* the collection property
*
* @return the determined target type.
*/
private TypeRapidBean determineColPropTargetType(final XmlNode subnode, final PropertyCollection colProp) {
TypeRapidBean subnodeType = null;
final TypePropertyCollection colPropType = (TypePropertyCollection) colProp.getType();
// take the 'rb:type' attribute as target type name if given
final String subnodeTypeName = subnode.getAttributeValue("@rb:type");
if (subnodeTypeName != null) {
subnodeType = TypeRapidBean.forName(subnodeTypeName);
}
// otherwise take XML Binding
if (subnodeType == null) {
subnodeType = colPropType.mapXmlElementToType(subnode.getName());
}
// otherwise take directly the collection property's target type
if (subnodeType == null) {
// if not given take the collection property's target type
subnodeType = colPropType.getTargetType();
}
if (subnodeType != null) {
if (!TypeRapidBean.isSameOrSubtype(colPropType.getTargetType(), subnodeType)) {
throw new RapidBeansRuntimeException("Error while deserializing file " + this.url.toString() + ":\n"
+ "Subnode type \"" + subnodeType.getName() + "\" is not a subtype of target type \""
+ colPropType.getTargetType().getName() + "\" of property \"" + subnode.getName() + "\"");
}
}
return subnodeType;
}
}