All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.conqat.engine.commons.util.JsonUtils Maven / Gradle / Ivy

The newest version!
/*
 * 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); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy