/******************************************************************************* * Copyright (c) 2013, 2015 Oracle and/or its affiliates. All rights reserved. * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 * which accompanies this distribution. * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * Contributors: * Denise Smith - 2.6 - initial implementation ******************************************************************************/ package org.eclipse.persistence.oxm.record; import java.io.IOException; import java.io.StringWriter; import java.util.List; import javax.xml.namespace.QName; import org.eclipse.persistence.exceptions.XMLMarshalException; import org.eclipse.persistence.internal.core.helper.CoreClassConstants; import org.eclipse.persistence.internal.core.helper.CoreConversionManager; import org.eclipse.persistence.internal.oxm.CharacterEscapeHandler; import org.eclipse.persistence.internal.oxm.Constants; import org.eclipse.persistence.internal.oxm.ConversionManager; import org.eclipse.persistence.internal.oxm.NamespaceResolver; import org.eclipse.persistence.internal.oxm.ObjectBuilder; import org.eclipse.persistence.internal.oxm.Root; import org.eclipse.persistence.internal.oxm.XMLBinaryDataHelper; import org.eclipse.persistence.internal.oxm.XMLMarshaller; import org.eclipse.persistence.internal.oxm.XPathFragment; import org.eclipse.persistence.internal.oxm.mappings.Descriptor; import org.eclipse.persistence.internal.oxm.record.ExtendedContentHandler; import org.eclipse.persistence.internal.oxm.record.XMLFragmentReader; import org.w3c.dom.Attr; import org.w3c.dom.Node; import org.xml.sax.Attributes; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.ext.LexicalHandler; public abstract class JsonRecord<T extends JsonRecord.Level> extends MarshalRecord <XMLMarshaller> { protected T position; protected CharacterEscapeHandler characterEscapeHandler; protected String attributePrefix; protected boolean isRootArray; protected static final String NULL="null"; protected boolean isLastEventStart; /** * INTERNAL: */ public void setMarshaller(XMLMarshaller marshaller) { super.setMarshaller(marshaller); attributePrefix = marshaller.getAttributePrefix(); if (marshaller.getValueWrapper() != null) { textWrapperFragment = new XPathFragment(); textWrapperFragment.setLocalName(marshaller.getValueWrapper()); } characterEscapeHandler = marshaller.getCharacterEscapeHandler(); } public void forceValueWrapper(){ setComplex(position, true); isLastEventStart = false; } @Override public void startDocument(String encoding, String version) { if(isRootArray){ if(position == null){ startCollection(); } position.setEmptyCollection(false); position = createNewLevel(false, position); isLastEventStart = true; }else{ startRootObject(); } } protected T createNewLevel(boolean collection, T parentLevel){ return (T)new Level(collection, position); } protected void startRootObject(){ position = createNewLevel(false, null); } @Override public void openStartElement(XPathFragment xPathFragment, NamespaceResolver namespaceResolver) { super.openStartElement(xPathFragment, namespaceResolver); if(position != null){ T newLevel = createNewLevel(false, position); if(isLastEventStart){ //this means 2 startevents in a row so the last this is a complex object setComplex(position, true); } String keyName = getKeyName(xPathFragment); if(position.isCollection && position.isEmptyCollection() ){ position.setKeyName(keyName); startEmptyCollection(); }else{ newLevel.setKeyName(keyName); } position = newLevel; isLastEventStart = true; } } protected void startEmptyCollection(){} /** * Handle marshal of an empty collection. * @param xPathFragment * @param namespaceResolver * @param openGrouping if grouping elements should be marshalled for empty collections * @return */ public boolean emptyCollection(XPathFragment xPathFragment, NamespaceResolver namespaceResolver, boolean openGrouping) { if(marshaller.isMarshalEmptyCollections()){ super.emptyCollection(xPathFragment, namespaceResolver, true); if (null != xPathFragment) { if(xPathFragment.isSelfFragment() || xPathFragment.nameIsText()){ String keyName = position.getKeyName(); setComplex(position, false); writeEmptyCollection((T)position.parentLevel, keyName); }else{ if(isLastEventStart){ setComplex(position, true); } String keyName = getKeyName(xPathFragment); if(keyName != null){ writeEmptyCollection(position, keyName); } } isLastEventStart = false; } return true; }else{ return super.emptyCollection(xPathFragment, namespaceResolver, openGrouping); } } protected abstract void writeEmptyCollection(T level, String keyName); @Override public void endDocument() { if(position != null){ finishLevel(); } } protected void finishLevel(){ position = (T)position.parentLevel; } public void startCollection() { if(position == null){ isRootArray = true; position = createNewLevel(true, null); startRootLevelCollection(); } else { if(isLastEventStart){ setComplex((T)position, true); } position = createNewLevel(true, position); } isLastEventStart = false; } protected void setComplex(T level, boolean complex){ level.setComplex(complex); } protected abstract void startRootLevelCollection(); protected String getKeyName(XPathFragment xPathFragment){ String keyName = xPathFragment.getLocalName(); if(isNamespaceAware()){ if(xPathFragment.getNamespaceURI() != null){ String prefix = null; if(getNamespaceResolver() !=null){ prefix = getNamespaceResolver().resolveNamespaceURI(xPathFragment.getNamespaceURI()); } else if(namespaceResolver != null){ prefix = namespaceResolver.resolveNamespaceURI(xPathFragment.getNamespaceURI()); } if(prefix != null && !prefix.equals(Constants.EMPTY_STRING)){ keyName = prefix + getNamespaceSeparator() + keyName; } } } if(xPathFragment.isAttribute() && attributePrefix != null){ keyName = attributePrefix + keyName; } return keyName; } public void attribute(XPathFragment xPathFragment, NamespaceResolver namespaceResolver, Object value, QName schemaType){ if(xPathFragment.getNamespaceURI() != null && xPathFragment.getNamespaceURI() == javax.xml.XMLConstants.XMLNS_ATTRIBUTE_NS_URI){ return; } xPathFragment.setAttribute(true); openStartElement(xPathFragment, namespaceResolver); characters(schemaType, value, null, false, true); endElement(xPathFragment, namespaceResolver); } /** * INTERNAL: */ @Override public void marshalWithoutRootElement(ObjectBuilder treeObjectBuilder, Object object, Descriptor descriptor, Root root, boolean isXMLRoot){ if(treeObjectBuilder != null){ addXsiTypeAndClassIndicatorIfRequired(descriptor, null, descriptor.getDefaultRootElementField(), root, object, isXMLRoot, true); treeObjectBuilder.marshalAttributes(this, object, session); } } /** * INTERNAL: * The character used to separate the prefix and uri portions when namespaces are present * @since 2.4 */ public char getNamespaceSeparator(){ return marshaller.getNamespaceSeparator(); } /** * INTERNAL: * The optional fragment used to wrap the text() mappings * @since 2.4 */ public XPathFragment getTextWrapperFragment() { return textWrapperFragment; } @Override public boolean isWrapperAsCollectionName() { return marshaller.isWrapperAsCollectionName(); } @Override public void element(XPathFragment frag) { isLastEventStart = false; } @Override public void attribute(XPathFragment xPathFragment,NamespaceResolver namespaceResolver, String value) { attribute(xPathFragment, namespaceResolver, value, null); } @Override public void attribute(String namespaceURI, String localName, String qName, String value) { XPathFragment xPathFragment = new XPathFragment(); xPathFragment.setNamespaceURI(namespaceURI); xPathFragment.setAttribute(true); xPathFragment.setLocalName(localName); openStartElement(xPathFragment, namespaceResolver); characters(null, value, null, false, true); endElement(xPathFragment, namespaceResolver); } @Override public void closeStartElement() {} @Override public void characters(String value) { writeValue(value, null, false); } @Override public void characters(QName schemaType, Object value, String mimeType, boolean isCDATA){ characters(schemaType, value, mimeType, isCDATA, false); } public void characters(QName schemaType, Object value, String mimeType, boolean isCDATA, boolean isAttribute){ if(mimeType != null) { if(value instanceof List){ value = XMLBinaryDataHelper.getXMLBinaryDataHelper().getBytesListForBinaryValues((List)value, marshaller, mimeType); }else{ value = XMLBinaryDataHelper.getXMLBinaryDataHelper().getBytesForBinaryValue(value, marshaller, mimeType).getData(); } } if(schemaType != null && Constants.QNAME_QNAME.equals(schemaType)){ String convertedValue = getStringForQName((QName)value); writeValue(convertedValue, null, isAttribute); } else if(value.getClass() == String.class){ //if schemaType is set and it's a numeric or boolean type don't treat as a string if(schemaType != null && isNumericOrBooleanType(schemaType)){ ConversionManager conversionManager = getConversionManager(); Class<?> theClass = conversionManager.javaType(schemaType); Object convertedValue = conversionManager.convertObject(value, theClass, schemaType); writeValue(convertedValue, schemaType, isAttribute); }else if(isCDATA){ cdata((String)value); }else{ writeValue((String)value, null, isAttribute); } }else{ Class<?> theClass = ((ConversionManager) session.getDatasourcePlatform().getConversionManager()).javaType(schemaType); if(schemaType == null || theClass == null){ if(value.getClass() == CoreClassConstants.BOOLEAN || CoreClassConstants.NUMBER.isAssignableFrom(value.getClass())){ writeValue(value, schemaType, isAttribute); }else{ String convertedValue = ((String) ((ConversionManager) session.getDatasourcePlatform().getConversionManager()).convertObject(value, CoreClassConstants.STRING, schemaType)); writeValue(convertedValue, schemaType, isAttribute); } }else if(schemaType != null && !isNumericOrBooleanType(schemaType)){ //if schemaType exists and is not boolean or number do write quotes (convert to string) String convertedValue = ((String) ((ConversionManager) session.getDatasourcePlatform().getConversionManager()).convertObject(value, CoreClassConstants.STRING, schemaType)); writeValue(convertedValue, schemaType, isAttribute); } else if(isCDATA){ String convertedValue = ((String) ((ConversionManager) session.getDatasourcePlatform().getConversionManager()).convertObject(value, CoreClassConstants.STRING, schemaType)); cdata(convertedValue); }else{ writeValue(value, schemaType, isAttribute); } } } private boolean isNumericOrBooleanType(QName schemaType){ if(schemaType == null){ return false; }else if(schemaType.equals(Constants.BOOLEAN_QNAME) || schemaType.equals(Constants.INTEGER_QNAME) || schemaType.equals(Constants.INT_QNAME) || schemaType.equals(Constants.BYTE_QNAME) || schemaType.equals(Constants.DECIMAL_QNAME) || schemaType.equals(Constants.FLOAT_QNAME) || schemaType.equals(Constants.DOUBLE_QNAME) || schemaType.equals(Constants.SHORT_QNAME) || schemaType.equals(Constants.LONG_QNAME) || schemaType.equals(Constants.NEGATIVE_INTEGER_QNAME) || schemaType.equals(Constants.NON_NEGATIVE_INTEGER_QNAME) || schemaType.equals(Constants.NON_POSITIVE_INTEGER_QNAME) || schemaType.equals(Constants.POSITIVE_INTEGER_QNAME) || schemaType.equals(Constants.UNSIGNED_BYTE_QNAME) || schemaType.equals(Constants.UNSIGNED_INT_QNAME) || schemaType.equals(Constants.UNSIGNED_LONG_QNAME) || schemaType.equals(Constants.UNSIGNED_SHORT_QNAME) ){ return true; } return false; } public void writeValue(Object value, QName schemaType, boolean isAttribute) { if (characterEscapeHandler != null && value instanceof String) { try { StringWriter stringWriter = new StringWriter(); characterEscapeHandler.escape(((String)value).toCharArray(), 0, ((String)value).length(), isAttribute, stringWriter); value = stringWriter.toString(); } catch (IOException e) { throw XMLMarshalException.marshalException(e); } } boolean textWrapperOpened = false; if(!isLastEventStart){ openStartElement(textWrapperFragment, namespaceResolver); textWrapperOpened = true; } T currentLevel = position; String keyName = position.getKeyName(); if(!position.isComplex){ currentLevel = (T)position.parentLevel; } addValue(currentLevel, keyName, value, schemaType); isLastEventStart = false; if(textWrapperOpened){ endElement(textWrapperFragment, namespaceResolver); } } @Override public void endElement(XPathFragment xPathFragment,NamespaceResolver namespaceResolver) { if(position != null){ if(isLastEventStart){ setComplex(position, true); } if(position.isComplex){ finishLevel(); }else{ position = (T) position.parentLevel; } isLastEventStart = false; } } private void addValue(T currentLevel, String keyName, Object value, QName schemaType){ if(currentLevel.isCollection()){ addValueToArray(currentLevel, value, schemaType); currentLevel.setEmptyCollection(false); } else { addValueToObject(currentLevel, keyName, value, schemaType); } } protected abstract void addValueToObject(T currentLevel, String keyName, Object value, QName schemaType); protected abstract void addValueToArray(T currentLevel, Object value, QName schemaType); @Override public void cdata(String value) { characters(value); } @Override public void node(Node node, NamespaceResolver resolver, String uri, String name) { if (node.getNodeType() == Node.ATTRIBUTE_NODE) { Attr attr = (Attr) node; String resolverPfx = null; if (getNamespaceResolver() != null) { resolverPfx = this.getNamespaceResolver().resolveNamespaceURI(attr.getNamespaceURI()); } // If the namespace resolver contains a prefix for the attribute's URI, // use it instead of what is set on the attribute if (resolverPfx != null) { attribute(attr.getNamespaceURI(), Constants.EMPTY_STRING, resolverPfx+Constants.COLON+attr.getLocalName(), attr.getNodeValue()); } else { attribute(attr.getNamespaceURI(), Constants.EMPTY_STRING, attr.getName(), attr.getNodeValue()); // May need to declare the URI locally if (attr.getNamespaceURI() != null) { attribute(javax.xml.XMLConstants.XMLNS_ATTRIBUTE_NS_URI, Constants.EMPTY_STRING, javax.xml.XMLConstants.XMLNS_ATTRIBUTE + Constants.COLON + attr.getPrefix(), attr.getNamespaceURI()); this.getNamespaceResolver().put(attr.getPrefix(), attr.getNamespaceURI()); } } } else if (node.getNodeType() == Node.TEXT_NODE) { writeValue(node.getNodeValue(), null, false); } else { try { JsonRecordContentHandler wrcHandler = new JsonRecordContentHandler(); XMLFragmentReader xfragReader = new XMLFragmentReader(namespaceResolver); xfragReader.setContentHandler(wrcHandler); xfragReader.setProperty("http://xml.org/sax/properties/lexical-handler", wrcHandler); xfragReader.parse(node, uri, name); } catch (SAXException sex) { throw XMLMarshalException.marshalException(sex); } } } protected String getStringForQName(QName qName){ if(null == qName) { return null; } CoreConversionManager xmlConversionManager = getSession().getDatasourcePlatform().getConversionManager(); return (String) xmlConversionManager.convertObject(qName, String.class); } /** * INTERNAL: */ public void namespaceDeclarations(NamespaceResolver namespaceResolver) { } public void namespaceDeclaration(String prefix, String namespaceURI){ } public void defaultNamespaceDeclaration(String defaultNamespace){ } /** * INTERNAL: */ public void nilComplex(XPathFragment xPathFragment, NamespaceResolver namespaceResolver){ XPathFragment groupingFragment = openStartGroupingElements(namespaceResolver); closeStartGroupingElements(groupingFragment); openStartElement(xPathFragment, namespaceResolver); characters(NULL); endElement(xPathFragment, namespaceResolver); } /** * INTERNAL: */ public void nilSimple(NamespaceResolver namespaceResolver){ XPathFragment groupingFragment = openStartGroupingElements(namespaceResolver); characters(NULL); closeStartGroupingElements(groupingFragment); } /** * Used when an empty simple value should be written * @since EclipseLink 2.4 */ public void emptySimple(NamespaceResolver namespaceResolver){ nilSimple(namespaceResolver); } public void emptyAttribute(XPathFragment xPathFragment,NamespaceResolver namespaceResolver){ XPathFragment groupingFragment = openStartGroupingElements(namespaceResolver); openStartElement(xPathFragment, namespaceResolver); characters(NULL); endElement(xPathFragment, namespaceResolver); closeStartGroupingElements(groupingFragment); } /** * Used when an empty complex item should be written * @since EclipseLink 2.4 */ public void emptyComplex(XPathFragment xPathFragment, NamespaceResolver namespaceResolver){ XPathFragment groupingFragment = openStartGroupingElements(namespaceResolver); closeStartGroupingElements(groupingFragment); openStartElement(xPathFragment, namespaceResolver); endElement(xPathFragment, namespaceResolver); } /** * This class will typically be used in conjunction with an XMLFragmentReader. * The XMLFragmentReader will walk a given XMLFragment node and report events * to this class - the event's data is then written to the enclosing class' * writer. * * @see org.eclipse.persistence.internal.oxm.record.XMLFragmentReader */ protected class JsonRecordContentHandler implements ExtendedContentHandler, LexicalHandler { JsonRecordContentHandler() { } // --------------------- CONTENTHANDLER METHODS --------------------- // public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { XPathFragment xPathFragment = new XPathFragment(localName); xPathFragment.setNamespaceURI(namespaceURI); openStartElement(xPathFragment, namespaceResolver); handleAttributes(atts); } public void endElement(String namespaceURI, String localName, String qName) throws SAXException { XPathFragment xPathFragment = new XPathFragment(localName); xPathFragment.setNamespaceURI(namespaceURI); JsonRecord.this.endElement(xPathFragment, namespaceResolver); } public void startPrefixMapping(String prefix, String uri) throws SAXException { } public void characters(char[] ch, int start, int length) throws SAXException { String characters = new String (ch, start, length); characters(characters); } public void characters(CharSequence characters) throws SAXException { JsonRecord.this.characters(characters.toString()); } // --------------------- LEXICALHANDLER METHODS --------------------- // public void comment(char[] ch, int start, int length) throws SAXException { } public void startCDATA() throws SAXException { } public void endCDATA() throws SAXException { } // --------------------- CONVENIENCE METHODS --------------------- // protected void handleAttributes(Attributes atts) { for (int i=0, attsLength = atts.getLength(); i<attsLength; i++) { String qName = atts.getQName(i); if((qName != null && (qName.startsWith(javax.xml.XMLConstants.XMLNS_ATTRIBUTE + Constants.COLON) || qName.equals(javax.xml.XMLConstants.XMLNS_ATTRIBUTE)))) { continue; } attribute(atts.getURI(i), atts.getLocalName(i), qName, atts.getValue(i)); } } protected void writeComment(char[] chars, int start, int length) { } protected void writeCharacters(char[] chars, int start, int length) { try { characters(chars, start, length); } catch (SAXException e) { throw XMLMarshalException.marshalException(e); } } // --------------- SATISFY CONTENTHANDLER INTERFACE --------------- // public void endPrefixMapping(String prefix) throws SAXException {} public void processingInstruction(String target, String data) throws SAXException {} public void setDocumentLocator(Locator locator) {} public void startDocument() throws SAXException {} public void endDocument() throws SAXException {} public void skippedEntity(String name) throws SAXException {} public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {} // --------------- SATISFY LEXICALHANDLER INTERFACE --------------- // public void startEntity(String name) throws SAXException {} public void endEntity(String name) throws SAXException {} public void startDTD(String name, String publicId, String systemId) throws SAXException {} public void endDTD() throws SAXException {} @Override public void setNil(boolean isNil) {} } /** * Instances of this class are used to maintain state about the current * level of the JSON message being marshalled. */ protected static class Level { protected boolean isCollection; protected boolean emptyCollection; protected String keyName; protected boolean isComplex; protected Level parentLevel; public Level(boolean isCollection, Level parentLevel) { setCollection(isCollection); emptyCollection = true; this.parentLevel = parentLevel; } public boolean isCollection() { return isCollection; } public void setCollection(boolean isCollection) { this.isCollection = isCollection; } public String getKeyName() { return keyName; } public void setKeyName(String keyName) { this.keyName = keyName; } public boolean isEmptyCollection() { return emptyCollection; } public void setEmptyCollection(boolean emptyCollection) { this.emptyCollection = emptyCollection; } public boolean isComplex() { return isComplex; } public void setComplex(boolean isComplex) { this.isComplex = isComplex; } } }