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}