/*
* #%L
* Wisdom-Framework
* %%
* Copyright (C) 2013 - 2014 Wisdom Framework
* %%
* 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.
* #L%
*/
package org.wisdom.content.jackson;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.felix.ipojo.annotations.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.wisdom.api.configuration.ApplicationConfiguration;
import org.wisdom.api.configuration.Configuration;
import org.wisdom.api.content.JacksonModuleRepository;
import org.wisdom.api.content.Json;
import org.wisdom.api.content.Xml;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.*;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.Set;
/**
* This component is a layer on top of Jackson and provides the {@link org.wisdom.api.content.Json}
* and {@link org.wisdom.api.content.Xml} services.
* <p/>
* This class manages Jackson module dynamically, and recreates a JSON Mapper and XML mapper every time a module arrives
* or leaves.
*/
@Component(immediate = true)
@Provides
@Instantiate
public class JacksonSingleton implements JacksonModuleRepository, Json, Xml {
/**
* An object used as lock.
*/
private final Object lock = new Object();
/**
* The current object mapper.
*/
private ObjectMapper mapper;
/**
* The current object mapper.
*/
private XmlMapper xml;
/**
* The document builder factory used to create new document.
*/
private DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
/**
* The logger.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(JacksonSingleton.class);
/**
* The current set of registered modules.
*/
private Set<Module> modules = new HashSet<>();
/**
* The application configuration to read the jackson enabled / disabled features.
*/
@Requires
public ApplicationConfiguration configuration;
/**
* Creates a new instance of {@link JacksonSingleton}.
*/
public JacksonSingleton() {
try {
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
} catch (ParserConfigurationException e) {
// Just logged even if it's quite important
// Some parser do not support the option (and should probably not be used).
LOGGER.error("Cannot use secure processing for XML document", e);
}
}
/**
* Gets the current mapper.
*
* @return the mapper.
*/
public ObjectMapper mapper() {
synchronized (lock) {
return mapper;
}
}
/**
* Converts an object to JsonNode.
*
* @param data Value to convert in Json.
* @return the resulting JSON Node
* @throws java.lang.RuntimeException if the JSON Node cannot be created
*/
public JsonNode toJson(final Object data) {
synchronized (lock) {
try {
return mapper.valueToTree(data);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
/**
* Gets the JSONP response for the given callback and value.
*
* @param callback the callback name
* @param data the data to transform to json
* @return the String built as follows: "callback(json(data))"
*/
public String toJsonP(final String callback, final Object data) {
synchronized (lock) {
try {
return callback + "(" + stringify((JsonNode) mapper.valueToTree(data)) + ");";
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
/**
* Converts a JsonNode to a Java value.
*
* @param json Json value to convert.
* @param clazz Expected Java value type.
* @return the created object
* @throws java.lang.RuntimeException if the object cannot be created
*/
public <A> A fromJson(JsonNode json, Class<A> clazz) {
synchronized (lock) {
try {
return mapper.treeToValue(json, clazz);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
/**
* Converts a Json String to a Java value.
*
* @param json Json string to convert.
* @param clazz Expected Java value type.
* @return the created object
* @throws java.lang.RuntimeException if the object cannot be created
*/
public <A> A fromJson(String json, Class<A> clazz) {
synchronized (lock) {
try {
JsonNode node = mapper.readTree(json);
return mapper.treeToValue(node, clazz);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
/**
* Converts a JsonNode to its string representation.
* This implementation use a `pretty printer`.
*
* @param json the json node
* @return the String representation of the given Json Object
* @throws java.lang.RuntimeException if the String form cannot be created
*/
public String stringify(JsonNode json) {
try {
return mapper().writerWithDefaultPrettyPrinter().writeValueAsString(json);
} catch (JsonProcessingException e) {
throw new RuntimeException("Cannot stringify the input json node", e);
}
}
/**
* Parses a String representing a json, and return it as a JsonNode.
*
* @param src the JSON String
* @return the Json Node
* @throws java.lang.RuntimeException if the given string is not a valid JSON String
*/
public JsonNode parse(String src) {
synchronized (lock) {
try {
return mapper.readValue(src, JsonNode.class);
} catch (Exception t) {
throw new RuntimeException(t);
}
}
}
/**
* Parses a stream representing a json, and return it as a JsonNode.
* The stream is <strong>not</strong> closed by the method.
*
* @param stream the JSON stream
* @return the JSON node
* @throws java.lang.RuntimeException if the given stream is not a valid JSON String
*/
public JsonNode parse(InputStream stream) {
synchronized (lock) {
try {
return mapper.readValue(stream, JsonNode.class);
} catch (Exception t) {
throw new RuntimeException(t);
}
}
}
/**
* Creates a new JSON Object.
*
* @return the new Object Node.
*/
@Override
public ObjectNode newObject() {
return mapper().createObjectNode();
}
/**
* Creates a new JSON Array.
*
* @return the new Array Node.
*/
@Override
public ArrayNode newArray() {
return mapper().createArrayNode();
}
/**
* Starts the JSON and XML support.
* An empty mapper is created.
*/
@Validate
public void validate() {
LOGGER.info("Starting JSON and XML support services");
setMappers(new ObjectMapper(), new XmlMapper());
}
/**
* Sets the object mapper.
*
* @param mapper the object mapper to use
*/
private void setMappers(ObjectMapper mapper, XmlMapper xml) {
synchronized (lock) {
this.mapper = mapper;
this.xml = xml;
// mapper and xml are set to null on invalidation.
if (mapper != null && xml != null) {
applyMapperConfiguration(mapper, xml);
}
}
}
private void applyMapperConfiguration(ObjectMapper mapper, XmlMapper xml) {
Configuration conf = null;
// Check for test.
if (configuration != null) {
conf = configuration.getConfiguration("jackson");
}
if (conf == null) {
LOGGER.info("Using default (Wisdom) configuration of Jackson");
LOGGER.info("FAIL_ON_UNKNOWN_PROPERTIES is disabled");
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
xml.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
} else {
LOGGER.info("Applying custom configuration on Jackson mapper");
Set<String> keys = conf.asMap().keySet();
for (String key : keys) {
setFeature(mapper, xml, key, conf.getBoolean(key));
}
}
}
private void setFeature(ObjectMapper mapper, XmlMapper xml, String key, Boolean value) {
try {
MapperFeature feature = MapperFeature.valueOf(key);
mapper.configure(feature, value);
xml.configure(feature, value);
return;
} catch (IllegalArgumentException e) {
// Next attempt
}
try {
DeserializationFeature feature = DeserializationFeature.valueOf(key);
mapper.configure(feature, value);
xml.configure(feature, value);
return;
} catch (IllegalArgumentException e) {
// Next attempt
}
try {
SerializationFeature feature = SerializationFeature.valueOf(key);
mapper.configure(feature, value);
xml.configure(feature, value);
return;
} catch (IllegalArgumentException e) {
// Next attempt
}
try {
JsonParser.Feature feature = JsonParser.Feature.valueOf(key);
mapper.configure(feature, value);
xml.configure(feature, value);
return;
} catch (IllegalArgumentException e) {
// Next attempt
}
try {
JsonGenerator.Feature feature = JsonGenerator.Feature.valueOf(key);
mapper.configure(feature, value);
xml.configure(feature, value);
return;
} catch (IllegalArgumentException e) {
// There is no other attempts, but we catch it because we want to customize the error message.
}
throw new IllegalArgumentException("Cannot find a feature with the following name : " + key);
}
/**
* Stops the JSON and XML management.
* Releases the current mappers.
*/
@Invalidate
public void invalidate() {
setMappers(null, null);
}
/**
* Registers a new Jackson Module.
*
* @param module the module to register
*/
@Override
public void register(Module module) {
if (module == null) {
return;
}
LOGGER.info("Adding JSON module {}", module.getModuleName());
synchronized (lock) {
modules.add(module);
rebuildMappers();
}
}
private void rebuildMappers() {
mapper = new ObjectMapper();
for (Module module : modules) {
mapper.registerModule(module);
}
xml = new XmlMapper();
for (Module module : modules) {
xml.registerModule(module);
}
applyMapperConfiguration(mapper, xml);
}
/**
* Un-registers a JSON Module.
*
* @param module the module
*/
@Override
public void unregister(Module module) {
if (module == null) {
// May happen on departure.
return;
}
LOGGER.info("Removing Jackson module {}", module.getModuleName());
synchronized (lock) {
if (modules.remove(module)) {
rebuildMappers();
}
}
}
/**
* Gets the current XML mapper.
*
* @return the mapper
*/
@Override
public XmlMapper xmlMapper() {
synchronized (lock) {
return xml;
}
}
/**
* Builds a new XML Document from the given (xml) string.
* By default this method uses UTF-8. If your document does not use UTF-8,
* use {@link #fromInputStream(java.io.InputStream, java.nio.charset.Charset)} that let you set the encoding.
*
* @param xml the xml to parse, must not be {@literal null}
* @return the document
* @throws java.io.IOException if the given string is not a valid XML document
*/
@Override
public Document fromString(String xml) throws IOException {
ByteArrayInputStream stream = null;
try {
stream = new ByteArrayInputStream(xml.getBytes(Charsets.UTF_8));
return fromInputStream(
stream,
Charsets.UTF_8
);
} catch (UnsupportedEncodingException e) {
throw new IOException(e);
} finally {
IOUtils.closeQuietly(stream);
}
}
/**
* Builds a new XML Document from the given input stream. The stream is not closed by this method,
* and so you must close it.
*
* @param stream the input stream, must not be {@literal null}
* @param encoding the encoding, if {@literal null}, UTF-8 is used.
* @return the built document
* @throws java.io.IOException if the given stream is not a valid XML document,
* or if the given encoding is not supported.
*/
@Override
public Document fromInputStream(InputStream stream, Charset encoding) throws IOException {
try {
DocumentBuilder builder = factory.newDocumentBuilder();
InputSource is = new InputSource(stream);
if (encoding == null) {
is.setEncoding(Charsets.UTF_8.name());
} else {
is.setEncoding(encoding.name());
}
return builder.parse(is); //NOSONAR The used factory is not exposed to XXE.
} catch (ParserConfigurationException | SAXException e) {
throw new IOException("Cannot parse the given XML document", e);
}
}
/**
* Builds a new instance of the given class <em>clazz</em> from the given XML document.
*
* @param document the XML document
* @param clazz the class of the instance to construct
* @return an instance of the class.
*/
@Override
public <A> A fromXML(Document document, Class<A> clazz) {
return fromXML(stringify(document), clazz);
}
/**
* Builds a new instance of the given class <em>clazz</em> from the given XML string.
*
* @param xml the XML string
* @param clazz the class of the instance to construct
* @return an instance of the class.
*/
@Override
public <A> A fromXML(String xml, Class<A> clazz) {
try {
return xmlMapper().readValue(xml, clazz);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Retrieves the string form of the given XML document.
*
* @param document the XML document, must not be {@literal null}
* @return the String form of the object
*/
@Override
public String stringify(Document document) {
try {
StringWriter sw = new StringWriter();
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
transformer.setOutputProperty(OutputKeys.METHOD, "xml");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.transform(new DOMSource(document), new StreamResult(sw));
return sw.toString();
} catch (TransformerException e) {
throw new RuntimeException(e);
}
}
/**
* Creates a new document.
*
* @return the document
*/
public Document newDocument() {
try {
return factory.newDocumentBuilder().newDocument();
} catch (ParserConfigurationException e) {
throw new RuntimeException(e);
}
}
/**
* Binds a module.
*
* @param module the module
*/
@Bind(optional = true, aggregate = true)
public synchronized void bindModule(Module module) {
register(module);
}
/**
* Unbinds a module.
*
* @param module the module
*/
@Unbind
public synchronized void unbindModule(Module module) {
unregister(module);
}
}