/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2010-2012 Oracle and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development * and Distribution License("CDDL") (collectively, the "License"). You * may not use this file except in compliance with the License. You can * obtain a copy of the License at * http://glassfish.java.net/public/CDDL+GPL_1_1.html * or packager/legal/LICENSE.txt. See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each * file and include the License file at packager/legal/LICENSE.txt. * * GPL Classpath Exception: * Oracle designates this particular file as subject to the "Classpath" * exception as provided by Oracle in the GPL Version 2 section of the License * file that accompanied this code. * * Modifications: * If applicable, add the following below the License Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyright [year] [name of copyright owner]" * * Contributor(s): * If you wish your version of this file to be governed by only the CDDL or * only the GPL Version 2, indicate your decision by adding "[Contributor] * elects to include this software in this distribution under the [CDDL or GPL * Version 2] license." If you don't indicate a single choice of license, a * recipient has the option to distribute your version of this file under * either the CDDL, the GPL Version 2 or to extend the choice of license to * its licensees as provided above. However, if you add GPL Version 2 code * and therefore, elected the GPL Version 2 license, then the option applies * only if the new code is made subject to such option by the copyright * holder. */ package com.sun.jersey.json.impl.writer; import java.io.IOException; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.XMLConstants; import javax.xml.bind.JAXBContext; import javax.xml.namespace.QName; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import com.sun.jersey.api.json.JSONConfiguration; import com.sun.jersey.json.impl.DefaultJaxbXmlDocumentStructure; import com.sun.jersey.json.impl.JaxbXmlDocumentStructure; import org.codehaus.jackson.JsonGenerator; /** * Implementation of {@link XMLStreamWriter} for JSON streams in natural notation. * * @author Jakub Podlesak (jakub.podlesak at oracle.com) * @author Michal Gajdos (michal.gajdos at oracle.com) */ public class Stax2JacksonWriter extends DefaultXmlStreamWriter implements XMLStreamWriter { private static class ProcessingInfo { boolean isArray; Type rawType; Type individualType; ProcessingInfo lastUnderlyingPI; boolean startObjectWritten = false; boolean afterFN = false; QName elementName; public ProcessingInfo(QName elementName, boolean isArray, Type rawType, Type individualType) { this.elementName = elementName; this.isArray = isArray; this.rawType = rawType; this.individualType = individualType; } public ProcessingInfo(ProcessingInfo pi) { this(pi.elementName, pi.isArray, pi.rawType, pi.individualType); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final ProcessingInfo other = (ProcessingInfo) obj; if (this.isArray != other.isArray) { return false; } if (this.elementName != other.elementName && (this.elementName == null || !this.elementName.equals(other.elementName))) { return false; } if (this.rawType != other.rawType && (this.rawType == null || !this.rawType.equals(other.rawType))) { return false; } if (this.individualType != other.individualType && (this.individualType == null || !this.individualType.equals(other.individualType))) { return false; } return true; } @Override public int hashCode() { int hash = 5; hash = 47 * hash + (this.isArray ? 1 : 0); hash = 47 * hash + (this.elementName != null ?this.elementName.hashCode() : 0); hash = 47 * hash + (this.rawType != null ? this.rawType.hashCode() : 0); hash = 47 * hash + (this.individualType != null ? this.individualType.hashCode() : 0); return hash; } } final private boolean attrsWithPrefix; final static String XML_SCHEMA_INSTANCE = "http://www.w3.org/2001/XMLSchema-instance"; JacksonStringMergingGenerator generator; final List<ProcessingInfo> processingStack = new ArrayList<ProcessingInfo>(); boolean writingAttr = false; /** * Document structure to obtain the expected elements and attributes from. */ private JaxbXmlDocumentStructure documentStructure; static <T> T pop(List<T> stack) { return stack.remove(stack.size() - 1); } static <T> T peek(List<T> stack) { return (stack.size() > 0) ? stack.get(stack.size() - 1) : null; } static <T> T peek2nd(List<T> stack) { return (stack.size() > 1) ? stack.get(stack.size() - 2) : null; } public Stax2JacksonWriter(final JsonGenerator generator, final Class<?> expectedType, final JAXBContext jaxbContext) { this(generator, JSONConfiguration.DEFAULT, expectedType, jaxbContext); } public Stax2JacksonWriter(final JsonGenerator generator, final JSONConfiguration config, final Class<?> expectedType, JAXBContext jaxbContext) { this.attrsWithPrefix = config.isUsingPrefixesAtNaturalAttributes(); this.generator = JacksonStringMergingGenerator.createGenerator(generator); this.documentStructure = DefaultJaxbXmlDocumentStructure.getXmlDocumentStructure(jaxbContext, expectedType, false); } @Override public void writeStartElement(String localName) throws XMLStreamException { writeStartElement(null, localName, null); } @Override public void writeStartElement(String namespaceURI, String localName) throws XMLStreamException { writeStartElement(null, localName, namespaceURI); } private void ensureStartObjectBeforeFieldName(ProcessingInfo pi) throws IOException { if ((pi != null) && pi.afterFN) { generator.writeStartObject(); peek2nd(processingStack).startObjectWritten = true; pi.afterFN = false; } } @Override public void writeStartElement(String prefix, String localName, String namespaceURI) throws XMLStreamException { try { if (!writingAttr) { pushPropInfo(namespaceURI, localName, null); } ProcessingInfo currentPI = peek(processingStack); ProcessingInfo parentPI = peek2nd(processingStack); if (!currentPI.isArray) { if ((parentPI != null) && (parentPI.lastUnderlyingPI != null) && (parentPI.lastUnderlyingPI.isArray)) { generator.writeEndArray(); parentPI.afterFN = false; } ensureStartObjectBeforeFieldName(parentPI); generator.writeFieldName(localName); currentPI.afterFN = true; } else { if ((parentPI == null) || (!currentPI.equals(parentPI.lastUnderlyingPI))) { // not the same array, need to close the last array off? if ((parentPI != null) && (parentPI.lastUnderlyingPI != null) && (parentPI.lastUnderlyingPI.isArray)) { generator.writeEndArray(); parentPI.afterFN = false; } // now start the new array ensureStartObjectBeforeFieldName(parentPI); generator.writeFieldName(localName); generator.writeStartArray(); currentPI.afterFN = true; } else { // next array element currentPI.afterFN = true; } } } catch (IOException ex) { Logger.getLogger(Stax2JacksonWriter.class.getName()).log(Level.SEVERE, null, ex); throw new XMLStreamException(ex); } } static final Type[] _pt = new Type[]{ byte.class, short.class, int.class, long.class, float.class, double.class, boolean.class, char.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, Boolean.class, Character.class, String.class }; static final Type[] _nst = new Type[]{ byte.class, short.class, int.class, long.class, float.class, double.class, boolean.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, Boolean.class, BigInteger.class, BigDecimal.class, }; static final Set<Type> primitiveTypes = new HashSet<Type>() { { addAll(Arrays.asList(_pt)); } }; static final Set<Type> nonStringTypes = new HashSet<Type>() { { addAll(Arrays.asList(_nst)); } }; private void pushPropInfo(String namespaceUri, String localName, String value) { final QName qname = new QName(namespaceUri == null ? XMLConstants.NULL_NS_URI : namespaceUri, localName); if (writingAttr) { documentStructure.handleAttribute(qname, value); } else { documentStructure.startElement(qname); } ProcessingInfo parentPI = peek(processingStack); // still the same array, no need to dig out runtime property info final boolean sameArrayCollection = documentStructure.isSameArrayCollection(); if ((localName != null) && (parentPI != null) && (parentPI.lastUnderlyingPI != null) // first is for RI and the second part of the next condition is for MOXy && (localName.equals(parentPI.lastUnderlyingPI.elementName.getLocalPart()) || sameArrayCollection)) { processingStack.add(new ProcessingInfo(parentPI.lastUnderlyingPI)); return; } final Type rt = documentStructure.getEntityType(qname, writingAttr); final Type individualType = documentStructure.getIndividualType(); // rt is null for root elements if (null == rt) { processingStack.add(new ProcessingInfo(qname, false, null, null)); return; } if (primitiveTypes.contains(rt)) { processingStack.add(new ProcessingInfo(qname, false, rt, individualType)); return; } // TODO: wildcard could still simulate an array by adding several elements of the same name if (documentStructure.isArrayCollection() && !writingAttr) { // another array if (!((parentPI != null) && (parentPI.isArray) && sameArrayCollection)) { // another array processingStack.add(new ProcessingInfo(qname, true, rt, individualType)); return; } } // something else processingStack.add(new ProcessingInfo(qname, false, rt, individualType)); } @Override public void writeEmptyElement(String localName) throws XMLStreamException { writeEmptyElement(null, localName, null); } @Override public void writeEmptyElement(String namespaceURI, String localName) throws XMLStreamException { writeEmptyElement(null, localName, namespaceURI); } @Override public void writeEmptyElement(String prefix, String localName, String namespaceURI) throws XMLStreamException { writeStartElement(prefix, localName, namespaceURI); writeEndElement(); } private void cleanlyEndObject(ProcessingInfo pi) throws IOException { if (pi.startObjectWritten) { generator.writeEndObject(); } else { if (pi.afterFN && pi.lastUnderlyingPI == null) { if(documentStructure.isArrayCollection() || documentStructure.hasSubElements()) { generator.writeStartObject(); generator.writeEndObject(); } else { generator.writeNull(); } } } } @Override public void writeEndElement() throws XMLStreamException { try { ProcessingInfo removedPI = pop(processingStack); ProcessingInfo currentPI = peek(processingStack); if (currentPI != null) { currentPI.lastUnderlyingPI = removedPI; } // need to check first, if there was an array to be closed off if ((removedPI.lastUnderlyingPI != null) && (removedPI.lastUnderlyingPI.isArray)) { generator.writeEndArray(); } cleanlyEndObject(removedPI); documentStructure.endElement(removedPI.elementName); } catch (IOException ex) { Logger.getLogger(Stax2JacksonWriter.class.getName()).log(Level.SEVERE, null, ex); throw new XMLStreamException(ex); } } @Override public void writeEndDocument() throws XMLStreamException { try { generator.writeEndObject(); } catch (IOException ex) { Logger.getLogger(Stax2JacksonWriter.class.getName()).log(Level.SEVERE, null, ex); throw new XMLStreamException(ex); } } @Override public void close() throws XMLStreamException { try { generator.close(); } catch (IOException ex) { Logger.getLogger(Stax2JacksonWriter.class.getName()).log(Level.SEVERE, null, ex); throw new XMLStreamException(ex); } } @Override public void flush() throws XMLStreamException { try { generator.flush(); } catch (IOException ex) { Logger.getLogger(Stax2JacksonWriter.class.getName()).log(Level.SEVERE, null, ex); throw new XMLStreamException(ex); } } @Override public void writeAttribute(String localName, String value) throws XMLStreamException { writeAttribute(null, null, localName, value); } @Override public void writeAttribute(String namespaceURI, String localName, String value) throws XMLStreamException { writeAttribute(null, namespaceURI, localName, value); } @Override public void writeAttribute(String prefix, String namespaceURI, String localName, String value) throws XMLStreamException { writingAttr = true; pushPropInfo(namespaceURI, localName, value); writeStartElement(prefix, attrsWithPrefix ? ("@" + localName) : localName, namespaceURI); writingAttr = false; // a dirty hack, since jaxb ri is giving us wrong info on the actual attribute type in this case writeCharacters(value, "type".equals(localName) && XML_SCHEMA_INSTANCE.equals(namespaceURI)); writeEndElement(); } @Override public void writeStartDocument() throws XMLStreamException { writeStartDocument(null, null); } @Override public void writeStartDocument(String version) throws XMLStreamException { writeStartDocument(null, version); } @Override public void writeStartDocument(String encoding, String version) throws XMLStreamException { try { generator.writeStartObject(); } catch (IOException ex) { //JRA-18973: Log Socket exceptions that can happen quite often when browsers close connections pre-maturely at DEBUG level. // Also don't need to throw the exception further up but just swallow it here. if (ex instanceof java.net.SocketTimeoutException || ex instanceof java.net.SocketException) { Logger.getLogger(Stax2JacksonWriter.class.getName()).log(Level.FINE, "Socket excption", ex); } else { Logger.getLogger(Stax2JacksonWriter.class.getName()).log(Level.SEVERE, "IO exception", ex); throw new XMLStreamException(ex); } } } private void writeCharacters(String text, boolean forceString) throws XMLStreamException { try { ProcessingInfo currentPI = peek(processingStack); if (currentPI.startObjectWritten && !currentPI.afterFN) { generator.writeFieldName("$"); } currentPI.afterFN = false; final Type valueType = getValueType(currentPI.rawType, currentPI.individualType); if (forceString || !nonStringTypes.contains(valueType)) { if (!currentPI.isArray) { generator.writeStringToMerge(text); } else { generator.writeString(text); } } else { writePrimitiveType(text, valueType); } } catch (IOException ex) { Logger.getLogger(Stax2JacksonWriter.class.getName()).log(Level.SEVERE, null, ex); throw new XMLStreamException(ex); } } private void writePrimitiveType(final String text, final Type valueType) throws IOException { if ((boolean.class == valueType) || (Boolean.class == valueType)) { generator.writeBoolean(Boolean.parseBoolean(text)); } else { generator.writeNumber(text); } } private Type getValueType(final Type rawType, final Type individualType) { // Individual type. if (individualType != null) { return individualType; } // Collections. if (rawType instanceof ParameterizedType) { final ParameterizedType parameterizedType = (ParameterizedType) rawType; final Type parameterizedTypeRawType = parameterizedType.getRawType(); if (parameterizedTypeRawType instanceof Class && Collection.class.isAssignableFrom((Class) parameterizedTypeRawType)) { final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); if (actualTypeArguments != null && actualTypeArguments.length > 0) { return actualTypeArguments[0]; } } } return rawType; } @Override public void writeCharacters(String text) throws XMLStreamException { writeCharacters(text, false); } @Override public void writeCharacters(char[] text, int start, int length) throws XMLStreamException { writeCharacters(new String(text, start, length)); } }