/** * Copyright (c) 2005-2010, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. * * WSO2 Inc. 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.synapse.commons.json; import org.apache.axis2.transport.http.HTTPConstants; import org.apache.synapse.commons.staxon.core.json.JsonXMLConfig; import org.apache.synapse.commons.staxon.core.json.JsonXMLConfigBuilder; import org.apache.synapse.commons.staxon.core.json.JsonXMLInputFactory; import org.apache.synapse.commons.staxon.core.json.JsonXMLOutputFactory; import org.apache.axiom.om.OMAbstractFactory; import org.apache.axiom.om.OMAttribute; import org.apache.axiom.om.OMElement; import org.apache.axiom.om.OMNamespace; import org.apache.axiom.om.OMNode; import org.apache.axiom.om.OMText; import org.apache.axiom.om.impl.builder.StAXOMBuilder; import org.apache.axiom.om.impl.llom.OMSourcedElementImpl; import org.apache.axiom.soap.SOAPBody; import org.apache.axiom.soap.SOAPEnvelope; import org.apache.axis2.AxisFault; import org.apache.axis2.context.MessageContext; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.ByteArrayOutputStream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.synapse.commons.util.MiscellaneousUtil; import javax.xml.namespace.QName; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLEventWriter; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Reader; import java.util.Iterator; import java.util.Properties; public final class JsonUtil { private static Log logger = LogFactory.getLog(JsonUtil.class.getName()); private static final String ORG_APACHE_SYNAPSE_COMMONS_JSON_JSON_INPUT_STREAM = "org.apache.synapse.commons.json.JsonInputStream"; private static final String ORG_APACHE_SYNAPSE_COMMONS_JSON_IS_JSON_OBJECT = "org.apache.synapse.commons.json.JsonInputStream.IsJsonObject"; private static final QName JSON_OBJECT = new QName("jsonObject"); private static final QName JSON_ARRAY = new QName("jsonArray"); /** * If this property is set to <tt>true</tt> the input stream of the JSON payload will be reset * after writing to the output stream within the #writeAsJson method. */ public static final String PRESERVE_JSON_STREAM = "preserve.json.stream"; /// JSON/XML INPUT OUTPUT Formatting Configuration // TODO: Build thie configuration from a "json.properties" file. Add a debug log to dump() the config to the log. // TODO: Add another param to empty xml element to null or empty json string mapping <a/> -> "a":null or "a":"" // TODO: Build this configuration into a separate class. // TODO: Property to remove root element from XML output // TODO: Axis2 property/synapse static property add XML Namespace to the root element private static boolean preserverNamespacesForJson = false; private static final boolean processNCNames; private static final boolean jsonOutAutoPrimitive; private static final char jsonOutNamespaceSepChar; private static final boolean jsonOutEnableNsDeclarations; private static final String jsonoutcustomRegex; private static final boolean jsonoutAutoArray; private static final boolean jsonoutMultiplePI; private static final boolean xmloutAutoArray; private static final boolean xmloutMultiplePI; private static final String jsonoutCustomReplaceRegex; private static final String jsonoutCustomReplaceSequence; static { Properties properties = MiscellaneousUtil.loadProperties("synapse.properties"); if (properties == null) { preserverNamespacesForJson = processNCNames = jsonOutEnableNsDeclarations = false; jsonOutAutoPrimitive = true; jsonOutNamespaceSepChar = '_'; jsonoutcustomRegex=null; jsonoutCustomReplaceRegex = null; jsonoutCustomReplaceSequence = ""; jsonoutAutoArray = true; jsonoutMultiplePI = false; xmloutAutoArray = true; xmloutMultiplePI = false; } else { // Preserve the namespace declarations() in the JSON output in the XML -> JSON transformation. String process = properties.getProperty(Constants.SYNAPSE_COMMONS_JSON_PRESERVE_NAMESPACE, "false").trim(); preserverNamespacesForJson = Boolean.parseBoolean(process.toLowerCase()); // Build valid XML NCNames when building XML element names in the JSON -> XML transformation. process = properties.getProperty(Constants.SYNAPSE_COMMONS_JSON_BUILD_VALID_NC_NAMES, "false").trim(); processNCNames = Boolean.parseBoolean(process.toLowerCase()); // Enable primitive types in json out put in the XML -> JSON transformation. process = properties.getProperty(Constants.SYNAPSE_COMMONS_JSON_OUTPUT_AUTO_PRIMITIVE, "true").trim(); jsonOutAutoPrimitive = Boolean.parseBoolean(process.toLowerCase()); // The namespace prefix separate character in the JSON output of the XML -> JSON transformation process = properties.getProperty(Constants.SYNAPSE_COMMONS_JSON_OUTPUT_NAMESPACE_SEP_CHAR, "_").trim(); jsonOutNamespaceSepChar = process.charAt(0); // Add XML namespace declarations in the JSON output in the XML -> JSON transformation. process = properties.getProperty(Constants.SYNAPSE_COMMONS_JSON_OUTPUT_ENABLE_NS_DECLARATIONS, "false").trim(); jsonOutEnableNsDeclarations = Boolean.parseBoolean(process.toLowerCase()); jsonoutCustomReplaceRegex = properties .getProperty("synapse.commons.json.json.output.disableAutoPrimitive.customReplaceRegex", null); jsonoutCustomReplaceSequence = properties .getProperty("synapse.commons.json.json.output.disableAutoPrimitive.customReplaceSequence", ""); jsonoutcustomRegex = properties.getProperty (Constants.SYNAPSE_COMMONS_JSON_OUTPUT_DISABLE_AUTO_PRIMITIVE_REGEX, null); jsonoutAutoArray = Boolean.parseBoolean(properties.getProperty (Constants.SYNAPSE_COMMONS_JSON_OUTPUT_JSON_OUT_AUTO_ARRAY, "true")); jsonoutMultiplePI = Boolean.parseBoolean(properties.getProperty (Constants.SYNAPSE_COMMONS_JSON_OUTPUT_JSON_OUT_MULTIPLE_PI, "true")); xmloutAutoArray = Boolean.parseBoolean(properties.getProperty (Constants.SYNAPSE_COMMONS_JSON_OUTPUT_XML_OUT_AUTO_ARRAY, "true")); xmloutMultiplePI = Boolean.parseBoolean(properties.getProperty (Constants.SYNAPSE_COMMONS_JSON_OUTPUT_XML_OUT_MULTIPLE_PI, "false")); process = properties.getProperty (Constants.SYNAPSE_COMMONS_JSON_OUTPUT_EMPTY_XML_ELEM_TO_EMPTY_STR, "true").trim(); } } /** * Configuration used to produce XML that has processing instructions in it. */ private static final JsonXMLConfig xmlOutputConfig = new JsonXMLConfigBuilder() .multiplePI(true) .autoArray(true) .autoPrimitive(true) .namespaceDeclarations(false) .namespaceSeparator('\u0D89') .customRegex(jsonoutcustomRegex) .build(); /** * Configuration used to produce XML that has no processing instructions in it. */ private static final JsonXMLConfig xmlOutputConfigNoPIs = new JsonXMLConfigBuilder() .multiplePI(xmloutMultiplePI) .autoArray(xmloutAutoArray) .autoPrimitive(true) .namespaceDeclarations(false) .namespaceSeparator('\u0D89') .build(); /** * This configuration is used to format the JSON output produced by the JSON writer. */ private static final JsonXMLConfig jsonOutputConfig = new JsonXMLConfigBuilder() .multiplePI(jsonoutMultiplePI) .autoArray(jsonoutAutoArray) .autoPrimitive(jsonOutAutoPrimitive) .namespaceDeclarations(jsonOutEnableNsDeclarations) .namespaceSeparator(jsonOutNamespaceSepChar) .customReplaceRegex(jsonoutCustomReplaceRegex) .customReplaceSequence(jsonoutCustomReplaceSequence) .customRegex(jsonoutcustomRegex) .build(); /// End of JSON/XML INPUT OUTPUT Formatting Configuration. /** * Factory used to create JSON Readers */ private static final JsonXMLInputFactory jsonInputFactory = new JsonXMLInputFactory(xmlOutputConfig); /** * Factory used to create JSON Readers */ private static final JsonXMLInputFactory xmlInputFactoryNoPIs = new JsonXMLInputFactory(xmlOutputConfigNoPIs); /** * Factory used to create JSON Writers */ private static final JsonXMLOutputFactory jsonOutputFactory = new JsonXMLOutputFactory(jsonOutputConfig); /** * Factory used to create XML Readers */ private static final XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance(); /** * Converts the XML payload of a message context into its JSON representation and writes it to an output stream.<br/> * If no XML payload is found, the existing JSON payload will be copied to the output stream.<br/> * Note that this method removes all existing namespace declarations and namespace prefixes of the payload that is <br/> * present in the provided message context. * * @param messageContext Axis2 Message context that holds the JSON/XML payload. * @param out Output stream to which the payload(JSON) must be written. * @throws org.apache.axis2.AxisFault */ public static void writeAsJson(MessageContext messageContext, OutputStream out) throws AxisFault { if (messageContext == null || out == null) { return; } OMElement element = messageContext.getEnvelope().getBody().getFirstElement(); Object o = jsonStream(messageContext, false); InputStream json = null; if (o != null) { json = (InputStream) o; } o = messageContext.getProperty(org.apache.synapse.commons.json.Constants.JSON_STRING); String jsonStr = null; if (o instanceof String) { jsonStr = (String) o; } if (json != null) { // there is a JSON stream try { if (element instanceof OMSourcedElementImpl) { if (isAJsonPayloadElement(element)) { writeJsonStream(json, messageContext, out); } else { // Ignore the JSON stream writeAsJson(element, out); } } else if (element != null) { // element is not an OMSourcedElementImpl. But we ignore the JSON stream. writeAsJson(element, out); } else { // element == null. writeJsonStream(json, messageContext, out); } } catch (Exception e) { //Close the stream IOUtils.closeQuietly(json); throw new AxisFault("Could not write JSON stream.", e); } } else if (element != null) { // No JSON stream found. Convert the existing element to JSON. writeAsJson(element, out); } else if (jsonStr != null) { // No JSON stream or element found. See if there's a JSON_STRING set. try { out.write(jsonStr.getBytes()); } catch (IOException e) { logger.error("#writeAsJson. Could not write JSON string. MessageID: " + messageContext.getMessageID() + ". Error>> " + e.getLocalizedMessage()); throw new AxisFault("Could not write JSON string.", e); } } else { logger.error("#writeAsJson. Payload could not be written as JSON. MessageID: " + messageContext.getMessageID()); throw new AxisFault("Payload could not be written as JSON."); } } /** * Converts a JSON input stream to its XML representation. * * @param jsonStream JSON input stream * @param pIs Whether or not to add XML processing instructions to the output XML.<br/> * This property is useful when converting JSON payloads with array objects. * @return OMElement that is the XML representation of the input JSON data. */ public static OMElement toXml(InputStream jsonStream, boolean pIs) throws AxisFault { if (jsonStream == null) { logger.error("#toXml. Could not convert JSON Stream to XML. JSON input stream is null."); return null; } try { XMLStreamReader streamReader = getReader(jsonStream, pIs); return new StAXOMBuilder(streamReader).getDocumentElement(); } catch (XMLStreamException e) {//invalid JSON? logger.error("#toXml. Could not convert JSON Stream to XML. Cannot handle JSON input. Error>>> " + e.getLocalizedMessage()); throw new AxisFault("Could not convert JSON Stream to XML. Cannot handle JSON input.", e); } } /** * Returns an XMLStreamReader for a JSON input stream * * @param jsonStream InputStream of JSON * @param pIs Whether to add XML PIs to the XML output. This is used as an instruction to the returned XML Stream Reader. * @return An XMLStreamReader * @throws javax.xml.stream.XMLStreamException */ public static XMLStreamReader getReader(InputStream jsonStream, boolean pIs) throws XMLStreamException { if (jsonStream == null) { logger.error("#getReader. Could not create XMLStreamReader from [null] input stream."); return null; } return pIs ? getReader(jsonStream) : new JsonReaderDelegate(xmlInputFactoryNoPIs.createXMLStreamReader(jsonStream, org.apache.synapse.commons.staxon.core.json.stream.impl.Constants.SCANNER.SCANNER_1), processNCNames); } /** * This method is useful when you need to get an XML reader directly for the input JSON stream <br/> * without adding any additional object wrapper elements such as 'jsonObject' and 'jsonArray'. * * @param jsonStream InputStream of JSON * @return An XMLStreamReader * @throws javax.xml.stream.XMLStreamException */ public static XMLStreamReader getReader(InputStream jsonStream) throws XMLStreamException { if (jsonStream == null) { logger.error("#getReader. Could not create XMLStreamReader from [null] input stream."); return null; } return new JsonReaderDelegate(jsonInputFactory.createXMLStreamReader(jsonStream, org.apache.synapse.commons.staxon.core.json.stream.impl.Constants.SCANNER.SCANNER_1), processNCNames); } /** * Converts an XML element to its JSON representation and writes it to an output stream.<br/> * Note that this method removes all existing namespace declarations and namespace prefixes of the provided XML element<br/> * * @param element XML element of which JSON representation is expected. * @param outputStream Output Stream to write the JSON representation.<br/> * At the end of a successful conversion, its flush method will be called. * @throws AxisFault */ public static void writeAsJson(OMElement element, OutputStream outputStream) throws AxisFault { XMLEventReader xmlEventReader = null; XMLEventWriter jsonWriter = null; if (element == null) { logger.error("#writeAsJson. OMElement is null. Cannot convert to JSON."); throw new AxisFault("OMElement is null. Cannot convert to JSON."); } if (outputStream == null) { return; } transformElement(element, true); try { org.apache.commons.io.output.ByteArrayOutputStream xmlStream = new org.apache.commons.io.output.ByteArrayOutputStream(); element.serialize(xmlStream); xmlStream.flush(); xmlEventReader = xmlInputFactory.createXMLEventReader( new XmlReaderDelegate(xmlInputFactory.createXMLStreamReader( new ByteArrayInputStream(xmlStream.toByteArray()) ), processNCNames) ); jsonWriter = jsonOutputFactory.createXMLEventWriter(outputStream); jsonWriter.add(xmlEventReader); outputStream.flush(); } catch (XMLStreamException e) { logger.error("#writeAsJson. Could not convert OMElement to JSON. Invalid XML payload. Error>>> " + e.getLocalizedMessage()); throw new AxisFault("Could not convert OMElement to JSON. Invalid XML payload.", e); } catch (IOException e) { logger.error("#writeAsJson. Could not convert OMElement to JSON. Error>>> " + e.getLocalizedMessage()); throw new AxisFault("Could not convert OMElement to JSON.", e); } finally { if (xmlEventReader != null) { try { xmlEventReader.close(); } catch (XMLStreamException ex) { //ignore } } if (jsonWriter != null) { try { jsonWriter.close(); } catch (XMLStreamException ex) { //ignore } } } } /** * Converts an XML element to its JSON representation and returns it as a String. * * @param element OMElement to be converted to JSON. * @return A String builder instance that contains the converted JSON string. */ public static StringBuilder toJsonString(OMElement element) throws AxisFault { if (element == null) { return new StringBuilder("{}"); } org.apache.commons.io.output.ByteArrayOutputStream byteStream = new org.apache.commons.io.output.ByteArrayOutputStream(); writeAsJson(element.cloneOMElement(), byteStream); return new StringBuilder(new String(byteStream.toByteArray())); } /** * Removes XML namespace declarations, and namespace prefixes from an XML element. * * @param element Source XML element * @param processAttrbs Whether to remove the namespaces from attributes as well */ public static void transformElement(OMElement element, boolean processAttrbs) { if (element == null) { return; } removeIndentations(element); if (!preserverNamespacesForJson) { removeNamespaces(element, processAttrbs); } if (logger.isDebugEnabled()) { logger.debug("#transformElement. Transformed OMElement. Result: " + element.toString()); } } private static void removeIndentations(OMElement elem) { Iterator children = elem.getChildren(); while (children.hasNext()) { OMNode child = (OMNode) children.next(); if (child instanceof OMText) { if ("".equals(((OMText) child).getText().trim())) { children.remove(); } } else if (child instanceof OMElement) { removeIndentations((OMElement) child); } } } private static void removeNamespaces(OMElement element, boolean processAttrbs) { OMNamespace ns = element.getNamespace(); Iterator i = element.getAllDeclaredNamespaces(); while (i.hasNext()) { i.next(); i.remove(); } String prefix; if (ns != null) { prefix = "";//element.getNamespace().getPrefix(); element.setNamespace(element.getOMFactory().createOMNamespace("", prefix)); } Iterator children = element.getChildElements(); while (children.hasNext()) { removeNamespaces((OMElement) children.next(), processAttrbs); } if (!processAttrbs) { return; } Iterator attrbs = element.getAllAttributes(); while (attrbs.hasNext()) { OMAttribute attrb = (OMAttribute) attrbs.next(); prefix = "";//attrb.getQName().getPrefix(); attrb.setOMNamespace(attrb.getOMFactory().createOMNamespace("", prefix)); //element.removeAttribute(attrb); } } /** * Builds and returns a new JSON payload for a message context with a stream of JSON content. <br/> * This is the recommended way to build a JSON payload into an Axis2 message context.<br/> * A JSON payload built into a message context with this method can only be removed by calling * {@link #removeJsonPayload(org.apache.axis2.context.MessageContext)} method. This added to avoid code breaks * for previous implementations. For new implementations use getNewJsonPayload method. * * @param messageContext Axis2 Message context to which the new JSON payload must be saved (if instructed with <tt>addAsNewFirstChild</tt>). * @param inputStream JSON content as an input stream. * @param removeChildren Whether to remove existing child nodes of the existing payload of the message context * @param addAsNewFirstChild Whether to add the new JSON payload as the first child of this message context *after* removing the existing first child element.<br/> * Setting this argument to <tt>true</tt> will have no effect if the value of the argument <tt>removeChildren</tt> is already <tt>false</tt>. * @return Payload object that stores the input JSON content as a Sourced object (See {@link org.apache.axiom.om.OMSourcedElement}) that can build the XML tree for contained JSON payload on demand. */ @Deprecated public static OMElement newJsonPayload(MessageContext messageContext, InputStream inputStream, boolean removeChildren, boolean addAsNewFirstChild) { try { return getNewJsonPayload(messageContext, inputStream, removeChildren, addAsNewFirstChild); } catch (AxisFault axisFault) { logger.error("Payload provided is not an JSON payload."); return null; } } /** * Builds and returns a new JSON payload for a message context with a stream of JSON content. <br/> * This is the recommended way to build a JSON payload into an Axis2 message context.<br/> * A JSON payload built into a message context with this method can only be removed by calling * {@link #removeJsonPayload(org.apache.axis2.context.MessageContext)} method. * * @param messageContext Axis2 Message context to which the new JSON payload must be saved (if instructed with <tt>addAsNewFirstChild</tt>). * @param inputStream JSON content as an input stream. * @param removeChildren Whether to remove existing child nodes of the existing payload of the message context * @param addAsNewFirstChild Whether to add the new JSON payload as the first child of this message context *after* removing the existing first child element.<br/> * Setting this argument to <tt>true</tt> will have no effect if the value of the argument <tt>removeChildren</tt> is already <tt>false</tt>. * @return Payload object that stores the input JSON content as a Sourced object (See {@link org.apache.axiom.om.OMSourcedElement}) that can build the XML tree for contained JSON payload on demand. */ public static OMElement getNewJsonPayload(MessageContext messageContext, InputStream inputStream, boolean removeChildren, boolean addAsNewFirstChild) throws AxisFault { if (messageContext == null) { logger.error("#getNewJsonPayload. Could not save JSON stream. Message context is null."); return null; } boolean isObject = false; boolean isArray = false; if (inputStream != null) { InputStream json = toReadOnlyStream(inputStream); messageContext.setProperty(ORG_APACHE_SYNAPSE_COMMONS_JSON_JSON_INPUT_STREAM, json); // read ahead few characters to see if the stream is valid... boolean isEmptyPayload = true; boolean valid = false; try { // check for empty/all-whitespace streams int c = json.read(); while (c != -1 && c != '{' && c != '[') { if (c != 32) { isEmptyPayload = false; } c = json.read(); } if (c != -1) { valid = true; } if (c == '{') { isObject = true; isArray = false; } else if (c == '[') { isArray = true; isObject = false; } json.reset(); } catch (IOException e) { logger.error( "#getNewJsonPayload. Could not determine availability of the JSON input stream. MessageID: " + messageContext.getMessageID() + ". Error>>> " + e.getLocalizedMessage()); return null; } if (!valid) { if (isEmptyPayload) { //This is a empty payload so return null without doing further processing. if (logger.isDebugEnabled()) { logger.debug("#emptyJsonPayload found.MessageID: " + messageContext.getMessageID()); } logger.debug("#emptyJsonPayload found.MessageID: " + messageContext.getMessageID()); return null; } else { /* * This method required to introduce due the GET request failure with query parameter and * contenttype=application/json. Because of the logic implemented with '=' sign below, * it expects a valid JSON string as the query parameter value (string after '=' sign) for GET * requests with application/json content type. Therefore it fails for requests like * https://localhost:8243/services/customer?format=xml and throws axis fault. With this fix, * HTTP method is checked and avoid throwing axis2 fault for GET requests. * https://wso2.org/jira/browse/ESBJAVA-4270 */ if (isValidPayloadRequired(messageContext)) { logger.error( "#getNewJsonPayload. Could not save JSON payload. Invalid input stream found. MessageID: " + messageContext.getMessageID()); throw new AxisFault("Payload is not a JSON string."); } else { return null; } } } QName jsonElement = null; if (isObject) { jsonElement = JSON_OBJECT; messageContext.setProperty(ORG_APACHE_SYNAPSE_COMMONS_JSON_IS_JSON_OBJECT, true); } if (isArray) { jsonElement = JSON_ARRAY; messageContext.setProperty(ORG_APACHE_SYNAPSE_COMMONS_JSON_IS_JSON_OBJECT, false); } OMElement elem = new OMSourcedElementImpl(jsonElement, OMAbstractFactory.getOMFactory(), new JsonDataSource( (InputStream) messageContext.getProperty(ORG_APACHE_SYNAPSE_COMMONS_JSON_JSON_INPUT_STREAM))); if (!removeChildren) { if (logger.isTraceEnabled()) { logger.trace( "#getNewJsonPayload. Not removing child elements from exiting message. Returning result. MessageID: " + messageContext.getMessageID()); } return elem; } SOAPEnvelope e = messageContext.getEnvelope(); if (e != null) { SOAPBody b = e.getBody(); if (b != null) { removeIndentations(b); Iterator children = b.getChildren(); while (children.hasNext()) { Object o = children.next(); if (o instanceof OMNode) { //((OMNode) o).detach(); children.remove(); } } if (logger.isTraceEnabled()) { logger.trace("#getNewJsonPayload. Removed child elements from exiting message. MessageID: " + messageContext.getMessageID()); } if (addAsNewFirstChild) { b.addChild(elem); if (logger.isTraceEnabled()) { logger.trace( "#getNewJsonPayload. Added the new JSON sourced element as the first child. MessageID: " + messageContext.getMessageID()); } } } } return elem; } return null; } /** * Builds and returns a new JSON payload for a message context with a JSON string. This is deprecated and use * getNewJsonPayload for new implementations. * * @param messageContext Axis2 Message context to which the new JSON payload must be saved (if instructed with <tt>addAsNewFirstChild</tt>). * @param jsonString JSON content as a String. * @param removeChildren Whether to remove existing child nodes of the existing payload of the message context * @param addAsNewFirstChild Whether to add the new JSON payload as the first child of this message context *after* removing the existing first child element.<br/> * Setting this argument to <tt>true</tt> will have no effect if the value of the argument <tt>removeChildren</tt> is already <tt>false</tt>. * @return Payload object that stores the input JSON content as a Sourced object (See {@link org.apache.axiom.om.OMSourcedElement}) that facilitates on demand building of the XML tree. * @see #getNewJsonPayload(org.apache.axis2.context.MessageContext, java.io.InputStream, boolean, boolean) */ @Deprecated public static OMElement newJsonPayload(MessageContext messageContext, String jsonString, boolean removeChildren, boolean addAsNewFirstChild) { try { return getNewJsonPayload(messageContext, jsonString, removeChildren, addAsNewFirstChild); } catch (AxisFault axisFault) { logger.error("Payload provided is not an JSON payload."); return null; } } /** * Builds and returns a new JSON payload for a message context with a JSON string.<br/> * * @param messageContext Axis2 Message context to which the new JSON payload must be saved (if instructed with <tt>addAsNewFirstChild</tt>). * @param jsonString JSON content as a String. * @param removeChildren Whether to remove existing child nodes of the existing payload of the message context * @param addAsNewFirstChild Whether to add the new JSON payload as the first child of this message context *after* removing the existing first child element.<br/> * Setting this argument to <tt>true</tt> will have no effect if the value of the argument <tt>removeChildren</tt> is already <tt>false</tt>. * @return Payload object that stores the input JSON content as a Sourced object (See {@link org.apache.axiom.om.OMSourcedElement}) that facilitates on demand building of the XML tree. * @see #getNewJsonPayload(org.apache.axis2.context.MessageContext, java.io.InputStream, boolean, boolean) */ public static OMElement getNewJsonPayload(MessageContext messageContext, String jsonString, boolean removeChildren, boolean addAsNewFirstChild) throws AxisFault { if (jsonString == null || jsonString.isEmpty()) { jsonString = "{}"; } return getNewJsonPayload(messageContext, new ByteArrayInputStream(jsonString.getBytes()), removeChildren, addAsNewFirstChild); } /** * Builds and returns a new JSON payload for a message context with a byte array containing JSON.This method is * now deprecated and replaced by getNewJsonPayload method. * * @param messageContext Axis2 Message context to which the new JSON payload must be saved (if instructed with <tt>addAsNewFirstChild</tt>). * @param json JSON content as a byte array. * @param offset starting position of the JSON content in the provided array * @param length how many bytes to read starting from the offset provided. * @param removeChildren Whether to remove existing child nodes of the existing payload of the message context * @param addAsNewFirstChild Whether to add the new JSON payload as the first child of this message context *after* removing the existing first child element.<br/> * Setting this argument to <tt>true</tt> will have no effect if the value of the argument <tt>removeChildren</tt> is already <tt>false</tt>. * @return Payload object that stores the input JSON content as a Sourced object (See {@link org.apache.axiom.om.OMSourcedElement}) that facilitates on demand building of the XML tree. * @see #getNewJsonPayload(org.apache.axis2.context.MessageContext, java.io.InputStream, boolean, boolean) */ @Deprecated public static OMElement newJsonPayload(MessageContext messageContext, byte[] json, int offset, int length, boolean removeChildren, boolean addAsNewFirstChild) { try { return getNewJsonPayload(messageContext, json, offset, length, removeChildren, addAsNewFirstChild); } catch (AxisFault axisFault) { logger.error("Payload provided is not an JSON payload."); return null; } } /** * Builds and returns a new JSON payload for a message context with a byte array containing JSON.<br/> * * @param messageContext Axis2 Message context to which the new JSON payload must be saved (if instructed with <tt>addAsNewFirstChild</tt>). * @param json JSON content as a byte array. * @param offset starting position of the JSON content in the provided array * @param length how many bytes to read starting from the offset provided. * @param removeChildren Whether to remove existing child nodes of the existing payload of the message context * @param addAsNewFirstChild Whether to add the new JSON payload as the first child of this message context *after* removing the existing first child element.<br/> * Setting this argument to <tt>true</tt> will have no effect if the value of the argument <tt>removeChildren</tt> is already <tt>false</tt>. * @return Payload object that stores the input JSON content as a Sourced object (See {@link org.apache.axiom.om.OMSourcedElement}) that facilitates on demand building of the XML tree. * @see #getNewJsonPayload(org.apache.axis2.context.MessageContext, java.io.InputStream, boolean, boolean) */ public static OMElement getNewJsonPayload(MessageContext messageContext, byte[] json, int offset, int length, boolean removeChildren, boolean addAsNewFirstChild) throws AxisFault{ InputStream is; if (json == null || json.length < 2) { json = new byte[]{'{', '}'}; is = new ByteArrayInputStream(json); } else { is = new ByteArrayInputStream(json, offset, length); } return getNewJsonPayload(messageContext, is, removeChildren, addAsNewFirstChild); } /** * Removes the existing JSON payload of a message context if any.<br/> * This method can only remove a JSON payload that has been set with {@link #getNewJsonPayload(org.apache.axis2.context.MessageContext, java.io.InputStream, boolean, boolean)} * and its variants. * * @param messageContext Axis2 Message context from which the JSON stream must be removed. * @return <tt>true</tt> if the operation is successful. */ public static boolean removeJsonPayload(MessageContext messageContext) { messageContext.removeProperty(ORG_APACHE_SYNAPSE_COMMONS_JSON_JSON_INPUT_STREAM); messageContext.removeProperty(ORG_APACHE_SYNAPSE_COMMONS_JSON_IS_JSON_OBJECT); boolean removeChildren = true; if (!removeChildren) { // don't change this. if (logger.isTraceEnabled()) { logger.trace("#removeJsonPayload. Removed JSON stream. MessageID: " + messageContext.getMessageID()); } return true; } SOAPEnvelope e = messageContext.getEnvelope(); if (e != null) { SOAPBody b = e.getBody(); if (b != null) { removeIndentations(b); // cleans payload by removing unnecessary characters Iterator children = b.getChildren(); while (children.hasNext()) { Object o = children.next(); if (o instanceof OMNode) { //((OMNode) o).detach(); children.remove(); } } if (logger.isTraceEnabled()) { logger.trace("#removeJsonPayload. Removed JSON stream and child elements of payload. MessageID: " + messageContext.getMessageID()); } } } return true; } /** * Returns the JSON stream associated with the payload of this message context. * * @param messageContext Axis2 Message context * @param reset Whether to reset the input stream that contains this JSON payload so that next read will start from the beginning of this stream. * @return JSON input stream */ private static InputStream jsonStream(MessageContext messageContext, boolean reset) { if (messageContext == null) { return null; } Object o = messageContext.getProperty(ORG_APACHE_SYNAPSE_COMMONS_JSON_JSON_INPUT_STREAM); if (o instanceof InputStream) { InputStream is = (InputStream) o; if (reset) { if (is.markSupported()) { try { is.reset(); } catch (IOException e) { logger.error("#jsonStream. Could not reuse JSON Stream. Error>>>\n", e); return null; } } } return is; } return null; } /** * Returns the READ-ONLY input stream of the JSON payload contained in the provided message context. * * @param messageContext Axis2 Message context * @return {@link java.io.InputStream} of JSON payload contained in the message context. Null otherwise.<br/> * It is possible to read from this stream right away. This InputStream cannot be <tt>close</tt>d, <tt>mark</tt>ed, or <tt>skip</tt>ped. <br/> * If <tt>close()</tt> is invoked on this input stream, it will be reset to the beginning. */ public static InputStream getJsonPayload(MessageContext messageContext) { return hasAJsonPayload(messageContext) ? jsonStream(messageContext, true) : null; } /** * Returns a copy of the JSON stream contained in the provided Message Context. * * @param messageContext Axis2 Message context that contains a JSON payload. * @return {@link java.io.InputStream} */ private static InputStream copyOfJsonPayload(MessageContext messageContext, boolean closable) { if (messageContext == null) { logger.error("#copyOfJsonPayload. Cannot copy JSON stream from message context. [null]."); return null; } InputStream jsonStream = jsonStream(messageContext, true); if (jsonStream == null) { logger.error("#copyOfJsonPayload. Cannot copy JSON stream from message context. [null] stream."); return null; } org.apache.commons.io.output.ByteArrayOutputStream out = new org.apache.commons.io.output.ByteArrayOutputStream(); try { IOUtils.copy(jsonStream, out); out.flush(); return closable ? new ByteArrayInputStream(out.toByteArray()) : toReadOnlyStream(new ByteArrayInputStream(out.toByteArray())); } catch (IOException e) { logger.error("#copyOfJsonPayload. Could not copy the JSON stream from message context. Error>>> " + e.getLocalizedMessage()); } return null; } private static void writeJsonStream(InputStream json, MessageContext messageContext, OutputStream out) throws AxisFault { try { if (json.markSupported()) { json.reset(); } IOUtils.copy(json, out); // Write the JSON stream if (messageContext.getProperty(PRESERVE_JSON_STREAM) != null) { if (json.markSupported()) { json.reset(); } messageContext.removeProperty(PRESERVE_JSON_STREAM); } } catch (IOException e) { logger.error("#writeJsonStream. Could not write JSON stream. MessageID: " + messageContext.getMessageID() + ". Error>> " + e.getLocalizedMessage()); throw new AxisFault("Could not write JSON stream.", e); } } /** * Returns a reusable cached copy of the JSON stream contained in the provided Message Context. * * @param messageContext Axis2 Message context that contains a JSON payload. * @return {@link java.io.InputStream} */ private static InputStream cachedCopyOfJsonPayload(MessageContext messageContext) { if (messageContext == null) { logger.error("#cachedCopyOfJsonPayload. Cannot copy JSON stream from message context. [null]."); return null; } InputStream jsonStream = jsonStream(messageContext, true); if (jsonStream == null) { logger.error("#cachedCopyOfJsonPayload. Cannot copy JSON stream from message context. [null] stream."); return null; } String inputStreamCache = Long.toString(jsonStream.hashCode()); Object o = messageContext.getProperty(inputStreamCache); if (o instanceof InputStream) { InputStream inputStream = (InputStream) o; try { inputStream.reset(); if (logger.isDebugEnabled()) { logger.debug("#cachedCopyOfJsonPayload. Cache HIT"); } return inputStream; } catch (IOException e) { logger.warn("#cachedCopyOfJsonPayload. Could not reuse the cached input stream. Error>>> " + e.getLocalizedMessage()); } } org.apache.commons.io.output.ByteArrayOutputStream out = new org.apache.commons.io.output.ByteArrayOutputStream(); try { IOUtils.copy(jsonStream, out); out.flush(); InputStream inputStream = toReadOnlyStream(new ByteArrayInputStream(out.toByteArray())); messageContext.setProperty(inputStreamCache, inputStream); if (logger.isDebugEnabled()) { logger.debug("#cachedCopyOfJsonPayload. Cache MISS"); } return inputStream; } catch (IOException e) { logger.error("#cachedCopyOfJsonPayload. Could not copy the JSON stream from message context. Error>>> " + e.getLocalizedMessage()); } return null; } /** * Returns a new instance of a reader that can read from the JSON payload contained in the provided message context. * * @param messageContext Axis2 Message context * @return {@link java.io.Reader} if a JSON payload is found in the message context. null otherwise. */ public static Reader newJsonPayloadReader(MessageContext messageContext) { if (messageContext == null) { return null; } InputStream is = jsonStream(messageContext, true); if (is == null) { return null; } return new InputStreamReader(is); } /** * Returns the JSON payload contained in the provided message context as a byte array. * * @param messageContext Axis2 Message context * @return <tt>byte</tt> array containing the JSON payload. Empty array if no JSON payload found or invalid message context is passed in. */ public static byte[] jsonPayloadToByteArray(MessageContext messageContext) { if (messageContext == null) { return new byte[0]; } InputStream is = jsonStream(messageContext, true); if (is == null) { return new byte[0]; } try { return IOUtils.toByteArray(is); // IOUtils.toByteArray() doesn't close the input stream. } catch (IOException e) { logger.warn("#jsonPayloadToByteArray. Could not convert JSON stream to byte array."); return new byte[0]; } } /** * Returns the JSON payload contained in the provided message context as a String. * * @param messageContext Axis2 Message context * @return <tt>java.lang.String</tt> representation of the JSON payload. Returns "{}" if no JSON payload found or invalid message context is passed in. */ public static String jsonPayloadToString(MessageContext messageContext) { if (messageContext == null) { return "{}"; } InputStream is = jsonStream(messageContext, true); if (is == null) { return "{}"; } try { return IOUtils.toString(is); // IOUtils.toByteArray() doesn't close the input stream. } catch (IOException e) { logger.warn("#jsonPayloadToString. Could not convert JSON stream to String."); return "{}"; } } /** * Returns whether the provided XML element is an element that stores a sourced JSON payload. * * @param element XML element * @return <tt>true</tt> if the element is a sourced JSON object (ie. an <tt>OMSourcedElement</tt> instance containing a JSON stream). */ public static boolean hasAJsonPayload(OMElement element) { return (element instanceof OMSourcedElementImpl) && isAJsonPayloadElement(element); } /** * Returns true if the element passed in as the parameter is an element that contains a JSON stream. * * @param element XML element * @return <tt>true</tt> if the element has the local name of a sourced (ie. an <tt>OMSourcedElement</tt>) JSON object. */ public static boolean isAJsonPayloadElement(OMElement element) { return element != null && (JSON_OBJECT.getLocalPart().equals(element.getLocalName()) || JSON_ARRAY.getLocalPart().equals(element.getLocalName())); } /** * Returns true if the payload stored in the provided message context is used as a JSON streaming payload. * * @param messageContext Axis2 Message context * @return <tt>true</tt> if the message context contains a Streaming JSON payload. */ public static boolean hasAJsonPayload(MessageContext messageContext) { if (messageContext == null || messageContext.getEnvelope() == null) { return false; } SOAPBody b = messageContext.getEnvelope().getBody(); return b != null && jsonStream(messageContext, false) != null && hasAJsonPayload(b.getFirstElement()); } /** * Clones the JSON stream payload contained in the source message context, if any, to the target message context. * * @param sourceMc Where to get the payload * @param targetMc Where to clone and copy the payload * @return <tt>true</tt> if the cloning was successful. */ public static boolean cloneJsonPayload(MessageContext sourceMc, MessageContext targetMc) { if (!hasAJsonPayload(sourceMc)) { return false; } InputStream json = jsonStream(sourceMc, true); try { byte[] stream = IOUtils.toByteArray(json); getNewJsonPayload(targetMc, new ByteArrayInputStream(stream), true, true); } catch (IOException e) { logger.error("#cloneJsonPayload. Could not clone JSON stream. Error>>> " + e.getLocalizedMessage()); return false; } return true; } /** * Sets JSON media type 'application/json' as the message type to the current message context. * * @param messageContext Axis2 Message context */ public static void setContentType(MessageContext messageContext) { if (messageContext == null) { return; } messageContext.setProperty(org.apache.axis2.Constants.Configuration.MESSAGE_TYPE, "application/json"); } /** * Returns a read only, re-readable input stream for an input stream. <br/> * The returned input stream cannot be closed, marked, or skipped, but it can be reset to the beginning of the stream. * * @param inputStream Input stream to be wrapped * @return {@link java.io.InputStream} */ public static InputStream toReadOnlyStream(InputStream inputStream) { if (inputStream == null) { return null; } return new ReadOnlyBIS(inputStream); } /** * Returns an input stream that contains the JSON representation of an XML element. * * @param element XML element of which JSON representation is expected. * @return {@link java.io.InputStream} */ public static InputStream toJsonStream(OMElement element) { if (element == null) { logger.error("#toJsonStream. Could not create input stream from XML element [null]"); return null; } org.apache.commons.io.output.ByteArrayOutputStream bos = new org.apache.commons.io.output.ByteArrayOutputStream(); try { JsonUtil.writeAsJson(element.cloneOMElement(), bos); } catch (AxisFault axisFault) { logger.error("#toJsonStream. Could not create input stream from XML element [" + element.toString() + "]. Error>>> " + axisFault.getLocalizedMessage()); return null; } return new ByteArrayInputStream(bos.toByteArray()); } /** * Returns a reader that can read from the JSON payload contained in the provided message context as a JavaScript source.<br/> * The reader returns the '(' character at the beginning of the stream and marks the end with the ')' character.<br/> * The reader returned by this method can be directly used with the JavaScript {@link javax.script.ScriptEngine#eval(java.io.Reader)} method. * * @param messageContext Axis2 Message context * @return {@link java.io.InputStreamReader} */ public static Reader newJavaScriptSourceReader(MessageContext messageContext) { InputStream jsonStream = jsonStream(messageContext, true); if (jsonStream == null) { logger.error("#newJavaScriptSourceReader. Could not create a JavaScript source. Error>>> No JSON stream found."); return null; } ByteArrayOutputStream out = new ByteArrayOutputStream(); try { out.write('('); IOUtils.copy(jsonStream, out); out.write(')'); out.flush(); } catch (IOException e) { logger.error("#newJavaScriptSourceReader. Could not create a JavaScript source. Error>>> " + e.getLocalizedMessage()); return null; } return new InputStreamReader(new ByteArrayInputStream(out.toByteArray())); } /** * Returns <tt>true</tt> if the message context contains a JSON payload that is a JSON Object. See {@link #hasAJsonArray(MessageContext)}<br/> * Example : {"a":1, "b":2} * * @param messageContext request message context * @return */ public static boolean hasAJsonObject(MessageContext messageContext) { return hasAJsonPayload(messageContext) && _hasAJsonObject(messageContext); } /** * Returns <tt>true</tt> if the message context contains a JSON payload that is a JSON Array. See {@link #hasAJsonObject(MessageContext)}<br/> * Example: [{"a":1}, 2, null] * * @param messageContext request message context * @return */ public static boolean hasAJsonArray(MessageContext messageContext) { return hasAJsonPayload(messageContext) && !_hasAJsonObject(messageContext); } private static boolean _hasAJsonObject(MessageContext messageContext) { Object isObject = messageContext.getProperty(ORG_APACHE_SYNAPSE_COMMONS_JSON_IS_JSON_OBJECT); return isObject != null && ((Boolean) isObject); } /** * An Un-closable, Read-Only, Reusable, BufferedInputStream */ private static class ReadOnlyBIS extends BufferedInputStream { private static final String LOG_STREAM = "org.apache.synapse.commons.json.JsonReadOnlyStream"; private static final Log logger = LogFactory.getLog(LOG_STREAM); public ReadOnlyBIS(InputStream inputStream) { super(inputStream); super.mark(Integer.MAX_VALUE); if (logger.isDebugEnabled()) { logger.debug("<init>"); } } @Override public void close() throws IOException { super.reset(); //super.mark(Integer.MAX_VALUE); if (logger.isDebugEnabled()) { logger.debug("#close"); } } @Override public void mark(int readlimit) { if (logger.isDebugEnabled()) { logger.debug("#mark"); } } @Override public boolean markSupported() { return true; //but we don't mark. } @Override public long skip(long n) { if (logger.isDebugEnabled()) { logger.debug("#skip"); } return 0; } } /** * Check whether the request HTTP method is required valid payload * * @param msgCtx Message Context of incoming request * @return true if payload required, false otherwise */ private static boolean isValidPayloadRequired(MessageContext msgCtx) { boolean isRequired = true; if (HTTPConstants.HEADER_GET.equals(msgCtx.getProperty(HTTPConstants.HTTP_METHOD)) || HTTPConstants .HEADER_DELETE.equals(msgCtx.getProperty(HTTPConstants.HTTP_METHOD))) { isRequired = false; } return isRequired; } }