diff options
author | 2023-09-14 19:29:36 +0200 | |
---|---|---|
committer | 2023-09-14 19:29:36 +0200 | |
commit | 98ed3b6db5f4e51961a161050cc31c66015116e8 (patch) | |
tree | 8bfd6d9bc8d6ed23b9eb0f889dd40b6c24fe8f92 /subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/RuntimeTypeAdapterFactory.java | |
parent | Merge pull request #38 from nagilooh/design-space-exploration (diff) | |
parent | Merge remote-tracking branch 'upstream/main' into partial-interpretation (diff) | |
download | refinery-98ed3b6db5f4e51961a161050cc31c66015116e8.tar.gz refinery-98ed3b6db5f4e51961a161050cc31c66015116e8.tar.zst refinery-98ed3b6db5f4e51961a161050cc31c66015116e8.zip |
Merge pull request #39 from kris7t/partial-interpretation
Implement partial interpretation based model generation
Diffstat (limited to 'subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/RuntimeTypeAdapterFactory.java')
-rw-r--r-- | subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/RuntimeTypeAdapterFactory.java | 304 |
1 files changed, 304 insertions, 0 deletions
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/RuntimeTypeAdapterFactory.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/RuntimeTypeAdapterFactory.java new file mode 100644 index 00000000..b16cf7df --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/RuntimeTypeAdapterFactory.java | |||
@@ -0,0 +1,304 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2011 Google Inc. | ||
3 | * Copyright (C) 2023 The Refinery Authors <https://refinery.tools/> | ||
4 | * | ||
5 | * SPDX-License-Identifier: Apache-2.0 | ||
6 | * | ||
7 | * Licensed under the Apache License, Version 2.0 (the "License"); | ||
8 | * you may not use this file except in compliance with the License. | ||
9 | * You may obtain a copy of the License at | ||
10 | * | ||
11 | * http://www.apache.org/licenses/LICENSE-2.0 | ||
12 | * | ||
13 | * Unless required by applicable law or agreed to in writing, software | ||
14 | * distributed under the License is distributed on an "AS IS" BASIS, | ||
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
16 | * See the License for the specific language governing permissions and | ||
17 | * limitations under the License. | ||
18 | * | ||
19 | * This file was copied into Refinery according to upstream instructions at | ||
20 | * https://github.com/google/gson/issues/1104#issuecomment-309582470. | ||
21 | * However, we changed the package name below to avoid potential clashes | ||
22 | * with other jars on the classpath. | ||
23 | */ | ||
24 | package tools.refinery.language.web.xtext.servlet; | ||
25 | |||
26 | import com.google.errorprone.annotations.CanIgnoreReturnValue; | ||
27 | import com.google.gson.Gson; | ||
28 | import com.google.gson.JsonElement; | ||
29 | import com.google.gson.JsonObject; | ||
30 | import com.google.gson.JsonParseException; | ||
31 | import com.google.gson.JsonPrimitive; | ||
32 | import com.google.gson.TypeAdapter; | ||
33 | import com.google.gson.TypeAdapterFactory; | ||
34 | import com.google.gson.reflect.TypeToken; | ||
35 | import com.google.gson.stream.JsonReader; | ||
36 | import com.google.gson.stream.JsonWriter; | ||
37 | import java.io.IOException; | ||
38 | import java.util.LinkedHashMap; | ||
39 | import java.util.Map; | ||
40 | |||
41 | /** | ||
42 | * Adapts values whose runtime type may differ from their declaration type. This | ||
43 | * is necessary when a field's type is not the same type that GSON should create | ||
44 | * when deserializing that field. For example, consider these types: | ||
45 | * <pre> {@code | ||
46 | * abstract class Shape { | ||
47 | * int x; | ||
48 | * int y; | ||
49 | * } | ||
50 | * class Circle extends Shape { | ||
51 | * int radius; | ||
52 | * } | ||
53 | * class Rectangle extends Shape { | ||
54 | * int width; | ||
55 | * int height; | ||
56 | * } | ||
57 | * class Diamond extends Shape { | ||
58 | * int width; | ||
59 | * int height; | ||
60 | * } | ||
61 | * class Drawing { | ||
62 | * Shape bottomShape; | ||
63 | * Shape topShape; | ||
64 | * } | ||
65 | * }</pre> | ||
66 | * <p>Without additional type information, the serialized JSON is ambiguous. Is | ||
67 | * the bottom shape in this drawing a rectangle or a diamond? <pre> {@code | ||
68 | * { | ||
69 | * "bottomShape": { | ||
70 | * "width": 10, | ||
71 | * "height": 5, | ||
72 | * "x": 0, | ||
73 | * "y": 0 | ||
74 | * }, | ||
75 | * "topShape": { | ||
76 | * "radius": 2, | ||
77 | * "x": 4, | ||
78 | * "y": 1 | ||
79 | * } | ||
80 | * }}</pre> | ||
81 | * This class addresses this problem by adding type information to the | ||
82 | * serialized JSON and honoring that type information when the JSON is | ||
83 | * deserialized: <pre> {@code | ||
84 | * { | ||
85 | * "bottomShape": { | ||
86 | * "type": "Diamond", | ||
87 | * "width": 10, | ||
88 | * "height": 5, | ||
89 | * "x": 0, | ||
90 | * "y": 0 | ||
91 | * }, | ||
92 | * "topShape": { | ||
93 | * "type": "Circle", | ||
94 | * "radius": 2, | ||
95 | * "x": 4, | ||
96 | * "y": 1 | ||
97 | * } | ||
98 | * }}</pre> | ||
99 | * Both the type field name ({@code "type"}) and the type labels ({@code | ||
100 | * "Rectangle"}) are configurable. | ||
101 | * | ||
102 | * <h2>Registering Types</h2> | ||
103 | * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field | ||
104 | * name to the {@link #of} factory method. If you don't supply an explicit type | ||
105 | * field name, {@code "type"} will be used. <pre> {@code | ||
106 | * RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory | ||
107 | * = RuntimeTypeAdapterFactory.of(Shape.class, "type"); | ||
108 | * }</pre> | ||
109 | * Next register all of your subtypes. Every subtype must be explicitly | ||
110 | * registered. This protects your application from injection attacks. If you | ||
111 | * don't supply an explicit type label, the type's simple name will be used. | ||
112 | * <pre> {@code | ||
113 | * shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle"); | ||
114 | * shapeAdapterFactory.registerSubtype(Circle.class, "Circle"); | ||
115 | * shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond"); | ||
116 | * }</pre> | ||
117 | * Finally, register the type adapter factory in your application's GSON builder: | ||
118 | * <pre> {@code | ||
119 | * Gson gson = new GsonBuilder() | ||
120 | * .registerTypeAdapterFactory(shapeAdapterFactory) | ||
121 | * .create(); | ||
122 | * }</pre> | ||
123 | * Like {@code GsonBuilder}, this API supports chaining: <pre> {@code | ||
124 | * RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class) | ||
125 | * .registerSubtype(Rectangle.class) | ||
126 | * .registerSubtype(Circle.class) | ||
127 | * .registerSubtype(Diamond.class); | ||
128 | * }</pre> | ||
129 | * | ||
130 | * <h2>Serialization and deserialization</h2> | ||
131 | * In order to serialize and deserialize a polymorphic object, | ||
132 | * you must specify the base type explicitly. | ||
133 | * <pre> {@code | ||
134 | * Diamond diamond = new Diamond(); | ||
135 | * String json = gson.toJson(diamond, Shape.class); | ||
136 | * }</pre> | ||
137 | * And then: | ||
138 | * <pre> {@code | ||
139 | * Shape shape = gson.fromJson(json, Shape.class); | ||
140 | * }</pre> | ||
141 | */ | ||
142 | public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory { | ||
143 | private final Class<?> baseType; | ||
144 | private final String typeFieldName; | ||
145 | private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<>(); | ||
146 | private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<>(); | ||
147 | private final boolean maintainType; | ||
148 | private boolean recognizeSubtypes; | ||
149 | |||
150 | private RuntimeTypeAdapterFactory( | ||
151 | Class<?> baseType, String typeFieldName, boolean maintainType) { | ||
152 | if (typeFieldName == null || baseType == null) { | ||
153 | throw new NullPointerException(); | ||
154 | } | ||
155 | this.baseType = baseType; | ||
156 | this.typeFieldName = typeFieldName; | ||
157 | this.maintainType = maintainType; | ||
158 | } | ||
159 | |||
160 | /** | ||
161 | * Creates a new runtime type adapter using for {@code baseType} using {@code | ||
162 | * typeFieldName} as the type field name. Type field names are case sensitive. | ||
163 | * | ||
164 | * @param maintainType true if the type field should be included in deserialized objects | ||
165 | */ | ||
166 | public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName, boolean maintainType) { | ||
167 | return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType); | ||
168 | } | ||
169 | |||
170 | /** | ||
171 | * Creates a new runtime type adapter using for {@code baseType} using {@code | ||
172 | * typeFieldName} as the type field name. Type field names are case sensitive. | ||
173 | */ | ||
174 | public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) { | ||
175 | return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, false); | ||
176 | } | ||
177 | |||
178 | /** | ||
179 | * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as | ||
180 | * the type field name. | ||
181 | */ | ||
182 | public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) { | ||
183 | return new RuntimeTypeAdapterFactory<>(baseType, "type", false); | ||
184 | } | ||
185 | |||
186 | /** | ||
187 | * Ensures that this factory will handle not just the given {@code baseType}, but any subtype | ||
188 | * of that type. | ||
189 | */ | ||
190 | @CanIgnoreReturnValue | ||
191 | public RuntimeTypeAdapterFactory<T> recognizeSubtypes() { | ||
192 | this.recognizeSubtypes = true; | ||
193 | return this; | ||
194 | } | ||
195 | |||
196 | /** | ||
197 | * Registers {@code type} identified by {@code label}. Labels are case | ||
198 | * sensitive. | ||
199 | * | ||
200 | * @throws IllegalArgumentException if either {@code type} or {@code label} | ||
201 | * have already been registered on this type adapter. | ||
202 | */ | ||
203 | @CanIgnoreReturnValue | ||
204 | public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) { | ||
205 | if (type == null || label == null) { | ||
206 | throw new NullPointerException(); | ||
207 | } | ||
208 | if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { | ||
209 | throw new IllegalArgumentException("types and labels must be unique"); | ||
210 | } | ||
211 | labelToSubtype.put(label, type); | ||
212 | subtypeToLabel.put(type, label); | ||
213 | return this; | ||
214 | } | ||
215 | |||
216 | /** | ||
217 | * Registers {@code type} identified by its {@link Class#getSimpleName simple | ||
218 | * name}. Labels are case sensitive. | ||
219 | * | ||
220 | * @throws IllegalArgumentException if either {@code type} or its simple name | ||
221 | * have already been registered on this type adapter. | ||
222 | */ | ||
223 | @CanIgnoreReturnValue | ||
224 | public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) { | ||
225 | return registerSubtype(type, type.getSimpleName()); | ||
226 | } | ||
227 | |||
228 | @Override | ||
229 | public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) { | ||
230 | if (type == null) { | ||
231 | return null; | ||
232 | } | ||
233 | Class<?> rawType = type.getRawType(); | ||
234 | boolean handle = | ||
235 | recognizeSubtypes ? baseType.isAssignableFrom(rawType) : baseType.equals(rawType); | ||
236 | if (!handle) { | ||
237 | return null; | ||
238 | } | ||
239 | |||
240 | final TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class); | ||
241 | final Map<String, TypeAdapter<?>> labelToDelegate = new LinkedHashMap<>(); | ||
242 | final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate = new LinkedHashMap<>(); | ||
243 | for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) { | ||
244 | TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); | ||
245 | labelToDelegate.put(entry.getKey(), delegate); | ||
246 | subtypeToDelegate.put(entry.getValue(), delegate); | ||
247 | } | ||
248 | |||
249 | return new TypeAdapter<R>() { | ||
250 | @Override public R read(JsonReader in) throws IOException { | ||
251 | JsonElement jsonElement = jsonElementAdapter.read(in); | ||
252 | JsonElement labelJsonElement; | ||
253 | if (maintainType) { | ||
254 | labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); | ||
255 | } else { | ||
256 | labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); | ||
257 | } | ||
258 | |||
259 | if (labelJsonElement == null) { | ||
260 | throw new JsonParseException("cannot deserialize " + baseType | ||
261 | + " because it does not define a field named " + typeFieldName); | ||
262 | } | ||
263 | String label = labelJsonElement.getAsString(); | ||
264 | @SuppressWarnings("unchecked") // registration requires that subtype extends T | ||
265 | TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label); | ||
266 | if (delegate == null) { | ||
267 | throw new JsonParseException("cannot deserialize " + baseType + " subtype named " | ||
268 | + label + "; did you forget to register a subtype?"); | ||
269 | } | ||
270 | return delegate.fromJsonTree(jsonElement); | ||
271 | } | ||
272 | |||
273 | @Override public void write(JsonWriter out, R value) throws IOException { | ||
274 | Class<?> srcType = value.getClass(); | ||
275 | String label = subtypeToLabel.get(srcType); | ||
276 | @SuppressWarnings("unchecked") // registration requires that subtype extends T | ||
277 | TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType); | ||
278 | if (delegate == null) { | ||
279 | throw new JsonParseException("cannot serialize " + srcType.getName() | ||
280 | + "; did you forget to register a subtype?"); | ||
281 | } | ||
282 | JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); | ||
283 | |||
284 | if (maintainType) { | ||
285 | jsonElementAdapter.write(out, jsonObject); | ||
286 | return; | ||
287 | } | ||
288 | |||
289 | JsonObject clone = new JsonObject(); | ||
290 | |||
291 | if (jsonObject.has(typeFieldName)) { | ||
292 | throw new JsonParseException("cannot serialize " + srcType.getName() | ||
293 | + " because it already defines a field named " + typeFieldName); | ||
294 | } | ||
295 | clone.add(typeFieldName, new JsonPrimitive(label)); | ||
296 | |||
297 | for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) { | ||
298 | clone.add(e.getKey(), e.getValue()); | ||
299 | } | ||
300 | jsonElementAdapter.write(out, clone); | ||
301 | } | ||
302 | }.nullSafe(); | ||
303 | } | ||
304 | } | ||