blob: bc80787f9b2e87c8d811a57ce8ca98521623ae28 [file] [log] [blame]
/*
* Copyright (c) 2013, 2021 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 v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/
// Contributors:
// Denise Smith - 2.6 - initial implementation
// Radek Felman - 2.7.5 - Bug 389815 - Enhancement Request - JSON specific multidimensional array support
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:
*/
@Override
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();
}
@Override
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, false);
isLastEventStart = true;
} else {
startRootObject();
}
}
protected T createNewLevel(boolean collection, T parentLevel, boolean nestedArray) {
return (T) new Level(collection, position, nestedArray);
}
protected void startRootObject() {
position = createNewLevel(false, null, false);
}
@Override
public void openStartElement(XPathFragment xPathFragment, NamespaceResolver namespaceResolver) {
super.openStartElement(xPathFragment, namespaceResolver);
String keyName1 = getKeyName(xPathFragment);
if (position != null && position.isCollection && xPathFragment.getXMLField() != null && xPathFragment.getXMLField().isNestedArray()
&& this.marshaller.getJsonTypeConfiguration().isJsonDisableNestedArrayName()) {
position.addSkip();
position.setKeyName(keyName1);
if (!position.isEmptyCollectionGenerated()) {
startEmptyCollection();
position.setEmptyCollectionGenerated(true);
}
return;
}
if (position != null) {
T newLevel;
if (xPathFragment.getXMLField() != null && xPathFragment.getXMLField().isNestedArray() && this.marshaller.getJsonTypeConfiguration().isJsonDisableNestedArrayName()) {
newLevel = createNewLevel(false, position, true);
} else {
newLevel = createNewLevel(false, position, false);
}
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 (keyName != null && !keyName.equals(Constants.EMPTY_STRING)) {
if (position.isCollection && position.isEmptyCollection()) {
position.setKeyName(keyName);
startEmptyCollection();
} else {
newLevel.setKeyName(keyName);
}
}
if (!(newLevel.isNestedArray() && newLevel.isComplex())) {
position = newLevel;
}
isLastEventStart = true;
}
}
protected void startEmptyCollection() {
}
/**
* Handle marshal of an empty collection.
*
* @param openGrouping if grouping elements should be marshalled for empty collections
*/
@Override
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() {
boolean notSkip = position.parentLevel == null || position.parentLevel.notSkip();
if (notSkip) {
position = (T) position.parentLevel;
}
}
@Override
public void startCollection() {
if (position == null) {
isRootArray = true;
position = createNewLevel(true, null, false);
startRootLevelCollection();
} else {
if (isLastEventStart) {
setComplex(position, true);
}
position = createNewLevel(true, position, position.isNestedArray());
}
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;
}
@Override
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
*/
@Override
public char getNamespaceSeparator() {
return marshaller.getNamespaceSeparator();
}
/**
* INTERNAL:
* The optional fragment used to wrap the text() mappings
*
* @since 2.4
*/
@Override
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(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);
}
}
}
@Override
protected String getStringForQName(QName qName) {
if (null == qName) {
return null;
}
CoreConversionManager xmlConversionManager = getSession().getDatasourcePlatform().getConversionManager();
return (String) xmlConversionManager.convertObject(qName, String.class);
}
/**
* INTERNAL:
*/
@Override
public void namespaceDeclarations(NamespaceResolver namespaceResolver) {
}
@Override
public void namespaceDeclaration(String prefix, String namespaceURI) {
}
@Override
public void defaultNamespaceDeclaration(String defaultNamespace) {
}
/**
* INTERNAL:
*/
@Override
public void nilComplex(XPathFragment xPathFragment, NamespaceResolver namespaceResolver) {
XPathFragment groupingFragment = openStartGroupingElements(namespaceResolver);
closeStartGroupingElements(groupingFragment);
openStartElement(xPathFragment, namespaceResolver);
characters(NULL);
endElement(xPathFragment, namespaceResolver);
}
/**
* INTERNAL:
*/
@Override
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
*/
@Override
public void emptySimple(NamespaceResolver namespaceResolver) {
nilSimple(namespaceResolver);
}
@Override
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
*/
@Override
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 --------------------- //
@Override
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);
}
@Override
public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
XPathFragment xPathFragment = new XPathFragment(localName);
xPathFragment.setNamespaceURI(namespaceURI);
JsonRecord.this.endElement(xPathFragment, namespaceResolver);
}
@Override
public void startPrefixMapping(String prefix, String uri) throws SAXException {
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
String characters = new String(ch, start, length);
characters(characters);
}
@Override
public void characters(CharSequence characters) throws SAXException {
JsonRecord.this.characters(characters.toString());
}
// --------------------- LEXICALHANDLER METHODS --------------------- //
@Override
public void comment(char[] ch, int start, int length) throws SAXException {
}
@Override
public void startCDATA() throws SAXException {
}
@Override
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 --------------- //
@Override
public void endPrefixMapping(String prefix) throws SAXException {
}
@Override
public void processingInstruction(String target, String data) throws SAXException {
}
@Override
public void setDocumentLocator(Locator locator) {
}
@Override
public void startDocument() throws SAXException {
}
@Override
public void endDocument() throws SAXException {
}
@Override
public void skippedEntity(String name) throws SAXException {
}
@Override
public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
}
// --------------- SATISFY LEXICALHANDLER INTERFACE --------------- //
@Override
public void startEntity(String name) throws SAXException {
}
@Override
public void endEntity(String name) throws SAXException {
}
@Override
public void startDTD(String name, String publicId, String systemId) throws SAXException {
}
@Override
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 boolean emptyCollectionGenerated;
protected String keyName;
protected boolean isComplex;
protected boolean nestedArray;
protected Level parentLevel;
private int skipCount;
public Level(boolean isCollection, Level parentLevel, boolean nestedArray) {
setCollection(isCollection);
emptyCollection = true;
this.parentLevel = parentLevel;
this.nestedArray = nestedArray;
this.skipCount = 0;
}
protected void addSkip() {
skipCount++;
}
protected boolean notSkip() {
if (skipCount > 0) {
skipCount--;
return false;
} else {
return true;
}
}
protected int getSkipCount() {
return skipCount;
}
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 isEmptyCollectionGenerated() {
return emptyCollectionGenerated;
}
public void setEmptyCollectionGenerated(boolean emptyCollectionGenerated) {
this.emptyCollectionGenerated = emptyCollectionGenerated;
}
public boolean isComplex() {
return isComplex;
}
public void setComplex(boolean isComplex) {
this.isComplex = isComplex;
}
public boolean isNestedArray() {
return nestedArray;
}
public void setNestedArray(boolean nestedArray) {
this.nestedArray = nestedArray;
}
}
}