/* * Copyright (c) 2016, 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.wso2.carbon.mediator.datamapper; import org.apache.axiom.om.OMElement; import org.apache.axiom.om.OMException; import org.apache.axiom.om.impl.llom.OMTextImpl; import org.apache.axiom.om.util.AXIOMUtil; import org.apache.axiom.soap.SOAP11Constants; import org.apache.axiom.soap.SOAP12Constants; import org.apache.axiom.soap.SOAPEnvelope; import org.apache.axis2.AxisFault; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.synapse.ManagedLifecycle; import org.apache.synapse.MessageContext; import org.apache.synapse.SynapseConstants; import org.apache.synapse.SynapseException; import org.apache.synapse.SynapseLog; import org.apache.synapse.commons.json.JsonUtil; import org.apache.synapse.config.SynapsePropertiesLoader; import org.apache.synapse.core.SynapseEnvironment; import org.apache.synapse.core.axis2.Axis2MessageContext; import org.apache.synapse.mediators.AbstractMediator; import org.apache.synapse.mediators.Value; import org.apache.synapse.mediators.template.TemplateContext; import org.apache.synapse.util.AXIOMUtils; import org.wso2.carbon.mediator.datamapper.engine.core.exceptions.JSException; import org.wso2.carbon.mediator.datamapper.engine.core.exceptions.ReaderException; import org.wso2.carbon.mediator.datamapper.engine.core.exceptions.SchemaException; import org.wso2.carbon.mediator.datamapper.engine.core.exceptions.WriterException; import org.wso2.carbon.mediator.datamapper.engine.core.mapper.MappingHandler; import org.wso2.carbon.mediator.datamapper.engine.core.mapper.MappingResource; import org.wso2.carbon.mediator.datamapper.engine.utils.InputOutputDataType; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Stack; import javax.xml.namespace.QName; import javax.xml.stream.XMLStreamException; import static org.wso2.carbon.mediator.datamapper.config.xml.DataMapperMediatorConstants.AXIS2_CLIENT_CONTEXT; import static org.wso2.carbon.mediator.datamapper.config.xml.DataMapperMediatorConstants.AXIS2_CONTEXT; import static org.wso2.carbon.mediator.datamapper.config.xml.DataMapperMediatorConstants.DEFAULT_CONTEXT; import static org.wso2.carbon.mediator.datamapper.config.xml.DataMapperMediatorConstants.EMPTY_STRING; import static org.wso2.carbon.mediator.datamapper.config.xml.DataMapperMediatorConstants.FUNCTION_CONTEXT; import static org.wso2.carbon.mediator.datamapper.config.xml.DataMapperMediatorConstants.OPERATIONS_CONTEXT; import static org.wso2.carbon.mediator.datamapper.config.xml.DataMapperMediatorConstants.SYNAPSE_CONTEXT; import static org.wso2.carbon.mediator.datamapper.config.xml.DataMapperMediatorConstants.TRANSPORT_CONTEXT; import static org.wso2.carbon.mediator.datamapper.config.xml.DataMapperMediatorConstants.TRANSPORT_HEADERS; import static org.wso2.carbon.mediator.datamapper.engine.utils.DataMapperEngineConstants.ORG_APACHE_SYNAPSE_DATAMAPPER_EXECUTOR_POOL_SIZE; /** * By using the input schema, output schema and mapping configuration, * DataMapperMediator generates the output required by the next mediator for the * input received by the previous mediator. */ public class DataMapperMediator extends AbstractMediator implements ManagedLifecycle { private static final Log log = LogFactory.getLog(DataMapperMediator.class); private static final String cSVToXMLOpeningTag = "<text xmlns=\"http://ws.apache.org/commons/ns/payload\">"; private static final String cSVToXMLClosingTag = "</text>"; private static final int INDEX_OF_CONTEXT = 0; private static final int INDEX_OF_NAME = 1; private Value mappingConfigurationKey = null; private Value inputSchemaKey = null; private Value outputSchemaKey = null; private String inputType = null; private String outputType = null; private MappingResource mappingResource = null; /** * Returns registry resources as input streams to create the MappingResourceLoader object * * @param synCtx Message context * @param key location in the registry * @return mapping configuration, inputSchema and outputSchema as inputStreams */ private static InputStream getRegistryResource(MessageContext synCtx, String key) { InputStream inputStream = null; Object entry = synCtx.getEntry(key); if (entry instanceof OMTextImpl) { if (log.isDebugEnabled()) { log.debug("Retrieving the key :" + key); } OMTextImpl text = (OMTextImpl) entry; String content = text.getText(); inputStream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); } return inputStream; } /** * Gets the key which is used to pick the mapping configuration from the * registry * * @return the key which is used to pick the mapping configuration from the * registry */ public Value getMappingConfigurationKey() { return mappingConfigurationKey; } /** * Sets the registry key in order to pick the mapping configuration * * @param dataMapperconfigKey registry key for the mapping configuration */ public void setMappingConfigurationKey(Value dataMapperconfigKey) { this.mappingConfigurationKey = dataMapperconfigKey; } /** * Gets the key which is used to pick the input schema from the * registry * * @return the key which is used to pick the input schema from the * registry */ public Value getInputSchemaKey() { return inputSchemaKey; } /** * Sets the registry key in order to pick the input schema * * @param dataMapperInSchemaKey registry key for the input schema */ public void setInputSchemaKey(Value dataMapperInSchemaKey) { this.inputSchemaKey = dataMapperInSchemaKey; } /** * Gets the key which is used to pick the output schema from the * registry * * @return the key which is used to pick the output schema from the * registry */ public Value getOutputSchemaKey() { return outputSchemaKey; } /** * Sets the registry key in order to pick the output schema * * @param dataMapperOutSchemaKey registry key for the output schema */ public void setOutputSchemaKey(Value dataMapperOutSchemaKey) { this.outputSchemaKey = dataMapperOutSchemaKey; } /** * Gets the input data type * * @return the input data type */ public String getInputType() { if (inputType != null) { return inputType; } else { log.warn("Input data type not found. Set to default value : " + InputOutputDataType.XML); return InputOutputDataType.XML.toString(); } } /** * Sets the input data type * * @param type the input data type */ public void setInputType(String type) { this.inputType = type; } /** * Gets the output data type * * @return the output data type */ public String getOutputType() { if (outputType != null) { return outputType; } else { log.warn("Output data type not found. Set to default value : " + InputOutputDataType.XML); return InputOutputDataType.XML.toString(); } } /** * Sets the output data type * * @param type the output data type */ public void setOutputType(String type) { this.outputType = type; } /** * Get the values from the message context to do the data mapping * * @param synCtx current message for the mediation * @return true if mediation happened successfully else false. */ @Override public boolean mediate(MessageContext synCtx) { SynapseLog synLog = getLog(synCtx); if (synCtx.getEnvironment().isDebuggerEnabled()) { if (super.divertMediationRoute(synCtx)) { return true; } } if (synLog.isTraceOrDebugEnabled()) { synLog.traceOrDebug("Start : DataMapper mediator"); if (synLog.isTraceTraceEnabled()) { synLog.traceTrace("Message :" + synCtx.getEnvelope()); } } if (mappingResource == null) { String configKey = mappingConfigurationKey.evaluateValue(synCtx); String inSchemaKey = inputSchemaKey.evaluateValue(synCtx); String outSchemaKey = outputSchemaKey.evaluateValue(synCtx); if (!(StringUtils.isNotEmpty(configKey) && StringUtils.isNotEmpty(inSchemaKey) && StringUtils.isNotEmpty(outSchemaKey))) { handleException("DataMapper mediator : Invalid configurations", synCtx); } else { // mapping resources needed to get the final output try { mappingResource = getMappingResource(synCtx, configKey, inSchemaKey, outSchemaKey); } catch (IOException e) { handleException("DataMapper mediator mapping resource generation failed", e, synCtx); } } } // Does message conversion and gives the final result transform(synCtx, getInputType(), getOutputType()); //setting output type in the axis2 message context switch (getOutputType()) { case "JSON": ((Axis2MessageContext) synCtx).getAxis2MessageContext().setProperty("messageType", "application/json"); ((Axis2MessageContext) synCtx).getAxis2MessageContext().setProperty("ContentType", "application/json"); break; case "XML": ((Axis2MessageContext) synCtx).getAxis2MessageContext().setProperty("messageType", "application/xml"); ((Axis2MessageContext) synCtx).getAxis2MessageContext().setProperty("ContentType", "application/xml"); break; case "CSV": ((Axis2MessageContext) synCtx).getAxis2MessageContext().setProperty("messageType", "text/xml"); ((Axis2MessageContext) synCtx).getAxis2MessageContext().setProperty("ContentType", "text/xml"); break; default: throw new SynapseException("Unsupported output data type found : " + getOutputType()); } if (synLog.isTraceOrDebugEnabled()) { synLog.traceOrDebug("End : DataMapper mediator"); if (synLog.isTraceTraceEnabled()) { synLog.traceTrace("Message : " + synCtx.getEnvelope()); } } return true; } /** * Does message conversion and gives the output message as the final result * * @param synCtx the message synCtx * @param configKey registry location of the mapping configuration * @param inSchemaKey registry location of the input schema */ private void transform(MessageContext synCtx, String configKey, String inSchemaKey) { try { String outputResult = null; Map<String, Map<String, Object>> propertiesMap; String dmExecutorPoolSize = SynapsePropertiesLoader .getPropertyValue(ORG_APACHE_SYNAPSE_DATAMAPPER_EXECUTOR_POOL_SIZE, null); MappingHandler mappingHandler = new MappingHandler(mappingResource, inputType, outputType, dmExecutorPoolSize); propertiesMap = getPropertiesMap(mappingResource.getPropertiesList(), synCtx); /* execute mapping on the input stream */ outputResult = mappingHandler .doMap(getInputStream(synCtx, inputType, mappingResource.getInputSchema().getName()), propertiesMap); if (InputOutputDataType.CSV.toString().equals(outputType)) { outputResult = cSVToXMLOpeningTag + outputResult + cSVToXMLClosingTag; } if (InputOutputDataType.XML.toString().equals(outputType) || InputOutputDataType.CSV.toString() .equals(outputType)) { OMElement outputMessage = AXIOMUtil.stringToOM(outputResult); if (outputMessage != null) { if (log.isDebugEnabled()) { log.debug("Output message received "); } // Use to create the SOAP message QName resultQName = outputMessage.getQName(); if (resultQName.getLocalPart().equals("Envelope") && ( resultQName.getNamespaceURI().equals(SOAP11Constants.SOAP_ENVELOPE_NAMESPACE_URI) || resultQName.getNamespaceURI() .equals(SOAP12Constants.SOAP_ENVELOPE_NAMESPACE_URI))) { SOAPEnvelope soapEnvelope = AXIOMUtils.getSOAPEnvFromOM(outputMessage); if (soapEnvelope != null) { try { if (log.isDebugEnabled()) { log.debug("Valid Envelope"); } synCtx.setEnvelope(soapEnvelope); } catch (AxisFault axisFault) { handleException("Invalid Envelope", axisFault, synCtx); } } } else { synCtx.getEnvelope().getBody().getFirstElement().detach(); synCtx.getEnvelope().getBody().addChild(outputMessage); } } else { synCtx.getEnvelope().getBody().getFirstElement().detach(); } } else if (InputOutputDataType.JSON.toString().equals(outputType)) { org.apache.axis2.context.MessageContext axis2MessageContext = ((Axis2MessageContext) synCtx) .getAxis2MessageContext(); JsonUtil.newJsonPayload(axis2MessageContext, outputResult, true, true); } } catch (ReaderException | InterruptedException | XMLStreamException | SchemaException | IOException | JSException | WriterException e) { handleException("DataMapper mediator : mapping failed", e, synCtx); } } private InputStream getInputStream(MessageContext context, String inputType, String inputStartElement) { InputStream inputStream = null; try { switch (InputOutputDataType.fromString(inputType)) { case XML: case CSV: if ("soapenv:Envelope".equals(inputStartElement)) { inputStream = new ByteArrayInputStream( context.getEnvelope().toString().getBytes(StandardCharsets.UTF_8)); } else { inputStream = new ByteArrayInputStream(context.getEnvelope().getBody().getFirstElement().toString() .getBytes(StandardCharsets.UTF_8)); } break; case JSON: org.apache.axis2.context.MessageContext a2mc = ((Axis2MessageContext) context).getAxis2MessageContext(); if (JsonUtil.hasAJsonPayload(a2mc)) { inputStream = JsonUtil.getJsonPayload(a2mc); } break; default: inputStream = new ByteArrayInputStream( context.getEnvelope().toString().getBytes(StandardCharsets.UTF_8)); } } catch (OMException e) { handleException("Unable to read input message in Data Mapper mediator reason : " + e.getMessage(), e, context); } return inputStream; } /** * State that DataMapperMediator interacts with the message context * * @return true if the DataMapperMediator is intending to interact with the * message context */ @Override public boolean isContentAware() { return true; } @Override public boolean isContentAltering() { return true; } @Override public void init(SynapseEnvironment se) { } /** * destroy the generated unique ID for the DataMapperMediator instance */ @Override public void destroy() { } /** * When Data mapper mediator has been invoked initially, this creates a new mapping resource * loader * * @param synCtx message context * @param configKey the location of the mapping configuration * @param inSchemaKey the location of the input schema * @param outSchemaKey the location of the output schema * @return the MappingResourceLoader object * @throws IOException */ private MappingResource getMappingResource(MessageContext synCtx, String configKey, String inSchemaKey, String outSchemaKey) throws IOException { InputStream configFileInputStream = getRegistryResource(synCtx, configKey); InputStream inputSchemaStream = getRegistryResource(synCtx, inSchemaKey); InputStream outputSchemaStream = getRegistryResource(synCtx, outSchemaKey); if (configFileInputStream == null) { handleException("DataMapper mediator : mapping configuration is null", synCtx); } if (inputSchemaStream == null) { handleException("DataMapper mediator : input schema is null", synCtx); } if (outputSchemaStream == null) { handleException("DataMapper mediator : output schema is null", synCtx); } try { // Creates a new mappingResourceLoader return new MappingResource(inputSchemaStream, outputSchemaStream, configFileInputStream, outputType); } catch (SchemaException | JSException e) { handleException(e.getMessage(), synCtx); } return null; } /** * Retrieve property values and insert into a map * * @param propertiesNamesList Required properties * @param synCtx Message context * @return Map filed with property name and the value */ private Map<String, Map<String, Object>> getPropertiesMap(List<String> propertiesNamesList, MessageContext synCtx) { Map<String, Map<String, Object>> propertiesMap = new HashMap<>(); String[] contextAndName; Object value; org.apache.axis2.context.MessageContext axis2MsgCtx = ((Axis2MessageContext) synCtx).getAxis2MessageContext(); HashMap functionProperties = new HashMap(); Stack<TemplateContext> templeteContextStack = ((Stack) synCtx .getProperty(SynapseConstants.SYNAPSE__FUNCTION__STACK)); if (templeteContextStack != null && !templeteContextStack.isEmpty()) { TemplateContext templateContext = templeteContextStack.peek(); functionProperties.putAll(templateContext.getMappedValues()); } for (String propertyName : propertiesNamesList) { contextAndName = propertyName.split("\\['|'\\]"); switch (contextAndName[INDEX_OF_CONTEXT].toUpperCase()) { case DEFAULT_CONTEXT: case SYNAPSE_CONTEXT: value = synCtx.getProperty(contextAndName[INDEX_OF_NAME]); break; case TRANSPORT_CONTEXT: value = ((Map) axis2MsgCtx.getProperty(TRANSPORT_HEADERS)).get(contextAndName[INDEX_OF_NAME]); break; case AXIS2_CONTEXT: value = axis2MsgCtx.getProperty(contextAndName[INDEX_OF_NAME]); break; case AXIS2_CLIENT_CONTEXT: value = axis2MsgCtx.getOptions().getProperty(contextAndName[INDEX_OF_NAME]); break; case OPERATIONS_CONTEXT: value = axis2MsgCtx.getOperationContext().getProperty(contextAndName[INDEX_OF_NAME]); break; case FUNCTION_CONTEXT: value = functionProperties.get(contextAndName[INDEX_OF_NAME]); break; default: log.warn(contextAndName[INDEX_OF_CONTEXT] + " scope is not found. Setting it to an empty value."); value = EMPTY_STRING; } if (value == null) { log.warn(propertyName + "not found. Setting it to an empty value."); value = EMPTY_STRING; } insertToMap(propertiesMap, contextAndName, value); } return propertiesMap; } /** * Insert a given value to the properties map * * @param propertiesMap Reference to the properties map * @param contextAndName Context and the name of the property * @param value Current value of the property */ private void insertToMap(Map<String, Map<String, Object>> propertiesMap, String[] contextAndName, Object value) { Map<String, Object> insideMap = propertiesMap.get(contextAndName[INDEX_OF_CONTEXT]); if (insideMap == null) { insideMap = new HashMap(); propertiesMap.put(contextAndName[INDEX_OF_CONTEXT], insideMap); } insideMap.put(contextAndName[INDEX_OF_NAME], value); } }