
org.smooks.cartridges.json.JSONReader Maven / Gradle / Ivy
/*-
* ========================LICENSE_START=================================
* smooks-json-cartridge
* %%
* Copyright (C) 2020 Smooks
* %%
* Licensed under the terms of the Apache License Version 2.0, or
* the GNU Lesser General Public License version 3.0 or later.
*
* SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-or-later
*
* ======================================================================
*
* Licensed 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.
*
* ======================================================================
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* =========================LICENSE_END==================================
*/
package org.smooks.cartridges.json;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.smooks.api.ExecutionContext;
import org.smooks.api.resource.config.Parameter;
import org.smooks.api.resource.config.ResourceConfig;
import org.smooks.api.resource.reader.SmooksXMLReader;
import org.w3c.dom.Element;
import org.xml.sax.*;
import org.xml.sax.helpers.AttributesImpl;
import jakarta.annotation.PostConstruct;
import javax.inject.Inject;
import javax.xml.XMLConstants;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Stack;
/**
* JSON to SAX event reader.
*
* This JSON Reader can be plugged into Smooks in order to convert a
* JSON based message stream into a stream of SAX events to be consumed by the other
* Smooks resources.
*
* Configuration
*
* <resource-config selector="org.xml.sax.driver">
* <resource>org.smooks.json.JSONReader</resource>
* <!--
* (Optional) The element name of the SAX document root. Default of 'json'.
* -->
* <param name="rootName"><root-name></param>
* <!--
* (Optional) The element name of a array element. Default of 'element'.
* -->
* <param name="arrayElementName"><array-element-name></param>
* <!--
* (Optional) The replacement string for JSON NULL values. Default is an empty string.
* -->
* <param name="nullValueReplacement"><null-value-replacement></param>
* <!--
* (Optional) The replacement character for whitespaces in a json map key. By default this not defined, so that the reader doesn't search for whitespaces.
* -->
* <param name="keyWhitspaceReplacement"><key-whitspace-replacement></param>
* <!--
* (Optional) The prefix character to add if the JSON node name starts with a number. By default this is not defined, so that the reader doesn't search for element names that start with a number.
* -->
* <param name="keyPrefixOnNumeric"><key-prefix-on-numeric></param>
* <!--
* (Optional) If illegal characters are encountered in a JSON element name then they are replaced with this value. By default this is not defined, so that the reader doesn't doesn't search for illegal characters.
* -->
* <param name="illegalElementNameCharReplacement"><illegal-element-name-char-replacement></param>
* <!--
* (Optional) Defines a map of keys and there replacement. The from key will be replaced with the to key or the contents of the element.
* -->
* <param name="keyMap">
* <key from="fromKey" to="toKey" />
* <key from="fromKey"><to></key>
* </param>
* <!--
* (Optional) The encoding of the input stream. Default of 'UTF-8'
* -->
* <param name="encoding"><encoding></param>
*
* </resource-config>
*
*
* Example Usage
* So the following configuration could be used to parse a JSON stream into
* a stream of SAX events:
*
* <resource-config selector="org.xml.sax.driver">
* <resource>org.smooks.json.JSONReader</resource>
* </smooks-resource>
*
* The "Acme-Order-List" input JSON message:
*
* [
* {
* "name" : "Maurice Zeijen",
* "address" : "Netherlands",
* "item" : "V1234",
* "quantity" : 3
* },
* {
* "name" : "Joe Bloggs",
* "address" : "England",
* "item" : "D9123",
* "quantity" : 7
* },
* ]
*
* Within Smooks, the stream of SAX events generated by the "Acme-Order-List" message (and this reader) will generate
* a DOM equivalent to the following:
*
* <json>
* <element>
* <name>Maurice Zeijen</name>
* <address>Netherlands</address>
* <item>V1234</item>
* <quantity>3</quantity>
* <element>
* <element>
* <name>Joe Bloggs</name>
* <address>England</address>
* <item>D9123</item>
* <quantity>7</quantity>
* <element>
* </json>
*
*
* @author [email protected]
*/
@SuppressWarnings("unchecked")
public class JSONReader implements SmooksXMLReader {
private static final Logger LOGGER = LoggerFactory.getLogger(JSONReader.class);
public static final String CONFIG_PARAM_KEY_MAP = "keyMap";
public static final String XML_ROOT = "json";
public static final String XML_ARRAY_ELEMENT_NAME = "element";
public static final String DEFAULT_NULL_VALUE_REPLACEMENT = "";
private static final Attributes EMPTY_ATTRIBS = new AttributesImpl();
private static final JsonFactory jsonFactory = new JsonFactory();
private ContentHandler contentHandler;
private ExecutionContext executionContext;
@Inject
private String rootName = XML_ROOT;
@Inject
private String arrayElementName = XML_ARRAY_ELEMENT_NAME;
@Inject
private Optional keyWhitspaceReplacement;
@Inject
private Optional keyPrefixOnNumeric;
@Inject
private Optional illegalElementNameCharReplacement;
@Inject
private String nullValueReplacement = DEFAULT_NULL_VALUE_REPLACEMENT;
@Inject
private Charset encoding = StandardCharsets.UTF_8;
@Inject
private Boolean indent = false;
@Inject
private ResourceConfig resourceConfig;
private boolean doKeyReplacement = false;
private boolean doKeyWhitspaceReplacement = false;
private boolean doPrefixOnNumericKey = false;
private boolean doIllegalElementNameCharReplacement = false;
private HashMap keyMap = new HashMap();
private enum Type {
OBJECT,
ARRAY
}
@PostConstruct
public void initialize() {
initKeyMap();
doKeyReplacement = !keyMap.isEmpty();
doKeyWhitspaceReplacement = keyWhitspaceReplacement.isPresent();
doPrefixOnNumericKey = keyPrefixOnNumeric.isPresent();
doIllegalElementNameCharReplacement = illegalElementNameCharReplacement.isPresent();
}
/*
* (non-Javadoc)
* @see org.smooks.xml.SmooksXMLReader#setExecutionContext(org.smooks.container.ExecutionContext)
*/
public void setExecutionContext(ExecutionContext request) {
this.executionContext = request;
}
/*
* (non-Javadoc)
* @see org.xml.sax.XMLReader#parse(org.xml.sax.InputSource)
*/
public void parse(InputSource csvInputSource) throws IOException, SAXException {
if(contentHandler == null) {
throw new IllegalStateException("'contentHandler' not set. Cannot parse JSON stream.");
}
if(executionContext == null) {
throw new IllegalStateException("Smooks container 'executionContext' not set. Cannot parse JSON stream.");
}
try {
// Get a reader for the JSON source...
Reader jsonStreamReader = csvInputSource.getCharacterStream();
if(jsonStreamReader == null) {
jsonStreamReader = new InputStreamReader(csvInputSource.getByteStream(), encoding);
}
// Create the JSON parser...
JsonParser jp = null;
try {
if(LOGGER.isTraceEnabled()) {
LOGGER.trace("Creating JSON parser");
}
jp = jsonFactory.createJsonParser(jsonStreamReader);
// Start the document and add the root "csv-set" element...
contentHandler.startDocument();
startElement(rootName, 0);
if(LOGGER.isTraceEnabled()) {
LOGGER.trace("Starting JSON parsing");
}
boolean first = true;
Stack elementStack = new Stack();
Stack typeStack = new Stack();
JsonToken t;
while ((t = jp.nextToken()) != null) {
if(LOGGER.isTraceEnabled()) {
LOGGER.trace("Token: " + t.name());
}
switch(t) {
case START_OBJECT:
case START_ARRAY:
if(!first) {
if(!typeStack.empty() && typeStack.peek() == Type.ARRAY) {
startElement(arrayElementName, typeStack.size());
}
}
typeStack.push(t == JsonToken.START_ARRAY ? Type.ARRAY : Type.OBJECT);
break;
case END_OBJECT:
case END_ARRAY:
typeStack.pop();
boolean typeStackPeekIsArray = !typeStack.empty() && typeStack.peek() == Type.ARRAY;
if(!elementStack.empty() && !typeStackPeekIsArray) {
endElement(elementStack.pop(), typeStack.size());
}
if(typeStackPeekIsArray) {
endElement(arrayElementName, typeStack.size());
}
break;
case FIELD_NAME:
String text = jp.getText();
if(LOGGER.isTraceEnabled()) {
LOGGER.trace("Field name: " + text);
}
String name = getElementName(text);
startElement(name, typeStack.size());
elementStack.add(name);
break;
default:
String value;
if(t == JsonToken.VALUE_NULL) {
value = nullValueReplacement;
} else {
value = jp.getText();
}
if(typeStack.peek() == Type.ARRAY) {
startElement(arrayElementName, typeStack.size());
}
contentHandler.characters(value.toCharArray(), 0, value.length());
if(typeStack.peek() == Type.ARRAY) {
endElement(arrayElementName);
} else {
endElement(elementStack.pop());
}
break;
}
first = false;
}
endElement(rootName, 0);
contentHandler.endDocument();
} finally {
try {
jp.close();
} catch (Exception e) {
}
}
} finally {
// These properties need to be reset for every execution (e.g. when reader is pooled).
contentHandler = null;
executionContext = null;
}
}
private static char[] INDENT = "\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t".toCharArray();
private void startElement(String name, int indent) throws SAXException {
indent(indent);
contentHandler.startElement(XMLConstants.NULL_NS_URI, name, "", EMPTY_ATTRIBS);
}
private void endElement(String name, int indent) throws SAXException {
indent(indent);
endElement(name);
}
private void endElement(String name) throws SAXException {
contentHandler.endElement(XMLConstants.NULL_NS_URI, name, "");
}
private void indent(int indentAmount) throws SAXException {
if(indent) {
if(indentAmount > 0) {
contentHandler.characters(INDENT, 0, indentAmount + 1);
} else {
contentHandler.characters(INDENT, 0, 1);
}
}
}
/**
* @param text
* @return
*/
private String getElementName(String text) {
boolean replacedKey = false;
if(doKeyReplacement) {
String mappedKey = keyMap.get(text);
replacedKey = mappedKey != null;
if(replacedKey) {
text = mappedKey;
}
}
if(!replacedKey) {
if(doKeyWhitspaceReplacement) {
text = text.replace(" ", keyWhitspaceReplacement.get());
}
if(doPrefixOnNumericKey && Character.isDigit(text.charAt(0))) {
text = keyPrefixOnNumeric.get() + text;
}
if(doIllegalElementNameCharReplacement) {
text = text.replaceAll("^[.]|[^a-zA-Z0-9_.-]", illegalElementNameCharReplacement.get());
}
}
return text;
}
/**
*
*/
private void initKeyMap() {
Parameter> keyMapParam = resourceConfig.getParameter(CONFIG_PARAM_KEY_MAP, Object.class);
if (keyMapParam != null) {
Object objValue = keyMapParam.getValue();
if(objValue instanceof Map) {
keyMap = (HashMap) objValue;
} else {
Element keyMapParamElement = keyMapParam.getXml();
if(keyMapParamElement != null) {
setKeyMap(KeyMapDigester.digest(keyMapParamElement));
} else {
LOGGER.error("Sorry, the key properties must be available as XML DOM. Please configure using XML.");
}
}
}
}
public void setContentHandler(ContentHandler contentHandler) {
this.contentHandler = contentHandler;
}
public ContentHandler getContentHandler() {
return contentHandler;
}
/**
* @return the keyMap
*/
public HashMap getKeyMap() {
return keyMap;
}
/**
* @param keyMap the keyMap to set
*/
public void setKeyMap(HashMap keyMap) {
this.keyMap = keyMap;
}
/**
* @return the rootName
*/
public String getRootName() {
return rootName;
}
/**
* @param rootName the rootName to set
*/
public void setRootName(String rootName) {
this.rootName = rootName;
}
/**
* @return the arrayElementName
*/
public String getArrayElementName() {
return arrayElementName;
}
/**
* @param arrayElementName the arrayElementName to set
*/
public void setArrayElementName(String arrayElementName) {
this.arrayElementName = arrayElementName;
}
/**
* @return the keyWhitspaceReplacement
*/
public String getKeyWhitspaceReplacement() {
return keyWhitspaceReplacement.orElse(nullValueReplacement);
}
/**
* @param keyWhitspaceReplacement the keyWhitspaceReplacement to set
*/
public void setKeyWhitspaceReplacement(String keyWhitspaceReplacement) {
this.keyWhitspaceReplacement = Optional.ofNullable(keyWhitspaceReplacement);
}
/**
* @return the keyPrefixOnNumeric
*/
public String getKeyPrefixOnNumeric() {
return keyPrefixOnNumeric.orElse(null);
}
/**
* @param keyPrefixOnNumeric the keyPrefixOnNumeric to set
*/
public void setKeyPrefixOnNumeric(String keyPrefixOnNumeric) {
this.keyPrefixOnNumeric = Optional.ofNullable(keyPrefixOnNumeric);
}
/**
* @return the illegalElementNameCharReplacement
*/
public String getIllegalElementNameCharReplacement() {
return illegalElementNameCharReplacement.orElse(null);
}
/**
* @param illegalElementNameCharReplacement the illegalElementNameCharReplacement to set
*/
public void setIllegalElementNameCharReplacement(
String illegalElementNameCharReplacement) {
this.illegalElementNameCharReplacement = Optional.ofNullable(illegalElementNameCharReplacement);
}
/**
* @return the nullValueReplacement
*/
public String getNullValueReplacement() {
return nullValueReplacement;
}
/**
* @param nullValueReplacement the nullValueReplacement to set
*/
public void setNullValueReplacement(String nullValueReplacement) {
this.nullValueReplacement = nullValueReplacement;
}
/**
* @return the encoding
*/
public Charset getEncoding() {
return encoding;
}
/**
* @param encoding the encoding to set
*/
public void setEncoding(Charset encoding) {
this.encoding = encoding;
}
public void setIndent(boolean indent) {
this.indent = indent;
}
/****************************************************************************
*
* The following methods are currently unimplemnted...
*
****************************************************************************/
public void parse(String systemId)
{
throw new UnsupportedOperationException("Operation not supports by this reader.");
}
public boolean getFeature(String name)
{
return false;
}
public void setFeature(String name, boolean value)
{
}
public DTDHandler getDTDHandler() {
return null;
}
public void setDTDHandler(DTDHandler arg0) {
}
public EntityResolver getEntityResolver() {
return null;
}
public void setEntityResolver(EntityResolver arg0) {
}
public ErrorHandler getErrorHandler() {
return null;
}
public void setErrorHandler(ErrorHandler arg0) {
}
public Object getProperty(String name)
{
return null;
}
public void setProperty(String name, Object value)
{
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy