/*
* Copyright 2002-2009 the original author or authors.
*
* Licensed 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 net.sf.json.xml;
import net.sf.json.JSON;
import net.sf.json.JSONArray;
import net.sf.json.JSONException;
import net.sf.json.JSONFunction;
import net.sf.json.JSONNull;
import net.sf.json.JSONObject;
import net.sf.json.util.JSONUtils;
import nu.xom.Attribute;
import nu.xom.Builder;
import nu.xom.Document;
import nu.xom.Element;
import nu.xom.Elements;
import nu.xom.Node;
import nu.xom.ProcessingInstruction;
import nu.xom.Serializer;
import nu.xom.Text;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* Utility class for transforming JSON to XML an back.<br>
* When transforming JSONObject and JSONArray instances to XML, this class will
* add hints for converting back to JSON.<br>
* Examples:<br>
*
* <pre>
* JSONObject json = JSONObject.fromObject("{\"name\":\"json\",\"bool\":true,\"int\":1}");
* String xml = new XMLSerializer().write( json );
* <xmp><o class="object">
* <name type="string">json</name>
* <bool type="boolean">true</bool>
* <int type="number">1</int>
* </o></xmp>
* </pre><pre>
* JSONArray json = JSONArray.fromObject("[1,2,3]");
* String xml = new XMLSerializer().write( json );
* <xmp><a class="array">
* <e type="number">1</e>
* <e type="number">2</e>
* <e type="number">3</e>
* </a></xmp>
* </pre>
*
* @author Andres Almiray <aalmiray@users.sourceforge.net>
*/
public class XMLSerializer {
private static final String[] EMPTY_ARRAY = new String[0];
private static final String JSON_PREFIX = "json_";
private static final Log log = LogFactory.getLog( XMLSerializer.class );
/**
* the name for an JSONArray Element
*/
private String arrayName;
/**
* the name for an JSONArray's element Element
*/
private String elementName;
/**
* list of properties to be expanded from child to parent
*/
private String[] expandableProperties;
private boolean forceTopLevelObject;
/**
* flag to be tolerant for incomplete namespace prefixes
*/
private boolean namespaceLenient;
/**
* Map of namespaces per element
*/
private Map namespacesPerElement = new TreeMap();
/**
* the name for an JSONObject Element
*/
private String objectName;
/**
* flag for trimming namespace prefix from element name
*/
private boolean removeNamespacePrefixFromElements;
/**
* the name for the root Element
*/
private String rootName;
/**
* Map of namespaces for root element
*/
private Map rootNamespace = new TreeMap();
/**
* flag for skipping namespaces while reading
*/
private boolean skipNamespaces;
/**
* flag for skipping whitespace elements while reading
*/
private boolean skipWhitespace;
/**
* flag for trimming spaces from string values
*/
private boolean trimSpaces;
/**
* flag for type hints naming compatibility
*/
private boolean typeHintsCompatibility;
/**
* flag for adding JSON types hints as attributes
*/
private boolean typeHintsEnabled;
/**
* flag for performing auto-expansion of arrays if
*/
private boolean isPerformAutoExpansion;
/**
* flag for if text with CDATA should keep the information in the value or not
*/
private boolean isKeepCData;
/**
* flag for if characters lower than ' ' should be escaped in texts.
*/
private boolean isEscapeLowerChars;
/**
* flag for if array name should be kept in JSON data
*/
private boolean keepArrayName;
/**
* Creates a new XMLSerializer with default options.<br>
* <ul>
* <li><code>objectName</code>: 'o'</li>
* <li><code>arrayName</code>: 'a'</li>
* <li><code>elementName</code>: 'e'</li>
* <li><code>typeHinstEnabled</code>: true</li>
* <li><code>typeHinstCompatibility</code>: true</li>
* <li><code>namespaceLenient</code>: false</li>
* <li><code>expandableProperties</code>: []</li>
* <li><code>skipNamespaces</code>: false</li>
* <li><code>removeNameSpacePrefixFromElement</code>: false</li>
* <li><code>trimSpaces</code>: false</li>
* <li><code>isPerformAutoExpansion</code>: false</li>
* </ul>
*/
public XMLSerializer() {
setObjectName( "o" );
setArrayName( "a" );
setElementName( "e" );
setTypeHintsEnabled( true );
setTypeHintsCompatibility( true );
setNamespaceLenient( false );
setSkipNamespaces( false );
setRemoveNamespacePrefixFromElements( false );
setTrimSpaces( false );
setExpandableProperties( EMPTY_ARRAY );
setSkipNamespaces( false );
setPerformAutoExpansion( false );
setKeepCData( false );
setEscapeLowerChars( false );
setKeepArrayName( false );
}
/**
* Adds a namespace declaration to the root element.
*
* @param prefix namespace prefix
* @param uri namespace uri
*/
public void addNamespace( String prefix, String uri ) {
addNamespace( prefix, uri, null );
}
/**
* Adds a namespace declaration to an element.<br>
* If the elementName param is null or blank, the namespace declaration will
* be added to the root element.
*
* @param prefix namespace prefix
* @param uri namespace uri
* @param elementName name of target element
*/
public void addNamespace( String prefix, String uri, String elementName ) {
if( StringUtils.isBlank( uri ) ){
return;
}
if( prefix == null ){
prefix = "";
}
if( StringUtils.isBlank( elementName ) ){
rootNamespace.put( prefix.trim(), uri.trim() );
}else{
Map nameSpaces = (Map) namespacesPerElement.get( elementName );
if( nameSpaces == null ){
nameSpaces = new TreeMap();
namespacesPerElement.put( elementName, nameSpaces );
}
nameSpaces.put( prefix, uri );
}
}
/**
* Removes all namespaces declarations (from root an elements).
*/
public void clearNamespaces() {
rootNamespace.clear();
namespacesPerElement.clear();
}
/**
* Removes all namespace declarations from an element.<br>
* If the elementName param is null or blank, the declarations will be
* removed from the root element.
*
* @param elementName name of target element
*/
public void clearNamespaces( String elementName ) {
if( StringUtils.isBlank( elementName ) ){
rootNamespace.clear();
}else{
namespacesPerElement.remove( elementName );
}
}
/**
* Returns the name used for JSONArray.
*/
public String getArrayName() {
return arrayName;
}
/**
* Returns the name used for JSONArray elements.
*/
public String getElementName() {
return elementName;
}
/**
* Returns a list of properties to be expanded from child to parent.
*/
public String[] getExpandableProperties() {
return expandableProperties;
}
/**
* Returns the name used for JSONArray.
*/
public String getObjectName() {
return objectName;
}
/**
* Returns the name used for the root element.
*/
public String getRootName() {
return rootName;
}
public boolean isForceTopLevelObject() {
return forceTopLevelObject;
}
/**
* Returns wether this serializer is tolerant to namespaces without URIs or
* not.
*/
public boolean isNamespaceLenient() {
return namespaceLenient;
}
/**
* Returns wether this serializer will remove namespace prefix from elements
* or not.
*/
public boolean isRemoveNamespacePrefixFromElements() {
return removeNamespacePrefixFromElements;
}
/**
* Returns wether this serializer will skip adding namespace declarations to
* elements or not.
*/
public boolean isSkipNamespaces() {
return skipNamespaces;
}
/**
* Returns wether this serializer will skip whitespace or not.
*/
public boolean isSkipWhitespace() {
return skipWhitespace;
}
/**
* Returns wether this serializer will trim leading and trealing whitespace
* from values or not.
*/
public boolean isTrimSpaces() {
return trimSpaces;
}
/**
* Returns true if types hints will have a 'json_' prefix or not.
*/
public boolean isTypeHintsCompatibility() {
return typeHintsCompatibility;
}
/**
* Returns true if JSON types will be included as attributes.
*/
public boolean isTypeHintsEnabled() {
return typeHintsEnabled;
}
/**
* Creates a JSON value from a XML string.
*
* @param xml A well-formed xml document in a String
* @return a JSONNull, JSONObject or JSONArray
* @throws JSONException if the conversion from XML to JSON can't be made for
* I/O or format reasons.
*/
public JSON read( String xml ) {
JSON json = null;
try{
Document doc = new Builder().build( new StringReader( xml ) );
Element root = doc.getRootElement();
if( isNullObject( root ) ){
return JSONNull.getInstance();
}
String defaultType = getType( root, JSONTypes.STRING );
if( isArray( root, true ) ){
json = processArrayElement( root, defaultType );
if( forceTopLevelObject ){
String key = removeNamespacePrefix( root.getQualifiedName() );
json = new JSONObject().element( key, json );
}
}else{
json = processObjectElement( root, defaultType );
if( forceTopLevelObject ){
String key = removeNamespacePrefix( root.getQualifiedName() );
json = new JSONObject().element( key, json );
}
}
}catch( JSONException jsone ){
throw jsone;
}catch( Exception e ){
throw new JSONException( e );
}
return json;
}
/**
* Creates a JSON value from a File.
*
* @param file
* @return a JSONNull, JSONObject or JSONArray
* @throws JSONException if the conversion from XML to JSON can't be made for
* I/O or format reasons.
*/
public JSON readFromFile( File file ) {
if( file == null ){
throw new JSONException( "File is null" );
}
if( !file.canRead() ){
throw new JSONException( "Can't read input file" );
}
if( file.isDirectory() ){
throw new JSONException( "File is a directory" );
}
try{
return readFromStream( new FileInputStream( file ) );
}catch( IOException ioe ){
throw new JSONException( ioe );
}
}
/**
* Creates a JSON value from a File.
*
* @param path
* @return a JSONNull, JSONObject or JSONArray
* @throws JSONException if the conversion from XML to JSON can't be made for
* I/O or format reasons.
*/
public JSON readFromFile( String path ) {
return readFromStream( Thread.currentThread()
.getContextClassLoader()
.getResourceAsStream( path ) );
}
/**
* Creates a JSON value from an input stream.
*
* @param stream
* @return a JSONNull, JSONObject or JSONArray
* @throws JSONException if the conversion from XML to JSON can't be made for
* I/O or format reasons.
*/
public JSON readFromStream( InputStream stream ) {
try{
StringBuffer xml = new StringBuffer();
BufferedReader in = new BufferedReader( new InputStreamReader( stream ) );
String line = null;
while( (line = in.readLine()) != null ){
xml.append( line );
}
return read( xml.toString() );
}catch( IOException ioe ){
throw new JSONException( ioe );
}
}
/**
* Removes a namespace from the root element.
*
* @param prefix namespace prefix
*/
public void removeNamespace( String prefix ) {
removeNamespace( prefix, null );
}
/**
* Removes a namespace from the root element.<br>
* If the elementName is null or blank, the namespace will be removed from
* the root element.
*
* @param prefix namespace prefix
* @param elementName name of target element
*/
public void removeNamespace( String prefix, String elementName ) {
if( prefix == null ){
prefix = "";
}
if( StringUtils.isBlank( elementName ) ){
rootNamespace.remove( prefix.trim() );
}else{
Map nameSpaces = (Map) namespacesPerElement.get( elementName );
nameSpaces.remove( prefix );
}
}
/**
* Sets the name used for JSONArray.<br>
* Default is 'a'.
*/
public void setArrayName( String arrayName ) {
this.arrayName = StringUtils.isBlank( arrayName ) ? "a" : arrayName;
}
/**
* Sets the name used for JSONArray elements.<br>
* Default is 'e'.
*/
public void setElementName( String elementName ) {
this.elementName = StringUtils.isBlank( elementName ) ? "e" : elementName;
}
/**
* Sets the list of properties to be expanded from child to parent.
*/
public void setExpandableProperties( String[] expandableProperties ) {
this.expandableProperties = expandableProperties == null ? EMPTY_ARRAY : expandableProperties;
}
public void setForceTopLevelObject( boolean forceTopLevelObject ) {
this.forceTopLevelObject = forceTopLevelObject;
}
/**
* Sets the namespace declaration to the root element.<br>
* Any previous values are discarded.
*
* @param prefix namespace prefix
* @param uri namespace uri
*/
public void setNamespace( String prefix, String uri ) {
setNamespace( prefix, uri, null );
}
/**
* Adds a namespace declaration to an element.<br>
* Any previous values are discarded. If the elementName param is null or
* blank, the namespace declaration will be added to the root element.
*
* @param prefix namespace prefix
* @param uri namespace uri
* @param elementName name of target element
*/
public void setNamespace( String prefix, String uri, String elementName ) {
if( StringUtils.isBlank( uri ) ){
return;
}
if( prefix == null ){
prefix = "";
}
if( StringUtils.isBlank( elementName ) ){
rootNamespace.clear();
rootNamespace.put( prefix.trim(), uri.trim() );
}else{
Map nameSpaces = (Map) namespacesPerElement.get( elementName );
if( nameSpaces == null ){
nameSpaces = new TreeMap();
namespacesPerElement.put( elementName, nameSpaces );
}
nameSpaces.clear();
nameSpaces.put( prefix, uri );
}
}
/**
* Sets whether this serializer should perform automatic expansion of array elements or not.
*/
public void setPerformAutoExpansion( boolean autoExpansion ) {
isPerformAutoExpansion = autoExpansion;
}
/**
* Sets whether this serializer should keep the CDATA information in the value or not.
*
* @param keepCData True to keep CDATA, false to only use the text value.
*/
public void setKeepCData( boolean keepCData ) {
isKeepCData = keepCData;
}
/**
* Sets whether this serializer should escape characters lower than ' ' in texts.
*
* @param escape True to escape, false otherwise.
*/
public void setEscapeLowerChars( boolean escape ) {
isEscapeLowerChars = escape;
}
/**
* Sets whether this serializer should keep the XML element being an array.
*
* @param keepName True to include the element name in the JSON object, false otherwise.
*/
public void setKeepArrayName( boolean keepName ) {
keepArrayName = keepName;
}
/**
* Sets whether this serializer is tolerant to namespaces without URIs or not.
*/
public void setNamespaceLenient( boolean namespaceLenient ) {
this.namespaceLenient = namespaceLenient;
}
/**
* Sets the name used for JSONObject.<br>
* Default is 'o'.
*/
public void setObjectName( String objectName ) {
this.objectName = StringUtils.isBlank( objectName ) ? "o" : objectName;
}
/**
* Sets if this serializer will remove namespace prefix from elements when
* reading.
*/
public void setRemoveNamespacePrefixFromElements( boolean removeNamespacePrefixFromElements ) {
this.removeNamespacePrefixFromElements = removeNamespacePrefixFromElements;
}
/**
* Sets the name used for the root element.
*/
public void setRootName( String rootName ) {
this.rootName = StringUtils.isBlank( rootName ) ? null : rootName;
}
/**
* Sets if this serializer will skip adding namespace declarations to
* elements when reading.
*/
public void setSkipNamespaces( boolean skipNamespaces ) {
this.skipNamespaces = skipNamespaces;
}
/**
* Sets if this serializer will skip whitespace when reading.
*/
public void setSkipWhitespace( boolean skipWhitespace ) {
this.skipWhitespace = skipWhitespace;
}
/**
* Sets if this serializer will trim leading and trealing whitespace from
* values when reading.
*/
public void setTrimSpaces( boolean trimSpaces ) {
this.trimSpaces = trimSpaces;
}
/**
* Sets wether types hints will have a 'json_' prefix or not.
*/
public void setTypeHintsCompatibility( boolean typeHintsCompatibility ) {
this.typeHintsCompatibility = typeHintsCompatibility;
}
/**
* Sets wether JSON types will be included as attributes.
*/
public void setTypeHintsEnabled( boolean typeHintsEnabled ) {
this.typeHintsEnabled = typeHintsEnabled;
}
/**
* Writes a JSON value into a XML string with UTF-8 encoding.<br>
*
* @param json The JSON value to transform
* @return a String representation of a well-formed xml document.
* @throws JSONException if the conversion from JSON to XML can't be made for
* I/O reasons.
*/
public String write( JSON json ) {
return write( json, null );
}
/**
* Writes a JSON value into a XML string with an specific encoding.<br>
* If the encoding string is null it will use UTF-8.
*
* @param json The JSON value to transform
* @param encoding The xml encoding to use
* @return a String representation of a well-formed xml document.
* @throws JSONException if the conversion from JSON to XML can't be made for
* I/O reasons or the encoding is not supported.
*/
public String write( JSON json, String encoding ) {
if( keepArrayName && typeHintsEnabled ){
throw new IllegalStateException( "Type Hints cannot be used together with 'keepArrayName'" );
}
if( JSONNull.getInstance()
.equals( json ) ){
Element root = null;
root = newElement( getRootName() == null ? getObjectName() : getRootName() );
root.addAttribute( new Attribute( addJsonPrefix( "null" ), "true" ) );
Document doc = new Document( root );
return writeDocument( doc, encoding );
}else if( json instanceof JSONArray ){
JSONArray jsonArray = (JSONArray) json;
Element root = processJSONArray( jsonArray,
newElement( getRootName() == null ? getArrayName() : getRootName() ),
expandableProperties );
Document doc = new Document( root );
return writeDocument( doc, encoding );
}else{
JSONObject jsonObject = (JSONObject) json;
Element root = null;
if( jsonObject.isNullObject() ){
root = newElement( getObjectName() );
root.addAttribute( new Attribute( addJsonPrefix( "null" ), "true" ) );
}else{
root = processJSONObject( jsonObject,
newElement( getRootName() == null ? getObjectName() : getRootName() ),
expandableProperties, true );
}
Document doc = new Document( root );
return writeDocument( doc, encoding );
}
}
private String addJsonPrefix( String str ) {
if( !isTypeHintsCompatibility() ){
return JSON_PREFIX + str;
}
return str;
}
private void addNameSpaceToElement( Element element ) {
String elementName = null;
if( element instanceof CustomElement ){
elementName = ((CustomElement) element).getQName();
}else{
elementName = element.getQualifiedName();
}
Map nameSpaces = (Map) namespacesPerElement.get( elementName );
if( nameSpaces != null && !nameSpaces.isEmpty() ){
setNamespaceLenient( true );
for( Iterator entries = nameSpaces.entrySet()
.iterator(); entries.hasNext(); ){
Map.Entry entry = (Map.Entry) entries.next();
String prefix = (String) entry.getKey();
String uri = (String) entry.getValue();
if( StringUtils.isBlank( prefix ) ){
element.setNamespaceURI( uri );
}else{
element.addNamespaceDeclaration( prefix, uri );
}
}
}
}
private boolean checkChildElements( Element element, boolean isTopLevel ) {
int childCount = element.getChildCount();
Elements elements = element.getChildElements();
int elementCount = elements.size();
if( childCount == 1 && element.getChild( 0 ) instanceof Text ){
return isTopLevel;
}
if( childCount == elementCount ){
if( elementCount == 0 ){
return true;
}
if( elementCount == 1 ){
if( skipWhitespace && element.getChild( 0 ) instanceof Text ){
return true;
}else{
return false;
}
}
}
if( childCount > elementCount ){
for( int i = 0; i < childCount; i++ ){
Node node = element.getChild( i );
if( node instanceof Text ){
Text text = (Text) node;
if( StringUtils.isNotBlank( StringUtils.strip( text.getValue() ) )
&& !skipWhitespace ){
return false;
}
}
}
}
String childName = elements.get( 0 )
.getQualifiedName();
for( int i = 1; i < elementCount; i++ ){
if( childName.compareTo( elements.get( i )
.getQualifiedName() ) != 0 ){
return false;
}
}
if( childName.equals( arrayName ) ){
return true;
}
return elementCount > 1;
}
private String getClass( Element element ) {
Attribute attribute = element.getAttribute( addJsonPrefix( "class" ) );
String clazz = null;
if( attribute != null ){
String clazzText = attribute.getValue()
.trim();
if( JSONTypes.OBJECT.compareToIgnoreCase( clazzText ) == 0 ){
clazz = JSONTypes.OBJECT;
}else if( JSONTypes.ARRAY.compareToIgnoreCase( clazzText ) == 0 ){
clazz = JSONTypes.ARRAY;
}
}
return clazz;
}
private String getType( Element element ) {
return getType( element, null );
}
private String getType( Element element, String defaultType ) {
Attribute attribute = element.getAttribute( addJsonPrefix( "type" ) );
String type = null;
if( attribute != null ){
String typeText = attribute.getValue()
.trim();
if( JSONTypes.BOOLEAN.compareToIgnoreCase( typeText ) == 0 ){
type = JSONTypes.BOOLEAN;
}else if( JSONTypes.NUMBER.compareToIgnoreCase( typeText ) == 0 ){
type = JSONTypes.NUMBER;
}else if( JSONTypes.INTEGER.compareToIgnoreCase( typeText ) == 0 ){
type = JSONTypes.INTEGER;
}else if( JSONTypes.FLOAT.compareToIgnoreCase( typeText ) == 0 ){
type = JSONTypes.FLOAT;
}else if( JSONTypes.OBJECT.compareToIgnoreCase( typeText ) == 0 ){
type = JSONTypes.OBJECT;
}else if( JSONTypes.ARRAY.compareToIgnoreCase( typeText ) == 0 ){
type = JSONTypes.ARRAY;
}else if( JSONTypes.STRING.compareToIgnoreCase( typeText ) == 0 ){
type = JSONTypes.STRING;
}else if( JSONTypes.FUNCTION.compareToIgnoreCase( typeText ) == 0 ){
type = JSONTypes.FUNCTION;
}
}else{
if( defaultType != null ){
log.info( "Using default type " + defaultType );
type = defaultType;
}
}
return type;
}
private boolean hasNamespaces( Element element ) {
int namespaces = 0;
for( int i = 0; i < element.getNamespaceDeclarationCount(); i++ ){
String prefix = element.getNamespacePrefix( i );
String uri = element.getNamespaceURI( prefix );
if( StringUtils.isBlank( uri ) ){
continue;
}
namespaces++;
}
return namespaces > 0;
}
private boolean isArray( Element element, boolean isTopLevel ) {
boolean isArray = false;
String clazz = getClass( element );
if( clazz != null && clazz.equals( JSONTypes.ARRAY ) ){
isArray = true;
}else if( element.getAttributeCount() == 0 ){
isArray = checkChildElements( element, isTopLevel );
}else if( element.getAttributeCount() == 1
&& (element.getAttribute( addJsonPrefix( "class" ) ) != null || element.getAttribute( addJsonPrefix( "type" ) ) != null) ){
isArray = checkChildElements( element, isTopLevel );
}else if( element.getAttributeCount() == 2
&& (element.getAttribute( addJsonPrefix( "class" ) ) != null && element.getAttribute( addJsonPrefix( "type" ) ) != null) ){
isArray = checkChildElements( element, isTopLevel );
}
if( isArray ){
// check namespace
for( int j = 0; j < element.getNamespaceDeclarationCount(); j++ ){
String prefix = element.getNamespacePrefix( j );
String uri = element.getNamespaceURI( prefix );
if( !StringUtils.isBlank( uri ) ){
return false;
}
}
}
return isArray;
}
private boolean isFunction( Element element ) {
int attrCount = element.getAttributeCount();
if( attrCount > 0 ){
Attribute typeAttr = element.getAttribute( addJsonPrefix( "type" ) );
Attribute paramsAttr = element.getAttribute( addJsonPrefix( "params" ) );
if( attrCount == 1 && paramsAttr != null ){
return true;
}
if( attrCount == 2 && paramsAttr != null && typeAttr != null && (typeAttr.getValue()
.compareToIgnoreCase( JSONTypes.STRING ) == 0 || typeAttr.getValue()
.compareToIgnoreCase( JSONTypes.FUNCTION ) == 0) ){
return true;
}
}
return false;
}
private boolean isNullObject( Element element ) {
if( element.getChildCount() == 0 ){
if( element.getAttributeCount() == 0 ){
return true;
}else if( element.getAttribute( addJsonPrefix( "null" ) ) != null ){
return true;
}else if( element.getAttributeCount() == 1
&& (element.getAttribute( addJsonPrefix( "class" ) ) != null || element.getAttribute( addJsonPrefix( "type" ) ) != null) ){
return true;
}else if( element.getAttributeCount() == 2
&& (element.getAttribute( addJsonPrefix( "class" ) ) != null && element.getAttribute( addJsonPrefix( "type" ) ) != null) ){
return true;
}
}
if( skipWhitespace && element.getChildCount() == 1 && element.getChild( 0 ) instanceof Text ){
return true;
}
return false;
}
private boolean isObject( Element element, boolean isTopLevel ) {
boolean isObject = false;
if( !isArray( element, isTopLevel ) && !isFunction( element ) ){
if( hasNamespaces( element ) ){
return true;
}
int attributeCount = element.getAttributeCount();
if( attributeCount > 0 ){
int attrs = element.getAttribute( addJsonPrefix( "null" ) ) == null ? 0 : 1;
attrs += element.getAttribute( addJsonPrefix( "class" ) ) == null ? 0 : 1;
attrs += element.getAttribute( addJsonPrefix( "type" ) ) == null ? 0 : 1;
switch( attributeCount ){
case 1:
if( attrs == 0 ){
return true;
}
break;
case 2:
if( attrs < 2 ){
return true;
}
break;
case 3:
if( attrs < 3 ){
return true;
}
break;
default:
return true;
}
}
int childCount = element.getChildCount();
if( childCount == 1 && element.getChild( 0 ) instanceof Text ){
return isTopLevel;
}
isObject = true;
}
return isObject;
}
private Element newElement( String name ) {
if( name.indexOf( ':' ) != -1 ){
namespaceLenient = true;
}
return namespaceLenient ? new CustomElement( name ) : new Element( name );
}
private JSON processArrayElement( Element element, String defaultType ) {
JSONArray jsonArray = new JSONArray();
// process children (including text)
int childCount = element.getChildCount();
for( int i = 0; i < childCount; i++ ){
Node child = element.getChild( i );
if( child instanceof Text ){
Text text = (Text) child;
if( StringUtils.isNotBlank( StringUtils.strip( text.getValue() ) ) ){
jsonArray.element( text.getValue() );
}
}else if( child instanceof Element ){
setValue( jsonArray, (Element) child, defaultType );
}
}
if( keepArrayName ){
boolean isSameElementNameInArray = true;
String arrayName = null;
for( int i = 0; i < element.getChildElements().size(); i++ ){
final String arrayElement = element.getChildElements().get( i ).getQualifiedName();
if( arrayName == null ){
arrayName = arrayElement;
}else if( !arrayName.equals( arrayElement ) ){
isSameElementNameInArray = false;
}
}
if( isSameElementNameInArray ){
JSONObject result = new JSONObject();
result.put( arrayName, jsonArray );
return result;
}
}
return jsonArray;
}
private Object processElement( Element element, String type ) {
if( isNullObject( element ) ){
return JSONNull.getInstance();
}else if( isArray( element, false ) ){
return processArrayElement( element, type );
}else if( isObject( element, false ) ){
return processObjectElement( element, type );
}else{
return trimSpaceFromValue( element.getValue() );
}
}
private Element processJSONArray( JSONArray array, Element root, String[] expandableProperties ) {
int l = array.size();
for( int i = 0; i < l; i++ ){
Object value = array.get( i );
Element element = processJSONValue( value, root, null, expandableProperties );
root.appendChild( element );
}
return root;
}
private Element processJSONObject( JSONObject jsonObject, Element root,
String[] expandableProperties, boolean isRoot ) {
if( jsonObject.isNullObject() ){
root.addAttribute( new Attribute( addJsonPrefix( "null" ), "true" ) );
return root;
}else if( jsonObject.isEmpty() ){
return root;
}
if( isRoot ){
if( !rootNamespace.isEmpty() ){
setNamespaceLenient( true );
for( Iterator entries = rootNamespace.entrySet()
.iterator(); entries.hasNext(); ){
Map.Entry entry = (Map.Entry) entries.next();
String prefix = (String) entry.getKey();
String uri = (String) entry.getValue();
if( StringUtils.isBlank( prefix ) ){
root.setNamespaceURI( uri );
}else{
root.addNamespaceDeclaration( prefix, uri );
}
}
}
}
addNameSpaceToElement( root );
Object[] names = jsonObject.names().toArray();
List unprocessed = new ArrayList();
for( int i = 0; i < names.length; i++ ){
String name = (String) names[i];
Object value = jsonObject.get( name );
if( name.startsWith( "@xmlns" ) ){
setNamespaceLenient( true );
int colon = name.indexOf( ':' );
if( colon == -1 ){
// do not override if already defined by nameSpaceMaps
if( StringUtils.isBlank( root.getNamespaceURI() ) ){
root.setNamespaceURI( String.valueOf( value ) );
}
}else{
String prefix = name.substring( colon + 1 );
if( StringUtils.isBlank( root.getNamespaceURI( prefix ) ) ){
root.addNamespaceDeclaration( prefix, String.valueOf( value ) );
}
}
}else{
unprocessed.add( name );
}
}
Element element = null;
for( int i = 0; i < unprocessed.size(); i++ ){
String name = (String) unprocessed.get( i );
Object value = jsonObject.get( name );
if( name.startsWith( "@" ) ){
int colon = name.indexOf( ':' );
if( colon == -1 ){
root.addAttribute( new Attribute( name.substring( 1 ), String.valueOf( value ) ) );
}else{
String prefix = name.substring( 1, colon );
final String namespaceURI = root.getNamespaceURI( prefix );
root.addAttribute( new Attribute( name.substring( 1 ), namespaceURI, String.valueOf( value ) ) );
}
}else if( name.equals( "#text" ) ){
if( value instanceof JSONArray ){
root.appendChild( ((JSONArray) value).join( "", true ) );
}else{
root.appendChild( String.valueOf( value ) );
}
}else if( value instanceof JSONArray
&& (((JSONArray) value).isExpandElements() || ArrayUtils.contains(
expandableProperties, name ) || (isPerformAutoExpansion && canAutoExpand( (JSONArray) value ))) ){
JSONArray array = (JSONArray) value;
int l = array.size();
for( int j = 0; j < l; j++ ){
Object item = array.get( j );
element = newElement( name );
root.appendChild( element );
if( item instanceof JSONObject ){
element = processJSONValue( (JSONObject) item, root, element,
expandableProperties );
}else if( item instanceof JSONArray ){
element = processJSONValue( (JSONArray) item, root, element, expandableProperties );
}else{
element = processJSONValue( item, root, element, expandableProperties );
}
addNameSpaceToElement( element );
}
}else{
element = newElement( name );
root.appendChild( element );
element = processJSONValue( value, root, element, expandableProperties );
addNameSpaceToElement( element );
}
}
return root;
}
/**
* Only perform auto expansion if all children are objects.
*
* @param array The array to check
* @return True if all children are objects, false otherwise.
*/
private boolean canAutoExpand( JSONArray array ) {
for( int i = 0; i < array.size(); i++ ){
if( !(array.get( i ) instanceof JSONObject) ){
return false;
}
}
return true;
}
private Element processJSONValue( Object value, Element root, Element target,
String[] expandableProperties ) {
if( target == null ){
target = newElement( getElementName() );
}
if( JSONUtils.isBoolean( value ) ){
if( isTypeHintsEnabled() ){
target.addAttribute( new Attribute( addJsonPrefix( "type" ), JSONTypes.BOOLEAN ) );
}
target.appendChild( value.toString() );
}else if( JSONUtils.isNumber( value ) ){
if( isTypeHintsEnabled() ){
target.addAttribute( new Attribute( addJsonPrefix( "type" ), JSONTypes.NUMBER ) );
}
target.appendChild( value.toString() );
}else if( JSONUtils.isFunction( value ) ){
if( value instanceof String ){
value = JSONFunction.parse( (String) value );
}
JSONFunction func = (JSONFunction) value;
if( isTypeHintsEnabled() ){
target.addAttribute( new Attribute( addJsonPrefix( "type" ), JSONTypes.FUNCTION ) );
}
String params = ArrayUtils.toString( func.getParams() );
params = params.substring( 1 );
params = params.substring( 0, params.length() - 1 );
target.addAttribute( new Attribute( addJsonPrefix( "params" ), params ) );
target.appendChild( new Text( "<![CDATA[" + func.getText() + "]]>" ) );
}else if( JSONUtils.isString( value ) ){
if( isTypeHintsEnabled() ){
target.addAttribute( new Attribute( addJsonPrefix( "type" ), JSONTypes.STRING ) );
}
target.appendChild( value.toString() );
}else if( value instanceof JSONArray ){
if( isTypeHintsEnabled() ){
target.addAttribute( new Attribute( addJsonPrefix( "class" ), JSONTypes.ARRAY ) );
}
target = processJSONArray( (JSONArray) value, target, expandableProperties );
}else if( value instanceof JSONObject ){
if( isTypeHintsEnabled() ){
target.addAttribute( new Attribute( addJsonPrefix( "class" ), JSONTypes.OBJECT ) );
}
target = processJSONObject( (JSONObject) value, target, expandableProperties, false );
}else if( JSONUtils.isNull( value ) ){
if( isTypeHintsEnabled() ){
target.addAttribute( new Attribute( addJsonPrefix( "class" ), JSONTypes.OBJECT ) );
}
target.addAttribute( new Attribute( addJsonPrefix( "null" ), "true" ) );
}
return target;
}
private JSON processObjectElement( Element element, String defaultType ) {
if( isNullObject( element ) ){
return JSONNull.getInstance();
}
JSONObject jsonObject = new JSONObject();
if( !skipNamespaces ){
for( int j = 0; j < element.getNamespaceDeclarationCount(); j++ ){
String prefix = element.getNamespacePrefix( j );
String uri = element.getNamespaceURI( prefix );
if( StringUtils.isBlank( uri ) ){
continue;
}
if( !StringUtils.isBlank( prefix ) ){
prefix = ":" + prefix;
}
setOrAccumulate( jsonObject, "@xmlns" + prefix, trimSpaceFromValue( uri ) );
}
}
// process attributes first
int attrCount = element.getAttributeCount();
for( int i = 0; i < attrCount; i++ ){
Attribute attr = element.getAttribute( i );
String attrname = attr.getQualifiedName();
if( isTypeHintsEnabled()
&& (addJsonPrefix( "class" ).compareToIgnoreCase( attrname ) == 0 || addJsonPrefix(
"type" ).compareToIgnoreCase( attrname ) == 0) ){
continue;
}
String attrvalue = attr.getValue();
setOrAccumulate( jsonObject, "@" + removeNamespacePrefix( attrname ),
trimSpaceFromValue( attrvalue ) );
}
// process children (including text)
int childCount = element.getChildCount();
for( int i = 0; i < childCount; i++ ){
Node child = element.getChild( i );
if( child instanceof Text ){
Text text = (Text) child;
if( StringUtils.isNotBlank( StringUtils.strip( text.getValue() ) ) ){
setOrAccumulate( jsonObject, "#text", trimSpaceFromValue( text.getValue() ) );
}
}else if( child instanceof Element ){
setValue( jsonObject, (Element) child, defaultType );
}
}
return jsonObject;
}
private String removeNamespacePrefix( String name ) {
if( isRemoveNamespacePrefixFromElements() ){
int colon = name.indexOf( ':' );
return colon != -1 ? name.substring( colon + 1 ) : name;
}
return name;
}
private void setOrAccumulate( JSONObject jsonObject, String key, Object value ) {
if( jsonObject.has( key ) ){
jsonObject.accumulate( key, value );
Object val = jsonObject.get( key );
if( val instanceof JSONArray ){
((JSONArray) val).setExpandElements( true );
}
}else{
jsonObject.element( key, value );
}
}
private void setValue( JSONArray jsonArray, Element element, String defaultType ) {
String clazz = getClass( element );
String type = getType( element );
type = (type == null) ? defaultType : type;
if( hasNamespaces( element ) && !skipNamespaces ){
jsonArray.element( simplifyValue( null, processElement( element, type ) ) );
return;
}else if( element.getAttributeCount() > 0 ){
if( isFunction( element ) ){
Attribute paramsAttribute = element.getAttribute( addJsonPrefix( "params" ) );
String[] params = null;
String text = element.getValue();
params = StringUtils.split( paramsAttribute.getValue(), "," );
jsonArray.element( new JSONFunction( params, text ) );
return;
}else{
jsonArray.element( simplifyValue( null, processElement( element, type ) ) );
return;
}
}
boolean classProcessed = false;
if( clazz != null ){
if( clazz.compareToIgnoreCase( JSONTypes.ARRAY ) == 0 ){
jsonArray.element( processArrayElement( element, type ) );
classProcessed = true;
}else if( clazz.compareToIgnoreCase( JSONTypes.OBJECT ) == 0 ){
jsonArray.element( simplifyValue( null, processObjectElement( element, type ) ) );
classProcessed = true;
}
}
if( !classProcessed ){
if( type.compareToIgnoreCase( JSONTypes.BOOLEAN ) == 0 ){
jsonArray.element( Boolean.valueOf( element.getValue() ) );
}else if( type.compareToIgnoreCase( JSONTypes.NUMBER ) == 0 ){
// try integer first
try{
jsonArray.element( Integer.valueOf( element.getValue() ) );
}catch( NumberFormatException e ){
jsonArray.element( Double.valueOf( element.getValue() ) );
}
}else if( type.compareToIgnoreCase( JSONTypes.INTEGER ) == 0 ){
jsonArray.element( Integer.valueOf( element.getValue() ) );
}else if( type.compareToIgnoreCase( JSONTypes.FLOAT ) == 0 ){
jsonArray.element( Double.valueOf( element.getValue() ) );
}else if( type.compareToIgnoreCase( JSONTypes.FUNCTION ) == 0 ){
String[] params = null;
String text = element.getValue();
Attribute paramsAttribute = element.getAttribute( addJsonPrefix( "params" ) );
if( paramsAttribute != null ){
params = StringUtils.split( paramsAttribute.getValue(), "," );
}
jsonArray.element( new JSONFunction( params, text ) );
}else if( type.compareToIgnoreCase( JSONTypes.STRING ) == 0 ){
// see if by any chance has a 'params' attribute
Attribute paramsAttribute = element.getAttribute( addJsonPrefix( "params" ) );
if( paramsAttribute != null ){
String[] params = null;
String text = element.getValue();
params = StringUtils.split( paramsAttribute.getValue(), "," );
jsonArray.element( new JSONFunction( params, text ) );
}else{
if( isArray( element, false ) ){
jsonArray.element( processArrayElement( element, defaultType ) );
}else if( isObject( element, false ) ){
jsonArray.element( simplifyValue( null, processObjectElement( element,
defaultType ) ) );
}else{
jsonArray.element( trimSpaceFromValue( element.getValue() ) );
}
}
}
}
}
private void setValue( JSONObject jsonObject, Element element, String defaultType ) {
String clazz = getClass( element );
String type = getType( element );
type = (type == null) ? defaultType : type;
String key = removeNamespacePrefix( element.getQualifiedName() );
if( hasNamespaces( element ) && !skipNamespaces ){
setOrAccumulate( jsonObject, key, simplifyValue( jsonObject,
processElement( element, type ) ) );
return;
}else if( element.getAttributeCount() > 0 ){
if( isFunction( element ) ){
Attribute paramsAttribute = element.getAttribute( addJsonPrefix( "params" ) );
String text = element.getValue();
String[] params = StringUtils.split( paramsAttribute.getValue(), "," );
setOrAccumulate( jsonObject, key, new JSONFunction( params, text ) );
return;
}/*else{
setOrAccumulate( jsonObject, key, simplifyValue( jsonObject, processElement( element,
type ) ) );
return;
}*/
}
boolean classProcessed = false;
if( clazz != null ){
if( clazz.compareToIgnoreCase( JSONTypes.ARRAY ) == 0 ){
setOrAccumulate( jsonObject, key, processArrayElement( element, type ) );
classProcessed = true;
}else if( clazz.compareToIgnoreCase( JSONTypes.OBJECT ) == 0 ){
setOrAccumulate( jsonObject, key, simplifyValue( jsonObject, processObjectElement(
element, type ) ) );
classProcessed = true;
}
}
if( !classProcessed ){
if( type.compareToIgnoreCase( JSONTypes.BOOLEAN ) == 0 ){
setOrAccumulate( jsonObject, key, Boolean.valueOf( element.getValue() ) );
}else if( type.compareToIgnoreCase( JSONTypes.NUMBER ) == 0 ){
// try integer first
try{
setOrAccumulate( jsonObject, key, Integer.valueOf( element.getValue() ) );
}catch( NumberFormatException e ){
setOrAccumulate( jsonObject, key, Double.valueOf( element.getValue() ) );
}
}else if( type.compareToIgnoreCase( JSONTypes.INTEGER ) == 0 ){
setOrAccumulate( jsonObject, key, Integer.valueOf( element.getValue() ) );
}else if( type.compareToIgnoreCase( JSONTypes.FLOAT ) == 0 ){
setOrAccumulate( jsonObject, key, Double.valueOf( element.getValue() ) );
}else if( type.compareToIgnoreCase( JSONTypes.FUNCTION ) == 0 ){
String[] params = null;
String text = element.getValue();
Attribute paramsAttribute = element.getAttribute( addJsonPrefix( "params" ) );
if( paramsAttribute != null ){
params = StringUtils.split( paramsAttribute.getValue(), "," );
}
setOrAccumulate( jsonObject, key, new JSONFunction( params, text ) );
}else if( type.compareToIgnoreCase( JSONTypes.STRING ) == 0 ){
// see if by any chance has a 'params' attribute
Attribute paramsAttribute = element.getAttribute( addJsonPrefix( "params" ) );
if( paramsAttribute != null ){
String[] params = null;
String text = element.getValue();
params = StringUtils.split( paramsAttribute.getValue(), "," );
setOrAccumulate( jsonObject, key, new JSONFunction( params, text ) );
}else{
if( isArray( element, false ) ){
setOrAccumulate( jsonObject, key, processArrayElement( element, defaultType ) );
}else if( isObject( element, false ) ){
setOrAccumulate( jsonObject, key, simplifyValue( jsonObject,
processObjectElement( element, defaultType ) ) );
}else{
String value;
if( isKeepCData && isCData( element ) ){
value = "<![CDATA[" + element.getValue() + "]]>";
}else{
value = element.getValue();
}
setOrAccumulate( jsonObject, key, trimSpaceFromValue( value ) );
}
}
}
}
}
private boolean isCData( Element element ) {
if( element.getChildCount() == 1 ){
final Node child = element.getChild( 0 );
if( child.toXML().startsWith( "<![CDATA[" ) ){
return true;
}
}
return false;
}
private Object simplifyValue( JSONObject parent, Object json ) {
if( json instanceof JSONObject ){
JSONObject object = (JSONObject) json;
if( parent != null ){
// remove all duplicated @xmlns from child
for( Iterator entries = parent.entrySet()
.iterator(); entries.hasNext(); ){
Map.Entry entry = (Map.Entry) entries.next();
String key = (String) entry.getKey();
Object value = entry.getValue();
if( key.startsWith( "@xmlns" ) && value.equals( object.opt( key ) ) ){
object.remove( key );
}
}
}
if( object.size() == 1 && object.has( "#text" ) ){
return object.get( "#text" );
}
}
return json;
}
private String trimSpaceFromValue( String value ) {
if( isTrimSpaces() ){
return value.trim();
}
return value;
}
private String writeDocument( Document doc, String encoding ) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try{
XomSerializer serializer = (encoding == null) ? new XomSerializer( baos )
: new XomSerializer( baos, encoding );
serializer.write( doc );
encoding = serializer.getEncoding();
}catch( IOException ioe ){
throw new JSONException( ioe );
}
String str = null;
try{
str = baos.toString( encoding );
}catch( UnsupportedEncodingException uee ){
throw new JSONException( uee );
}
return str;
}
private static class CustomElement extends Element {
private static String getName( String name ) {
int colon = name.indexOf( ':' );
if( colon != -1 ){
return name.substring( colon + 1 );
}
return name;
}
private static String getPrefix( String name ) {
int colon = name.indexOf( ':' );
if( colon != -1 ){
return name.substring( 0, colon );
}
return "";
}
private String prefix;
public CustomElement( String name ) {
super( CustomElement.getName( name ) );
prefix = CustomElement.getPrefix( name );
}
public final String getQName() {
if( prefix.length() == 0 ){
return getLocalName();
}else{
return prefix + ":" + getLocalName();
}
}
}
private class XomSerializer extends Serializer {
public XomSerializer( OutputStream out ) {
super( out );
}
public XomSerializer( OutputStream out, String encoding ) throws UnsupportedEncodingException {
super( out, encoding );
}
protected void write( Text text ) throws IOException {
String value = text.getValue();
if( value.startsWith( "<![CDATA[" ) && value.endsWith( "]]>" ) ){
value = value.substring( 9 );
value = value.substring( 0, value.length() - 3 );
writeRaw( "<![CDATA[" );
writeRaw( value );
writeRaw( "]]>" );
}else{
if( isEscapeLowerChars ){
writeRaw( escape( value ) );
}else{
super.write( text );
}
}
}
private String escape( String text ) {
StringBuffer buffer = new StringBuffer();
for( int i = 0; i < text.length(); i++ ){
final char c = text.charAt( i );
if( c < ' ' ){
buffer.append( "" );
buffer.append( Integer.toHexString( c ).toUpperCase() );
buffer.append( ";" );
}else{
buffer.append( c );
}
}
return buffer.toString();
}
protected void writeEmptyElementTag( Element element ) throws IOException {
if( element instanceof CustomElement && isNamespaceLenient() ){
writeTagBeginning( (CustomElement) element );
writeRaw( "/>" );
}else{
super.writeEmptyElementTag( element );
}
}
protected void writeEndTag( Element element ) throws IOException {
if( element instanceof CustomElement && isNamespaceLenient() ){
writeRaw( "</" );
writeRaw( ((CustomElement) element).getQName() );
writeRaw( ">" );
}else{
super.writeEndTag( element );
}
}
protected void writeNamespaceDeclaration( String prefix, String uri ) throws IOException {
if( !StringUtils.isBlank( uri ) ){
super.writeNamespaceDeclaration( prefix, uri );
}
}
protected void writeStartTag( Element element ) throws IOException {
if( element instanceof CustomElement && isNamespaceLenient() ){
writeTagBeginning( (CustomElement) element );
writeRaw( ">" );
}else{
super.writeStartTag( element );
}
}
private void writeTagBeginning( CustomElement element ) throws IOException {
writeRaw( "<" );
writeRaw( element.getQName() );
writeAttributes( element );
writeNamespaceDeclarations( element );
}
}
}