// This file is part of OpenTSDB.
// Copyright (C) 2013-2014 The OpenTSDB Authors.
//
// 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 2.1 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,
// see <http://www.gnu.org/licenses/>.
package net.opentsdb.utils;
import java.io.IOException;
import java.io.InputStream;
import net.opentsdb.search.SearchQuery;
import net.opentsdb.search.SearchQuery.SearchType;
import net.opentsdb.tree.TreeRule;
import net.opentsdb.tree.TreeRule.TreeRuleType;
import net.opentsdb.uid.UniqueId;
import net.opentsdb.uid.UniqueId.UniqueIdType;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.util.JSONPObject;
/**
* This class simply provides a static initialization and configuration of the
* Jackson ObjectMapper for use throughout OpenTSDB. Since the mapper takes a
* fair amount of construction and is thread safe, the Jackson docs recommend
* initializing it once per app.
* <p>
* The class also provides some simple wrappers around commonly used
* serialization and deserialization methods for POJOs as well as a JSONP
* wrapper. These work wonderfully for smaller objects and you can use JAVA
* annotations to control the de/serialization for your POJO class.
* <p>
* For streaming of large objects, access the mapper directly via {@link
* #getMapper()} or {@link #getFactory()}
* <p>
* Unfortunately since Jackson provides typed exceptions, most of these
* methods will pass them along so you'll have to handle them where
* you are making a call.
* <p>
* Troubleshooting POJO de/serialization:
* <p>
* If you get mapping errors, check some of these
* <ul><li>The class must provide a constructor without parameters</li>
* <li>Make sure fields are accessible via getters/setters or by the
* {@code @JsonAutoDetect} annotation</li>
* <li>Make sure any child objects are accessible, have the empty constructor
* and applicable annotations</li></ul>
* <p>
* Useful Class Annotations:
* {@code @JsonAutoDetect(fieldVisibility = Visibility.ANY)} - will serialize
* any, public or private values
* <p>
* {@code @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL)} - will
* automatically ignore any fields set to NULL, otherwise they are serialized
* with a literal null value
* <p>
* Useful Method Annotations:
* {@code @JsonIgnore} - Ignores the method for de/serialization purposes.
* CRITICAL for any methods that could cause a de/serialization infinite loop
* @since 2.0
*/
public final class JSON {
/**
* Jackson de/serializer initialized, configured and shared
*/
private static final ObjectMapper jsonMapper = new ObjectMapper();
static {
// allows parsing NAN and such without throwing an exception. This is
// important
// for incoming data points with multiple points per put so that we can
// toss only the bad ones but keep the good
jsonMapper.configure(JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS, true);
}
/**
* Deserializes a JSON formatted string to a specific class type
* <b>Note:</b> If you get mapping exceptions you may need to provide a
* TypeReference
* @param json The string to deserialize
* @param pojo The class type of the object used for deserialization
* @return An object of the {@code pojo} type
* @throws IllegalArgumentException if the data or class was null or parsing
* failed
* @throws JSONException if the data could not be parsed
*/
public static final <T> T parseToObject(final String json,
final Class<T> pojo) {
if (json == null || json.isEmpty())
throw new IllegalArgumentException("Incoming data was null or empty");
if (pojo == null)
throw new IllegalArgumentException("Missing class type");
try {
return jsonMapper.readValue(json, pojo);
} catch (JsonParseException e) {
throw new IllegalArgumentException(e);
} catch (JsonMappingException e) {
throw new IllegalArgumentException(e);
} catch (IOException e) {
throw new JSONException(e);
}
}
/**
* Deserializes a JSON formatted byte array to a specific class type
* <b>Note:</b> If you get mapping exceptions you may need to provide a
* TypeReference
* @param json The byte array to deserialize
* @param pojo The class type of the object used for deserialization
* @return An object of the {@code pojo} type
* @throws IllegalArgumentException if the data or class was null or parsing
* failed
* @throws JSONException if the data could not be parsed
*/
public static final <T> T parseToObject(final byte[] json,
final Class<T> pojo) {
if (json == null)
throw new IllegalArgumentException("Incoming data was null");
if (pojo == null)
throw new IllegalArgumentException("Missing class type");
try {
return jsonMapper.readValue(json, pojo);
} catch (JsonParseException e) {
throw new IllegalArgumentException(e);
} catch (JsonMappingException e) {
throw new IllegalArgumentException(e);
} catch (IOException e) {
throw new JSONException(e);
}
}
/**
* Deserializes a JSON formatted string to a specific class type
* @param json The string to deserialize
* @param type A type definition for a complex object
* @return An object of the {@code pojo} type
* @throws IllegalArgumentException if the data or type was null or parsing
* failed
* @throws JSONException if the data could not be parsed
*/
@SuppressWarnings("unchecked")
public static final <T> T parseToObject(final String json,
final TypeReference<T> type) {
if (json == null || json.isEmpty())
throw new IllegalArgumentException("Incoming data was null or empty");
if (type == null)
throw new IllegalArgumentException("Missing type reference");
try {
return (T)jsonMapper.readValue(json, type);
} catch (JsonParseException e) {
throw new IllegalArgumentException(e);
} catch (JsonMappingException e) {
throw new IllegalArgumentException(e);
} catch (IOException e) {
throw new JSONException(e);
}
}
/**
* Deserializes a JSON formatted byte array to a specific class type
* @param json The byte array to deserialize
* @param type A type definition for a complex object
* @return An object of the {@code pojo} type
* @throws IllegalArgumentException if the data or type was null or parsing
* failed
* @throws JSONException if the data could not be parsed
*/
@SuppressWarnings("unchecked")
public static final <T> T parseToObject(final byte[] json,
final TypeReference<T> type) {
if (json == null)
throw new IllegalArgumentException("Incoming data was null");
if (type == null)
throw new IllegalArgumentException("Missing type reference");
try {
return (T)jsonMapper.readValue(json, type);
} catch (JsonParseException e) {
throw new IllegalArgumentException(e);
} catch (JsonMappingException e) {
throw new IllegalArgumentException(e);
} catch (IOException e) {
throw new JSONException(e);
}
}
/**
* Parses a JSON formatted string into raw tokens for streaming or tree
* iteration
* <b>Warning:</b> This method can parse an invalid JSON object without
* throwing an error until you start processing the data
* @param json The string to parse
* @return A JsonParser object to be used for iteration
* @throws IllegalArgumentException if the data was null or parsing failed
* @throws JSONException if the data could not be parsed
*/
public static final JsonParser parseToStream(final String json) {
if (json == null || json.isEmpty())
throw new IllegalArgumentException("Incoming data was null or empty");
try {
return jsonMapper.getFactory().createJsonParser(json);
} catch (JsonParseException e) {
throw new IllegalArgumentException(e);
} catch (IOException e) {
throw new JSONException(e);
}
}
/**
* Parses a JSON formatted byte array into raw tokens for streaming or tree
* iteration
* <b>Warning:</b> This method can parse an invalid JSON object without
* throwing an error until you start processing the data
* @param json The byte array to parse
* @return A JsonParser object to be used for iteration
* @throws IllegalArgumentException if the data was null or parsing failed
* @throws JSONException if the data could not be parsed
*/
public static final JsonParser parseToStream(final byte[] json) {
if (json == null)
throw new IllegalArgumentException("Incoming data was null");
try {
return jsonMapper.getFactory().createJsonParser(json);
} catch (JsonParseException e) {
throw new IllegalArgumentException(e);
} catch (IOException e) {
throw new JSONException(e);
}
}
/**
* Parses a JSON formatted inputs stream into raw tokens for streaming or tree
* iteration
* <b>Warning:</b> This method can parse an invalid JSON object without
* throwing an error until you start processing the data
* @param json The input stream to parse
* @return A JsonParser object to be used for iteration
* @throws IllegalArgumentException if the data was null or parsing failed
* @throws JSONException if the data could not be parsed
*/
public static final JsonParser parseToStream(final InputStream json) {
if (json == null)
throw new IllegalArgumentException("Incoming data was null");
try {
return jsonMapper.getFactory().createJsonParser(json);
} catch (JsonParseException e) {
throw new IllegalArgumentException(e);
} catch (IOException e) {
throw new JSONException(e);
}
}
/**
* Serializes the given object to a JSON string
* @param object The object to serialize
* @return A JSON formatted string
* @throws IllegalArgumentException if the object was null
* @throws JSONException if the object could not be serialized
* @throws IOException Thrown when there was an issue reading the object
*/
public static final String serializeToString(final Object object) {
if (object == null)
throw new IllegalArgumentException("Object was null");
try {
return jsonMapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
throw new JSONException(e);
}
}
/**
* Serializes the given object to a JSON byte array
* @param object The object to serialize
* @return A JSON formatted byte array
* @throws IllegalArgumentException if the object was null
* @throws JSONException if the object could not be serialized
* @throws IOException Thrown when there was an issue reading the object
*/
public static final byte[] serializeToBytes(final Object object) {
if (object == null)
throw new IllegalArgumentException("Object was null");
try {
return jsonMapper.writeValueAsBytes(object);
} catch (JsonProcessingException e) {
throw new JSONException(e);
}
}
/**
* Serializes the given object and wraps it in a callback function
* i.e. <callback>(<json>)
* Note: This will not append a trailing semicolon
* @param callback The name of the Javascript callback to prepend
* @param object The object to serialize
* @return A JSONP formatted string
* @throws IllegalArgumentException if the callback method name was missing
* or object was null
* @throws JSONException if the object could not be serialized
* @throws IOException Thrown when there was an issue reading the object
*/
public static final String serializeToJSONPString(final String callback,
final Object object) {
if (callback == null || callback.isEmpty())
throw new IllegalArgumentException("Missing callback name");
if (object == null)
throw new IllegalArgumentException("Object was null");
try {
return jsonMapper.writeValueAsString(new JSONPObject(callback, object));
} catch (JsonProcessingException e) {
throw new JSONException(e);
}
}
/**
* Serializes the given object and wraps it in a callback function
* i.e. <callback>(<json>)
* Note: This will not append a trailing semicolon
* @param callback The name of the Javascript callback to prepend
* @param object The object to serialize
* @return A JSONP formatted byte array
* @throws IllegalArgumentException if the callback method name was missing
* or object was null
* @throws JSONException if the object could not be serialized
* @throws IOException Thrown when there was an issue reading the object
*/
public static final byte[] serializeToJSONPBytes(final String callback,
final Object object) {
if (callback == null || callback.isEmpty())
throw new IllegalArgumentException("Missing callback name");
if (object == null)
throw new IllegalArgumentException("Object was null");
try {
return jsonMapper.writeValueAsBytes(new JSONPObject(callback, object));
} catch (JsonProcessingException e) {
throw new JSONException(e);
}
}
/**
* Returns a reference to the static ObjectMapper
* @return The ObjectMapper
*/
public final static ObjectMapper getMapper() {
return jsonMapper;
}
/**
* Returns a reference to the JsonFactory for streaming creation
* @return The JsonFactory object
*/
public final static JsonFactory getFactory() {
return jsonMapper.getFactory();
}
/**
* Helper class for deserializing UID type enum from human readable strings
*/
public static class UniqueIdTypeDeserializer
extends JsonDeserializer<UniqueIdType> {
@Override
public UniqueIdType deserialize(final JsonParser parser, final
DeserializationContext context) throws IOException {
return UniqueId.stringToUniqueIdType(parser.getValueAsString());
}
}
/**
* Helper class for deserializing Tree Rule type enum from human readable
* strings
*/
public static class TreeRuleTypeDeserializer
extends JsonDeserializer<TreeRuleType> {
@Override
public TreeRuleType deserialize(final JsonParser parser, final
DeserializationContext context) throws IOException {
return TreeRule.stringToType(parser.getValueAsString());
}
}
/**
* Helper class for deserializing Search type enum from human readable
* strings
*/
public static class SearchTypeDeserializer
extends JsonDeserializer<SearchType> {
@Override
public SearchType deserialize(final JsonParser parser, final
DeserializationContext context) throws IOException {
return SearchQuery.parseSearchType(parser.getValueAsString());
}
}
}