/*
* 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.json;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
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.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.OptionHolder;
import org.wso2.siddhi.core.util.transport.TemplateBuilder;
import org.wso2.siddhi.query.api.definition.StreamDefinition;
import java.util.Map;
/**
* Mapper class to convert a Siddhi message to a JSON message. User can provide a JSON template or else we will be
* using a predefined JSON message format. In some instances
* coding best practices have been compensated for performance concerns.
*/
@Extension(
name = "json",
namespace = "sinkMapper",
description = "Event to JSON output mapper. Transports which publish messages can utilize this extension"
+ "to convert the Siddhi event to JSON message. Users can either send a pre-defined JSON "
+ "format or a custom JSON message.",
parameters = {
@Parameter(name = "validate.json",
description = "This property will enable JSON validation for generated JSON message. By "
+ "default value of the property will be false. When enabled DAS will validate the "
+ "generated JSON message and drop the message if it does not adhere to proper JSON "
+ "standards. ",
type = {DataType.BOOL}),
@Parameter(name = "enclosing.element",
description =
"Used to specify the enclosing element in case of sending multiple events in same "
+ "JSON message. WSO2 DAS will treat the child element of given enclosing "
+ "element as events"
+ " and execute json expressions on child elements. If enclosing.element "
+ "is not provided "
+ "multiple event scenario is disregarded and json path will be evaluated "
+ "with respect to "
+ "root element.",
type = {DataType.STRING})
},
examples = {
@Example(
syntax = "@sink(type='inMemory', topic='stock', @map(type='json'))\n"
+ "define stream FooStream (symbol string, price float, volume long);\n",
description = "Above configuration will do a default JSON input mapping which will "
+ "generate below "
+ "output"
+ "{\n"
+ " \"event\":{\n"
+ " \"symbol\":WSO2,\n"
+ " \"price\":55.6,\n"
+ " \"volume\":100\n"
+ " }\n"
+ "}\n"),
@Example(
syntax = "@sink(type='inMemory', topic='{{symbol}}', @map(type='json', enclosing"
+ ".element='$.portfolio', validate.json='true', @payload( "
+ "\"{\"StockData\":{\"Symbol\":\"{{symbol}}\",\"Price\":{{price}}}\")))\n"
+ "define stream BarStream (symbol string, price float, volume long);",
description = "Above configuration will perform a custom JSON mapping which will "
+ "produce below "
+ "output JSON message"
+ "{"
+ "\"portfolio\":{\n"
+ " \"StockData\":{\n"
+ " \"Symbol\":WSO2,\n"
+ " \"Price\":55.6\n"
+ " }\n"
+ " }\n"
+ "}")
}
)
public class JsonSinkMapper extends SinkMapper {
private static final Logger log = Logger.getLogger(JsonSinkMapper.class);
private static final String EVENT_PARENT_TAG = "event";
private static final String ENCLOSING_ELEMENT_IDENTIFIER = "enclosing.element";
private static final String DEFAULT_ENCLOSING_ELEMENT = "$";
private static final String JSON_VALIDATION_IDENTIFIER = "validate.json";
private static final String JSON_EVENT_SEPERATOR = ",";
private static final String JSON_KEYVALUE_SEPERATOR = ":";
private static final String JSON_ARRAY_START_SYMBOL = "[";
private static final String JSON_ARRAY_END_SYMBOL = "]";
private static final String JSON_EVENT_START_SYMBOL = "{";
private static final String JSON_EVENT_END_SYMBOL = "}";
private static final String UNDEFINED = "undefined";
private String[] attributeNameArray;
private String enclosingElement = null;
private boolean isJsonValidationEnabled = false;
@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
*/
@Override
public void init(StreamDefinition streamDefinition, OptionHolder optionHolder,
TemplateBuilder payloadTemplateBuilder, ConfigReader mapperConfigReader) {
attributeNameArray = streamDefinition.getAttributeNameArray();
enclosingElement = optionHolder.validateAndGetStaticValue(ENCLOSING_ELEMENT_IDENTIFIER, null);
isJsonValidationEnabled = Boolean.parseBoolean(optionHolder
.validateAndGetStaticValue(JSON_VALIDATION_IDENTIFIER, "false"));
}
@Override
public void mapAndSend(Event[] events, OptionHolder optionHolder, TemplateBuilder payloadTemplateBuilder,
SinkListener sinkListener, DynamicOptions dynamicOptions)
throws ConnectionUnavailableException {
StringBuilder sb = new StringBuilder();
if (payloadTemplateBuilder == null) {
String jsonString = constructJsonForDefaultMapping(events);
sb.append(jsonString);
} else {
sb.append(constructJsonForCustomMapping(events, payloadTemplateBuilder));
}
if (!isJsonValidationEnabled) {
sinkListener.publish(sb.toString(), dynamicOptions);
} else if (isValidJson(sb.toString())) {
sinkListener.publish(sb.toString(), dynamicOptions);
} else {
log.error("Invalid json string : " + sb.toString() + ". Hence dropping the message.");
}
}
@Override
public void mapAndSend(Event event, OptionHolder optionHolder, TemplateBuilder payloadTemplateBuilder,
SinkListener sinkListener, DynamicOptions dynamicOptions)
throws ConnectionUnavailableException {
StringBuilder sb = null;
if (payloadTemplateBuilder == null) {
String jsonString = constructJsonForDefaultMapping(event);
if (jsonString != null) {
sb = new StringBuilder();
sb.append(jsonString);
}
} else {
sb = new StringBuilder();
sb.append(constructJsonForCustomMapping(event, payloadTemplateBuilder));
}
if (sb != null) {
if (!isJsonValidationEnabled) {
sinkListener.publish(sb.toString(), dynamicOptions);
} else if (isValidJson(sb.toString())) {
sinkListener.publish(sb.toString(), dynamicOptions);
} else {
log.error("Invalid json string : " + sb.toString() + ". Hence dropping the message.");
}
}
}
private String constructJsonForDefaultMapping(Object eventObj) {
StringBuilder sb = new StringBuilder();
int numberOfOuterObjects;
if (enclosingElement != null) {
String[] nodeNames = enclosingElement.split("\\.");
if (DEFAULT_ENCLOSING_ELEMENT.equals(nodeNames[0])) {
numberOfOuterObjects = nodeNames.length - 1;
} else {
numberOfOuterObjects = nodeNames.length;
}
for (String nodeName : nodeNames) {
if (!DEFAULT_ENCLOSING_ELEMENT.equals(nodeName)) {
sb.append(JSON_EVENT_START_SYMBOL).append("\"").append(nodeName).append("\"")
.append(JSON_KEYVALUE_SEPERATOR);
}
}
if (eventObj instanceof Event) {
Event event = (Event) eventObj;
JsonObject jsonEvent = constructSingleEventForDefaultMapping(doPartialProcessing(event));
sb.append(jsonEvent);
} else if (eventObj instanceof Event[]) {
JsonArray eventArray = new JsonArray();
for (Event event : (Event[]) eventObj) {
eventArray.add(constructSingleEventForDefaultMapping(doPartialProcessing(event)));
}
sb.append(eventArray.toString());
} else {
log.error("Invalid object type. " + eventObj.toString() +
" cannot be converted to an event or event array. Hence dropping message.");
return null;
}
for (int i = 0; i < numberOfOuterObjects; i++) {
sb.append(JSON_EVENT_END_SYMBOL);
}
return sb.toString();
} else {
if (eventObj instanceof Event) {
Event event = (Event) eventObj;
JsonObject jsonEvent = constructSingleEventForDefaultMapping(doPartialProcessing(event));
return jsonEvent.toString();
} else if (eventObj instanceof Event[]) {
JsonArray eventArray = new JsonArray();
for (Event event : (Event[]) eventObj) {
eventArray.add(constructSingleEventForDefaultMapping(doPartialProcessing(event)));
}
return (eventArray.toString());
} else {
log.error("Invalid object type. " + eventObj.toString() +
" cannot be converted to an event or event array.");
return null;
}
}
}
private String constructJsonForCustomMapping(Object eventObj, TemplateBuilder payloadTemplateBuilder) {
StringBuilder sb = new StringBuilder();
int numberOfOuterObjects = 0;
if (enclosingElement != null) {
String[] nodeNames = enclosingElement.split("\\.");
if (DEFAULT_ENCLOSING_ELEMENT.equals(nodeNames[0])) {
numberOfOuterObjects = nodeNames.length - 1;
} else {
numberOfOuterObjects = nodeNames.length;
}
for (String nodeName : nodeNames) {
if (!DEFAULT_ENCLOSING_ELEMENT.equals(nodeName)) {
sb.append(JSON_EVENT_START_SYMBOL).append("\"").append(nodeName).append("\"")
.append(JSON_KEYVALUE_SEPERATOR);
}
}
if (eventObj instanceof Event) {
Event event = doPartialProcessing((Event) eventObj);
sb.append(payloadTemplateBuilder.build(event));
} else if (eventObj instanceof Event[]) {
String jsonEvent;
sb.append(JSON_ARRAY_START_SYMBOL);
for (Event e : (Event[]) eventObj) {
jsonEvent = payloadTemplateBuilder.build(doPartialProcessing(e));
if (jsonEvent != null) {
sb.append(jsonEvent).append(JSON_EVENT_SEPERATOR).append("\n");
}
}
sb.delete(sb.length() - 2, sb.length());
sb.append(JSON_ARRAY_END_SYMBOL);
} else {
log.error("Invalid object type. " + eventObj.toString() +
" cannot be converted to an event or event array. Hence dropping message.");
return null;
}
for (int i = 0; i < numberOfOuterObjects; i++) {
sb.append(JSON_EVENT_END_SYMBOL);
}
return sb.toString();
} else {
if (eventObj.getClass() == Event.class) {
return payloadTemplateBuilder.build(doPartialProcessing((Event) eventObj));
} else if (eventObj.getClass() == Event[].class) {
String jsonEvent;
sb.append(JSON_ARRAY_START_SYMBOL);
for (Event event : (Event[]) eventObj) {
jsonEvent = payloadTemplateBuilder.build(doPartialProcessing(event));
if (jsonEvent != null) {
sb.append(jsonEvent).append(JSON_EVENT_SEPERATOR).append("\n");
}
}
sb.delete(sb.length() - 2, sb.length());
sb.append(JSON_ARRAY_END_SYMBOL);
return sb.toString();
} else {
log.error("Invalid object type. " + eventObj.toString() +
" cannot be converted to an event or event array. Hence dropping message.");
return null;
}
}
}
private JsonObject constructSingleEventForDefaultMapping(Event event) {
Object[] data = event.getData();
JsonObject jsonEventObject = new JsonObject();
JsonObject innerParentObject = new JsonObject();
String attributeName;
Object attributeValue;
Gson gson = new Gson();
for (int i = 0; i < data.length; i++) {
attributeName = attributeNameArray[i];
attributeValue = data[i];
if (attributeValue != null) {
if (attributeValue.getClass() == String.class) {
innerParentObject.addProperty(attributeName, attributeValue.toString());
} else if (attributeValue instanceof Number) {
innerParentObject.addProperty(attributeName, (Number) attributeValue);
} else if (attributeValue instanceof Boolean) {
innerParentObject.addProperty(attributeName, (Boolean) attributeValue);
} else if (attributeValue instanceof Map) {
if (!((Map) attributeValue).isEmpty()) {
innerParentObject.add(attributeName, gson.toJsonTree(attributeValue));
}
}
}
}
jsonEventObject.add(EVENT_PARENT_TAG, innerParentObject);
return jsonEventObject;
}
private Event doPartialProcessing(Event event) {
Object[] data = event.getData();
for (int i = 0; i < data.length; i++) {
if (data[i] == null) {
data[i] = UNDEFINED;
}
}
return event;
}
private static boolean isValidJson(String jsonInString) {
try {
new Gson().fromJson(jsonInString, Object.class);
return true;
} catch (com.google.gson.JsonSyntaxException ex) {
return false;
}
}
}