/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.openjpa.persistence.jest; import static org.apache.openjpa.persistence.jest.Constants.ATTR_ID; import static org.apache.openjpa.persistence.jest.Constants.ATTR_KEY_TYPE; import static org.apache.openjpa.persistence.jest.Constants.ATTR_MEMBER_TYPE; import static org.apache.openjpa.persistence.jest.Constants.ATTR_NAME; import static org.apache.openjpa.persistence.jest.Constants.ATTR_NULL; import static org.apache.openjpa.persistence.jest.Constants.ATTR_TYPE; import static org.apache.openjpa.persistence.jest.Constants.ATTR_VALUE_TYPE; import static org.apache.openjpa.persistence.jest.Constants.ATTR_VERSION; import static org.apache.openjpa.persistence.jest.Constants.ELEMENT_DESCRIPTION; import static org.apache.openjpa.persistence.jest.Constants.ELEMENT_ENTRY; import static org.apache.openjpa.persistence.jest.Constants.ELEMENT_ENTRY_KEY; import static org.apache.openjpa.persistence.jest.Constants.ELEMENT_ENTRY_VALUE; import static org.apache.openjpa.persistence.jest.Constants.ELEMENT_INSTANCE; import static org.apache.openjpa.persistence.jest.Constants.ELEMENT_MEMBER; import static org.apache.openjpa.persistence.jest.Constants.ELEMENT_NULL_REF; import static org.apache.openjpa.persistence.jest.Constants.ELEMENT_REF; import static org.apache.openjpa.persistence.jest.Constants.ELEMENT_URI; import static org.apache.openjpa.persistence.jest.Constants.JEST_INSTANCE_XSD; import static org.apache.openjpa.persistence.jest.Constants.MIME_TYPE_XML; import static org.apache.openjpa.persistence.jest.Constants.NULL_VALUE; import static org.apache.openjpa.persistence.jest.Constants.ROOT_ELEMENT_INSTANCE; import static org.apache.openjpa.persistence.jest.Constants.ROOT_ELEMENT_MODEL; import java.io.BufferedReader; import java.io.CharArrayWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Reader; import java.io.Writer; import java.util.Arrays; import java.util.BitSet; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; import javax.persistence.metamodel.Attribute; import javax.persistence.metamodel.ManagedType; import javax.persistence.metamodel.MapAttribute; import javax.persistence.metamodel.Metamodel; import javax.persistence.metamodel.PluralAttribute; import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import javax.xml.validation.Validator; import org.apache.openjpa.kernel.OpenJPAStateManager; import org.apache.openjpa.kernel.StoreContext; import org.apache.openjpa.lib.util.Localizer; import org.apache.openjpa.meta.ClassMetaData; import org.apache.openjpa.meta.FieldMetaData; import org.apache.openjpa.meta.JavaTypes; import org.apache.openjpa.meta.ValueMetaData; import org.apache.openjpa.persistence.meta.Members; import org.apache.openjpa.util.InternalException; import org.w3c.dom.CDATASection; import org.w3c.dom.Document; import org.w3c.dom.Element; /** * Marshals a root instance and its persistent closure as an XML element. * The closure is resolved against the persistence context that contains the root instance. * The XML document adheres to the <code>jest-instance.xsd</code> schema. * * @author Pinaki Poddar * */ public class XMLFormatter implements ObjectFormatter<Document> { public static final Schema _xsd; private static final DocumentBuilder _builder; private static final Transformer _transformer; private static final String EMPTY_TEXT = " "; protected static Localizer _loc = Localizer.forPackage(XMLFormatter.class); static { try { _builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); _transformer = TransformerFactory.newInstance().newTransformer(); SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); InputStream xsd = XMLFormatter.class.getResourceAsStream(JEST_INSTANCE_XSD); _xsd = factory.newSchema(new StreamSource(xsd)); _transformer.setOutputProperty(OutputKeys.METHOD, "xml"); _transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); _transformer.setOutputProperty(OutputKeys.INDENT, "yes"); _transformer.setOutputProperty(OutputKeys.STANDALONE, "no"); _transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); _transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); } catch (Exception e) { throw new RuntimeException(e); } } public String getMimeType() { return MIME_TYPE_XML; } /** * Encodes the closure of given collection of managed instance into a new XML document * according to JEST Instance XML Schema. * * @param sm a collection of managed instances. * @param parent the parent node to which the new node be attached. */ public Document encode(final Collection<OpenJPAStateManager> sms, Metamodel model) { Element root = newDocument(ROOT_ELEMENT_INSTANCE); Closure closure = new Closure(sms); for (OpenJPAStateManager sm : closure) { encodeManagedInstance(sm, root, false, model); } return root.getOwnerDocument(); } /** * Encodes the given meta-model into a new XML document according to JEST Domain XML Schema. * * @param model a persistent domain model. Must not be null. */ public Document encode(Metamodel model) { Element root = newDocument(ROOT_ELEMENT_MODEL); for (ManagedType<?> t : model.getManagedTypes()) { encodeManagedType(t, root); } return root.getOwnerDocument(); } /** * Create a new document with the given tag as the root element. * * @param rootTag the tag of the root element * * @return the document element of a new document */ public Element newDocument(String rootTag) { Document doc = _builder.newDocument(); Element root = doc.createElement(rootTag); doc.appendChild(root); String[] nvpairs = new String[] { "xmlns:xsi", XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, // "xsi:noNamespaceSchemaLocation", JEST_INSTANCE_XSD, ATTR_VERSION, "1.0", }; for (int i = 0; i < nvpairs.length; i += 2) { root.setAttribute(nvpairs[i], nvpairs[i+1]); } return root; } @Override public Document writeOut(Collection<OpenJPAStateManager> objs, Metamodel model, String title, String desc, String uri, OutputStream out) throws IOException { Document doc = encode(objs, model); decorate(doc, title, desc, uri); write(doc, out); return doc; } @Override public Document writeOut(Metamodel model, String title, String desc, String uri, OutputStream out) throws IOException { Document doc = encode(model); decorate(doc, title, desc, uri); write(doc, out); return doc; } Document decorate(Document doc, String title, String desc, String uri) { Element root = doc.getDocumentElement(); Element instance = (Element)root.getElementsByTagName(ELEMENT_INSTANCE).item(0); Element uriElement = doc.createElement(ELEMENT_URI); uriElement.setTextContent(uri == null ? NULL_VALUE : uri); Element descElement = doc.createElement(ELEMENT_DESCRIPTION); descElement.setTextContent(desc == null ? NULL_VALUE : desc); root.insertBefore(uriElement, instance); root.insertBefore(descElement, instance); return doc; } public void write(Document doc, OutputStream out) throws IOException { try { _transformer.transform(new DOMSource(doc), new StreamResult(out)); } catch (Exception e) { throw new IOException(e); } } public void write(Document doc, Writer writer) throws IOException { try { _transformer.transform(new DOMSource(doc), new StreamResult(writer)); } catch (Exception e) { throw new IOException(e); } } /** * Encodes the closure of a persistent instance into a XML element. * * @param sm the managed instance to be encoded. Can be null. * @param parent the parent XML element to which the new XML element be added. Must not be null. Must be * owned by a document. * @param visited the persistent instances that had been encoded already. Must not be null or immutable. * * @return the new element. The element has been appended as a child to the given parent in this method. */ private Element encodeManagedInstance(final OpenJPAStateManager sm, final Element parent, boolean isRef, Metamodel model) { if (parent == null) throw new InternalException(_loc.get("format-xml-null-parent")); Document doc = parent.getOwnerDocument(); if (doc == null) throw new InternalException(_loc.get("format-xml-null-doc")); if (sm == null || isRef) { return encodeRef(parent, sm); } Element root = doc.createElement(ELEMENT_INSTANCE); parent.appendChild(root); root.setAttribute(ATTR_ID, ior(sm)); Element child = null; BitSet loaded = sm.getLoaded(); StoreContext ctx = (StoreContext)sm.getGenericContext(); List<Attribute<?, ?>> attrs = MetamodelHelper.getAttributesInOrder(sm.getMetaData(), model); for (int i = 0; i < attrs.size(); child = null, i++) { Members.Member<?, ?> attr = (Members.Member<?, ?>) attrs.get(i); FieldMetaData fmd = attr.fmd; if (!loaded.get(fmd.getIndex())) continue; String tag = MetamodelHelper.getTagByAttributeType(attr); Object value = sm.fetch(fmd.getIndex()); switch (fmd.getDeclaredTypeCode()) { case JavaTypes.BOOLEAN: case JavaTypes.BYTE: case JavaTypes.CHAR: case JavaTypes.DOUBLE: case JavaTypes.FLOAT: case JavaTypes.INT: case JavaTypes.LONG: case JavaTypes.SHORT: case JavaTypes.BOOLEAN_OBJ: case JavaTypes.BYTE_OBJ: case JavaTypes.CHAR_OBJ: case JavaTypes.DOUBLE_OBJ: case JavaTypes.FLOAT_OBJ: case JavaTypes.INT_OBJ: case JavaTypes.LONG_OBJ: case JavaTypes.SHORT_OBJ: case JavaTypes.BIGDECIMAL: case JavaTypes.BIGINTEGER: case JavaTypes.DATE: case JavaTypes.NUMBER: case JavaTypes.CALENDAR: case JavaTypes.LOCALE: case JavaTypes.STRING: case JavaTypes.ENUM: child = doc.createElement(tag); child.setAttribute(ATTR_NAME, fmd.getName()); if (value == null) { encodeNull(child); } else { encodeBasic(child, value, fmd.getDeclaredType()); } break; case JavaTypes.OID: child = doc.createElement(ELEMENT_REF); child.setAttribute(ATTR_NAME, fmd.getName()); if (value == null) { encodeNull(child); } else { encodeBasic(child, value, fmd.getDeclaredType()); } break; case JavaTypes.PC: child = doc.createElement(tag); child.setAttribute(ATTR_NAME, fmd.getName()); child.setAttribute(ATTR_TYPE, typeOf(fmd)); OpenJPAStateManager other = ctx.getStateManager(value); encodeManagedInstance(other, child, true, model); break; case JavaTypes.ARRAY: Object[] values = (Object[])value; value = Arrays.asList(values); // no break; case JavaTypes.COLLECTION: child = doc.createElement(tag); child.setAttribute(ATTR_NAME, fmd.getName()); child.setAttribute(ATTR_TYPE, typeOf(fmd)); child.setAttribute(ATTR_MEMBER_TYPE, typeOf(fmd.getElement().getDeclaredType())); if (value == null) { encodeNull(child); break; } Collection<?> members = (Collection<?>)value; boolean basic = fmd.getElement().getTypeMetaData() == null; for (Object o : members) { Element member = doc.createElement(ELEMENT_MEMBER); child.appendChild(member); if (o == null) { encodeNull(member); } else { if (basic) { encodeBasic(member, o, o.getClass()); } else { encodeManagedInstance(ctx.getStateManager(o), member, true, model); } } } break; case JavaTypes.MAP: child = doc.createElement(tag); child.setAttribute(ATTR_NAME, fmd.getName()); child.setAttribute(ATTR_TYPE, typeOf(fmd)); child.setAttribute(ATTR_KEY_TYPE, typeOf(fmd.getElement().getDeclaredType())); child.setAttribute(ATTR_VALUE_TYPE, typeOf(fmd.getValue().getDeclaredType())); if (value == null) { encodeNull(child); break; } Set<Map.Entry<?,?>> entries = ((Map)value).entrySet(); boolean basicKey = fmd.getElement().getTypeMetaData() == null; boolean basicValue = fmd.getValue().getTypeMetaData() == null; for (Map.Entry<?,?> e : entries) { Element entry = doc.createElement(ELEMENT_ENTRY); Element entryKey = doc.createElement(ELEMENT_ENTRY_KEY); Element entryValue = doc.createElement(ELEMENT_ENTRY_VALUE); entry.appendChild(entryKey); entry.appendChild(entryValue); child.appendChild(entry); if (e.getKey() == null) { encodeNull(entryKey); } else { if (basicKey) { encodeBasic(entryKey, e.getKey(), e.getKey().getClass()); } else { encodeManagedInstance(ctx.getStateManager(e.getKey()), entryKey, true, model); } } if (e.getValue() == null) { encodeNull(entryValue); } else { if (basicValue) { encodeBasic(entryValue, e.getValue(), e.getValue().getClass()); } else { encodeManagedInstance(ctx.getStateManager(e.getValue()), entryValue, true, model); } } } break; case JavaTypes.INPUT_STREAM: case JavaTypes.INPUT_READER: child = doc.createElement(tag); child.setAttribute(ATTR_NAME, fmd.getName()); child.setAttribute(ATTR_TYPE, typeOf(fmd)); if (value == null) { encodeNull(child); } else { CDATASection data = doc.createCDATASection(streamToString(value)); child.appendChild(data); } break; case JavaTypes.PC_UNTYPED: case JavaTypes.OBJECT: // START - ALLOW PRINT STATEMENTS System.err.println("Not handled " + fmd.getName() + " of type " + fmd.getDeclaredType()); // STOP - ALLOW PRINT STATEMENTS } if (child != null) { root.appendChild(child); } } return root; } /** * Sets the given value element as null. The <code>null</code> attribute is set to true. * * @param element the XML element to be set */ private void encodeNull(Element element) { element.setAttribute(ATTR_NULL, "true"); } private Element encodeRef(Element parent, OpenJPAStateManager sm) { Element ref = parent.getOwnerDocument().createElement(sm == null ? ELEMENT_NULL_REF : ELEMENT_REF); if (sm != null) ref.setAttribute(ATTR_ID, ior(sm)); // IMPORTANT: for xml transformer not to omit the closing tag, otherwise dojo is confused ref.setTextContent(EMPTY_TEXT); parent.appendChild(ref); return ref; } /** * Sets the given value element. The <code>type</code> is set to the given runtime type. * String form of the given object is set as the text content. * * @param element the XML element to be set * @param obj value of the element. Never null. */ private void encodeBasic(Element element, Object obj, Class<?> runtimeType) { element.setAttribute(ATTR_TYPE, typeOf(runtimeType)); if (obj instanceof Date) element.setTextContent(dateFormat.format(obj)); else element.setTextContent(obj == null ? NULL_VALUE : obj.toString()); } /** * Convert the given stream (either an InutStream or a Reader) to a String * to be included in CDATA section of a XML document. * * @param value the field value to be converted. Can not be null */ private String streamToString(Object value) { Reader reader = null; if (value instanceof InputStream) { reader = new BufferedReader(new InputStreamReader((InputStream)value)); } else if (value instanceof Reader) { reader = (Reader)value; } else { throw new RuntimeException(); } CharArrayWriter writer = new CharArrayWriter(); try { for (int c; (c = reader.read()) != -1;) { writer.write(c); } } catch (IOException ex) { throw new RuntimeException(ex); } return writer.toString(); } private void encodeManagedType(ManagedType<?> type, Element parent) { Document doc = parent.getOwnerDocument(); Element root = doc.createElement(type.getPersistenceType().toString().toLowerCase()); parent.appendChild(root); root.setAttribute(ATTR_NAME, type.getJavaType().getSimpleName()); List<Attribute<?,?>> attributes = MetamodelHelper.getAttributesInOrder(type); for (Attribute<?,?> a : attributes) { String tag = MetamodelHelper.getTagByAttributeType(a); Element child = doc.createElement(tag); root.appendChild(child); child.setAttribute(ATTR_TYPE, typeOf(a.getJavaType())); if (a instanceof PluralAttribute) { if (a instanceof MapAttribute) { child.setAttribute(ATTR_KEY_TYPE, typeOf(((MapAttribute)a).getKeyJavaType())); child.setAttribute(ATTR_VALUE_TYPE, typeOf(((MapAttribute)a).getBindableJavaType())); } else { child.setAttribute(ATTR_MEMBER_TYPE, typeOf(((PluralAttribute)a).getBindableJavaType())); } } child.setTextContent(a.getName()); } } void validate(Document doc) throws Exception { Validator validator = _xsd.newValidator(); validator.validate(new DOMSource(doc)); } String ior(OpenJPAStateManager sm) { return typeOf(sm) + "-" + sm.getObjectId(); } String typeOf(OpenJPAStateManager sm) { return sm.getMetaData().getDescribedType().getSimpleName(); } String typeOf(Class<?> cls) { return cls.getSimpleName(); } String typeOf(ClassMetaData meta) { return meta.getDescribedType().getSimpleName(); } String typeOf(ValueMetaData vm) { if (vm.getTypeMetaData() == null) return typeOf(vm.getType()); return typeOf(vm.getTypeMetaData()); } String typeOf(FieldMetaData fmd) { return fmd.getType().getSimpleName(); } }