001/*
002 *   Copyright 2024 Vonage
003 *
004 *   Licensed under the Apache License, Version 2.0 (the "License");
005 *   you may not use this file except in compliance with the License.
006 *   You may obtain a copy of the License at
007 *
008 *        http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *   Unless required by applicable law or agreed to in writing, software
011 *   distributed under the License is distributed on an "AS IS" BASIS,
012 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *   See the License for the specific language governing permissions and
014 *   limitations under the License.
015 */
016package com.vonage.client;
017
018import com.fasterxml.jackson.annotation.JsonInclude;
019import com.fasterxml.jackson.core.JsonProcessingException;
020import com.fasterxml.jackson.databind.DeserializationFeature;
021import com.fasterxml.jackson.databind.ObjectMapper;
022import com.fasterxml.jackson.databind.SerializationFeature;
023import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
024import java.io.IOException;
025import java.lang.reflect.Constructor;
026import java.lang.reflect.Modifier;
027
028/**
029 * Indicates that a class can be serialized to and parsed from JSON.
030 *
031 * @since 7.7.0
032 */
033public interface Jsonable {
034
035        /**
036         * Convenience method for creating an ObjectMapper with standard settings.
037         *
038         * @return A new ObjectMapper with appropriate configuration.
039         */
040        static ObjectMapper createDefaultObjectMapper() {
041                return new ObjectMapper()
042                                .registerModule(new JavaTimeModule())
043                                .setSerializationInclusion(JsonInclude.Include.NON_NULL)
044                                .configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false)
045                                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
046                                .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
047        }
048
049        /**
050         * Serialises this class to a JSON payload.
051         *
052         * @return The JSON string representing this class's marked properties.
053         */
054        default String toJson() {
055                try {
056                        ObjectMapper mapper = this instanceof JsonableBaseObject ?
057                                        ((JsonableBaseObject) this).createJsonObjectMapper() : createDefaultObjectMapper();
058
059                        return mapper.writeValueAsString(this);
060                }
061                catch (JsonProcessingException jpe) {
062                        throw new VonageUnexpectedException("Failed to produce JSON from "+getClass().getSimpleName()+" object.", jpe);
063                }
064        }
065
066        /**
067         * Updates this class's fields from the JSON payload.
068         *
069         * @param json The JSON string.
070         *
071         * @throws VonageResponseParseException If the JSON was invalid or this class couldn't be updated.
072         */
073        default void updateFromJson(String json) {
074                if (json == null || json.trim().isEmpty()) return;
075                try {
076                        ObjectMapper mapper = this instanceof JsonableBaseObject ?
077                                        ((JsonableBaseObject) this).createJsonObjectMapper() : createDefaultObjectMapper();
078
079                        mapper.readerForUpdating(this).readValue(json);
080                }
081                catch (IOException ex) {
082                        throw new VonageResponseParseException("Failed to produce "+getClass().getSimpleName()+" from JSON.", ex);
083                }
084        }
085
086        /**
087         * Delegates to {@linkplain #fromJson(String, Class)}, using the type varargs for inferring the class.
088         *
089         * @param json The JSON string to parse.
090         * @param type Unused. This is a hack to get the array class's component type.
091         *
092         * @return A new instance of the inferred Jsonable.
093         *
094         * @param <J> A class which implements this interface.
095         *
096         * @throws VonageUnexpectedException If a no-args constructor for the class was not found.
097         * @throws VonageResponseParseException If the JSON was invalid or this class couldn't be updated.
098         */
099        @SuppressWarnings("unchecked")
100        static <J extends Jsonable> J fromJson(String json, J... type) {
101                return fromJson(json, (Class<J>) type.getClass().getComponentType());
102        }
103
104        /**
105         * Creates a new instance of the designated Jsonable class, calling its no-args constructor
106         * followed by {@link #updateFromJson(String)}.
107         *
108         * @param json The JSON string to parse.
109         * @param jsonable The Jsonable class to construct.
110         *
111         * @return A new instance of the Jsonable class.
112         *
113         * @param <J> A class which implements this interface.
114         *
115         * @throws VonageUnexpectedException If a no-args constructor for the class was not found.
116         * @throws VonageResponseParseException If the JSON was invalid or this class couldn't be updated.
117         */
118        static <J extends Jsonable> J fromJson(String json, Class<? extends J> jsonable) {
119                try {
120                        if (Modifier.isAbstract(jsonable.getModifiers())) {
121                                return createDefaultObjectMapper().readValue(json, jsonable);
122                        }
123                        Constructor<? extends J> constructor = jsonable.getDeclaredConstructor();
124                        if (!(constructor.isAccessible())) {
125                                constructor.setAccessible(true);
126                        }
127                        J instance = constructor.newInstance();
128                        instance.updateFromJson(json);
129                        return instance;
130                }
131                catch (ReflectiveOperationException | JsonProcessingException ex) {
132                        throw new VonageUnexpectedException(ex);
133                }
134    }
135}