org.conqat.engine.commons.util.JsonUtils Maven / Gradle / Ivy
Show all versions of teamscale-commons Show documentation
/*
* Copyright (c) CQSE GmbH
*
* 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.
*/
package org.conqat.engine.commons.util;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Optional;
import org.conqat.engine.commons.util.canonical.CanonicalJsonModule;
import org.conqat.lib.commons.function.FunctionWithException;
import org.conqat.lib.commons.string.StringUtils;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.StreamReadConstraints;
import com.fasterxml.jackson.core.json.JsonReadFeature;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.cfg.MapperBuilder;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.guava.GuavaModule;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
/**
* Utility code for dealing with JSON. This uses Jackson for serialization and deserialization to
* JSON.
*/
public class JsonUtils {
/**
* The shared jackson object mapper instance. Once configured the object is thread-safe. In most
* case you want to use this mapper with the object writer returned by
* {@link #getDefaultWriter(ObjectMapper)}.
*/
private static final ObjectMapper OBJECT_MAPPER = defaultObjectMapperBuilder().build();
/**
* Shared Jackson {@link ObjectMapper}, that produces canonical JSON. Once configured the object is
* thread-safe.
*
* @see #serializeToJSONCanonical(Object)
*/
private static final ObjectMapper CANONICAL_OBJECT_MAPPER = defaultObjectMapperBuilder()
// sort all properties alphabetically
.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) //
// and don't perform special handling for @JsonCreator properties
.disable(MapperFeature.SORT_CREATOR_PROPERTIES_FIRST) //
// Sort Map automatically by keys
.enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS) //
// Add support for custom canonical json support
.addModule(new CanonicalJsonModule()) //
.build();
/** The default Jackson {@link ObjectMapper} builder. */
public static JsonMapper.Builder defaultObjectMapperBuilder() {
StreamReadConstraints streamReadConstraints = StreamReadConstraints.builder().maxStringLength(Integer.MAX_VALUE)
.build();
JsonFactory jsonFactory = JsonFactory.builder() //
.enable(JsonReadFeature.ALLOW_TRAILING_COMMA) //
.enable(JsonReadFeature.ALLOW_UNQUOTED_FIELD_NAMES) //
.enable(JsonReadFeature.ALLOW_SINGLE_QUOTES) //
.enable(JsonReadFeature.ALLOW_JAVA_COMMENTS) //
.streamReadConstraints(streamReadConstraints) //
.build();
JsonMapper.Builder builder = JsonMapper.builder(jsonFactory);
return configureDefaultSettings(builder);
}
/**
* Configures the provided {@code builder} with all the default settings used for Json
* serialization.
*/
public static > B configureDefaultSettings(B builder) {
return builder //
.addModule(new ColorSerializationModule()) //
.addModule(new CompactLinesSerializationModule()) //
.addModule(new UnmodifiableCollectionsModule()) //
.addModule(new CollectionMapSerializationModule()) //
.addModule(new GuavaModule()) //
.addModule(new JavaTimeModule()) //
.addModule(new JavaPathModule()) //
.addModule(new Jdk8Module()) //
.addMixIn(Rectangle2D.class, Rectangle2DJsonIgnore.class)
// Should these visibilities ever change, please also update
// ApiReader::allowPrivateBeanParamFields to reflect the same change
.visibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)
.visibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
.serializationInclusion(JsonInclude.Include.NON_NULL)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
}
/** Mixin interface to avoid endless loop when serializing Rectangle objects. */
public interface Rectangle2DJsonIgnore {
/**
* We need to ignore the bounds2d field as it references to itself.
*
* @see Rectangle2D#getBounds2D()
*/
@JsonIgnore
@SuppressWarnings("unused")
// is used by jackson during type introspection of mix-ins
String getBounds2D();
}
/**
* Serializes the given object to JSON.
*/
public static String serializeToJSON(Object object) {
try {
return getDefaultWriter(OBJECT_MAPPER).writeValueAsString(object);
} catch (JsonProcessingException e) {
throw new JsonSerializationRuntimeException(e);
}
}
/**
* Serializes the given object to JSON using the default pretty print writer.
*/
public static String serializeToJSONPrettyPrint(Object object) {
try {
return getDefaultWriter(OBJECT_MAPPER).withDefaultPrettyPrinter().writeValueAsString(object);
} catch (JsonProcessingException e) {
throw new JsonSerializationRuntimeException(e);
}
}
/** @see #OBJECT_MAPPER */
public static ObjectMapper getObjectMapper() {
return OBJECT_MAPPER;
}
/** Returns a {@link JavaType} of the given class. */
private static JavaType getJavaType(Class resultClass) {
return OBJECT_MAPPER.constructType(resultClass);
}
/** Returns a {@link JavaType} of a list of the given class. */
public static JavaType getJavaListType(Class resultClass) {
return OBJECT_MAPPER.getTypeFactory().constructCollectionType(List.class, resultClass);
}
/** Returns a {@link JavaType} of a nested list of the given class. */
public static JavaType getNestedJavaListType(Class resultClass) {
return OBJECT_MAPPER.getTypeFactory().constructCollectionType(List.class,
OBJECT_MAPPER.getTypeFactory().constructCollectionType(List.class, resultClass));
}
/**
* Wrapper exception for {@link JsonProcessingException} as the other is a checked exception, and we
* don't want to check it everywhere.
*/
public static class JsonSerializationRuntimeException extends RuntimeException {
/** Version used for serialization. */
private static final long serialVersionUID = 1;
public JsonSerializationRuntimeException(Throwable cause) {
super(cause);
}
}
/**
* Deserializes a JSON string.
*
* @throws JsonSerializationException
* if the input string could not be parsed as JSON or the class is not constructible.
*/
public static T deserializeFromJson(String json, Class expectedClass) throws JsonSerializationException {
return deserializeFromJson(new StringReader(json), expectedClass);
}
/**
* Deserializes a JSON string retrieved from the {@code jsonReader}.
*
* @throws JsonSerializationException
* if the input string could not be parsed as JSON or the class is not constructible.
*/
public static T deserializeFromJson(Reader jsonReader, Class expectedClass)
throws JsonSerializationException {
return deserializeFromJson(jsonReader, getJavaType(expectedClass));
}
/**
* Deserializes a JSON string.
*
* @throws JsonSerializationException
* if the input string could not be parsed as JSON or the class is not constructible.
*/
public static T deserializeFromJson(String json, TypeReference expectedType)
throws JsonSerializationException {
return deserializeFromJson(new StringReader(json), expectedType);
}
/**
* Deserializes a JSON string retrieved from the {@code jsonReader}.
*
* @throws JsonSerializationException
* if the input string could not be parsed as JSON or the class is not constructible.
*/
public static T deserializeFromJson(Reader jsonReader, TypeReference expectedType)
throws JsonSerializationException {
return deserializeFromJson(jsonReader, OBJECT_MAPPER.getTypeFactory().constructType(expectedType));
}
/**
* Deserializes a JSON string.
*
* @throws JsonSerializationException
* if the input string could not be parsed as JSON or the class is not constructible.
*/
public static T deserializeFromJson(String json, JavaType expectedType) throws JsonSerializationException {
return deserializeFromJson(new StringReader(json), expectedType);
}
/**
* Deserializes a JSON string retrieved from the {@code jsonReader}.
*
* @throws JsonSerializationException
* if the input string could not be parsed as JSON or the class is not constructible.
*/
public static T deserializeFromJson(Reader jsonReader, JavaType expectedType)
throws JsonSerializationException {
return safeConvert(objectMapper -> objectMapper.readValue(jsonReader, expectedType));
}
/**
* Deserializes a JSON string into a {@link JsonNode}.
*
* @throws JsonSerializationException
* if the input string could not be parsed as JSON or the class is not constructible.
*/
public static JsonNode deserializeFromJson(String json) throws JsonSerializationException {
return safeConvert(objectMapper -> objectMapper.readTree(json));
}
/**
* Deserializes a JSON string. Throws an error if the resulting object is null or contains an
* attribute that is null.
*
* @throws JsonSerializationException
* if the input string could not be parsed as JSON, or the class is not constructible,
* or the resulting object or one of its attributes would be null.
*/
public static T deserializeFromJsonWithNullCheck(String json, Class expectedClass)
throws JsonSerializationException {
T parsedObject = deserializeFromJson(json, expectedClass);
return NullableFieldValidator.ensureAllFieldsNonNull(parsedObject, json);
}
/**
* Returns the json value or {@code null} for the given key, while trying 1. a lowercase version of
* the key (e.g. 'project'), and 2. a capitalized version of the lowercase key ('Project');
*/
public static Optional getWithCapitalizedOrLowercaseKey(JsonNode jsonNode, String key) {
key = key.toLowerCase();
if (jsonNode.has(key)) {
return Optional.of(jsonNode.get(key));
}
return Optional.ofNullable(jsonNode.get(StringUtils.capitalize(key)));
}
/** Returns whether a string is a valid json or not. */
public static boolean isValidJson(String json) {
try {
OBJECT_MAPPER.readTree(json);
return true;
} catch (IOException e) {
return false;
}
}
/** Reformats a JSON string to be pretty printed. */
public static String prettyPrintJSON(String json) throws JsonSerializationException {
return safeConvert(objectMapper -> {
JsonNode jsonInstance = objectMapper.readTree(json);
return getDefaultWriter(objectMapper).withDefaultPrettyPrinter().writeValueAsString(jsonInstance);
});
}
/** Reformats a JSON string to be pretty printed. */
public static String serializeToJSONPrettyPrinted(Object object) throws JsonSerializationException {
return safeConvert(
objectMapper -> getDefaultWriter(objectMapper).withDefaultPrettyPrinter().writeValueAsString(object));
}
/**
* Serializes the provided object into a canonical JSON.
*
* A canonical JSON has stable ordering, so that the resulting JSON can be easily compared to
* previous/later versions.
*/
public static String serializeToJSONCanonical(Object object) throws JsonSerializationException {
return safeConvert(
objectMapper -> getDefaultWriter(objectMapper).withDefaultPrettyPrinter().writeValueAsString(object),
CANONICAL_OBJECT_MAPPER);
}
/**
* Safely converts json to an object by calling the given function with the {@link #OBJECT_MAPPER
* default object mapper} and wrapping any thrown exceptions in {@link JsonSerializationException}.
*
* @see #safeConvert(FunctionWithException, ObjectMapper)
*/
private static T safeConvert(FunctionWithException convertAction)
throws JsonSerializationException {
return safeConvert(convertAction, OBJECT_MAPPER);
}
/**
* Safely converts json to an object by calling the given function with the provided
* {@link ObjectMapper} and wrapping any thrown exceptions in {@link JsonSerializationException}.
*/
private static T safeConvert(FunctionWithException convertAction,
ObjectMapper objectMapper) throws JsonSerializationException {
try {
return convertAction.apply(objectMapper);
} catch (JsonProcessingException e) {
throw new JsonSerializationException("Input was invalid JSON.", e);
} catch (Throwable t) {
throw new JsonSerializationException("Trouble during JSON processing: " + t.getMessage(), t);
}
}
/**
* @return whether the given field is not serialized, i.e. skipped in JSON responses to the client.
*/
public static boolean isNotSerialized(Field field) {
int fieldModifiers = field.getModifiers();
// Jackson excludes transient and static fields by default.
if (Modifier.isStatic(fieldModifiers) || Modifier.isTransient(fieldModifiers)) {
return true;
}
// JsonIgnore indicates to skip this field
if (field.isAnnotationPresent(JsonIgnore.class)) {
return true;
}
// This is either due to dynamically inserted fields created by Jacoco during
// test execution or to anonymous inner classes
return field.getName().contains("$");
}
/**
* Returns an {@link com.fasterxml.jackson.databind.ObjectWriter} that uses the default
* serialization view. This prevents the serializing of fields that are specific
* {@link com.fasterxml.jackson.annotation.JsonView}. If you are interested in them, don't use this
* method but use the objectMapper directly.
*/
private static ObjectWriter getDefaultWriter(ObjectMapper objectMapper) {
return objectMapper.writerWithView(SerializationViews.DefaultView.class);
}
}