/* * 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( "&#x" ); 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 ); } } }