Java tutorial
/** * PlasmaSDO License * * This is a community release of PlasmaSDO, a dual-license * Service Data Object (SDO) 2.1 implementation. * This particular copy of the software is released under the * version 2 of the GNU General Public License. PlasmaSDO was developed by * TerraMeta Software, Inc. * * Copyright (c) 2013, TerraMeta Software, Inc. All rights reserved. * * General License information can be found below. * * This distribution may include materials developed by third * parties. For license and attribution notices for these * materials, please refer to the documentation that accompanies * this distribution (see the "Licenses for Third-Party Components" * appendix) or view the online documentation at * <http://plasma-sdo.org/licenses/>. * */ package org.plasma.sdo.xml; import java.io.InputStream; import java.io.Reader; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Stack; import javanet.staxutils.events.EventAllocator; import javax.xml.namespace.QName; import javax.xml.stream.Location; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLResolver; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import javax.xml.stream.events.Attribute; import javax.xml.stream.events.XMLEvent; import javax.xml.stream.util.XMLEventAllocator; import javax.xml.transform.Source; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.plasma.sdo.DataFlavor; import org.plasma.sdo.PlasmaDataObject; import org.plasma.sdo.PlasmaProperty; import org.plasma.sdo.PlasmaType; import org.plasma.sdo.core.CoreHelper; import org.plasma.sdo.core.CoreXMLDocument; import org.plasma.sdo.helper.PlasmaDataFactory; import org.plasma.sdo.helper.PlasmaDataHelper; import org.plasma.sdo.helper.PlasmaTypeHelper; import org.plasma.sdo.profile.KeyType; import org.plasma.xml.schema.SchemaConstants; import org.plasma.xml.schema.SchemaUtil; import commonj.sdo.DataGraph; import commonj.sdo.Property; import commonj.sdo.helper.XMLDocument; /** * A Streaming API for XML (StAX) parser based XML unmarshaller which converts * various (stream-based) XML input source types into an SDO * data graph. The resulting data graph is ready to be used, e.g. modified or * committed using a Data Access Service (DAS). * * This unmarshaller coalesces or merges XML nodes associated * with SDO types which have external key properties, creating both containment * and non-containment reference (property) associations between data objects. * * Since the final coalesced data graph structure is not known until the entire * stream is parsed, lightweight "scaffolding" structures are first used to collect * objects and property values into an initial node graph. This graph is then * walked and the nodes coalesced into the final data-object graph. The * scaffolding nodes are then discarded. * */ //FIXME: add XML Option to specify desired state of the graph e.g. whether the //change summary is set up for (insert, update, merge, ...). Currently is is set //up only for insert. public class StreamUnmarshaller extends Unmarshaller { private static Log log = LogFactory.getLog(StreamUnmarshaller.class); private XMLEventAllocator allocator = null; private XMLInputFactory factory; private Stack<StreamNode> stack = new Stack<StreamNode>(); private Map<Object, StreamObject> keyMap = new HashMap<Object, StreamObject>(); private String currentNamespaceUri = null; private XMLOptions options; private String locationURI; private Timestamp snapshotDate = new Timestamp((new Date()).getTime()); private StringBuilder charbuf = new StringBuilder(); public StreamUnmarshaller(XMLOptions options, String locationURI) { super(UnmarshallerFlavor.STAX); this.options = options; this.locationURI = locationURI; setup(); } public XMLDocument getResult() { if (this.result == null) throw new IllegalStateException("unmarshaling not yet performed - call unmarshal(...)"); return this.result; } private void setup() { this.factory = XMLInputFactory.newInstance(); factory.setXMLResolver(new XMLResolver() { public Object resolveEntity(String publicID, String systemID, String baseURI, String namespace) throws XMLStreamException { log.debug("resolve: " + publicID + ":" + systemID + ":" + baseURI + ":" + namespace); return null; } }); factory.setEventAllocator(new EventAllocator()); factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.FALSE); factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.TRUE); //set the IS_COALESCING property to true , if application desires to //get whole text data as one event. //factory.setProperty(XMLInputFactory.IS_COALESCING , Boolean.TRUE); allocator = factory.getEventAllocator(); } /** * initialize for a new unmarshal run */ private void init() { this.snapshotDate = new Timestamp((new Date()).getTime()); this.stack.clear(); this.keyMap.clear(); this.charbuf = new StringBuilder(); } /** * Reads the given input stream in its entirety, and closes the stream when * complete. The data graph result is retrieved using the {@link #getResult() getResult} * method. * @param stream the XML stream to read * @throws XMLStreamException if the given document is found to be malformed * @throws UnmarshallerException if undefined properties, or classes are found in the * XML stream. */ public void unmarshal(InputStream stream) throws XMLStreamException, UnmarshallerException { XMLStreamReader streamReader = factory.createXMLStreamReader(stream); init(); try { unmarshal(streamReader); } finally { streamReader.close(); } } /** * Reads the given source as a stream in its entirety, and closes the stream when * complete. The data graph result is retrieved using the {@link #getResult() getResult} * method. * @param source the XML source to read * @throws XMLStreamException if the given document is found to be malformed * @throws UnmarshallerException if undefined properties, or classes are found in the * XML stream. */ public void unmarshal(Source source) throws XMLStreamException, UnmarshallerException { XMLStreamReader streamReader = factory.createXMLStreamReader(source); init(); try { unmarshal(streamReader); } finally { streamReader.close(); } } /** * Reads the given reader as a stream in its entirety, and closes the stream when * complete. The data graph result is retrieved using the {@link #getResult() getResult} * method. * @param reader the XML reader to read * @throws XMLStreamException if the given document is found to be malformed * @throws UnmarshallerException if undefined properties, or classes are found in the * XML stream. */ public void unmarshal(Reader reader) throws XMLStreamException, UnmarshallerException { XMLStreamReader streamReader = factory.createXMLStreamReader(reader); init(); try { unmarshal(streamReader); } finally { streamReader.close(); } } private void unmarshal(XMLStreamReader streamReader) throws XMLStreamException, UnmarshallerException { // read the stream StreamObject root = read(streamReader); final DataGraph dataGraph = PlasmaDataFactory.INSTANCE.createDataGraph(); dataGraph.getChangeSummary().beginLogging(); // log changes from this point // setup containment graph StreamObjectVisitor visitor = new StreamObjectVisitor() { public void visit(StreamObject target, StreamObject source, PlasmaProperty sourceProperty, int level) { if (target.isNonContainmentReference()) return; // ignore non-containment reference objects PlasmaDataObject dataObject = null; if (source == null) { dataObject = (PlasmaDataObject) dataGraph.createRootObject(target.getType()); } else { PlasmaDataObject parent = source.getDataObject(); dataObject = (PlasmaDataObject) parent.createDataObject(target.getSource()); } target.setDataObject(dataObject); // Map containment reference objects w/an serial key // Determine a containment reference based on an // xsi:type match with the SDO type. Object key = target.getSerialId(); StreamObject existing = keyMap.get(key); if (existing != null) { String msg = "line:col[" + target.getLine() + ":" + target.getColumn() + "]"; msg += " - attempt to instantiate multiple instances of '" + target.getLocalName() + "' using the same reference key '" + String.valueOf(key) + "'"; throw new DuplicateContainmentReferenceException(msg); } keyMap.put(key, target); // set properties Iterator<PlasmaProperty> iter = target.getValues().keySet().iterator(); while (iter.hasNext()) { PlasmaProperty property = iter.next(); if (property.getType().isDataType()) { Object value = target.get(property); if (!property.isReadOnly()) { if (!property.isMany()) dataObject.set(property, value); else dataObject.setList(property, (List) value); } else { CoreHelper.set(dataObject, property.getName(), value); } } } } }; root.accept(visitor); // traverse // setup non-containment references visitor = new StreamObjectVisitor() { @SuppressWarnings("unchecked") public void visit(StreamObject target, StreamObject source, PlasmaProperty sourceProperty, int level) { if (!target.isNonContainmentReference()) return; Object key = target.getSerialId(); if (key == null) throw new IllegalStateException("expected key"); StreamObject existing = keyMap.get(key); if (existing != null) { if (!target.getSource().isMany()) { source.getDataObject().set(target.getSource(), existing.getDataObject()); } else { List list = (List) source.getDataObject().get(target.getSource()); if (list == null) list = new ArrayList(); list.add(existing.getDataObject()); source.getDataObject().set(target.getSource(), list); } } else { String msg = "line:col[" + target.getLine() + ":" + target.getColumn() + "]"; msg += " - reference value '" + String.valueOf(key) + "' not found within document"; throw new InvalidReferenceException(msg); } } }; root.accept(visitor); // traverse visitor = new StreamObjectVisitor() { public void visit(StreamObject target, StreamObject source, PlasmaProperty sourceProperty, int level) { target.getValues().clear(); target.setDataObject(null); } }; root.accept(visitor); // traverse this.result = new CoreXMLDocument(dataGraph.getRootObject(), this.options); } private StreamObject read(XMLStreamReader streamReader) throws XMLStreamException, UnmarshallerException { int eventType; StreamObject root = null; while (streamReader.hasNext()) { eventType = streamReader.next(); XMLEvent event = allocateXMLEvent(streamReader); switch (eventType) { case XMLEvent.START_ELEMENT: QName name = event.asStartElement().getName(); String uri = name.getNamespaceURI(); if (uri != null && uri.trim().length() > 0) this.currentNamespaceUri = uri.trim(); if (stack.size() == 0) { String typeName = name.getLocalPart(); PlasmaType type = (PlasmaType) PlasmaTypeHelper.INSTANCE.getType(currentNamespaceUri, typeName); if (log.isDebugEnabled()) log.debug("unmarshaling root: " + type.getURI() + "#" + type.getName()); root = new StreamObject(type, name, event.getLocation()); stack.push(root); } else { StreamObject sreamObject = (StreamObject) stack.peek(); PlasmaType type = sreamObject.getType(); QName elemName = event.asStartElement().getName(); PlasmaProperty property = getPropertyByLocalName(event, type, elemName.getLocalPart()); if (property.getType().isDataType()) { // still need characters event to populate this property. We expect to // pop this back off the stack after its characters event is processed below stack.push(new StreamProperty((PlasmaType) property.getType(), property, name, event.getLocation())); break; // we expect no attributes !! } else { if (log.isDebugEnabled()) log.debug("unmarshaling: " + property.getType().getURI() + "#" + property.getType().getName()); // The source is a reference property but we don't know at this point // whether its a reference object. // Push the new DO so we can value its contents on subsequent events stack.push(new StreamObject((PlasmaType) property.getType(), property, name, event.getLocation())); } } StreamObject streamObject = (StreamObject) stack.peek(); readAttributes(event, streamObject); break; case XMLEvent.END_ELEMENT: StreamNode node = stack.pop(); if (stack.size() == 0) break; // link stream objects creating an initial graph StreamObject other = (StreamObject) stack.peek(); if (node instanceof StreamProperty) { StreamProperty streamProp = (StreamProperty) node; if (this.charbuf.length() > 0) { readCharacters(streamProp, this.charbuf.toString(), event); this.charbuf.setLength(0); } link((StreamProperty) node, other); } else { link((StreamObject) node, other); } break; case XMLEvent.CHARACTERS: if (stack.size() == 0) break; String data = event.asCharacters().getData(); if (log.isDebugEnabled()) log.debug("unmarshaling characters: " + String.valueOf(data)); if (data == null) { break; // ignore null } if (data.contains(">")) { //Note: we even get escaped '>' char here so // can't accurately determine well-formedness Location loc = event.getLocation(); String msg = "line:col[" + loc.getLineNumber() + ":" + loc.getColumnNumber() + "]"; msg += " - document may not be well-formed"; log.warn(msg); } StreamNode streamNode = stack.peek(); if (streamNode instanceof StreamProperty) { this.charbuf.append(data); } else { if (log.isDebugEnabled()) { StreamObject streamObj = (StreamObject) streamNode; Location loc = event.getLocation(); String msg = "line:col[" + loc.getLineNumber() + ":" + loc.getColumnNumber() + "]"; msg += " - ignoring character(s) data '" + data + "' for complex type " + streamObj.getType().getURI() + "#" + streamObj.getType().getName(); log.debug(msg); } } break; default: logEventInfo(event); } } return root; } private void readCharacters(StreamProperty streamProperty, String data, XMLEvent event) throws UnmarshallerException { PlasmaType type = streamProperty.getType(); PlasmaProperty property = streamProperty.getProperty(); if (!property.getType().isDataType()) { Location loc = event.getLocation(); String msg = "line:col[" + loc.getLineNumber() + ":" + loc.getColumnNumber() + "]"; msg += " - cannot set reference propery (" + type.getURI() + "#" + type.getName() + "." + property.getName() + ") - from character data"; throw new UnmarshallerException(msg); } if (!property.isReadOnly()) { if (!property.isMany()) { Object value = PlasmaDataHelper.INSTANCE.convert(property, data); streamProperty.set(value); } else { Object value = PlasmaDataHelper.INSTANCE.convert(property, data); streamProperty.set(value); } } else { Location loc = event.getLocation(); String msg = "line:col[" + loc.getLineNumber() + ":" + loc.getColumnNumber() + "]"; msg += " - cannot set readonly propery (" + property.getName() + ") - ignoring character data '" + data + "' for readonly propery, " + type.getURI() + "#" + type.getName() + "." + property.getName(); log.warn(msg); } } private void link(StreamProperty propertyNode, StreamObject parent) { Object value = propertyNode.get(); if (value != null) { // maybe no characters data parent.set(propertyNode.getProperty(), value); } } private void link(StreamObject objectNode, StreamObject parent) throws UnmarshallerException { if (!objectNode.getSource().isMany()) { StreamObject existingValue = (StreamObject) parent.get(objectNode.getSource()); if (existingValue != null) { String msg = "line:col[" + objectNode.getLine() + ":" + objectNode.getColumn() + "]"; msg += " - invalid element '" + objectNode.getLocalName() + "' - found existing element " + "for singular property, " + parent.getType().getURI() + "#" + parent.getType().getName() + "." + objectNode.getSource().getName(); throw new UnmarshallerException(msg); } parent.set(objectNode.getSource(), objectNode); } else { List<Object> list = (List<Object>) parent.get(objectNode.getSource()); if (list == null) { list = new ArrayList<Object>(); parent.set(objectNode.getSource(), list); } list.add(objectNode); } } @SuppressWarnings("unchecked") private void readAttributes(XMLEvent event, StreamObject prototype) throws UnmarshallerException { boolean serialIdAttributeFound = false; PlasmaType type = prototype.getType(); Iterator<Attribute> iter = event.asStartElement().getAttributes(); while (iter.hasNext()) { Attribute attrib = (Attribute) iter.next(); QName attribName = attrib.getName(); String localPart = attribName.getLocalPart(); if (XMLConstants.ATTRIB_TARGET_NAMESPACE.equals(localPart)) { continue; } else if (XMLConstants.XMLSCHEMA_INSTANCE_NAMESPACE_URI.equals(attribName.getNamespaceURI())) { // its an XSI attribute we care about if ("type".equals(localPart)) { String xsiTypeLocalName = attrib.getValue(); int delim = xsiTypeLocalName.indexOf(":"); if (delim >= 0) xsiTypeLocalName = xsiTypeLocalName.substring(delim + 1); // In order to validate XML graphs with both containment // and non containment references with an XML schema, // the Schema types must be generated with XSI 'xsi:type' // attribute to indicate a subclass. The subclass name will be // either 1.) matching the local for the type, wherein we // assume a containment reference or 2.) matching a generated // synthetic name for the non-containment reference for the // type, e.g. 'MyTypeRef'. So for containment references // we should find 'xsi:type="prefix:MyType" and for // non-containment references we should find 'xsi:type="prefix:MyTypeRef" // or some other generated suffix with no name collisions. if (!xsiTypeLocalName.equals(type.getLocalName())) { if (log.isDebugEnabled()) log.debug("type as non-containment reference, " + type.getURI() + "#" + type.getName()); prototype.setNonContainmentReference(true); } } continue; } PlasmaProperty property = findPropertyByLocalName(type, localPart); if (property == null) { if (!SchemaUtil.getSerializationAttributeName().equals(localPart)) { Location loc = event.getLocation(); String msg = "line:col[" + loc.getLineNumber() + ":" + loc.getColumnNumber() + "]"; msg += " - no property '" + localPart + "' found defined for type " + type.getURI() + "#" + type.getName(); throw new UnmarshallerException(msg); } else { prototype.setSerialId(attrib.getValue()); serialIdAttributeFound = true; continue; } } if (property.getType().isDataType()) { if (property.isMany()) { Location loc = event.getLocation(); String msg = "line:col[" + loc.getLineNumber() + ":" + loc.getColumnNumber() + "]"; msg += " - unexpected 'many' propery (" + type.getURI() + "#" + type.getName() + "." + property.getName() + ") can only set singular properties from attribute '" + localPart + "'"; throw new UnmarshallerException(msg); } Object value = PlasmaDataHelper.INSTANCE.convert(property, attrib.getValue()); if (!property.isReadOnly()) { prototype.set(property, value); } else { DataFlavor dataFlavor = property.getDataFlavor(); switch (dataFlavor) { case integral: Location loc = event.getLocation(); String msg = "line:col[" + loc.getLineNumber() + ":" + loc.getColumnNumber() + "]"; msg += " - cannot set integral readonly propery (" + property.getName() + ") - ignoring attribute data '" + attrib.getValue() + "' for readonly propery, " + type.getURI() + "#" + type.getName() + "." + property.getName(); log.warn(msg); break; default: prototype.set(property, value); } } } else { Location loc = event.getLocation(); String msg = "line:col[" + loc.getLineNumber() + ":" + loc.getColumnNumber() + "]"; msg += " - expected datatype property - attribute '" + localPart + "' is a reference property as defined in type " + type.getURI() + "#" + type.getName(); throw new UnmarshallerException(msg); } } if (!serialIdAttributeFound) { Location loc = event.getLocation(); String msg = "line:col[" + loc.getLineNumber() + ":" + loc.getColumnNumber() + "]"; msg += " - expected serialization attribute '" + SchemaUtil.getSerializationAttributeName() + "' for type " + type.getURI() + "#" + type.getName(); throw new UnmarshallerException(msg); } } private PlasmaProperty getPropertyByLocalName(XMLEvent event, PlasmaType type, String localName) throws UnmarshallerException { PlasmaProperty property = findPropertyByLocalName(type, localName); if (property == null) { Location loc = event.getLocation(); String msg = "line:col[" + loc.getLineNumber() + ":" + loc.getColumnNumber() + "]"; msg += " - no property with local name (" + localName + ") found defined for type " + type.getURI() + "#" + type.getName(); ; throw new UnmarshallerException(msg); } return property; } /** * Returns a property from the given type based on the given local name. * @param type the containing SDO Type * @param localName the property local name * @return the property */ // FIXME: does this justify mapping by local names? private PlasmaProperty findPropertyByLocalName(PlasmaType type, String localName) { List<Property> props = type.getProperties(); if (props != null) for (Property p : type.getProperties()) { PlasmaProperty property = (PlasmaProperty) p; if (property.getLocalName().equals(localName)) return property; } return null; } /** Get the immutable XMLEvent from given XMLStreamReader using XMLEventAllocator */ private XMLEvent allocateXMLEvent(XMLStreamReader reader) throws XMLStreamException { return this.allocator.allocate(reader); } private void logEventInfo(XMLEvent event) { if (log.isDebugEnabled()) { Location loc = event.getLocation(); String msg = getEventTypeString(event.getEventType()); msg += " line:col[" + loc.getLineNumber() + ":" + loc.getColumnNumber() + "] - "; msg += event.toString(); log.debug(msg); } } /** * Returns the String representation of the given integer constant. * * @param eventType Type of event. * @return String representation of the event */ public final static String getEventTypeString(int eventType) { switch (eventType) { case XMLEvent.START_ELEMENT: return "START_ELEMENT"; case XMLEvent.END_ELEMENT: return "END_ELEMENT"; case XMLEvent.PROCESSING_INSTRUCTION: return "PROCESSING_INSTRUCTION"; case XMLEvent.CHARACTERS: return "CHARACTERS"; case XMLEvent.COMMENT: return "COMMENT"; case XMLEvent.START_DOCUMENT: return "START_DOCUMENT"; case XMLEvent.END_DOCUMENT: return "END_DOCUMENT"; case XMLEvent.ENTITY_REFERENCE: return "ENTITY_REFERENCE"; case XMLEvent.ATTRIBUTE: return "ATTRIBUTE"; case XMLEvent.DTD: return "DTD"; case XMLEvent.CDATA: return "CDATA"; case XMLEvent.SPACE: return "SPACE"; } return "UNKNOWN_EVENT_TYPE , " + eventType; } } /* Attempt at Schema validation with StAX if (this.locationURI != null) { SchemaFactory factory = SchemaFactory.newInstance(SchemaConstants.XMLSCHEMA_NAMESPACE_URI); try { URI uri = URI.create(this.locationURI); javax.xml.validation.Schema schema = factory.newSchema(new File(uri)); Validator validator = schema.newValidator(); validator.validate(new StreamSource(stream)); } catch (SAXException e) { throw new UnmarshallerRuntimeException(e); } catch (IOException e) { throw new UnmarshallerRuntimeException(e); } } */