/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.camel.dataformat.xmljson; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import net.sf.json.JSON; import net.sf.json.JSONSerializer; import net.sf.json.xml.XMLSerializer; import org.apache.camel.Exchange; import org.apache.camel.spi.DataFormat; import org.apache.camel.spi.DataFormatName; import org.apache.camel.support.ServiceSupport; import org.apache.camel.util.IOHelper; /** * A <a href="http://camel.apache.org/data-format.html">data format</a> ({@link DataFormat}) using * <a href="http://json-lib.sourceforge.net/">json-lib</a> to convert between XML * and JSON directly. */ public class XmlJsonDataFormat extends ServiceSupport implements DataFormat, DataFormatName { private XMLSerializer serializer; private String encoding; private String elementName; private String arrayName; private Boolean forceTopLevelObject; private Boolean namespaceLenient; private List<NamespacesPerElementMapping> namespaceMappings; private String rootName; private Boolean skipWhitespace; private Boolean trimSpaces; private Boolean skipNamespaces; private Boolean removeNamespacePrefixes; private List<String> expandableProperties; private TypeHintsEnum typeHints; private boolean contentTypeHeader = true; public XmlJsonDataFormat() { } @Override public String getDataFormatName() { return "xmljson"; } @Override protected void doStart() throws Exception { serializer = new XMLSerializer(); if (forceTopLevelObject != null) { serializer.setForceTopLevelObject(forceTopLevelObject); } if (namespaceLenient != null) { serializer.setNamespaceLenient(namespaceLenient); } if (namespaceMappings != null) { for (NamespacesPerElementMapping nsMapping : namespaceMappings) { for (Entry<String, String> entry : nsMapping.namespaces.entrySet()) { // prefix, URI, elementName (which can be null or empty // string, in which case the // mapping is added to the root element serializer.addNamespace(entry.getKey(), entry.getValue(), nsMapping.element); } } } if (rootName != null) { serializer.setRootName(rootName); } if (elementName != null) { serializer.setElementName(elementName); } if (arrayName != null) { serializer.setArrayName(arrayName); } if (expandableProperties != null && expandableProperties.size() != 0) { serializer.setExpandableProperties(expandableProperties.toArray(new String[expandableProperties.size()])); } if (skipWhitespace != null) { serializer.setSkipWhitespace(skipWhitespace); } if (trimSpaces != null) { serializer.setTrimSpaces(trimSpaces); } if (skipNamespaces != null) { serializer.setSkipNamespaces(skipNamespaces); } if (removeNamespacePrefixes != null) { serializer.setRemoveNamespacePrefixFromElements(removeNamespacePrefixes); } if (typeHints == TypeHintsEnum.YES || typeHints == TypeHintsEnum.WITH_PREFIX) { serializer.setTypeHintsEnabled(true); if (typeHints == TypeHintsEnum.WITH_PREFIX) { serializer.setTypeHintsCompatibility(false); } else { serializer.setTypeHintsCompatibility(true); } } else { serializer.setTypeHintsEnabled(false); serializer.setTypeHintsCompatibility(false); } } @Override protected void doStop() throws Exception { // noop } /** * Marshal from XML to JSON */ @Override public void marshal(Exchange exchange, Object graph, OutputStream stream) throws Exception { boolean streamTreatment = true; // try to process as an InputStream if it's not a String Object xml = graph instanceof String ? null : exchange.getContext().getTypeConverter().convertTo(InputStream.class, exchange, graph); // if conversion to InputStream was unfeasible, fall back to String if (xml == null) { xml = exchange.getContext().getTypeConverter().mandatoryConvertTo(String.class, exchange, graph); streamTreatment = false; } JSON json; // perform the marshaling to JSON if (streamTreatment) { json = serializer.readFromStream((InputStream) xml); } else { json = serializer.read((String) xml); } // don't return the default setting here String encoding = IOHelper.getCharsetName(exchange, false); if (encoding == null) { encoding = getEncoding(); } OutputStreamWriter osw = null; if (encoding != null) { osw = new OutputStreamWriter(stream, encoding); } else { osw = new OutputStreamWriter(stream); } json.write(osw); osw.flush(); if (contentTypeHeader) { if (exchange.hasOut()) { exchange.getOut().setHeader(Exchange.CONTENT_TYPE, "application/json"); } else { exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "application/json"); } } } /** * Convert from JSON to XML */ @Override public Object unmarshal(Exchange exchange, InputStream stream) throws Exception { Object inBody = exchange.getIn().getBody(); JSON toConvert; // if the incoming object is already a JSON object, process as-is, // otherwise parse it as a String if (inBody instanceof JSON) { toConvert = (JSON) inBody; } else { String jsonString = exchange.getContext().getTypeConverter().convertTo(String.class, exchange, inBody); toConvert = JSONSerializer.toJSON(jsonString); } Object answer = convertToXMLUsingEncoding(toConvert); if (contentTypeHeader) { if (exchange.hasOut()) { exchange.getOut().setHeader(Exchange.CONTENT_TYPE, "application/xml"); } else { exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "application/xml"); } } return answer; } private String convertToXMLUsingEncoding(JSON json) { if (encoding == null) { return serializer.write(json); } else { return serializer.write(json, encoding); } } // Properties // ------------------------------------------------------------------------- public XMLSerializer getSerializer() { return serializer; } public String getEncoding() { return encoding; } /** * Sets the encoding for the call to {@link XMLSerializer#write(JSON, String)} */ public void setEncoding(String encoding) { this.encoding = encoding; } public Boolean getForceTopLevelObject() { return forceTopLevelObject; } /** * See {@link XMLSerializer#setForceTopLevelObject(boolean)} */ public void setForceTopLevelObject(Boolean forceTopLevelObject) { this.forceTopLevelObject = forceTopLevelObject; } public Boolean getNamespaceLenient() { return namespaceLenient; } /** * See {@link XMLSerializer#setNamespaceLenient(boolean)} */ public void setNamespaceLenient(Boolean namespaceLenient) { this.namespaceLenient = namespaceLenient; } public List<NamespacesPerElementMapping> getNamespaceMappings() { return namespaceMappings; } /** * Sets associations between elements and namespace mappings. Will only be used when converting from JSON to XML. * For every association, the whenever a JSON element is found that matches {@link NamespacesPerElementMapping#element}, * the namespaces declarations specified by {@link NamespacesPerElementMapping#namespaces} will be output. * @see {@link XMLSerializer#addNamespace(String, String, String)} */ public void setNamespaceMappings(List<NamespacesPerElementMapping> namespaceMappings) { this.namespaceMappings = namespaceMappings; } public String getRootName() { return rootName; } /** * See {@link XMLSerializer#setRootName(String)} */ public void setRootName(String rootName) { this.rootName = rootName; } public Boolean getSkipWhitespace() { return skipWhitespace; } /** * See {@link XMLSerializer#setSkipWhitespace(boolean)} */ public void setSkipWhitespace(Boolean skipWhitespace) { this.skipWhitespace = skipWhitespace; } public Boolean getTrimSpaces() { return trimSpaces; } /** * See {@link XMLSerializer#setTrimSpaces(boolean)} */ public void setTrimSpaces(Boolean trimSpaces) { this.trimSpaces = trimSpaces; } public TypeHintsEnum getTypeHints() { return typeHints; } /** * See {@link XMLSerializer#setTypeHintsEnabled(boolean)} and {@link XMLSerializer#setTypeHintsCompatibility(boolean)} * @param typeHints a key in the {@link TypeHintsEnum} enumeration */ public void setTypeHints(String typeHints) { this.typeHints = TypeHintsEnum.valueOf(typeHints); } public Boolean getSkipNamespaces() { return skipNamespaces; } /** * See {@link XMLSerializer#setSkipNamespaces(boolean)} */ public void setSkipNamespaces(Boolean skipNamespaces) { this.skipNamespaces = skipNamespaces; } /** * See {@link XMLSerializer#setElementName(String)} */ public void setElementName(String elementName) { this.elementName = elementName; } public String getElementName() { return elementName; } /** * See {@link XMLSerializer#setArrayName(String)} */ public void setArrayName(String arrayName) { this.arrayName = arrayName; } public String getArrayName() { return arrayName; } public boolean isContentTypeHeader() { return contentTypeHeader; } /** * If enabled then XmlJson will set the Content-Type header to <tt>application/json</tt> when marshalling, * and <tt>application/xml</tt> when unmarshalling. */ public void setContentTypeHeader(boolean contentTypeHeader) { this.contentTypeHeader = contentTypeHeader; } /** * See {@link XMLSerializer#setExpandableProperties(String[])} */ public void setExpandableProperties(List<String> expandableProperties) { this.expandableProperties = expandableProperties; } public List<String> getExpandableProperties() { return expandableProperties; } /** * See {@link XMLSerializer#setRemoveNamespacePrefixFromElements(boolean)} */ public void setRemoveNamespacePrefixes(Boolean removeNamespacePrefixes) { this.removeNamespacePrefixes = removeNamespacePrefixes; } public Boolean getRemoveNamespacePrefixes() { return removeNamespacePrefixes; } /** * Encapsulates the information needed to bind namespace declarations to XML elements when performing JSON to XML conversions * Given the following JSON: { "root:": { "element": "value", "element2": "value2" }}, it will produce the following XML when "element" is * bound to prefix "ns1" and namespace URI "http://mynamespace.org": * <root><element xmlns:ns1="http://mynamespace.org">value</element><element2>value2</element2></root> * For convenience, the {@link NamespacesPerElementMapping#NamespacesPerElementMapping(String, String)} constructor allows to specify * multiple prefix-namespaceURI pairs in just one String line, the format being: |ns1|http://mynamespace.org|ns2|http://mynamespace2.org| * */ public static class NamespacesPerElementMapping { public String element; public Map<String, String> namespaces; public NamespacesPerElementMapping(String element, Map<String, String> namespaces) { this.element = element; this.namespaces = namespaces; } public NamespacesPerElementMapping(String element, String prefix, String uri) { this.element = element; this.namespaces = new HashMap<String, String>(); this.namespaces.put(prefix, uri); } public NamespacesPerElementMapping(String element, String pipeSeparatedMappings) { this.element = element; this.namespaces = new HashMap<String, String>(); String[] origTokens = pipeSeparatedMappings.split("\\|"); // drop the first token String[] tokens = Arrays.copyOfRange(origTokens, 1, origTokens.length); if (tokens.length % 2 != 0) { throw new IllegalArgumentException("Even number of prefix-namespace tokens is expected, number of tokens parsed: " + tokens.length); } int i = 0; // |ns1|http://test.org|ns2|http://test2.org| while (i < (tokens.length - 1)) { this.namespaces.put(tokens[i], tokens[++i]); i++; } } } }