/*
* Copyright (c) 2017, 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.siddhi.extension.output.mapper.xml;
import org.apache.log4j.Logger;
import org.wso2.siddhi.annotation.Example;
import org.wso2.siddhi.annotation.Extension;
import org.wso2.siddhi.annotation.Parameter;
import org.wso2.siddhi.annotation.util.DataType;
import org.wso2.siddhi.core.event.Event;
import org.wso2.siddhi.core.exception.ConnectionUnavailableException;
import org.wso2.siddhi.core.exception.ExecutionPlanCreationException;
import org.wso2.siddhi.core.stream.output.sink.SinkListener;
import org.wso2.siddhi.core.stream.output.sink.SinkMapper;
import org.wso2.siddhi.core.util.config.ConfigReader;
import org.wso2.siddhi.core.util.transport.DynamicOptions;
import org.wso2.siddhi.core.util.transport.Option;
import org.wso2.siddhi.core.util.transport.OptionHolder;
import org.wso2.siddhi.core.util.transport.TemplateBuilder;
import org.wso2.siddhi.query.api.definition.StreamDefinition;
import org.xml.sax.SAXException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
/**
* Mapper class to convert a Siddhi message to a XML message. User can provide a XML template or else we will be
* using a predefined XML message format. In case of null elements xsi:nil="true" will be used. In some instances
* coding best practices have been compensated for performance concerns.
*/
@Extension(
name = "xml",
namespace = "sinkMapper",
description = "Event to XML output mapper. Transports which publish XML messages can utilize this extension"
+ "to convert the Siddhi event to XML message. Users can either send a pre-defined XML "
+ "format or a custom XML message.",
parameters = {
@Parameter(name = "validate.xml",
description = "This property will enable XML validation for generated XML message. By "
+ "default value of the property will be false. When enabled DAS will validate the "
+ "generated XML message and drop the message if it does not adhere to proper XML "
+ "standards. ",
type = {DataType.BOOL}),
@Parameter(name = "enclosing.element",
description =
"Used to specify the enclosing element in case of sending multiple events in same "
+ "XML message. WSO2 DAS will treat the child element of given enclosing "
+ "element as events"
+ " and execute xpath expressions on child elements. If enclosing.element "
+ "is not provided "
+ "multiple event scenario is disregarded and xpaths will be evaluated "
+ "with respect to "
+ "root element.",
type = {DataType.STRING})
},
examples = {
@Example(
syntax = "@sink(type='inMemory', topic='stock', @map(type='xml'))\n"
+ "define stream FooStream (symbol string, price float, volume long);\n",
description = "Above configuration will do a default XML input mapping which will "
+ "generate below "
+ "output"
+ "<events>\n"
+ " <event>\n"
+ " <symbol>WSO2</symbol>\n"
+ " <price>55.6</price>\n"
+ " <volume>100</volume>\n"
+ " </event>\n"
+ "</events>\n"),
@Example(
syntax = "@sink(type='inMemory', topic='{{symbol}}', @map(type='xml', enclosing"
+ ".element='<portfolio>', validate.xml='true', @payload( "
+ "\"<StockData><Symbol>{{symbol}}</Symbol><Price>{{price}}</Price></StockData>\")))\n"
+ "define stream BarStream (symbol string, price float, volume long);",
description = "Above configuration will perform a custom XML mapping which will "
+ "produce below "
+ "output MXL message"
+ "<portfolio>\n"
+ " <StockData>\n"
+ " <Symbol>WSO2</Symbol>\n"
+ " <Price>55.6</Price>\n"
+ " </StockData>\n"
+ "</portfolio>")
}
)
public class XMLSinkMapper extends SinkMapper {
private static final Logger log = Logger.getLogger(XMLSinkMapper.class);
private static final String EVENTS_PARENT_OPENING_TAG = "<events>";
private static final String EVENTS_PARENT_CLOSING_TAG = "</events>";
private static final String EVENT_PARENT_OPENING_TAG = "<event>";
private static final String EVENT_PARENT_CLOSING_TAG = "</event>";
private static final String OPTION_ENCLOSING_ELEMENT = "enclosing.element";
private static final String OPTION_VALIDATE_XML = "validate.xml";
private static final String NS_XSI_NIL_ENABLE = " xsi:nil=\"true\"/";
private StreamDefinition streamDefinition;
private String enclosingElement = null;
private boolean xmlValidationEnabled = false;
private DocumentBuilder builder;
private String endingElement = "";
@Override
public String[] getSupportedDynamicOptions() {
return new String[0];
}
/**
* Initialize the mapper and the mapping configurations.
* @param streamDefinition The stream definition
* @param optionHolder Option holder containing static and dynamic options
* @param payloadTemplateBuilder Unmapped payload for reference
* @param mapperConfigReader
*/
@Override
public void init(StreamDefinition streamDefinition, OptionHolder optionHolder,
TemplateBuilder payloadTemplateBuilder, ConfigReader mapperConfigReader) {
this.streamDefinition = streamDefinition;
enclosingElement = optionHolder.getOrCreateOption(OPTION_ENCLOSING_ELEMENT, null).getValue();
if (enclosingElement != null) {
endingElement = getClosingElement(enclosingElement);
}
Option validateXmlOption = optionHolder.getOrCreateOption(OPTION_VALIDATE_XML, null);
if (validateXmlOption != null) {
xmlValidationEnabled = Boolean.parseBoolean(validateXmlOption.getValue());
if (xmlValidationEnabled) {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(false);
//We only need to check the parsability of xml we are generating. Hence setting validating to false.
factory.setNamespaceAware(true);
try {
builder = factory.newDocumentBuilder();
} catch (ParserConfigurationException e) {
throw new ExecutionPlanCreationException("Error occurred when initializing XML validator", e);
}
}
}
}
@Override
public void mapAndSend(Event event, OptionHolder optionHolder, TemplateBuilder payloadTemplateBuilder,
SinkListener sinkListener, DynamicOptions dynamicOptions)
throws ConnectionUnavailableException {
StringBuilder sb = new StringBuilder();
if (payloadTemplateBuilder != null) { //custom mapping
if (enclosingElement != null) {
sb.append(enclosingElement);
}
sb.append(payloadTemplateBuilder.build(event));
sb.append(endingElement);
if (xmlValidationEnabled) {
try {
builder.parse(new ByteArrayInputStream(sb.toString().getBytes(Charset.forName("UTF-8"))));
} catch (SAXException e) {
log.error("Parse error occurred when validating output XML event. " +
"Reason: " + e.getMessage() + "Dropping event: " + sb.toString());
return;
} catch (IOException e) {
log.error("IO error occurred when validating output XML event. " +
"Reason: " + e.getMessage() + "Dropping event: " + sb.toString());
return;
} finally {
builder.reset();
}
}
} else {
sb.append(constructDefaultMapping(event));
if (enclosingElement != null) {
sb.insert(0, enclosingElement);
sb.append(endingElement);
} else {
sb.insert(0, EVENTS_PARENT_OPENING_TAG);
sb.append(EVENTS_PARENT_CLOSING_TAG);
}
}
sinkListener.publish(sb.toString(), dynamicOptions);
}
/**
* Map and publish the given {@link Event} array
*
* @param events Event object array
* @param optionHolder option holder containing static and dynamic options
* @param payloadTemplateBuilder Unmapped payload for reference
* @param sinkListener output transport callback
*/
@Override
public void mapAndSend(Event[] events, OptionHolder optionHolder, TemplateBuilder payloadTemplateBuilder,
SinkListener sinkListener, DynamicOptions dynamicOptions)
throws ConnectionUnavailableException {
if (events.length < 1) { //todo valid case?
return;
}
StringBuilder sb = new StringBuilder();
if (payloadTemplateBuilder != null) { //custom mapping
if (enclosingElement != null) {
sb.append(enclosingElement);
}
for (Event event : events) {
sb.append(payloadTemplateBuilder.build(event));
}
sb.append(endingElement);
if (xmlValidationEnabled) {
try {
builder.parse(new ByteArrayInputStream(sb.toString().getBytes(Charset.forName("UTF-8"))));
} catch (SAXException | IOException e) {
log.error("Error occurred when validating output XML event. " +
"Reason: " + e.getMessage() + "Dropping event: " + sb.toString());
return;
} finally {
builder.reset();
}
}
} else {
for (Event event : events) {
sb.append(constructDefaultMapping(event));
}
if (enclosingElement != null) {
sb.insert(0, enclosingElement);
sb.append(endingElement);
} else {
sb.insert(0, EVENTS_PARENT_OPENING_TAG);
sb.append(EVENTS_PARENT_CLOSING_TAG);
}
}
sinkListener.publish(sb.toString(), dynamicOptions);
}
/**
* Method to construct the ending element using enclosing element.
* Have to strip off any namespace values and add XML ending tags.
*
* @param enclosingElement User provided enclosing element
* @return Calculated closing element
*/
private String getClosingElement(String enclosingElement) {
String[] results = enclosingElement.split(" ");
if (results.length == 1) {
StringBuilder builder = new StringBuilder(enclosingElement);
builder.insert(1, "/");
return builder.toString();
} else {
StringBuilder builder = new StringBuilder(results[0]);
builder.append(">");
builder.insert(1, "/");
return builder.toString();
}
}
/**
* Convert the given {@link Event} to XML string
*
* @param event Event object
* @return the constructed XML string
*/
private String constructDefaultMapping(Event event) {
StringBuilder builder = new StringBuilder(EVENT_PARENT_OPENING_TAG);
Object[] data = event.getData();
for (int i = 0; i < data.length; i++) {
String attributeName = streamDefinition.getAttributeNameArray()[i];
builder.append("<").append(attributeName).append(">");
if (data[i] != null) {
builder.append(data[i].toString()).append("</").append(attributeName).append(">");
} else {
builder.insert(builder.toString().length() - 1, NS_XSI_NIL_ENABLE);
}
}
builder.append(EVENT_PARENT_CLOSING_TAG);
// TODO: remove if we are not going to support arbitraryDataMap
// Get arbitrary data from event
/* Map<String, Object> arbitraryDataMap = event.getArbitraryDataMap();
if (arbitraryDataMap != null && !arbitraryDataMap.isEmpty()) {
// Add arbitrary data map to the default template
OMElement parentPropertyElement = factory.createOMElement(new QName(EVENT_ARBITRARY_DATA_MAP_TAG));
for (Map.Entry<String, Object> entry : arbitraryDataMap.entrySet()) {
OMElement propertyElement = factory.createOMElement(new QName(entry.getKey()));
propertyElement.setText(entry.getValue().toString());
parentPropertyElement.addChild(propertyElement);
}
compositeEventElement.getFirstElement().addChild(parentPropertyElement);
}*/
return builder.toString();
}
}