Creating dynamic EMF model from XSDs and loading its instances from XML as SDOs
This post describes how to read a dynamic EMF model from a set of XML schema files (XSDs) and how to use that model to transform XMLs to SDO DataObjects or EMF EObjects, all this in a stand-alone environment.
A little EMF reminder: With EMF you first declare a model, for example based on UML or XSDs (check my previous post for a general, brief EMF introduction). The model may be either static, in which case a Java class is generated for each model EClass with normal getters and setters, or it may be dynamic, which doesn't require any code generation and attributes are accessible only via the generic eGet, eSet methods. You can then create, load and save instances of the model, for example from/to XML.
To get from an XSD to XML transformed into a runtime E[Data]Object you need to:
You may also ask why to use a dynamic model, which is less efficient than a static one (though EMF reflection access is still faster than native Java one) and certainly much less readable and easy to use with its strange eSet(EStructuralFeature feature, Object newValue). Well, the reason is flexibility - if your model changes then you only need to update your XSDs (which could be downloaded from somewhere or stored in a database). You don't need to regenerate any classes and redeploy your application. If you know the bureaucracy of large companies, you understand it can save you weeks or even months. Of course everything is a question of pros and cons.
If you're interested in loading XSDs from an InputStream, check the EMF FAQ How can I load a XSDSchema from a simple Java String or from a DOM Tree?.
Next the loaded EPackages must be registered with EMF under their namespace URIs so that it can find an appropriate package when parsing an XML (see my previous post regarding namespace declarations and EMF). But let's first see how the XSDs to be loaded are referenced:
The XSDs are located on the classpath (under WEB-INF/classes/) and their paths like "/xsd/AbstractBridgeMessage.xsd" are turned to absolute URLs and then to EMF URIs.
then one of the ways to let EMF know about them is to extract the schema manually into an XSD of its own and to declare types for the elements (the only change is the replacement of an xsd:element by its nested xsd:complexType while preserving the name):
Beware class name case If your root element's name begins with a lower case (as in "updateLearningActivityResponse") then the complexType you create for it must also start with a lower case for EMF to be able to match the corresponding EClass with the element. But EClass.getName() will return the name with the first letter upper-cased so if you try to find the EClass in the EPackage by yourself, don't forget to search for it with this change.
The method createModelObjectFactory() is described in the next section.
Important: If the input XML has no namespace declared on the root element we would need to register the package to be used for parsing the XML (also) as default for the null namespace, see my previous post about EMF and namespace declaration in input XMLs.
Notice that to load the XML from a stream we need to make a fake URI with an extension mapped to the desired resource factory (.xml in this case) and pass in an InputStream (source).
The method createXmlResourceDeSerializationOptions() only sets the options OPTION_EXTENDED_META_DATA and OPTION_ENCODING as described in my previous post under General notes on XML saving/loading in EMF.
Basically we just load the model at startup and then use it to parse XMLs.
When running this code under Websphere Application Server 7.0 you need no additional libraries. When running in another environment, check the libs needed in EMF FAQ.
A little EMF reminder: With EMF you first declare a model, for example based on UML or XSDs (check my previous post for a general, brief EMF introduction). The model may be either static, in which case a Java class is generated for each model EClass with normal getters and setters, or it may be dynamic, which doesn't require any code generation and attributes are accessible only via the generic eGet, eSet methods. You can then create, load and save instances of the model, for example from/to XML.
To get from an XSD to XML transformed into a runtime E[Data]Object you need to:
- Load the model elements defined in the XSDs into EPackages
- Register the loaded packages either in the global package registry or with the package registry of ResourceSet to be used for loading XMLs
- Tell EMF what objects to produce for the model (EMF EObjects or SDO EDataObjects)
- Load a model instance XML
Why to bother?
You may wonder why to do such a complicated thing like this. Well, for us the answer is simple - we want to reuse some old code, which uses SDO DataObjects, and it needs to be exposed via webservices. The simplest way to achieve that without adding other dependencies such as Apache Tuscany or Websphere SCA fix pack is this. We're running it on Websphere and thus EMF 2.2.1 is at our disposal. But there are certainly other cases where at least part of this approach may be useful.You may also ask why to use a dynamic model, which is less efficient than a static one (though EMF reflection access is still faster than native Java one) and certainly much less readable and easy to use with its strange eSet(EStructuralFeature feature, Object newValue). Well, the reason is flexibility - if your model changes then you only need to update your XSDs (which could be downloaded from somewhere or stored in a database). You don't need to regenerate any classes and redeploy your application. If you know the bureaucracy of large companies, you understand it can save you weeks or even months. Of course everything is a question of pros and cons.
Load the model elements defined in the XSDs into EPackages
First of all you need to create a dynamic EMF model from the XSDs, which is done with the help of an XSDEcoreBuilder:
import org.eclipse.xsd.ecore.XSDEcoreBuilder;
import org.eclipse.emf.ecore.EPackage;
...
public class EmfSdoModel {
...
private ResourceSet loadedModelResources = null;
/** Load EMF/SDO model from XSDs and set the this.loadedModelResources ResourceSet with the EPackages found. */
public void initModelFromXsd() {
final Collection<Object> loadedPackagesEtc = new XSDEcoreBuilder().generate(getSchemaUris());
final Collection<EPackage> eCorePackages = new LinkedList<EPackage>();
for (Object loadedObject : loadedPackagesEtc) {
if (loadedObject instanceof EPackage) {
eCorePackages.add((EPackage) loadedObject);
} else {
final String typeInfo = (loadedObject == null)?
"N/A" : loadedObject.getClass().getName();
LOG.info("initModelFromXsd: A non-EPackage in the input: " + typeInfo);
}
}
// TODO Fail if no packages found
this.loadedModelResources = registerDynamicPackages(eCorePackages);
}
...
}
If you're interested in loading XSDs from an InputStream, check the EMF FAQ How can I load a XSDSchema from a simple Java String or from a DOM Tree?.
Next the loaded EPackages must be registered with EMF under their namespace URIs so that it can find an appropriate package when parsing an XML (see my previous post regarding namespace declarations and EMF). But let's first see how the XSDs to be loaded are referenced:
import org.eclipse.emf.common.util.URI;
...
private Collection<URI> getSchemaUris() {
final Collection<URI> result = new LinkedList<URI>();
for (String schemaOnCp : this.schemasOnClasspath) {
final URL xsdUrl = getClass().getResource(schemaOnCp); // fail if null
result.add(URI.createURI(
xsdUrl.toExternalForm()));
}
return result;
}
The XSDs are located on the classpath (under WEB-INF/classes/) and their paths like "/xsd/AbstractBridgeMessage.xsd" are turned to absolute URLs and then to EMF URIs.
Dealing with types defined in a WSDL
If you want to use EMF to create model instances based on a webservice message and some of the types - likely the "container" types for the request and response - are defined in an embedded xsd:schema in its WSDL file as below:
<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions ...>
<wsdl:types>
<xsd:schema targetNamespace="http://w3.ibm.com/xmlns/ibmww/hr/learning/lms/br/la"
xmlns:bons1="http://w3.ibm.com/xmlns/ibmww/hr/learning/lms/br"
xmlns:tns="http://w3.ibm.com/xmlns/ibmww/hr/learning/lms/br/la"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:import namespace="http://w3.ibm.com/xmlns/ibmww/hr/learning/lms/br" schemaLocation="../xsd-includes/http.w3.ibm.com.xmlns.ibmww.hr.learning.lms.br.xsd"/>
<xsd:include schemaLocation="LearningActivityMessage.xsd"/>
<xsd:element name="updateLearningActivity">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="learningActivityMsg" nillable="true" type="tns:LearningActivityMessage"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="updateLearningActivityResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="result" nillable="true" type="bons1:TransactionResponseMessage"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
</wsdl:types>
...
</wsdl:definitions>
then one of the ways to let EMF know about them is to extract the schema manually into an XSD of its own and to declare types for the elements (the only change is the replacement of an xsd:element by its nested xsd:complexType while preserving the name):
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema targetNamespace="http://w3.ibm.com/xmlns/ibmww/hr/learning/lms/br/la"
xmlns:bons1="http://w3.ibm.com/xmlns/ibmww/hr/learning/lms/br"
xmlns:tns="http://w3.ibm.com/xmlns/ibmww/hr/learning/lms/br/la"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:import namespace="http://w3.ibm.com/xmlns/ibmww/hr/learning/lms/br"
schemaLocation="../xsd-includes/http.w3.ibm.com.xmlns.ibmww.hr.learning.lms.br.xsd" />
<xsd:include schemaLocation="LearningActivityMessage.xsd" />
<!-- Originally xsd.elements turned to xsd:complexType nam -->
<xsd:complexType name="updateLearningActivity">
<xsd:sequence>
<xsd:element name="learningActivityMsg" nillable="true"
type="tns:LearningActivityMessage" />
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="updateLearningActivityResponse">
<xsd:sequence>
<xsd:element name="result" nillable="true"
type="bons1:TransactionResponseMessage" />
</xsd:sequence>
</xsd:complexType>
</xsd:schema>
Beware class name case If your root element's name begins with a lower case (as in "updateLearningActivityResponse") then the complexType you create for it must also start with a lower case for EMF to be able to match the corresponding EClass with the element. But EClass.getName() will return the name with the first letter upper-cased so if you try to find the EClass in the EPackage by yourself, don't forget to search for it with this change.
Register the loaded packages with a package registry
The aforementioned method registerDynamicPackages creates a ResourceSet and registers the imported dynamic EMF model with it so that it can be used for loading its model instances from XML (remember that EMF must be able to find the EPackage corresponding to any XML element it encounters, which is done via lookup in the registry):
private ResourceSet registerDynamicPackages(
final Collection<EPackage> eCorePackages) {
final ResourceSet resourceSet = new ResourceSetImpl();
// This is necessary when running standalone for no factories have been registered yet:
resourceSet.getResourceFactoryRegistry().getExtensionToFactoryMap().put( "xml",
new XMLResourceFactoryImpl());
for (EPackage ePackage: eCorePackages) {
resourceSet.getPackageRegistry().put(ePackage.getNsURI(), ePackage);
// or register globally: EPackage.Registry.INSTANCE.put(ePackage.getNsURI(), ePackage);
// Create SDO's EDataObjects or EMF's EObjects or st. else?
ePackage.setEFactoryInstance(createModelObjectFactory());
}
return resourceSet;
}
The method createModelObjectFactory() is described in the next section.
Important: If the input XML has no namespace declared on the root element we would need to register the package to be used for parsing the XML (also) as default for the null namespace, see my previous post about EMF and namespace declaration in input XMLs.
Tell EMF what objects to produce for the model
By default EMF 2.2 creates EObjects when importing a model instance but we can force it to produce for example EMF SDO's commonj.sdo.DataObject implementation, in particular the DynamicEDataObjectImpl, by setting a factory on each EPackage:
import org.eclipse.emf.ecore.sdo.impl.DynamicEDataObjectImpl;
...
private ResourceSet registerDynamicPackages(final Collection<EPackage> c) {
...
ePackage.setEFactoryInstance(createModelObjectFactory());
...
}
private FactoryImpl createModelObjectFactory() {
return new DynamicEDataObjectImpl.FactoryImpl();
}
Load a model instance XML
Finally, when we've imported the model and prepared the ResourceSet for loading its instances from XML, we can do so:
public DataObject loadFromXml(final InputStream xmlStream) throws IOException {
final Resource resource = loadedModelResources.createResource(
URI.createURI("inputStream://dummyUriWithValidSuffix.xml")); // fake URI
resource.load(xmlStream, createXmlResourceDeSerializationOptions());
// May throw org.eclipse.emf.ecore.resource.Resource$IOWrappedException: Class 'myRootElement' not found.
// <= ecore.xmi.ClassNotFoundException: Class 'myRootElement' not found.
// if no EClass found for the root XML element given its name and namespace
LOG.info("Resource loaded:" + resource + ", contents:" + resource.getContents());
// => [DynamicEObjectImpl (eClass: EClassImpl(name: myRootElement) (instanceClassName: null) (abstract: false, interface: false))]
final EDataObject loadedEObject = (EDataObject) resource.getContents().get(0);
return loadedEObject;
}
Notice that to load the XML from a stream we need to make a fake URI with an extension mapped to the desired resource factory (.xml in this case) and pass in an InputStream (source).
The method createXmlResourceDeSerializationOptions() only sets the options OPTION_EXTENDED_META_DATA and OPTION_ENCODING as described in my previous post under General notes on XML saving/loading in EMF.
Putting it all together
Finally we will create a webservice that transform its XML input into an SDO object. I've left out the unrelated lines and methods, you can find them in my previous post Creating JAX-WS webservice using Service Data Objects (SDO) instead of JAXB-bound POJOs. The relevant code is:
@javax.xml.ws.ServiceMode(value=javax.xml.ws.Service.Mode.PAYLOAD)
@javax.xml.ws.WebServiceProvider(...)
public class MyRawXmlServiceImpl implements Provider<Source> {
...
private EmfSdoModel emfSdoModel;
@javax.annotation.PostConstruct
public void initializeEmfModel() {
emfSdoModel = new EmfSdoModel();
emfSdoModel.initModelFromXsd();
}
public Source invoke(final Source request) {
final String requestXml = convertRequestToXml(request);
DataObject requestSDO;
try {
final InputStream xmlStream = new ByteArrayInputStream(
requestXml.getBytes("UTF-8"));
requestSDO = emfSdoModel.loadFromXml(xmlStream);
} catch (IOException e) {
throw new RuntimeException("XML->SDO covnersion failed: " + e, e);
}
final DataObject responseSDO = sdoInstance_.updateLearningActivity(requestSDO);
return convertResponse(responseSDO);
}
...
}
Basically we just load the model at startup and then use it to parse XMLs.
When running this code under Websphere Application Server 7.0 you need no additional libraries. When running in another environment, check the libs needed in EMF FAQ.
Summary
I've demonstrated how to create a dynamic EMF model based on XSDs in a web application and how to use that model to parse XMLs into SDO DataObjects or EMF EObjects and also how to integrate that with a JAX-WS webservice.Resources
- Article Build metamodels with dynamic EMF - Create dynamic Ecore-based models on demand without generating Java implementation classes (2007) - Create EClasses, add EAttributes as EStructuralFeatures to them, create an EPackage and add the classes there)
- How to create a dynamic DataObject with an e-enabled map attribute, from an EclipseZone forum - I haven't used it but somebody may need it
- How to create a dynamic SDO model from the dW's articleIntroduction to Service Data Objects
- EMF wiki: EMF/Generating Dynamic Ecore from XML Schema
- My related blog posts: