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}