001
002package io.vrap.rmf.base.client.utils.json;
003
004import java.io.IOException;
005import java.io.InputStream;
006import java.util.*;
007
008import com.fasterxml.jackson.annotation.JsonInclude;
009import com.fasterxml.jackson.core.JsonProcessingException;
010import com.fasterxml.jackson.core.type.TypeReference;
011import com.fasterxml.jackson.databind.*;
012import com.fasterxml.jackson.databind.module.SimpleModule;
013import com.fasterxml.jackson.databind.node.ArrayNode;
014import com.fasterxml.jackson.databind.node.ObjectNode;
015import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
016
017import io.vrap.rmf.base.client.utils.json.modules.ModuleOptions;
018import io.vrap.rmf.base.client.utils.json.modules.ZonedDateTimeDeserializationModule;
019import io.vrap.rmf.base.client.utils.json.modules.ZonedDateTimeSerializationModule;
020
021/**
022 * Class with methods to customize the JSON serialization/deserialization
023 */
024public class JsonUtils {
025
026    private static final ObjectMapper OBJECT_MAPPER;
027
028    static {
029        OBJECT_MAPPER = createObjectMapper();
030    }
031
032    /**
033     * creates a new {@link ObjectMapper } instance
034     * @return ObjectMapper
035     */
036    public static ObjectMapper createObjectMapper() {
037        return createObjectMapper(name -> null);
038    }
039
040    /**
041     *
042     * @param options configuration for jackson modules supplied by a {@link ModuleSupplier}
043     * @return ObjectMapper
044     */
045    public static ObjectMapper createObjectMapper(final ModuleOptions options) {
046        ServiceLoader<SimpleModule> loader = ServiceLoader.load(SimpleModule.class,
047            SimpleModule.class.getClassLoader());
048
049        ServiceLoader<ModuleSupplier> suppliers = ServiceLoader.load(ModuleSupplier.class,
050            ModuleSupplier.class.getClassLoader());
051        final List<SimpleModule> moduleList = new ArrayList<>();
052        suppliers.iterator().forEachRemaining(moduleSupplier -> moduleList.add(moduleSupplier.getModule(options)));
053
054        final ObjectMapper objectMapper = new ObjectMapper();
055        objectMapper.registerModule(new JavaTimeModule()) //provides serialization and deserialization for LocalDate and LocalTime (JSR310 Jackson module)
056                .registerModule(new ZonedDateTimeSerializationModule()) //custom serializer for LocalDate, LocalTime and ZonedDateTime
057                .registerModule(new ZonedDateTimeDeserializationModule()) //custom deserializer for ZonedDateTime
058                .registerModules(loader)
059                .registerModules(moduleList)
060                .setSerializationInclusion(JsonInclude.Include.NON_NULL) //ignore null fields
061                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
062                .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
063        return objectMapper;
064    }
065
066    /**
067     * serializes the given object to JSON as a byte array
068     * @param value object to be serialized
069     * @return json byte array
070     * @throws JsonProcessingException serialization errors
071     */
072    public static byte[] toJsonByteArray(final Object value) throws JsonProcessingException {
073        return OBJECT_MAPPER.writeValueAsBytes(value);
074    }
075
076    /**
077     * serializes the given object to JSON as a byte array
078     * @param value object to be serialized
079     * @return json string
080     * @throws JsonProcessingException serialization errors
081     */
082    public static String toJsonString(final Object value) throws JsonProcessingException {
083        return OBJECT_MAPPER.writeValueAsString(value);
084    }
085
086    /**
087     * deserializes the given json string to the given class
088     * @param clazz class to serialize to
089     * @param content json as string
090     * @param <T> type of the result
091     * @return deserialized object
092     */
093    public static <T> T fromJsonString(final String content, final Class<T> clazz) {
094        return executing(() -> OBJECT_MAPPER.readValue(content, clazz));
095    }
096
097    /**
098     * Reads a Java object from JSON data (String).
099     *
100     * @param jsonAsString  the JSON data which represents sth. of type {@code <T>}
101     * @param typeReference the full generic type information about the object to create
102     * @param <T>           the type of the result
103     * @return the created objected
104     */
105    public static <T> T fromJsonString(final String jsonAsString, final TypeReference<T> typeReference) {
106        return executing(() -> OBJECT_MAPPER.readValue(jsonAsString, typeReference));
107    }
108
109    /**
110     * Reads a Java object from JsonNode data.
111     * <p>
112     *
113     * @param jsonNode      the JSON data which represents sth. of type {@code <T>}
114     * @param typeReference the full generic type information about the object to create
115     * @param <T>           the type of the result
116     * @return the created objected
117     */
118    public static <T> T fromJsonNode(final JsonNode jsonNode, final TypeReference<T> typeReference) {
119        return executing(() -> OBJECT_MAPPER.readerFor(typeReference).readValue(jsonNode));
120    }
121
122    /**
123     * Converts a commercetools Composable Commerce Java object to JSON as {@link JsonNode}.
124     * <p>If {@code value} is of type String and contains JSON data, that will be ignored, {@code value} will be treated as just any String.
125     * If you want to parse a JSON String to a JsonNode use {@link JsonUtils#parse(java.lang.String)} instead.</p>
126     * <p>
127     *
128     * @param value the object to convert
129     * @return new json
130     */
131    public static JsonNode toJsonNode(final Object value) {
132        return OBJECT_MAPPER.valueToTree(value);
133    }
134
135    /**
136     * Parses a String containing JSON data and produces a {@link JsonNode}.
137     *
138     * @param jsonAsString json data
139     * @return new JsonNode
140     */
141    public static JsonNode parse(final String jsonAsString) {
142        return executing(() -> OBJECT_MAPPER.readTree(jsonAsString));
143    }
144
145    /**
146     * deserializes the given json string to the given class
147     * @param clazz class to serialize to
148     * @param content json as byte array
149     * @return deserialized object
150     * @throws JsonException deserialization errors
151     * @param <T> type of the result
152     */
153    public static <T> T fromJsonByteArray(final byte[] content, final Class<T> clazz) {
154        return executing(() -> OBJECT_MAPPER.readValue(content, clazz));
155    }
156
157    /**
158     * deserializes the given json string to the given class
159     * @param clazz class to serialize to
160     * @param content json as inputstream
161     * @return deserialized object
162     * @throws JsonException deserialization errors
163     * @param <T> type of the result
164     */
165    public static <T> T fromInputStream(final InputStream content, final Class<T> clazz) {
166        return executing(() -> OBJECT_MAPPER.readValue(content, clazz));
167    }
168
169    /**
170     * default {@link ObjectMapper}
171     * @return ObjectMapper
172     */
173    public static ObjectMapper getConfiguredObjectMapper() {
174        return OBJECT_MAPPER;
175    }
176
177    /**
178     * Very simple way to "erase" passwords -
179     * replaces all field values whose names contains {@code 'pass'} by {@code '**removed from output**'}.
180     * @param node Json object to be redacted
181     * @return Json object
182     */
183    private static JsonNode secure(final JsonNode node) {
184        if (node.isObject()) {
185            ObjectNode objectNode = (ObjectNode) node;
186            Iterator<Map.Entry<String, JsonNode>> fields = node.fields();
187            while (fields.hasNext()) {
188                Map.Entry<String, JsonNode> field = fields.next();
189                if (field.getValue().isTextual() && (field.getKey().toLowerCase().contains("pass")
190                        || field.getKey().toLowerCase().contains("access_token")
191                        || field.getKey().toLowerCase().contains("refresh_token"))) {
192                    objectNode.put(field.getKey(), "**removed from output**");
193                }
194                else {
195                    secure(field.getValue());
196                }
197            }
198            return objectNode;
199        }
200        else if (node.isArray()) {
201            ArrayNode arrayNode = (ArrayNode) node;
202            Iterator<JsonNode> elements = arrayNode.elements();
203            while (elements.hasNext()) {
204                secure(elements.next());
205            }
206            return arrayNode;
207        }
208        else {
209            return node;
210        }
211    }
212
213    /**
214     * Pretty prints a given JSON string.
215     *
216     * @param json JSON code as String which should be formatted
217     * @return <code>json</code> formatted
218     */
219    public static String prettyPrint(final String json) {
220        return executing(() -> {
221            final ObjectMapper jsonParser = new ObjectMapper();
222            final JsonNode jsonTree = jsonParser.readValue(json, JsonNode.class);
223            secure(jsonTree);
224            final ObjectWriter writer = jsonParser.writerWithDefaultPrettyPrinter();
225            return writer.writeValueAsString(jsonTree);
226        });
227    }
228
229    public static <T> T executing(final SupplierThrowingIOException<T> supplier) {
230        try {
231            return supplier.get();
232        }
233        catch (final IOException e) {
234            throw new JsonException(e);
235        }
236    }
237
238    @FunctionalInterface
239    public interface SupplierThrowingIOException<T> {
240        T get() throws IOException;
241    }
242}