http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/xml/XmlDocSerializer.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/xml/XmlDocSerializer.java b/juneau-core/src/main/java/org/apache/juneau/xml/XmlDocSerializer.java new file mode 100644 index 0000000..5c567e8 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/xml/XmlDocSerializer.java @@ -0,0 +1,64 @@ +/*************************************************************************************************************************** + * 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.juneau.xml; + +import static org.apache.juneau.xml.XmlSerializerContext.*; + +import org.apache.juneau.annotation.*; +import org.apache.juneau.serializer.*; + +/** + * Serializes POJOs to HTTP responses as XML. + * + * + * <h6 class='topic'>Media types</h6> + * <p> + * Handles <code>Accept</code> types: <code>text/xml</code> + * <p> + * Produces <code>Content-Type</code> types: <code>text/xml</code> + * + * + * <h6 class='topic'>Description</h6> + * <p> + * Same as {@link XmlSerializer}, except prepends <code><xt><?xml</xt> <xa>version</xa>=<xs>'1.0'</xs> <xa>encoding</xa>=<xs>'UTF-8'</xs><xt>?></xt></code> to the response + * to make it a valid XML document. + * + * + * @author James Bognar ([email protected]) + */ +public class XmlDocSerializer extends XmlSerializer { + + /** Default serializer without namespaces. */ + @Produces(value="text/xml+simple",contentType="text/xml") + public static class Simple extends XmlDocSerializer { + /** Constructor */ + public Simple() { + setProperty(XML_enableNamespaces, false); + } + } + + //-------------------------------------------------------------------------------- + // Overridden methods + //-------------------------------------------------------------------------------- + + @Override /* Serializer */ + protected void doSerialize(SerializerSession session, Object o) throws Exception { + XmlSerializerSession s = (XmlSerializerSession)session; + XmlWriter w = s.getWriter(); + w.append("<?xml") + .attr("version", "1.0") + .attr("encoding", "UTF-8") + .appendln("?>"); + super.doSerialize(s, o); + } +}
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/xml/XmlParser.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/xml/XmlParser.java b/juneau-core/src/main/java/org/apache/juneau/xml/XmlParser.java new file mode 100644 index 0000000..0ed36b9 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/xml/XmlParser.java @@ -0,0 +1,523 @@ +/*************************************************************************************************************************** + * 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.juneau.xml; + +import static javax.xml.stream.XMLStreamConstants.*; +import static org.apache.juneau.internal.StringUtils.*; +import static org.apache.juneau.xml.annotation.XmlFormat.*; + +import java.lang.reflect.*; +import java.util.*; + +import javax.xml.stream.*; + +import org.apache.juneau.*; +import org.apache.juneau.annotation.*; +import org.apache.juneau.parser.*; +import org.apache.juneau.transform.*; +import org.apache.juneau.xml.annotation.*; + +/** + * Parses text generated by the {@link XmlSerializer} class back into a POJO model. + * + * + * <h6 class='topic'>Media types</h6> + * <p> + * Handles <code>Content-Type</code> types: <code>text/xml</code> + * + * + * <h6 class='topic'>Description</h6> + * <p> + * See the {@link XmlSerializer} class for a description of Juneau-generated XML. + * + * + * <h6 class='topic'>Configurable properties</h6> + * <p> + * This class has the following properties associated with it: + * <ul> + * <li>{@link XmlParserContext} + * <li>{@link BeanContext} + * </ul> + * + * + * @author James Bognar ([email protected]) + */ +@SuppressWarnings({ "rawtypes", "unchecked" }) +@Consumes({"text/xml","application/xml"}) +public class XmlParser extends ReaderParser { + + /** Default parser, all default settings.*/ + public static final XmlParser DEFAULT = new XmlParser().lock(); + + private static final int UNKNOWN=0, OBJECT=1, ARRAY=2, STRING=3, NUMBER=4, BOOLEAN=5, NULL=6; + + + private <T> T parseAnything(XmlParserSession session, ClassMeta<T> nt, String currAttr, XMLStreamReader r, Object outer, boolean isRoot) throws Exception { + + BeanContext bc = session.getBeanContext(); + if (nt == null) + nt = (ClassMeta<T>)object(); + PojoTransform<T,Object> transform = (PojoTransform<T,Object>)nt.getPojoTransform(); + ClassMeta<?> ft = nt.getTransformedClassMeta(); + session.setCurrentClass(ft); + + String wrapperAttr = (isRoot && session.isPreserveRootElement()) ? r.getName().getLocalPart() : null; + String typeAttr = r.getAttributeValue(null, "type"); + int jsonType = getJsonType(typeAttr); + String b = r.getAttributeValue(session.getXsiNs(), "nil"); + if (b == null) + b = r.getAttributeValue(null, "nil"); + boolean isNull = b != null && b.equals("true"); + if (jsonType == 0) { + String elementName = session.decodeString(r.getLocalName()); + if (elementName == null || elementName.equals(currAttr)) + jsonType = UNKNOWN; + else + jsonType = getJsonType(elementName); + } + if (! ft.canCreateNewInstance(outer)) { + String c = r.getAttributeValue(null, "_class"); + if (c != null) { + ft = nt = (ClassMeta<T>)bc.getClassMetaFromString(c); + } + } + Object o = null; + + if (jsonType == NULL) { + r.nextTag(); // Discard end tag + return null; + } + if (isNull) { + while (true) { + int e = r.next(); + if (e == END_ELEMENT) + return null; + } + } + + if (ft.isObject()) { + if (jsonType == OBJECT) { + ObjectMap m = new ObjectMap(bc); + parseIntoMap(session, r, m, string(), object()); + if (wrapperAttr != null) + m = new ObjectMap(bc).append(wrapperAttr, m); + o = m.cast(); + } else if (jsonType == ARRAY) + o = parseIntoCollection(session, r, new ObjectList(bc), object()); + else if (jsonType == STRING) { + o = session.decodeString(r.getElementText()); + if (ft.isChar()) + o = o.toString().charAt(0); + } + else if (jsonType == NUMBER) + o = parseNumber(session.decodeLiteral(r.getElementText()), null); + else if (jsonType == BOOLEAN) + o = Boolean.parseBoolean(session.decodeLiteral(r.getElementText())); + else if (jsonType == UNKNOWN) + o = getUnknown(session, r); + } else if (ft.isBoolean()) { + o = Boolean.parseBoolean(session.decodeLiteral(r.getElementText())); + } else if (ft.isCharSequence()) { + o = session.decodeString(r.getElementText()); + } else if (ft.isChar()) { + o = session.decodeString(r.getElementText()).charAt(0); + } else if (ft.isMap()) { + Map m = (ft.canCreateNewInstance(outer) ? (Map)ft.newInstance(outer) : new ObjectMap(bc)); + o = parseIntoMap(session, r, m, ft.getKeyType(), ft.getValueType()); + if (wrapperAttr != null) + o = new ObjectMap(bc).append(wrapperAttr, m); + } else if (ft.isCollection()) { + Collection l = (ft.canCreateNewInstance(outer) ? (Collection)ft.newInstance(outer) : new ObjectList(bc)); + o = parseIntoCollection(session, r, l, ft.getElementType()); + } else if (ft.isNumber()) { + o = parseNumber(session.decodeLiteral(r.getElementText()), (Class<? extends Number>)ft.getInnerClass()); + } else if (ft.canCreateNewInstanceFromObjectMap(outer)) { + ObjectMap m = new ObjectMap(bc); + parseIntoMap(session, r, m, string(), object()); + o = ft.newInstanceFromObjectMap(outer, m); + } else if (ft.canCreateNewBean(outer)) { + if (ft.getXmlMeta().getFormat() == XmlFormat.COLLAPSED) { + String fieldName = r.getLocalName(); + BeanMap m = bc.newBeanMap(outer, ft.getInnerClass()); + BeanPropertyMeta bpm = m.getMeta().getXmlMeta().getPropertyMeta(fieldName); + ClassMeta<?> cm = m.getMeta().getClassMeta(); + Object value = parseAnything(session, cm, currAttr, r, m.getBean(false), false); + setName(cm, value, currAttr); + bpm.set(m, value); + o = m.getBean(); + } else { + BeanMap m = bc.newBeanMap(outer, ft.getInnerClass()); + o = parseIntoBean(session, r, m).getBean(); + } + } else if (ft.isArray()) { + ArrayList l = (ArrayList)parseIntoCollection(session, r, new ArrayList(), ft.getElementType()); + o = bc.toArray(ft, l); + } else if (ft.canCreateNewInstanceFromString(outer)) { + o = ft.newInstanceFromString(outer, session.decodeString(r.getElementText())); + } else if (ft.canCreateNewInstanceFromNumber(outer)) { + o = ft.newInstanceFromNumber(outer, parseNumber(session.decodeLiteral(r.getElementText()), ft.getNewInstanceFromNumberClass())); + } else { + throw new ParseException(session, "Class ''{0}'' could not be instantiated. Reason: ''{1}''", ft.getInnerClass().getName(), ft.getNotABeanReason()); + } + + if (transform != null && o != null) + o = transform.normalize(o, nt); + + if (outer != null) + setParent(nt, o, outer); + + return (T)o; + } + + private <K,V> Map<K,V> parseIntoMap(XmlParserSession session, XMLStreamReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType) throws Exception { + BeanContext bc = session.getBeanContext(); + int depth = 0; + for (int i = 0; i < r.getAttributeCount(); i++) { + String a = r.getAttributeLocalName(i); + // TODO - Need better handling of namespaces here. + if (! (a.equals("type"))) { + K key = session.trim(convertAttrToType(session, m, a, keyType)); + V value = session.trim(convertAttrToType(session, m, r.getAttributeValue(i), valueType)); + setName(valueType, value, key); + m.put(key, value); + } + } + do { + int event = r.nextTag(); + String currAttr; + if (event == START_ELEMENT) { + depth++; + currAttr = session.decodeString(r.getLocalName()); + K key = convertAttrToType(session, m, currAttr, keyType); + V value = parseAnything(session, valueType, currAttr, r, m, false); + setName(valueType, value, currAttr); + if (valueType.isObject() && m.containsKey(key)) { + Object o = m.get(key); + if (o instanceof List) + ((List)o).add(value); + else + m.put(key, (V)new ObjectList(o, value).setBeanContext(bc)); + } else { + m.put(key, value); + } + } else if (event == END_ELEMENT) { + depth--; + return m; + } + } while (depth > 0); + return m; + } + + private <E> Collection<E> parseIntoCollection(XmlParserSession session, XMLStreamReader r, Collection<E> l, ClassMeta<E> elementType) throws Exception { + int depth = 0; + do { + int event = r.nextTag(); + if (event == START_ELEMENT) { + depth++; + E value = parseAnything(session, elementType, null, r, l, false); + l.add(value); + } else if (event == END_ELEMENT) { + depth--; + return l; + } + } while (depth > 0); + return l; + } + + private Object[] doParseArgs(XmlParserSession session, XMLStreamReader r, ClassMeta<?>[] argTypes) throws Exception { + int depth = 0; + Object[] o = new Object[argTypes.length]; + int i = 0; + do { + int event = r.nextTag(); + if (event == START_ELEMENT) { + depth++; + o[i] = parseAnything(session, argTypes[i], null, r, null, false); + i++; + } else if (event == END_ELEMENT) { + depth--; + return o; + } + } while (depth > 0); + return o; + } + + private int getJsonType(String s) { + if (s == null) + return UNKNOWN; + char c = s.charAt(0); + switch(c) { + case 'o': return (s.equals("object") ? OBJECT : UNKNOWN); + case 'a': return (s.equals("array") ? ARRAY : UNKNOWN); + case 's': return (s.equals("string") ? STRING : UNKNOWN); + case 'b': return (s.equals("boolean") ? BOOLEAN : UNKNOWN); + case 'n': { + c = s.charAt(2); + switch(c) { + case 'm': return (s.equals("number") ? NUMBER : UNKNOWN); + case 'l': return (s.equals("null") ? NULL : UNKNOWN); + } + //return NUMBER; + } + } + return UNKNOWN; + } + + private <T> BeanMap<T> parseIntoBean(XmlParserSession session, XMLStreamReader r, BeanMap<T> m) throws Exception { + BeanMeta bMeta = m.getMeta(); + XmlBeanMeta xmlMeta = bMeta.getXmlMeta(); + + for (int i = 0; i < r.getAttributeCount(); i++) { + String key = session.decodeString(r.getAttributeLocalName(i)); + String val = r.getAttributeValue(i); + BeanPropertyMeta bpm = xmlMeta.getPropertyMeta(key); + if (bpm == null) { + if (m.getMeta().isSubTyped()) { + m.put(key, val); + } else { + Location l = r.getLocation(); + onUnknownProperty(session, key, m, l.getLineNumber(), l.getColumnNumber()); + } + } else { + bpm.set(m, val); + } + } + + BeanPropertyMeta cp = xmlMeta.getXmlContentProperty(); + if (cp != null) { + XmlContentHandler h = xmlMeta.getXmlContentHandler(); + if (h != null) { + h.parse(r, m.getBean()); + } else { + String text = r.getElementText(); + cp.set(m, session.decodeString(text)); + } + return m; + } + + int depth = 0; + do { + int event = r.nextTag(); + String currAttr; + if (event == START_ELEMENT) { + depth++; + currAttr = session.decodeString(r.getLocalName()); + BeanPropertyMeta pMeta = xmlMeta.getPropertyMeta(currAttr); + if (pMeta == null) { + if (m.getMeta().isSubTyped()) { + Object value = parseAnything(session, string(), currAttr, r, m.getBean(false), false); + m.put(currAttr, value); + } else { + Location l = r.getLocation(); + onUnknownProperty(session, currAttr, m, l.getLineNumber(), l.getColumnNumber()); + skipCurrentTag(r); + } + } else { + session.setCurrentProperty(pMeta); + XmlFormat xf = pMeta.getXmlMeta().getXmlFormat(); + if (xf == COLLAPSED) { + ClassMeta<?> et = pMeta.getClassMeta().getElementType(); + Object value = parseAnything(session, et, currAttr, r, m.getBean(false), false); + setName(et, value, currAttr); + pMeta.add(m, value); + } else if (xf == ATTR) { + pMeta.set(m, session.decodeString(r.getAttributeValue(0))); + r.nextTag(); + } else { + ClassMeta<?> cm = pMeta.getClassMeta(); + Object value = parseAnything(session, cm, currAttr, r, m.getBean(false), false); + setName(cm, value, currAttr); + pMeta.set(m, value); + } + session.setCurrentProperty(null); + } + } else if (event == END_ELEMENT) { + depth--; + return m; + } + } while (depth > 0); + return m; + } + + private void skipCurrentTag(XMLStreamReader r) throws XMLStreamException { + int depth = 1; + do { + int event = r.next(); + if (event == START_ELEMENT) + depth++; + else if (event == END_ELEMENT) + depth--; + } while (depth > 0); + } + + private Object getUnknown(XmlParserSession session, XMLStreamReader r) throws Exception { + BeanContext bc = session.getBeanContext(); + if (r.getEventType() != XMLStreamConstants.START_ELEMENT) { + throw new XMLStreamException("parser must be on START_ELEMENT to read next text", r.getLocation()); + } + ObjectMap m = null; + + // If this element has attributes, then it's always an ObjectMap. + if (r.getAttributeCount() > 0) { + m = new ObjectMap(bc); + for (int i = 0; i < r.getAttributeCount(); i++) { + String key = session.decodeString(r.getAttributeLocalName(i)); + String val = r.getAttributeValue(i); + if (! key.equals("type")) + m.put(key, val); + } + } + int eventType = r.next(); + StringBuilder sb = new StringBuilder(); + while (eventType != XMLStreamConstants.END_ELEMENT) { + if (eventType == XMLStreamConstants.CHARACTERS || eventType == XMLStreamConstants.CDATA || eventType == XMLStreamConstants.SPACE || eventType == XMLStreamConstants.ENTITY_REFERENCE) { + sb.append(r.getText()); + } else if (eventType == XMLStreamConstants.PROCESSING_INSTRUCTION || eventType == XMLStreamConstants.COMMENT) { + // skipping + } else if (eventType == XMLStreamConstants.END_DOCUMENT) { + throw new XMLStreamException("Unexpected end of document when reading element text content", r.getLocation()); + } else if (eventType == XMLStreamConstants.START_ELEMENT) { + // Oops...this has an element in it. + // Parse it as a map. + if (m == null) + m = new ObjectMap(bc); + int depth = 0; + do { + int event = (eventType == -1 ? r.nextTag() : eventType); + String currAttr; + if (event == START_ELEMENT) { + depth++; + currAttr = session.decodeString(r.getLocalName()); + String key = convertAttrToType(session, null, currAttr, string()); + Object value = parseAnything(session, object(), currAttr, r, null, false); + if (m.containsKey(key)) { + Object o = m.get(key); + if (o instanceof ObjectList) + ((ObjectList)o).add(value); + else + m.put(key, new ObjectList(o, value).setBeanContext(bc)); + } else { + m.put(key, value); + } + + } else if (event == END_ELEMENT) { + depth--; + break; + } + eventType = -1; + } while (depth > 0); + break; + } else { + throw new XMLStreamException("Unexpected event type " + eventType, r.getLocation()); + } + eventType = r.next(); + } + String s = sb.toString(); + s = session.decodeString(s); + if (m != null) { + if (! s.isEmpty()) + m.put("contents", s); + return m; + } + return s; + } + + + //-------------------------------------------------------------------------------- + // Overridden methods + //-------------------------------------------------------------------------------- + + @Override /* Parser */ + public XmlParserSession createSession(Object input, ObjectMap properties, Method javaMethod, Object outer) { + return new XmlParserSession(getContext(XmlParserContext.class), getBeanContext(), input, properties, javaMethod, outer); + } + + @Override /* Parser */ + protected <T> T doParse(ParserSession session, ClassMeta<T> type) throws Exception { + XmlParserSession s = (XmlParserSession)session; + type = s.getBeanContext().normalizeClassMeta(type); + return parseAnything(s, type, null, s.getXmlStreamReader(), s.getOuter(), true); + } + + @Override /* ReaderParser */ + protected <K,V> Map<K,V> doParseIntoMap(ParserSession session, Map<K,V> m, Type keyType, Type valueType) throws Exception { + XmlParserSession s = (XmlParserSession)session; + ClassMeta cm = s.getBeanContext().getMapClassMeta(m.getClass(), keyType, valueType); + return parseIntoMap(s, m, cm.getKeyType(), cm.getValueType()); + } + + @Override /* ReaderParser */ + protected <E> Collection<E> doParseIntoCollection(ParserSession session, Collection<E> c, Type elementType) throws Exception { + XmlParserSession s = (XmlParserSession)session; + ClassMeta cm = s.getBeanContext().getCollectionClassMeta(c.getClass(), elementType); + return parseIntoCollection(s,c, cm.getElementType()); + } + + @Override /* ReaderParser */ + protected Object[] doParseArgs(ParserSession session, ClassMeta<?>[] argTypes) throws Exception { + XmlParserSession s = (XmlParserSession)session; + return doParseArgs(s, s.getXmlStreamReader(), argTypes); + } + + @Override /* CoreApi */ + public XmlParser setProperty(String property, Object value) throws LockedException { + super.setProperty(property, value); + return this; + } + + @Override /* CoreApi */ + public XmlParser setProperties(ObjectMap properties) throws LockedException { + super.setProperties(properties); + return this; + } + + @Override /* CoreApi */ + public XmlParser addNotBeanClasses(Class<?>...classes) throws LockedException { + super.addNotBeanClasses(classes); + return this; + } + + @Override /* CoreApi */ + public XmlParser addTransforms(Class<?>...classes) throws LockedException { + super.addTransforms(classes); + return this; + } + + @Override /* CoreApi */ + public <T> XmlParser addImplClass(Class<T> interfaceClass, Class<? extends T> implClass) throws LockedException { + super.addImplClass(interfaceClass, implClass); + return this; + } + + @Override /* CoreApi */ + public XmlParser setClassLoader(ClassLoader classLoader) throws LockedException { + super.setClassLoader(classLoader); + return this; + } + + @Override /* Lockable */ + public XmlParser lock() { + super.lock(); + return this; + } + + @Override /* Lockable */ + public XmlParser clone() { + try { + XmlParser c = (XmlParser)super.clone(); + return c; + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); // Shouldn't happen. + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/xml/XmlParserContext.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/xml/XmlParserContext.java b/juneau-core/src/main/java/org/apache/juneau/xml/XmlParserContext.java new file mode 100644 index 0000000..1957943 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/xml/XmlParserContext.java @@ -0,0 +1,156 @@ +/*************************************************************************************************************************** + * 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.juneau.xml; + +import javax.xml.stream.*; +import javax.xml.stream.util.*; + +import org.apache.juneau.*; +import org.apache.juneau.parser.*; + +/** + * Configurable properties on the {@link XmlParser} class. + * <p> + * Context properties are set by calling {@link ContextFactory#setProperty(String, Object)} on the context factory + * returned {@link CoreApi#getContextFactory()}. + * <p> + * The following convenience methods are also provided for setting context properties: + * <ul> + * <li>{@link XmlParser#setProperty(String,Object)} + * <li>{@link XmlParser#setProperties(ObjectMap)} + * <li>{@link XmlParser#addNotBeanClasses(Class[])} + * <li>{@link XmlParser#addTransforms(Class[])} + * <li>{@link XmlParser#addImplClass(Class,Class)} + * </ul> + * <p> + * See {@link ContextFactory} for more information about context properties. + * + * @author James Bognar ([email protected]) + */ +public final class XmlParserContext extends ParserContext { + + /** + * XMLSchema-instance namespace URI ({@link String}, default=<js>"http://www.w3.org/2001/XMLSchema-instance"</js>). + * <p> + * The XMLSchema namespace. + */ + public static final String XML_xsiNs = "XmlParser.xsiNs"; + + /** + * Trim whitespace from text elements ({@link Boolean}, default=<jk>true</jk>). + * <p> + * If <jk>true</jk>, whitespace in text elements will be automatically trimmed. + */ + public static final String XML_trimWhitespace = "XmlParser.trimWhitespace"; + + /** + * Set validating mode ({@link Boolean}, default=<jk>false</jk>). + * <p> + * If <jk>true</jk>, XML document will be validated. + * See {@link XMLInputFactory#IS_VALIDATING} for more info. + */ + public static final String XML_validating = "XmlParser.validating"; + + /** + * Set coalescing mode ({@link Boolean}, default=<jk>false</jk>). + * <p> + * If <jk>true</jk>, XML text elements will be coalesced. + * See {@link XMLInputFactory#IS_COALESCING} for more info. + */ + public static final String XML_coalescing = "XmlParser.coalescing"; + + /** + * Replace entity references ({@link Boolean}, default=<jk>true</jk>). + * <p> + * If <jk>true</jk>, entity references will be replace during parsing. + * See {@link XMLInputFactory#IS_REPLACING_ENTITY_REFERENCES} for more info. + */ + public static final String XML_replaceEntityReferences = "XmlParser.replaceEntityReferences"; + + /** + * XML reporter ({@link XMLReporter}, default=<jk>null</jk>). + * <p> + * Associates an {@link XMLReporter} with this parser. + * <p> + * Note: Reporters are not copied to new parsers during a clone. + */ + public static final String XML_reporter = "XmlParser.reporter"; + + /** + * XML resolver ({@link XMLResolver}, default=<jk>null</jk>). + * <p> + * Associates an {@link XMLResolver} with this parser. + */ + public static final String XML_resolver = "XmlParser.resolver"; + + /** + * XML event allocator. ({@link XMLEventAllocator}, default=<jk>false</jk>). + * <p> + * Associates an {@link XMLEventAllocator} with this parser. + */ + public static final String XML_eventAllocator = "XmlParser.eventAllocator"; + + /** + * Preserve root element during generalized parsing ({@link Boolean}, default=<jk>false</jk>). + * <p> + * If <jk>true</jk>, when parsing into a generic {@link ObjectMap}, the map will + * contain a single entry whose key is the root element name. + * + * Example: + * <table class='styled'> + * <tr> + * <td>XML</td> + * <td>ObjectMap.toString(), preserveRootElement==false</td> + * <td>ObjectMap.toString(), preserveRootElement==true</td> + * </tr> + * <tr> + * <td><code><xt><root><a></xt>foobar<xt></a></root></xt><code></td> + * <td><code>{ a:<js>'foobar'</js> }</code></td> + * <td><code>{ root: { a:<js>'foobar'</js> }}</code></td> + * </tr> + * </table> + * + */ + public static final String XML_preserveRootElement = "XmlParser.preserveRootElement"; + + final String xsiNs; + final boolean + trimWhitespace, + validating, + coalescing, + replaceEntityReferences, + preserveRootElement; + final XMLReporter reporter; + final XMLResolver resolver; + final XMLEventAllocator eventAllocator; + + /** + * Constructor. + * <p> + * Typically only called from {@link ContextFactory#getContext(Class)}. + * + * @param cf The factory that created this context. + */ + public XmlParserContext(ContextFactory cf) { + super(cf); + xsiNs = cf.getProperty(XML_xsiNs, String.class, "http://www.w3.org/2001/XMLSchema-instance"); + trimWhitespace = cf.getProperty(XML_trimWhitespace, boolean.class, true); + validating = cf.getProperty(XML_validating, boolean.class, false); + coalescing = cf.getProperty(XML_coalescing, boolean.class, false); + replaceEntityReferences = cf.getProperty(XML_replaceEntityReferences, boolean.class, true); + preserveRootElement = cf.getProperty(XML_preserveRootElement, boolean.class, false); + reporter = cf.getProperty(XML_reporter, XMLReporter.class, null); + resolver = cf.getProperty(XML_resolver, XMLResolver.class, null); + eventAllocator = cf.getProperty(XML_eventAllocator, XMLEventAllocator.class, null); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/xml/XmlParserSession.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/xml/XmlParserSession.java b/juneau-core/src/main/java/org/apache/juneau/xml/XmlParserSession.java new file mode 100644 index 0000000..bddd1fd --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/xml/XmlParserSession.java @@ -0,0 +1,189 @@ +/*************************************************************************************************************************** + * 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.juneau.xml; + +import static org.apache.juneau.xml.XmlParserContext.*; + +import java.io.*; +import java.lang.reflect.*; + +import javax.xml.stream.*; +import javax.xml.stream.util.*; + +import org.apache.juneau.*; +import org.apache.juneau.internal.*; +import org.apache.juneau.parser.*; + +/** + * Session object that lives for the duration of a single use of {@link XmlParser}. + * <p> + * This class is NOT thread safe. It is meant to be discarded after one-time use. + * + * @author James Bognar ([email protected]) + */ +public final class XmlParserSession extends ParserSession { + + private final String xsiNs; + private final boolean + trimWhitespace, + validating, + coalescing, + replaceEntityReferences, + preserveRootElement; + private final XMLReporter reporter; + private final XMLResolver resolver; + private final XMLEventAllocator eventAllocator; + private XMLStreamReader xmlStreamReader; + + /** + * Create a new session using properties specified in the context. + * + * @param ctx The context creating this session object. + * The context contains all the configuration settings for this object. + * @param beanContext The bean context being used. + * @param input The input. Can be any of the following types: + * <ul> + * <li><jk>null</jk> + * <li>{@link Reader} + * <li>{@link CharSequence} + * <li>{@link InputStream} containing UTF-8 encoded text. + * <li>{@link File} containing system encoded text. + * </ul> + * @param op The override properties. + * These override any context properties defined in the context. + * @param javaMethod The java method that called this parser, usually the method in a REST servlet. + * @param outer The outer object for instantiating top-level non-static inner classes. + */ + public XmlParserSession(XmlParserContext ctx, BeanContext beanContext, Object input, ObjectMap op, Method javaMethod, Object outer) { + super(ctx, beanContext, input, op, javaMethod, outer); + if (op == null || op.isEmpty()) { + xsiNs = ctx.xsiNs; + trimWhitespace = ctx.trimWhitespace; + validating = ctx.validating; + coalescing = ctx.coalescing; + replaceEntityReferences = ctx.replaceEntityReferences; + reporter = ctx.reporter; + resolver = ctx.resolver; + eventAllocator = ctx.eventAllocator; + preserveRootElement = ctx.preserveRootElement; + } else { + xsiNs = op.getString(XML_xsiNs, ctx.xsiNs); + trimWhitespace = op.getBoolean(XML_trimWhitespace, ctx.trimWhitespace); + validating = op.getBoolean(XML_validating, ctx.validating); + coalescing = op.getBoolean(XML_coalescing, ctx.coalescing); + replaceEntityReferences = op.getBoolean(XML_replaceEntityReferences, ctx.replaceEntityReferences); + reporter = (XMLReporter)op.get(XML_reporter, ctx.reporter); + resolver = (XMLResolver)op.get(XML_resolver, ctx.resolver); + eventAllocator = (XMLEventAllocator)op.get(XML_eventAllocator, ctx.eventAllocator); + preserveRootElement = op.getBoolean(XML_preserveRootElement, ctx.preserveRootElement); + } + } + + /** + * Returns the {@link XmlParserContext#XML_xsiNs} setting value for this session. + * + * @return The {@link XmlParserContext#XML_xsiNs} setting value for this session. + */ + public final String getXsiNs() { + return xsiNs; + } + + /** + * Returns the {@link XmlParserContext#XML_preserveRootElement} setting value for this session. + * + * @return The {@link XmlParserContext#XML_preserveRootElement} setting value for this session. + */ + public final boolean isPreserveRootElement() { + return preserveRootElement; + } + + /** + * Wrap the specified reader in a STAX reader based on settings in this context. + * + * @return The new STAX reader. + * @throws Exception If problem occurred trying to create reader. + */ + public final XMLStreamReader getXmlStreamReader() throws Exception { + try { + Reader r = IOUtils.getBufferedReader(getReader()); + XMLInputFactory factory = XMLInputFactory.newInstance(); + factory.setProperty(XMLInputFactory.IS_VALIDATING, validating); + factory.setProperty(XMLInputFactory.IS_COALESCING, coalescing); + factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, replaceEntityReferences); + if (factory.isPropertySupported(XMLInputFactory.REPORTER) && reporter != null) + factory.setProperty(XMLInputFactory.REPORTER, reporter); + if (factory.isPropertySupported(XMLInputFactory.RESOLVER) && resolver != null) + factory.setProperty(XMLInputFactory.RESOLVER, resolver); + if (factory.isPropertySupported(XMLInputFactory.ALLOCATOR) && eventAllocator != null) + factory.setProperty(XMLInputFactory.ALLOCATOR, eventAllocator); + xmlStreamReader = factory.createXMLStreamReader(r); + xmlStreamReader.nextTag(); + } catch (Error e) { + close(); + throw new ParseException(e.getLocalizedMessage()); + } catch (XMLStreamException e) { + close(); + throw new ParseException(e); + } + + return xmlStreamReader; + } + + /** + * Decodes and trims the specified string. + * + * @param s The string to be decoded. + * @return The decoded string. + */ + public final String decodeString(String s) { + if (s == null || s.isEmpty()) + return s; + if (trimWhitespace) + s = s.trim(); + s = XmlUtils.decode(s); + if (isTrimStrings()) + s = s.trim(); + return s; + } + + /** + * Decodes the specified literal (e.g. <js>"true"</js>, <js>"123"</js>). + * <p> + * Unlike <code>decodeString(String)</code>, the input string is ALWAYS trimmed before decoding, and + * NEVER trimmed after decoding. + * + * @param s The string to trim. + * @return The trimmed string, or <jk>null</jk> if the string was <jk>null</jk>. + */ + public final String decodeLiteral(String s) { + if (s == null || s.isEmpty()) + return s; + s = s.trim(); + s = XmlUtils.decode(s); + return s; + } + + /** + * Silently closes the XML stream. + */ + @Override /* ParserContext */ + public void close() throws ParseException { + super.close(); + try { + if (xmlStreamReader != null) + xmlStreamReader.close(); + } catch (XMLStreamException e) { + // Ignore. + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/xml/XmlSchemaDocSerializer.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/xml/XmlSchemaDocSerializer.java b/juneau-core/src/main/java/org/apache/juneau/xml/XmlSchemaDocSerializer.java new file mode 100644 index 0000000..640fbb7 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/xml/XmlSchemaDocSerializer.java @@ -0,0 +1,67 @@ +/*************************************************************************************************************************** + * 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.juneau.xml; + +import org.apache.juneau.*; +import org.apache.juneau.serializer.*; + +/** + * Serializes POJO metadata to HTTP responses as XML. + * + * + * <h6 class='topic'>Media types</h6> + * <p> + * Handles <code>Accept</code> types: <code>text/xml+schema</code> + * <p> + * Produces <code>Content-Type</code> types: <code>text/xml</code> + * + * + * <h6 class='topic'>Description</h6> + * <p> + * Same as {@link XmlSchemaSerializer}, except prepends <code><xt><?xml</xt> <xa>version</xa>=<xs>'1.0'</xs> <xa>encoding</xa>=<xs>'UTF-8'</xs><xt>?></xt></code> to the response + * to make it a valid XML document. + * + * + * @author James Bognar ([email protected]) + */ +public class XmlSchemaDocSerializer extends XmlSchemaSerializer { + + //-------------------------------------------------------------------------------- + // Overridden methods + //-------------------------------------------------------------------------------- + + /** + * Constructor. + */ + public XmlSchemaDocSerializer() {} + + /** + * Constructor. + * + * @param cf The context factory to use for creating the context for this serializer. + */ + protected XmlSchemaDocSerializer(ContextFactory cf) { + super(cf); + } + + @Override /* Serializer */ + protected void doSerialize(SerializerSession session, Object o) throws Exception { + XmlSerializerSession s = (XmlSerializerSession)session; + XmlWriter w = s.getWriter(); + w.append("<?xml") + .attr("version", "1.0") + .attr("encoding", "UTF-8") + .appendln("?>"); + super.doSerialize(s, o); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/xml/XmlSchemaSerializer.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/xml/XmlSchemaSerializer.java b/juneau-core/src/main/java/org/apache/juneau/xml/XmlSchemaSerializer.java new file mode 100644 index 0000000..7a2c0f4 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/xml/XmlSchemaSerializer.java @@ -0,0 +1,605 @@ +/*************************************************************************************************************************** + * 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.juneau.xml; + +import static org.apache.juneau.xml.annotation.XmlFormat.*; + +import java.io.*; +import java.lang.reflect.*; +import java.text.*; +import java.util.*; +import java.util.regex.*; + +import javax.xml.*; +import javax.xml.transform.stream.*; +import javax.xml.validation.*; + +import org.apache.juneau.*; +import org.apache.juneau.annotation.*; +import org.apache.juneau.internal.*; +import org.apache.juneau.serializer.*; +import org.apache.juneau.xml.annotation.*; +import org.w3c.dom.bootstrap.*; +import org.w3c.dom.ls.*; + +/** + * Serializes POJO metadata to HTTP responses as XML. + * + * + * <h6 class='topic'>Media types</h6> + * <p> + * Handles <code>Accept</code> types: <code>text/xml+schema</code> + * <p> + * Produces <code>Content-Type</code> types: <code>text/xml</code> + * + * + * <h6 class='topic'>Description</h6> + * <p> + * Produces the XML-schema representation of the XML produced by the {@link XmlSerializer} class with the same properties. + * + * + * <h6 class='topic'>Configurable properties</h6> + * <p> + * This class has the following properties associated with it: + * <ul> + * <li>{@link XmlSerializerContext} + * <li>{@link BeanContext} + * </ul> + * + * @author James Bognar ([email protected]) + */ +@Produces(value="text/xml+schema",contentType="text/xml") +public class XmlSchemaSerializer extends XmlSerializer { + + /** + * Constructor + */ + public XmlSchemaSerializer() { + super(); + } + + /** + * Constructor. + * + * @param config Initialize with the specified config property store. + */ + protected XmlSchemaSerializer(ContextFactory config) { + getContextFactory().copyFrom(config); + } + + @Override /* XmlSerializer */ + protected void doSerialize(SerializerSession session, Object o) throws Exception { + XmlSerializerSession s = (XmlSerializerSession)session; + + if (s.isEnableNamespaces() && s.isAutoDetectNamespaces()) + findNsfMappings(s, o); + + Namespace xs = s.getXsNamespace(); + Namespace[] allNs = ArrayUtils.append(new Namespace[]{s.getDefaultNamespace()}, s.getNamespaces()); + + Schemas schemas = new Schemas(s, xs, s.getDefaultNamespace(), allNs); + schemas.process(s, o); + schemas.serializeTo(session.getWriter()); + } + + /** + * Returns an XML-Schema validator based on the output returned by {@link #doSerialize(SerializerSession, Object)}; + * + * @param session The serializer session object return by {@link #createSession(Object, ObjectMap, Method)}.<br> + * Can be <jk>null</jk>. + * @param o The object to serialize. + * @return The new validator. + * @throws Exception If a problem was detected in the XML-Schema output produced by this serializer. + */ + public Validator getValidator(SerializerSession session, Object o) throws Exception { + doSerialize(session, o); + String xmlSchema = session.getWriter().toString(); + + // create a SchemaFactory capable of understanding WXS schemas + SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + + if (xmlSchema.indexOf('\u0000') != -1) { + + // Break it up into a map of namespaceURI->schema document + final Map<String,String> schemas = new HashMap<String,String>(); + String[] ss = xmlSchema.split("\u0000"); + xmlSchema = ss[0]; + for (String s : ss) { + Matcher m = pTargetNs.matcher(s); + if (m.find()) + schemas.put(m.group(1), s); + } + + // Create a custom resolver + factory.setResourceResolver( + new LSResourceResolver() { + + @Override /* LSResourceResolver */ + public LSInput resolveResource(String type, String namespaceURI, String publicId, String systemId, String baseURI) { + + String schema = schemas.get(namespaceURI); + if (schema == null) + throw new RuntimeException(MessageFormat.format("No schema found for namespaceURI ''{0}''", namespaceURI)); + + try { + DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance(); + DOMImplementationLS domImplementationLS = (DOMImplementationLS)registry.getDOMImplementation("LS 3.0"); + LSInput in = domImplementationLS.createLSInput(); + in.setCharacterStream(new StringReader(schema)); + in.setSystemId(systemId); + return in; + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + ); + } + return factory.newSchema(new StreamSource(new StringReader(xmlSchema))).newValidator(); + } + + private static Pattern pTargetNs = Pattern.compile("targetNamespace=['\"]([^'\"]+)['\"]"); + + + /* An instance of a global element, global attribute, or XML type to be serialized. */ + private static class QueueEntry { + Namespace ns; + String name; + ClassMeta<?> cm; + QueueEntry(Namespace ns, String name, ClassMeta<?> cm) { + this.ns = ns; + this.name = name; + this.cm = cm; + } + } + + /* An encapsulation of all schemas present in the metamodel of the serialized object. */ + private class Schemas extends LinkedHashMap<Namespace,Schema> { + + private static final long serialVersionUID = 1L; + + private Namespace defaultNs; + private LinkedList<QueueEntry> + elementQueue = new LinkedList<QueueEntry>(), + attributeQueue = new LinkedList<QueueEntry>(), + typeQueue = new LinkedList<QueueEntry>(); + + private Schemas(XmlSerializerSession session, Namespace xs, Namespace defaultNs, Namespace[] allNs) throws IOException { + this.defaultNs = defaultNs; + for (Namespace ns : allNs) + put(ns, new Schema(session, this, xs, ns, defaultNs, allNs)); + } + + private Schema getSchema(Namespace ns) { + if (ns == null) + ns = defaultNs; + Schema s = get(ns); + if (s == null) + throw new RuntimeException("No schema defined for namespace '"+ns+"'"); + return s; + } + + private void process(SerializerSession session, Object o) throws IOException { + ClassMeta<?> cm = session.getBeanContext().getClassMetaForObject(o); + Namespace ns = defaultNs; + if (cm == null) + queueElement(ns, "null", object()); + else { + XmlClassMeta xmlMeta = cm.getXmlMeta(); + if (xmlMeta.getElementName() != null && xmlMeta.getNamespace() != null) + ns = xmlMeta.getNamespace(); + queueElement(ns, xmlMeta.getElementName(), cm); + } + processQueue(); + } + + + private void processQueue() throws IOException { + boolean b; + do { + b = false; + while (! elementQueue.isEmpty()) { + QueueEntry q = elementQueue.removeFirst(); + b |= getSchema(q.ns).processElement(q.name, q.cm); + } + while (! typeQueue.isEmpty()) { + QueueEntry q = typeQueue.removeFirst(); + b |= getSchema(q.ns).processType(q.name, q.cm); + } + while (! attributeQueue.isEmpty()) { + QueueEntry q = attributeQueue.removeFirst(); + b |= getSchema(q.ns).processAttribute(q.name, q.cm); + } + } while (b); + } + + private void queueElement(Namespace ns, String name, ClassMeta<?> cm) { + elementQueue.add(new QueueEntry(ns, name, cm)); + } + + private void queueType(Namespace ns, String name, ClassMeta<?> cm) { + if (name == null) + name = XmlUtils.encodeElementName(cm); + typeQueue.add(new QueueEntry(ns, name, cm)); + } + + private void queueAttribute(Namespace ns, String name, ClassMeta<?> cm) { + attributeQueue.add(new QueueEntry(ns, name, cm)); + } + + private void serializeTo(Writer w) throws IOException { + boolean b = false; + for (Schema s : values()) { + if (b) + w.append('\u0000'); + w.append(s.toString()); + b = true; + } + } + } + + /* An encapsulation of a single schema. */ + private class Schema { + private StringWriter sw = new StringWriter(); + private XmlWriter w; + private XmlSerializerSession session; + private Namespace defaultNs, targetNs; + private Schemas schemas; + private Set<String> + processedTypes = new HashSet<String>(), + processedAttributes = new HashSet<String>(), + processedElements = new HashSet<String>(); + + public Schema(XmlSerializerSession session, Schemas schemas, Namespace xs, Namespace targetNs, Namespace defaultNs, Namespace[] allNs) throws IOException { + this.schemas = schemas; + this.defaultNs = defaultNs; + this.targetNs = targetNs; + this.session = session; + w = new XmlWriter(sw, session.isUseIndentation(), session.isTrimStrings(), session.getQuoteChar(), null, null, true, null); + int i = session.getIndent(); + w.oTag(i, "schema"); + w.attr("xmlns", xs.getUri()); + w.attr("targetNamespace", targetNs.getUri()); + w.attr("elementFormDefault", "qualified"); + if (targetNs != defaultNs) + w.attr("attributeFormDefault", "qualified"); + for (Namespace ns2 : allNs) + w.attr("xmlns", ns2.name, ns2.uri); + w.append('>').nl(); + for (Namespace ns : allNs) { + if (ns != targetNs) { + w.oTag(i+1, "import") + .attr("namespace", ns.getUri()) + .attr("schemaLocation", ns.getName()+".xsd") + .append("/>").nl(); + } + } + } + + private boolean processElement(String name, ClassMeta<?> cm) throws IOException { + if (processedElements.contains(name)) + return false; + processedElements.add(name); + + ClassMeta<?> ft = cm.getTransformedClassMeta(); + int i = session.getIndent() + 1; + if (name == null) + name = getElementName(ft); + Namespace ns = first(ft.getXmlMeta().getNamespace(), defaultNs); + String type = getXmlType(ns, ft); + + w.oTag(i, "element") + .attr("name", XmlUtils.encodeElementName(name)) + .attr("type", type) + .append('/').append('>').nl(); + + schemas.queueType(ns, null, ft); + schemas.processQueue(); + return true; + } + + private boolean processAttribute(String name, ClassMeta<?> cm) throws IOException { + if (processedAttributes.contains(name)) + return false; + processedAttributes.add(name); + + int i = session.getIndent() + 1; + String type = getXmlAttrType(cm); + + w.oTag(i, "attribute") + .attr("name", name) + .attr("type", type) + .append('/').append('>').nl(); + + return true; + } + + private boolean processType(String name, ClassMeta<?> cm) throws IOException { + if (processedTypes.contains(name)) + return false; + processedTypes.add(name); + + int i = session.getIndent() + 1; + + cm = cm.getTransformedClassMeta(); + + w.oTag(i, "complexType") + .attr("name", name); + + // This element can have mixed content if: + // 1) It's a generic Object (so it can theoretically be anything) + // 2) The bean has a property defined with @XmlFormat.CONTENT. + if ((cm.isBean() && cm.getBeanMeta().getXmlMeta().getXmlContentProperty() != null) || cm.isObject()) + w.attr("mixed", "true"); + + w.cTag().nl(); + + if (! (cm.isMap() || cm.isBean() || cm.hasToObjectMapMethod() || cm.isCollection() || cm.isArray() || (cm.isAbstract() && ! cm.isNumber()) || cm.isObject())) { + String base = getXmlAttrType(cm); + w.sTag(i+1, "simpleContent").nl(); + w.oTag(i+2, "extension") + .attr("base", base); + if (session.isAddJsonTypeAttrs() || (session.isAddJsonStringTypeAttrs() && base.equals("string"))) { + w.cTag().nl(); + w.oTag(i+3, "attribute") + .attr("name", "type") + .attr("type", "string") + .ceTag().nl(); + w.eTag(i+2, "extension").nl(); + } else { + w.ceTag().nl(); + } + w.eTag(i+1, "simpleContent").nl(); + + } else { + + //----- Bean ----- + if (cm.isBean()) { + BeanMeta<?> bm = cm.getBeanMeta(); + + boolean hasChildElements = false; + + for (BeanPropertyMeta<?> pMeta : bm.getPropertyMetas()) + if (pMeta.getXmlMeta().getXmlFormat() != XmlFormat.ATTR && pMeta.getXmlMeta().getXmlFormat() != XmlFormat.CONTENT) + hasChildElements = true; + + if (bm.getXmlMeta().getXmlContentProperty() != null) { + w.sTag(i+1, "sequence").nl(); + w.oTag(i+2, "any") + .attr("processContents", "skip") + .attr("minOccurs", 0) + .ceTag().nl(); + w.eTag(i+1, "sequence").nl(); + + } else if (hasChildElements) { + w.sTag(i+1, "sequence").nl(); + + boolean hasOtherNsElement = false; + + for (BeanPropertyMeta<?> pMeta : bm.getPropertyMetas()) { + XmlBeanPropertyMeta<?> xmlMeta = pMeta.getXmlMeta(); + if (xmlMeta.getXmlFormat() != XmlFormat.ATTR) { + boolean isCollapsed = xmlMeta.getXmlFormat() == COLLAPSED; + ClassMeta<?> ct2 = pMeta.getClassMeta(); + String childName = pMeta.getName(); + if (isCollapsed) { + if (xmlMeta.getChildName() != null) + childName = xmlMeta.getChildName(); + ct2 = pMeta.getClassMeta().getElementType(); + } + Namespace cNs = first(xmlMeta.getNamespace(), ct2.getXmlMeta().getNamespace(), cm.getXmlMeta().getNamespace(), defaultNs); + if (xmlMeta.getNamespace() == null) { + w.oTag(i+2, "element") + .attr("name", XmlUtils.encodeElementName(childName), true) + .attr("type", getXmlType(cNs, ct2)); + if (isCollapsed) { + w.attr("minOccurs", 0); + w.attr("maxOccurs", "unbounded"); + } else { + if (! session.isTrimNulls()) + w.attr("nillable", true); + else + w.attr("minOccurs", 0); + } + + w.ceTag().nl(); + } else { + // Child element is in another namespace. + schemas.queueElement(cNs, pMeta.getName(), ct2); + hasOtherNsElement = true; + } + + } + } + + // If this bean has any child elements in another namespace, + // we need to add an <any> element. + if (hasOtherNsElement) + w.oTag(i+2, "any") + .attr("minOccurs", 0) + .attr("maxOccurs", "unbounded") + .ceTag().nl(); + w.eTag(i+1, "sequence").nl(); + } + + for (BeanPropertyMeta<?> pMeta : bm.getXmlMeta().getXmlAttrProperties().values()) { + Namespace pNs = pMeta.getXmlMeta().getNamespace(); + if (pNs == null) + pNs = defaultNs; + + // If the bean attribute has a different namespace than the bean, then it needs to + // be added as a top-level entry in the appropriate schema file. + if (pNs != targetNs) { + schemas.queueAttribute(pNs, pMeta.getName(), pMeta.getClassMeta()); + w.oTag(i+1, "attribute") + //.attr("name", pMeta.getName(), true) + .attr("ref", pNs.getName() + ':' + pMeta.getName()) + .ceTag().nl(); + } + + // Otherwise, it's just a plain attribute of this bean. + else { + w.oTag(i+1, "attribute") + .attr("name", pMeta.getName(), true) + .attr("type", getXmlAttrType(pMeta.getClassMeta())) + .ceTag().nl(); + } + } + + //----- Collection ----- + } else if (cm.isCollection() || cm.isArray()) { + ClassMeta<?> elementType = cm.getElementType(); + if (elementType.isObject()) { + w.sTag(i+1, "sequence").nl(); + w.oTag(i+2, "any") + .attr("processContents", "skip") + .attr("maxOccurs", "unbounded") + .attr("minOccurs", "0") + .ceTag().nl(); + w.eTag(i+1, "sequence").nl(); + } else { + Namespace cNs = first(elementType.getXmlMeta().getNamespace(), cm.getXmlMeta().getNamespace(), defaultNs); + schemas.queueType(cNs, null, elementType); + w.sTag(i+1, "sequence").nl(); + w.oTag(i+2, "choice") + .attr("minOccurs", 0) + .attr("maxOccurs", "unbounded") + .cTag().nl(); + w.oTag(i+3, "element") + .attr("name", XmlUtils.encodeElementName(getElementName(elementType))) + .attr("type", getXmlType(cNs, elementType)) + .ceTag().nl(); + w.oTag(i+3, "element") + .attr("name", "null") + .attr("type", "string") + .ceTag().nl(); + w.eTag(i+2, "choice").nl(); + w.eTag(i+1, "sequence").nl(); + } + + //----- Map ----- + } else if (cm.isMap() || cm.hasToObjectMapMethod() || cm.isAbstract() || cm.isObject()) { + w.sTag(i+1, "sequence").nl(); + w.oTag(i+2, "any") + .attr("processContents", "skip") + .attr("maxOccurs", "unbounded") + .attr("minOccurs", "0") + .ceTag().nl(); + w.eTag(i+1, "sequence").nl(); + } + + if (session.isAddClassAttrs()) { + w.oTag(i+1, "attribute") + .attr("name", "_class") + .attr("type", "string") + .ceTag().nl(); + } + if (session.isAddJsonTypeAttrs()) { + w.oTag(i+1, "attribute") + .attr("name", "type") + .attr("type", "string") + .ceTag().nl(); + } + } + + w.eTag(i, "complexType").nl(); + schemas.processQueue(); + + return true; + } + + private String getElementName(ClassMeta<?> cm) { + cm = cm.getTransformedClassMeta(); + String name = cm.getXmlMeta().getElementName(); + + if (name == null) { + if (cm.isBoolean()) + name = "boolean"; + else if (cm.isNumber()) + name = "number"; + else if (cm.isArray() || cm.isCollection()) + name = "array"; + else if (! (cm.isMap() || cm.hasToObjectMapMethod() || cm.isBean() || cm.isCollection() || cm.isArray() || cm.isObject() || cm.isAbstract())) + name = "string"; + else + name = "object"; + } + return name; + } + + @Override /* Object */ + public String toString() { + try { + w.eTag(session.getIndent(), "schema").nl(); + } catch (IOException e) { + throw new RuntimeException(e); // Shouldn't happen. + } + return sw.toString(); + } + + private String getXmlType(Namespace currentNs, ClassMeta<?> cm) { + String name = null; + cm = cm.getTransformedClassMeta(); + if (currentNs == targetNs && ! session.isAddJsonTypeAttrs()) { + if (cm.isBoolean()) + name = "boolean"; + else if (cm.isNumber()) { + if (cm.isDecimal()) + name = "decimal"; + else + name = "integer"; + } + if (name == null && ! session.isAddJsonStringTypeAttrs()) { + if (! (cm.isMap() || cm.hasToObjectMapMethod() || cm.isBean() || cm.isCollection() || cm.isArray() || cm.isObject() || cm.isAbstract())) + name = "string"; + } + } + if (name == null) { + name = XmlUtils.encodeElementName(cm); + schemas.queueType(currentNs, name, cm); + return currentNs.getName() + ":" + name; + } + + return name; + } + } + + private <T> T first(T...tt) { + for (T t : tt) + if (t != null) + return t; + return null; + } + + + private static String getXmlAttrType(ClassMeta<?> cm) { + if (cm.isBoolean()) + return "boolean"; + if (cm.isNumber()) { + if (cm.isDecimal()) + return "decimal"; + return "integer"; + } + return "string"; + } + + @Override /* Serializer */ + public XmlSerializerSession createSession(Object output, ObjectMap properties, Method javaMethod) { + // This serializer must always have namespaces enabled. + if (properties == null) + properties = new ObjectMap(); + properties.put(XmlSerializerContext.XML_enableNamespaces, true); + return new XmlSerializerSession(getContext(XmlSerializerContext.class), getBeanContext(), output, properties, javaMethod); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/xml/XmlSerializer.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/xml/XmlSerializer.java b/juneau-core/src/main/java/org/apache/juneau/xml/XmlSerializer.java new file mode 100644 index 0000000..a686412 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/xml/XmlSerializer.java @@ -0,0 +1,709 @@ +/*************************************************************************************************************************** + * 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.juneau.xml; + +import static org.apache.juneau.serializer.SerializerContext.*; +import static org.apache.juneau.xml.XmlSerializerContext.*; +import static org.apache.juneau.xml.annotation.XmlFormat.*; + +import java.lang.reflect.*; +import java.util.*; + +import org.apache.juneau.*; +import org.apache.juneau.annotation.*; +import org.apache.juneau.json.*; +import org.apache.juneau.serializer.*; +import org.apache.juneau.transform.*; +import org.apache.juneau.xml.annotation.*; + +/** + * Serializes POJO models to XML. + * + * + * <h6 class='topic'>Media types</h6> + * <p> + * Handles <code>Accept</code> types: <code>text/xml</code> + * <p> + * Produces <code>Content-Type</code> types: <code>text/xml</code> + * + * + * <h6 class='topic'>Description</h6> + * <p> + * See the {@link JsonSerializer} class for details on how Java models map to JSON. + * <p> + * For example, the following JSON... + * <p class='bcode'> + * { + * name:<js>'John Smith'</js>, + * address: { + * streetAddress: <js>'21 2nd Street'</js>, + * city: <js>'New York'</js>, + * state: <js>'NY'</js>, + * postalCode: <js>10021</js> + * }, + * phoneNumbers: [ + * <js>'212 555-1111'</js>, + * <js>'212 555-2222'</js> + * ], + * additionalInfo: <jk>null</jk>, + * remote: <jk>false</jk>, + * height: <js>62.4</js>, + * <js>'fico score'</js>: <js>' > 640'</js> + * } + * <p> + * ...maps to the following XML... + * <p class='bcode'> + * <xt><object></xt> + * <xt><name</xt> <xa>type</xa>=<xs>'string'</xs><xt>></xt>John Smith<xt></name></xt> + * <xt><address</xt> <xa>type</xa>=<xs>'object'</xs><xt>></xt> + * <xt><streetAddress</xt> <xa>type</xa>=<xs>'string'</xs><xt>></xt>21 2nd Street<xt></streetAddress></xt> + * <xt><city</xt> <xa>type</xa>=<xs>'string'</xs><xt>></xt>New York<xt></city></xt> + * <xt><state</xt> <xa>type</xa>=<xs>'string'</xs><xt>></xt>NY<xt></state></xt> + * <xt><postalCode</xt> <xa>type</xa>=<xs>'number'</xs><xt>></xt>10021<xt></postalCode></xt> + * <xt></address></xt> + * <xt><phoneNumbers</xt> <xa>type</xa>=<xs>'array'</xs><xt>></xt> + * <xt><string></xt>212 555-1111<xt></string></xt> + * <xt><string></xt>212 555-2222<xt></string></xt> + * <xt></phoneNumbers></xt> + * <xt><additionalInfo</xt> <xa>type</xa>=<xs>'null'</xs><xt>></additionalInfo></xt> + * <xt><remote</xt> <xa>type</xa>=<xs>'boolean'</xs><xt>></xt>false<xt></remote></xt> + * <xt><height</xt> <xa>type</xa>=<xs>'number'</xs><xt>></xt>62.4<xt></height></xt> + * <xt><fico_x0020_score</xt> <xa>type</xa>=<xs>'string'</xs><xt>></xt> &gt; 640<xt></fico_x0020_score></xt> + * <xt></object></xt> + * <p> + * This serializer provides several serialization options. Typically, one of the predefined <jsf>DEFAULT</jsf> serializers will be sufficient. + * However, custom serializers can be constructed to fine-tune behavior. + * <p> + * If an attribute name contains any non-valid XML element characters, they will be escaped using standard {@code _x####_} notation. + * + * + * <h6 class='topic'>Configurable properties</h6> + * <p> + * This class has the following properties associated with it: + * <ul> + * <li>{@link XmlSerializerContext} + * <li>{@link BeanContext} + * </ul> + * + * + * <h6 class='topic'>Behavior-specific subclasses</h6> + * <p> + * The following direct subclasses are provided for convenience: + * <ul class='spaced-list'> + * <li>{@link Sq} - Default serializer, single quotes. + * <li>{@link SqReadable} - Default serializer, single quotes, whitespace added. + * <li>{@link XmlJson} - Default serializer with JSON attribute tags. + * <li>{@link XmlJsonSq} - Default serializer with JSON attribute tags, single quotes. + * </ul> + * + * + * @author James Bognar ([email protected]) + */ +@SuppressWarnings({ "rawtypes", "unchecked" }) +@Produces("text/xml") +public class XmlSerializer extends WriterSerializer { + + /** Default serializer, all default settings. */ + public static final XmlSerializer DEFAULT = new XmlSerializer().lock(); + + /** Default serializer, single quotes. */ + public static final XmlSerializer DEFAULT_SQ = new XmlSerializer.Sq().lock(); + + /** Default serializer, single quotes, whitespace added. */ + public static final XmlSerializer DEFAULT_SQ_READABLE = new XmlSerializer.SqReadable().lock(); + + /** Default serializer with JSON attribute tags. */ + public static final XmlSerializer DEFAULT_XMLJSON = new XmlSerializer.XmlJson().lock(); + + /** Default serializer with JSON attribute tags, single quotes. */ + public static final XmlSerializer DEFAULT_XMLJSON_SQ = new XmlSerializer.XmlJsonSq().lock(); + + /** Default serializer without namespaces. */ + public static final XmlSerializer DEFAULT_SIMPLE = new XmlSerializer.Simple().lock(); + + /** Default serializer without namespaces, with single quotes. */ + public static final XmlSerializer DEFAULT_SIMPLE_SQ = new XmlSerializer.SimpleSq().lock(); + + /** Default serializer without namespaces, with JSON attribute tags and single quotes. */ + public static final XmlSerializer DEFAULT_SIMPLE_XMLJSON_SQ = new XmlSerializer.SimpleXmlJsonSq().lock(); + + + /** Default serializer, single quotes. */ + public static class Sq extends XmlSerializer { + /** Constructor */ + public Sq() { + setProperty(SERIALIZER_quoteChar, '\''); + } + } + + /** Default serializer, single quotes, whitespace added. */ + public static class SqReadable extends Sq { + /** Constructor */ + public SqReadable() { + setProperty(SERIALIZER_useIndentation, true); + } + } + + /** Default serializer with JSON attribute tags. */ + @Produces(value="text/xml+json",contentType="text/xml") + public static class XmlJson extends XmlSerializer { + /** Constructor */ + public XmlJson() { + setProperty(XML_addJsonTypeAttrs, true); + } + } + + /** Default serializer with JSON attribute tags, single quotes. */ + public static class XmlJsonSq extends XmlJson { + /** Constructor */ + public XmlJsonSq() { + setProperty(SERIALIZER_quoteChar, '\''); + } + } + + /** Default serializer without namespaces. */ + @Produces(value="text/xml+simple",contentType="text/xml") + public static class Simple extends XmlSerializer { + /** Constructor */ + public Simple() { + setProperty(XML_enableNamespaces, false); + } + } + + /** Default serializer without namespaces, single quotes. */ + public static class SimpleSq extends Simple { + /** Constructor */ + public SimpleSq() { + setProperty(SERIALIZER_quoteChar, '\''); + } + } + + /** Default serializer with JSON attribute tags, single quotes. */ + public static class SimpleXmlJsonSq extends SimpleSq { + /** Constructor */ + public SimpleXmlJsonSq() { + setProperty(XML_addJsonTypeAttrs, true); + } + } + + /** + * Recursively searches for the XML namespaces on the specified POJO and adds them to the serializer context object. + * + * @param session The context that exists for the duration of a single serialization. + * @param o The POJO to check. + * @throws SerializeException + */ + protected void findNsfMappings(XmlSerializerSession session, Object o) throws SerializeException { + BeanContext bc = session.getBeanContext(); + ClassMeta<?> aType = null; // The actual type + aType = session.push(null, o, null); + + if (aType != null) { + Namespace ns = aType.getXmlMeta().getNamespace(); + if (ns != null) { + if (ns.uri != null) + session.addNamespace(ns); + else + ns = null; + } + } + + // Handle recursion + if (aType != null && ! aType.isPrimitive()) { + + BeanMap<?> bm = null; + if (aType.isBeanMap()) { + bm = (BeanMap)o; + } else if (aType.isBean()) { + bm = bc.forBean(o); + } else if (aType.isDelegate()) { + ClassMeta innerType = ((Delegate)o).getClassMeta(); + Namespace ns = innerType.getXmlMeta().getNamespace(); + if (ns != null) { + if (ns.uri != null) + session.addNamespace(ns); + else + ns = null; + } + + if (innerType.isBean()) { + for (BeanPropertyMeta bpm : (Collection<BeanPropertyMeta>)innerType.getBeanMeta().getPropertyMetas()) { + ns = bpm.getXmlMeta().getNamespace(); + if (ns != null && ns.uri != null) + session.addNamespace(ns); + } + + } else if (innerType.isMap()) { + for (Object o2 : ((Map)o).values()) + findNsfMappings(session, o2); + } else if (innerType.isCollection()) { + for (Object o2 : ((Collection)o)) + findNsfMappings(session, o2); + } + + } else if (aType.isMap()) { + for (Object o2 : ((Map)o).values()) + findNsfMappings(session, o2); + } else if (aType.isCollection()) { + for (Object o2 : ((Collection)o)) + findNsfMappings(session, o2); + } else if (aType.isArray() && ! aType.getElementType().isPrimitive()) { + for (Object o2 : ((Object[])o)) + findNsfMappings(session, o2); + } + if (bm != null) { + for (BeanPropertyValue p : bm.getValues(false, session.isTrimNulls())) { + + Namespace ns = p.getMeta().getXmlMeta().getNamespace(); + if (ns != null && ns.uri != null) + session.addNamespace(ns); + + try { + findNsfMappings(session, p.getValue()); + } catch (Throwable x) { + // Ignore + } + } + } + } + + session.pop(); + } + + /** + * Workhorse method. + * + * @param session The serializer context. + * @param out The writer to send the output to. + * @param o The object to serialize. + * @param eType The expected type if this is a bean property value being serialized. + * @param elementName The root element name. + * @param elementNamespace The namespace of the element. + * @param addNamespaceUris Flag indicating that namespace URIs need to be added. + * @param format The format to serialize the output to. + * @param pMeta The bean property metadata if this is a bean property being serialized. + * @return The same writer passed in so that calls to the writer can be chained. + * @throws Exception If a problem occurred trying to convert the output. + */ + protected XmlWriter serializeAnything(XmlSerializerSession session, XmlWriter out, Object o, + ClassMeta eType, String elementName, Namespace elementNamespace, boolean addNamespaceUris, + XmlFormat format, BeanPropertyMeta<?> pMeta) throws Exception { + + BeanContext bc = session.getBeanContext(); + String ts = null; // The type string (e.g. <type> or <x x='type'> + int indent = session.indent; // Current indentation + ClassMeta<?> aType = null; // The actual type + ClassMeta<?> wType = null; // The wrapped type + ClassMeta<?> gType = object(); // The generic type + + aType = session.push(elementName, o, eType); + + if (eType == null) + eType = object(); + + // Handle recursion + if (aType == null) { + o = null; + aType = object(); + } + + if (o != null) { + + if (aType.isDelegate()) { + wType = aType; + aType = ((Delegate)o).getClassMeta(); + } + + gType = aType.getTransformedClassMeta(); + + // Transform if necessary + PojoTransform transform = aType.getPojoTransform(); + if (transform != null) { + o = transform.transform(o); + + // If the transform's getTransformedClass() method returns Object, we need to figure out + // the actual type now. + if (gType.isObject()) + gType = bc.getClassMetaForObject(o); + } + } else { + gType = eType.getTransformedClassMeta(); + } + + String classAttr = null; + if (session.isAddClassAttrs()) { + if (o != null && ! eType.equals(aType)) + classAttr = aType.toString(); + else if (o == null) + classAttr = eType.toString(); + } + + // char '\0' is interpreted as null. + if (o != null && gType.isChar() && ((Character)o).charValue() == 0) + o = null; + + boolean isCollapsed = false; // If 'true', this is a collection and we're not rendering the outer element. + + // Get the JSON type string. + if (gType.isCharSequence() || gType.isChar()) + ts = "string"; + else if (gType.isNumber()) + ts = "number"; + else if (gType.isBoolean()) + ts = "boolean"; + else if (gType.isMap() || gType.isBean() || gType.hasToObjectMapMethod()) { + isCollapsed = gType.getXmlMeta().getFormat() == XmlFormat.COLLAPSED; + ts = "object"; + } + else if (gType.isCollection() || gType.isArray()) { + isCollapsed = (format == COLLAPSED && ! addNamespaceUris); + ts = "array"; + } + else + ts = "string"; + + + // Is there a name associated with this bean? + if (elementName == null) + elementName = gType.getXmlMeta().getElementName(); + if (elementName == null) + elementName = aType.getXmlMeta().getElementName(); + + // If the value is null then it's either going to be <null/> or <XmlSerializer nil='true'/> + // depending on whether the element has a name. + boolean isNullTag = (elementName == null && o == null); + + if (isNullTag) + ts = "null"; + + if (session.isEnableNamespaces()) { + if (elementNamespace == null) + elementNamespace = gType.getXmlMeta().getNamespace(); + if (elementNamespace == null) + elementNamespace = aType.getXmlMeta().getNamespace(); + if (elementNamespace != null && elementNamespace.uri == null) + elementNamespace = null; + if (elementNamespace == null) + elementNamespace = session.getDefaultNamespace(); + } else { + elementNamespace = null; + } + + // Do we need a carriage return after the start tag? + boolean cr = o != null && (gType.isMap() || gType.isCollection() || gType.isArray() || gType.isBean() || gType.hasToObjectMapMethod()); + + String en = (elementName == null ? ts : elementName); + boolean encodeEn = elementName != null; + String ns = (elementNamespace == null ? null : elementNamespace.name); + String xsi = null, dns = null, elementNs = null; + if (session.isEnableNamespaces()) { + xsi = session.getXsiNamespace().name; + dns = elementName == null && session.getDefaultNamespace() != null ? session.getDefaultNamespace().name : null; + elementNs = elementName == null ? dns : ns; + if (elementName == null) + elementNamespace = null; + } + + // Render the start tag. + if (! isCollapsed) { + out.oTag(indent, elementNs, en, encodeEn); + if (addNamespaceUris) { + out.attr((String)null, "xmlns", session.getDefaultNamespace().getUri()); + + for (Namespace n : session.getNamespaces()) + out.attr("xmlns", n.getName(), n.getUri()); + + Namespace xsiNs = session.getXsiNamespace(); + if (xsiNs != null) + out.attr("xmlns", xsiNs.name, xsiNs.uri); + } + if (elementName != null && session.isAddJsonTypeAttrs() && (session.isAddJsonStringTypeAttrs() || ! ts.equals("string"))) + out.attr(dns, "type", ts); + if (classAttr != null) + out.attr(dns, "_class", classAttr); + if (o == null) { + if (! isNullTag) + out.attr(xsi, "nil", "true"); + if ((gType.isBoolean() || gType.isNumber()) && ! gType.isNullable()) + o = gType.getPrimitiveDefault(); + } + + if (o != null && !(gType.isMap() || gType.isBean() || gType.hasToObjectMapMethod())) + out.append('>'); + + if (cr && !(gType.isMap() || gType.isBean() || gType.hasToObjectMapMethod())) + out.nl(); + } + + boolean hasChildren = true; + + // Render the tag contents. + if (o != null) { + if (gType.isUri() || (pMeta != null && pMeta.isUri())) + out.appendUri(o); + else if (gType.isCharSequence() || gType.isChar()) + out.encodeText(session.trim(o)); + else if (gType.isNumber() || gType.isBoolean()) + out.append(o); + else if (gType.isMap() || (wType != null && wType.isMap())) { + if (o instanceof BeanMap) + hasChildren = serializeBeanMap(session, out, (BeanMap)o, elementNamespace, isCollapsed); + else + hasChildren = serializeMap(session, out, (Map)o, gType); + } + else if (gType.hasToObjectMapMethod()) + hasChildren = serializeMap(session, out, gType.toObjectMap(o), gType); + else if (gType.isBean()) + hasChildren = serializeBeanMap(session, out, bc.forBean(o), elementNamespace, isCollapsed); + else if (gType.isCollection() || (wType != null && wType.isCollection())) { + if (isCollapsed) + session.indent--; + serializeCollection(session, out, (Collection)o, gType, pMeta); + if (isCollapsed) + session.indent++; + } + else if (gType.isArray()) { + if (isCollapsed) + session.indent--; + serializeCollection(session, out, toList(gType.getInnerClass(), o), gType, pMeta); + if (isCollapsed) + session.indent++; + } + else + out.encodeText(session.toString(o)); + } + + session.pop(); + + // Render the end tag. + if (! isCollapsed) { + if (o == null || ! hasChildren) + out.append('/').append('>').nl(); + else + out.i(cr ? indent : 0).eTag(elementNs, en, encodeEn).nl(); + } + + return out; + } + + private boolean serializeMap(XmlSerializerSession session, XmlWriter out, Map m, ClassMeta<?> type) throws Exception { + + m = session.sort(m); + + ClassMeta<?> keyType = type.getKeyType(), valueType = type.getValueType(); + + boolean hasChildren = false; + for (Iterator i = m.entrySet().iterator(); i.hasNext();) { + Map.Entry e = (Map.Entry)i.next(); + + Object k = e.getKey(); + if (k == null) { + k = "\u0000"; + } else { + k = session.generalize(k, keyType); + if (session.isTrimStrings() && k instanceof String) + k = k.toString().trim(); + } + + Object value = e.getValue(); + + if (! hasChildren) { + hasChildren = true; + out.append('>').nl(); + } + serializeAnything(session, out, value, valueType, session.toString(k), null, false, NORMAL, null); + } + return hasChildren; + } + + private boolean serializeBeanMap(XmlSerializerSession session, XmlWriter out, BeanMap<?> m, Namespace elementNs, boolean isCollapsed) throws Exception { + boolean hasChildren = false; + BeanMeta bm = m.getMeta(); + + List<BeanPropertyValue> lp = m.getValues(false, session.isTrimNulls()); + + Map<String,BeanPropertyMeta> xmlAttrs = bm.getXmlMeta().getXmlAttrProperties(); + Object content = null; + for (BeanPropertyValue p : lp) { + if (xmlAttrs.containsKey(p.getName())) { + BeanPropertyMeta pMeta = p.getMeta(); + String key = p.getName(); + Object value = p.getValue(); + Throwable t = p.getThrown(); + if (t != null) + session.addBeanGetterWarning(pMeta, t); + + if (session.canIgnoreValue(pMeta.getClassMeta(), key, value)) + continue; + + Namespace ns = (session.isEnableNamespaces() && pMeta.getXmlMeta().getNamespace() != elementNs ? pMeta.getXmlMeta().getNamespace() : null); + + if (pMeta.isBeanUri() || pMeta.isUri()) + out.attrUri(ns, key, value); + else + out.attr(ns, key, value); + } + } + + boolean hasContent = false; + + for (BeanPropertyValue p : lp) { + BeanPropertyMeta pMeta = p.getMeta(); + XmlFormat xf = pMeta.getXmlMeta().getXmlFormat(); + + if (xf == CONTENT) { + content = p.getValue(); + hasContent = true; + } else if (xf == ATTR) { + // Do nothing + } else { + String key = p.getName(); + Object value = p.getValue(); + Throwable t = p.getThrown(); + if (t != null) + session.addBeanGetterWarning(pMeta, t); + + if (session.canIgnoreValue(pMeta.getClassMeta(), key, value)) + continue; + + if (! hasChildren) { + hasChildren = true; + out.appendIf(! isCollapsed, '>').nl(); + } + serializeAnything(session, out, value, pMeta.getClassMeta(), key, pMeta.getXmlMeta().getNamespace(), false, pMeta.getXmlMeta().getXmlFormat(), pMeta); + } + } + if ((! hasContent) || session.canIgnoreValue(string(), null, content)) + return hasChildren; + out.append('>').cr(session.indent); + + // Serialize XML content. + XmlContentHandler h = bm.getXmlMeta().getXmlContentHandler(); + if (h != null) + h.serialize(out, m.getBean()); + else + out.encodeText(content); + out.nl(); + return true; + } + + private XmlWriter serializeCollection(XmlSerializerSession session, XmlWriter out, Collection c, ClassMeta<?> type, BeanPropertyMeta<?> ppMeta) throws Exception { + + c = session.sort(c); + + ClassMeta<?> elementType = type.getElementType(); + + String eName = null; + Namespace eNs = null; + + if (ppMeta != null) { + eName = ppMeta.getXmlMeta().getChildName(); + eNs = ppMeta.getXmlMeta().getNamespace(); + } + + if (eName == null) { + eName = type.getXmlMeta().getChildName(); + eNs = type.getXmlMeta().getNamespace(); + } + + if (eName == null && ! elementType.isObject()) { + eName = elementType.getXmlMeta().getElementName(); + eNs = elementType.getXmlMeta().getNamespace(); + } + + for (Iterator i = c.iterator(); i.hasNext();) { + Object value = i.next(); + serializeAnything(session, out, value, elementType, eName, eNs, false, NORMAL, null); + } + return out; + } + + /** + * Returns the schema serializer based on the settings of this serializer. + * @return The schema serializer. + */ + public XmlSerializer getSchemaSerializer() { + XmlSchemaSerializer s = new XmlSchemaSerializer(getContextFactory()); + return s; + } + + + //-------------------------------------------------------------------------------- + // Overridden methods + //-------------------------------------------------------------------------------- + + @Override /* Serializer */ + protected void doSerialize(SerializerSession session, Object o) throws Exception { + XmlSerializerSession s = (XmlSerializerSession)session; + if (s.isEnableNamespaces() && s.isAutoDetectNamespaces()) + findNsfMappings(s, o); + serializeAnything(s, s.getWriter(), o, null, null, null, s.isEnableNamespaces() && s.isAddNamespaceUrlsToRoot(), NORMAL, null); + } + + @Override /* Serializer */ + public XmlSerializerSession createSession(Object output, ObjectMap properties, Method javaMethod) { + return new XmlSerializerSession(getContext(XmlSerializerContext.class), getBeanContext(), output, properties, javaMethod); + } + + @Override /* CoreApi */ + public XmlSerializer setProperty(String property, Object value) throws LockedException { + super.setProperty(property, value); + return this; + } + + @Override /* CoreApi */ + public XmlSerializer setProperties(ObjectMap properties) throws LockedException { + super.setProperties(properties); + return this; + } + + @Override /* CoreApi */ + public XmlSerializer addNotBeanClasses(Class<?>...classes) throws LockedException { + super.addNotBeanClasses(classes); + return this; + } + + @Override /* CoreApi */ + public XmlSerializer addTransforms(Class<?>...classes) throws LockedException { + super.addTransforms(classes); + return this; + } + + @Override /* CoreApi */ + public <T> XmlSerializer addImplClass(Class<T> interfaceClass, Class<? extends T> implClass) throws LockedException { + super.addImplClass(interfaceClass, implClass); + return this; + } + + @Override /* CoreApi */ + public XmlSerializer setClassLoader(ClassLoader classLoader) throws LockedException { + super.setClassLoader(classLoader); + return this; + } + + @Override /* Lockable */ + public XmlSerializer lock() { + super.lock(); + return this; + } + + @Override /* Lockable */ + public XmlSerializer clone() { + try { + XmlSerializer c = (XmlSerializer)super.clone(); + return c; + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); // Shouldn't happen. + } + } +}
