2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
15 * https://raw.githubusercontent.com/google/gson/master/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java
16 * and repackaged here with additional content from
17 * com.google.gson.internal.{Streams,TypeAdapters,LazilyParsedNumber}
18 * to avoid using the internal package.
20 package org.openhab.binding.lametrictime.internal.api.common.impl.typeadapters.imported;
22 import java.io.EOFException;
23 import java.io.IOException;
24 import java.io.ObjectStreamException;
25 import java.math.BigDecimal;
26 import java.util.LinkedHashMap;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
32 import com.google.gson.Gson;
33 import com.google.gson.JsonArray;
34 import com.google.gson.JsonElement;
35 import com.google.gson.JsonIOException;
36 import com.google.gson.JsonNull;
37 import com.google.gson.JsonObject;
38 import com.google.gson.JsonParseException;
39 import com.google.gson.JsonPrimitive;
40 import com.google.gson.JsonSyntaxException;
41 import com.google.gson.TypeAdapter;
42 import com.google.gson.TypeAdapterFactory;
43 import com.google.gson.reflect.TypeToken;
44 import com.google.gson.stream.JsonReader;
45 import com.google.gson.stream.JsonWriter;
46 import com.google.gson.stream.MalformedJsonException;
49 * Adapts values whose runtime type may differ from their declaration type. This
50 * is necessary when a field's type is not the same type that GSON should create
51 * when deserializing that field. For example, consider these types:
56 * abstract class Shape {
60 * class Circle extends Shape {
63 * class Rectangle extends Shape {
67 * class Diamond extends Shape {
78 * Without additional type information, the serialized JSON is ambiguous. Is
79 * the bottom shape in this drawing a rectangle or a diamond?
98 * This class addresses this problem by adding type information to the
99 * serialized JSON and honoring that type information when the JSON is
121 * Both the type field name ({@code "type"}) and the type labels ({@code
122 * "Rectangle"}) are configurable.
124 * <h3>Registering Types</h3>
125 * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field
126 * name to the {@link #of} factory method. If you don't supply an explicit type
127 * field name, {@code "type"} will be used.
132 * RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class, "type");
136 * Next register all of your subtypes. Every subtype must be explicitly
137 * registered. This protects your application from injection attacks. If you
138 * don't supply an explicit type label, the type's simple name will be used.
142 * shapeAdapter.registerSubtype(Rectangle.class, "Rectangle");
143 * shapeAdapter.registerSubtype(Circle.class, "Circle");
144 * shapeAdapter.registerSubtype(Diamond.class, "Diamond");
148 * Finally, register the type adapter factory in your application's GSON builder:
153 * Gson gson = new GsonBuilder().registerTypeAdapterFactory(shapeAdapterFactory).create();
157 * Like {@code GsonBuilder}, this API supports chaining:
162 * RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
163 * .registerSubtype(Rectangle.class).registerSubtype(Circle.class).registerSubtype(Diamond.class);
167 * @author Christophe Bornet - Initial contribution
170 public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
171 private final Class<?> baseType;
172 private final String typeFieldName;
173 private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>();
174 private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, String>();
176 private RuntimeTypeAdapterFactory(Class<?> baseType, String typeFieldName) {
177 if (typeFieldName == null || baseType == null) {
178 throw new IllegalArgumentException();
180 this.baseType = baseType;
181 this.typeFieldName = typeFieldName;
185 * Creates a new runtime type adapter using for {@code baseType} using {@code
186 * typeFieldName} as the type field name. Type field names are case sensitive.
188 public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
189 return new RuntimeTypeAdapterFactory<T>(baseType, typeFieldName);
193 * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as
194 * the type field name.
196 public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) {
197 return new RuntimeTypeAdapterFactory<T>(baseType, "type");
201 * Registers {@code type} identified by {@code label}. Labels are case
204 * @throws IllegalArgumentException if either {@code type} or {@code label}
205 * have already been registered on this type adapter.
207 public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
208 if (type == null || label == null) {
209 throw new IllegalArgumentException();
211 if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
212 throw new IllegalArgumentException("types and labels must be unique");
214 labelToSubtype.put(label, type);
215 subtypeToLabel.put(type, label);
220 * Registers {@code type} identified by its {@link Class#getSimpleName simple
221 * name}. Labels are case sensitive.
223 * @throws IllegalArgumentException if either {@code type} or its simple name
224 * have already been registered on this type adapter.
226 public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
227 return registerSubtype(type, type.getSimpleName());
231 public @Nullable <R> TypeAdapter<R> create(@Nullable Gson gson, @Nullable TypeToken<R> type) {
232 if (type.getRawType() != baseType) {
236 final Map<String, TypeAdapter<?>> labelToDelegate = new LinkedHashMap<String, TypeAdapter<?>>();
237 final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate = new LinkedHashMap<Class<?>, TypeAdapter<?>>();
238 for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
239 TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
240 labelToDelegate.put(entry.getKey(), delegate);
241 subtypeToDelegate.put(entry.getValue(), delegate);
244 return new TypeAdapter<R>() {
246 public @Nullable R read(JsonReader in) throws IOException {
247 JsonElement jsonElement = RuntimeTypeAdapterFactory.parse(in);
248 JsonElement labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
249 if (labelJsonElement == null) {
250 throw new JsonParseException("cannot deserialize " + baseType
251 + " because it does not define a field named " + typeFieldName);
253 String label = labelJsonElement.getAsString();
254 @SuppressWarnings("unchecked") // registration requires that subtype extends T
255 TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
256 if (delegate == null) {
257 throw new JsonParseException("cannot deserialize " + baseType + " subtype named " + label
258 + "; did you forget to register a subtype?");
260 return delegate.fromJsonTree(jsonElement);
264 public void write(JsonWriter out, @Nullable R value) throws IOException {
265 Class<?> srcType = value.getClass();
266 String label = subtypeToLabel.get(srcType);
267 @SuppressWarnings("unchecked") // registration requires that subtype extends T
268 TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
269 if (delegate == null) {
270 throw new JsonParseException(
271 "cannot serialize " + srcType.getName() + "; did you forget to register a subtype?");
273 JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();
274 if (jsonObject.has(typeFieldName)) {
275 throw new JsonParseException("cannot serialize " + srcType.getName()
276 + " because it already defines a field named " + typeFieldName);
278 JsonObject clone = new JsonObject();
279 clone.add(typeFieldName, new JsonPrimitive(label));
280 for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
281 clone.add(e.getKey(), e.getValue());
283 RuntimeTypeAdapterFactory.write(clone, out);
289 * Takes a reader in any state and returns the next value as a JsonElement.
291 private static @Nullable JsonElement parse(JsonReader reader) throws JsonParseException {
292 boolean isEmpty = true;
296 return RuntimeTypeAdapterFactory.JSON_ELEMENT.read(reader);
297 } catch (EOFException e) {
299 * For compatibility with JSON 1.5 and earlier, we return a JsonNull for
300 * empty documents instead of throwing.
303 return JsonNull.INSTANCE;
305 // The stream ended prematurely so it is likely a syntax error.
306 throw new JsonSyntaxException(e);
307 } catch (MalformedJsonException e) {
308 throw new JsonSyntaxException(e);
309 } catch (IOException e) {
310 throw new JsonIOException(e);
311 } catch (NumberFormatException e) {
312 throw new JsonSyntaxException(e);
317 * Writes the JSON element to the writer, recursively.
319 private static void write(JsonElement element, JsonWriter writer) throws IOException {
320 RuntimeTypeAdapterFactory.JSON_ELEMENT.write(writer, element);
323 private static final TypeAdapter<JsonElement> JSON_ELEMENT = new TypeAdapter<JsonElement>() {
325 public @Nullable JsonElement read(@Nullable JsonReader in) throws IOException {
328 return new JsonPrimitive(in.nextString());
330 String number = in.nextString();
331 return new JsonPrimitive(new LazilyParsedNumber(number));
333 return new JsonPrimitive(in.nextBoolean());
336 return JsonNull.INSTANCE;
338 JsonArray array = new JsonArray();
340 while (in.hasNext()) {
346 JsonObject object = new JsonObject();
348 while (in.hasNext()) {
349 object.add(in.nextName(), read(in));
358 throw new IllegalArgumentException();
363 public void write(JsonWriter out, @Nullable JsonElement value) throws IOException {
364 if (value == null || value.isJsonNull()) {
366 } else if (value.isJsonPrimitive()) {
367 JsonPrimitive primitive = value.getAsJsonPrimitive();
368 if (primitive.isNumber()) {
369 out.value(primitive.getAsNumber());
370 } else if (primitive.isBoolean()) {
371 out.value(primitive.getAsBoolean());
373 out.value(primitive.getAsString());
376 } else if (value.isJsonArray()) {
378 for (JsonElement e : value.getAsJsonArray()) {
383 } else if (value.isJsonObject()) {
385 for (Map.Entry<String, JsonElement> e : value.getAsJsonObject().entrySet()) {
386 out.name(e.getKey());
387 write(out, e.getValue());
392 throw new IllegalArgumentException("Couldn't write " + value.getClass());
398 * This class holds a number value that is lazily converted to a specific number type
400 * @author Inderjeet Singh
402 public static final class LazilyParsedNumber extends Number {
403 private final String value;
405 public LazilyParsedNumber(String value) {
410 public int intValue() {
412 return Integer.parseInt(value);
413 } catch (NumberFormatException e) {
415 return (int) Long.parseLong(value);
416 } catch (NumberFormatException nfe) {
417 return new BigDecimal(value).intValue();
423 public long longValue() {
425 return Long.parseLong(value);
426 } catch (NumberFormatException e) {
427 return new BigDecimal(value).longValue();
432 public float floatValue() {
433 return Float.parseFloat(value);
437 public double doubleValue() {
438 return Double.parseDouble(value);
442 public String toString() {
447 * If somebody is unlucky enough to have to serialize one of these, serialize
448 * it as a BigDecimal so that they won't need Gson on the other side to
451 private Object writeReplace() throws ObjectStreamException {
452 return new BigDecimal(value);