001/*-------------------------------------------------------------------------+ 002| | 003| Copyright 2005-2011 the ConQAT Project | 004| | 005| Licensed under the Apache License, Version 2.0 (the "License"); | 006| you may not use this file except in compliance with the License. | 007| You may obtain a copy of the License at | 008| | 009| http://www.apache.org/licenses/LICENSE-2.0 | 010| | 011| Unless required by applicable law or agreed to in writing, software | 012| distributed under the License is distributed on an "AS IS" BASIS, | 013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 014| See the License for the specific language governing permissions and | 015| limitations under the License. | 016+-------------------------------------------------------------------------*/ 017package org.conqat.engine.commons.util; 018 019import java.io.IOException; 020import java.lang.reflect.Field; 021import java.lang.reflect.Modifier; 022import java.util.List; 023import java.util.Optional; 024 025import org.conqat.engine.core.core.ConQATException; 026import org.conqat.lib.commons.collections.CollectionUtils; 027import org.conqat.lib.commons.string.StringUtils; 028 029import com.fasterxml.jackson.annotation.JsonAutoDetect; 030import com.fasterxml.jackson.annotation.JsonIgnore; 031import com.fasterxml.jackson.annotation.JsonInclude; 032import com.fasterxml.jackson.annotation.PropertyAccessor; 033import com.fasterxml.jackson.core.JsonParser; 034import com.fasterxml.jackson.core.JsonProcessingException; 035import com.fasterxml.jackson.core.type.TypeReference; 036import com.fasterxml.jackson.databind.DeserializationFeature; 037import com.fasterxml.jackson.databind.JavaType; 038import com.fasterxml.jackson.databind.JsonNode; 039import com.fasterxml.jackson.databind.ObjectMapper; 040import com.fasterxml.jackson.datatype.guava.GuavaModule; 041 042/** 043 * Utility code for dealing with JSON. This uses Jackson for serialization and 044 * deserialization to JSON. 045 */ 046public class JsonUtils { 047 048 /** 049 * The shared jackson object mapper instance. Once configured the object is 050 * thread-safe. 051 */ 052 private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); 053 054 static { 055 OBJECT_MAPPER.registerModule(new ColorSerializationModule()); 056 OBJECT_MAPPER.registerModule(new ListMapSerializationModule()); 057 OBJECT_MAPPER.registerModule(new GuavaModule()); 058 OBJECT_MAPPER.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); 059 OBJECT_MAPPER.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); 060 OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL); 061 OBJECT_MAPPER.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES); 062 OBJECT_MAPPER.enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES); 063 OBJECT_MAPPER.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); 064 } 065 066 /** 067 * Serializes the given object to JSON. 068 */ 069 public static String serializeToJSON(Object object) { 070 try { 071 return OBJECT_MAPPER.writeValueAsString(object); 072 } catch (JsonProcessingException e) { 073 throw new JsonSerializationException(e); 074 } 075 } 076 077 /** 078 * Serializes the given object to JSON as UTF-8 encoded byte array. 079 */ 080 public static byte[] serializeToJSONByteArray(Object object) { 081 try { 082 return OBJECT_MAPPER.writeValueAsBytes(object); 083 } catch (JsonProcessingException e) { 084 throw new JsonSerializationException(e); 085 } 086 } 087 088 /** @see #OBJECT_MAPPER */ 089 public static ObjectMapper getObjectMapper() { 090 return OBJECT_MAPPER; 091 } 092 093 public static <T> JavaType getJavaType(Class<T> resultClass) { 094 return OBJECT_MAPPER.constructType(resultClass); 095 } 096 097 public static <T> JavaType getJavaListType(Class<T> resultClass) { 098 return OBJECT_MAPPER.getTypeFactory().constructCollectionType(List.class, resultClass); 099 } 100 101 /** 102 * Wrapper exception for {@link JsonProcessingException} as the other is a 103 * checked exception and we don't want to check it everywhere. 104 */ 105 public static class JsonSerializationException extends RuntimeException { 106 private JsonSerializationException(Throwable cause) { 107 super(cause); 108 } 109 } 110 111 /** 112 * Deserializes a JSON string. 113 * 114 * @throws ConQATException 115 * if the input string could not be parsed as JSON or the class is 116 * not constructible. 117 */ 118 public static <T> T deserializeFromJson(String json, Class<T> expectedClass) throws ConQATException { 119 return deserializeFromJson(json, getJavaType(expectedClass)); 120 } 121 122 /** 123 * Deserializes a JSON string. 124 * 125 * @throws ConQATException 126 * if the input string could not be parsed as JSON or the class is 127 * not constructible. 128 */ 129 public static <T> T deserializeFromJson(String json, TypeReference<T> expectedType) throws ConQATException { 130 return deserializeFromJson(json, OBJECT_MAPPER.getTypeFactory().constructType(expectedType)); 131 } 132 133 /** 134 * Deserializes a JSON string. 135 * 136 * @throws ConQATException 137 * if the input string could not be parsed as JSON or the class is 138 * not constructible. 139 */ 140 public static <T> T deserializeFromJson(String json, JavaType expectedType) throws ConQATException { 141 return safeConvert(objectMapper -> objectMapper.readValue(json, expectedType)); 142 } 143 144 /** 145 * Deserializes a JSON string into a {@link JsonNode}. 146 * 147 * @throws ConQATException 148 * if the input string could not be parsed as JSON or the class is 149 * not constructible. 150 */ 151 public static JsonNode deserializeFromJson(String json) throws ConQATException { 152 return safeConvert(objectMapper -> objectMapper.readTree(json)); 153 } 154 155 /** 156 * Deserializes a JSON string. Throws an error if the resulting object is null 157 * or contains an attribute that is null. 158 * 159 * @throws ConQATException 160 * if the input string could not be parsed as JSON, or the class is 161 * not constructible, or the resulting object or one of its 162 * attributes would be null. 163 */ 164 public static <T> T deserializeFromJsonWithNullCheck(String json, Class<T> expectedClass) throws ConQATException { 165 T parsedObject = deserializeFromJson(json, expectedClass); 166 return NullableFieldValidator.ensureAllFieldsNonNull(parsedObject, json); 167 } 168 169 /** 170 * Returns the json value or {@code null} for the given key, while trying 1. a 171 * lowercase version of the key (e.g. 'project'), and 2. a capitalized version 172 * of the lowercase key ('Project'); 173 */ 174 public static Optional<JsonNode> getWithCapitalizedOrLowercaseKey(JsonNode jsonNode, String key) { 175 key = key.toLowerCase(); 176 if (jsonNode.has(key)) { 177 return Optional.of(jsonNode.get(key)); 178 } 179 return Optional.ofNullable(jsonNode.get(StringUtils.capitalize(key))); 180 } 181 182 /** Returns whether a string is a valid json or not. */ 183 public static boolean isValidJson(String json) { 184 try { 185 OBJECT_MAPPER.readTree(json); 186 return true; 187 } catch (IOException e) { 188 return false; 189 } 190 } 191 192 /** Reformats a JSON string to be pretty printed. */ 193 public static String prettyPrintJSON(String json) throws ConQATException { 194 return safeConvert(objectMapper -> { 195 JsonNode jsonInstance = objectMapper.readTree(json); 196 return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonInstance); 197 }); 198 } 199 200 /** Reformats a JSON string to be pretty printed. */ 201 public static String serializeToJSONPrettyPrinted(Object object) throws ConQATException { 202 return safeConvert(objectMapper -> objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object)); 203 } 204 205 /** 206 * Safely converts json to an object by calling the given supplier and wrapping 207 * any thrown exceptions in {@link ConQATException}. 208 */ 209 private static <T> T safeConvert(CollectionUtils.FunctionWithException<ObjectMapper, T, IOException> convertAction) 210 throws ConQATException { 211 try { 212 return convertAction.apply(OBJECT_MAPPER); 213 } catch (JsonProcessingException e) { 214 throw new ConQATException("Input was invalid JSON.", e); 215 } catch (Throwable t) { 216 throw new ConQATException("Trouble during JSON processing: " + t.getMessage(), t); 217 } 218 } 219 220 /** 221 * Wrapper exception for {@link JsonProcessingException} as the other is a 222 * checked exception and we don't want to check it everywhere. 223 */ 224 public static class JsonDeserializationException extends RuntimeException { 225 public JsonDeserializationException(String message, Throwable cause) { 226 super(message, cause); 227 } 228 } 229 230 /** 231 * @return whether the given field is not serialized, i.e. skipped in JSON 232 * responses to the client. 233 */ 234 public static boolean isNotSerialized(Field field) { 235 236 int fieldModifiers = field.getModifiers(); 237 238 // Jackson excludes transient and static fields by default. 239 if (Modifier.isStatic(fieldModifiers) || Modifier.isTransient(fieldModifiers)) { 240 return true; 241 } 242 // JsonIgnore indicates to skip this field 243 if (field.isAnnotationPresent(JsonIgnore.class)) { 244 return true; 245 } 246 // This is either due to dynamically inserted fields created by Jacoco during 247 // test execution or to anonymous inner classes 248 return field.getName().contains("$"); 249 250 } 251 252}