source: josm/trunk/src/org/openstreetmap/josm/data/StructUtils.java

Last change on this file was 18871, checked in by taylor.smock, 6 months ago

See #23218: Use newer error_prone versions when compiling on Java 11+

error_prone 2.11 dropped support for compiling with Java 8, although it still
supports compiling for Java 8. The "major" new check for us is NotJavadoc since
we used /** in quite a few places which were not javadoc.

Other "new" checks that are of interest:

  • AlreadyChecked: if (foo) { doFoo(); } else if (!foo) { doBar(); }
  • UnnecessaryStringBuilder: Avoid StringBuilder (Java converts + to StringBuilder behind-the-scenes, but may also do something else if it performs better)
  • NonApiType: Avoid specific interface types in function definitions
  • NamedLikeContextualKeyword: Avoid using restricted names for classes and methods
  • UnusedMethod: Unused private methods should be removed

This fixes most of the new error_prone issues and some SonarLint issues.

File size: 14.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data;
3
4import java.io.StringReader;
5import java.io.StringWriter;
6import java.lang.annotation.Retention;
7import java.lang.annotation.RetentionPolicy;
8import java.lang.reflect.Field;
9import java.util.ArrayList;
10import java.util.Arrays;
11import java.util.Collection;
12import java.util.Collections;
13import java.util.HashMap;
14import java.util.LinkedHashMap;
15import java.util.List;
16import java.util.Map;
17import java.util.Objects;
18import java.util.Optional;
19import java.util.Set;
20import java.util.stream.Collectors;
21
22import org.openstreetmap.josm.spi.preferences.IPreferences;
23import org.openstreetmap.josm.tools.JosmRuntimeException;
24import org.openstreetmap.josm.tools.Logging;
25import org.openstreetmap.josm.tools.MultiMap;
26import org.openstreetmap.josm.tools.ReflectionUtils;
27import org.openstreetmap.josm.tools.StringParser;
28import org.openstreetmap.josm.tools.Utils;
29
30import jakarta.json.Json;
31import jakarta.json.JsonArray;
32import jakarta.json.JsonArrayBuilder;
33import jakarta.json.JsonObject;
34import jakarta.json.JsonObjectBuilder;
35import jakarta.json.JsonReader;
36import jakarta.json.JsonString;
37import jakarta.json.JsonValue;
38import jakarta.json.JsonWriter;
39
40/**
41 * Utility methods to convert struct-like classes to a string map and back.
42 * <p>
43 * A "struct" is a class that has some fields annotated with {@link StructEntry}.
44 * Those fields will be respected when converting an object to a {@link Map} and back.
45 * @since 12851
46 */
47public final class StructUtils {
48
49 private static final StringParser STRING_PARSER = new StringParser(StringParser.DEFAULT)
50 .registerParser(Map.class, StructUtils::mapFromJson)
51 .registerParser(MultiMap.class, StructUtils::multiMapFromJson);
52
53 private StructUtils() {
54 // hide constructor
55 }
56
57 /**
58 * Annotation used for converting objects to String Maps and vice versa.
59 * Indicates that a certain field should be considered in the conversion process. Otherwise it is ignored.
60 *
61 * @see #serializeStruct
62 * @see #deserializeStruct(java.util.Map, java.lang.Class)
63 */
64 @Retention(RetentionPolicy.RUNTIME) // keep annotation at runtime
65 public @interface StructEntry { }
66
67 /**
68 * Annotation used for converting objects to String Maps.
69 * Indicates that a certain field should be written to the map, even if the value is the same as the default value.
70 *
71 * @see #serializeStruct
72 */
73 @Retention(RetentionPolicy.RUNTIME) // keep annotation at runtime
74 public @interface WriteExplicitly { }
75
76 /**
77 * Get a list of hashes which are represented by a struct-like class.
78 * Possible properties are given by fields of the class klass that have the @StructEntry annotation.
79 * Default constructor is used to initialize the struct objects, properties then override some of these default values.
80 * @param <T> klass type
81 * @param preferences preferences to look up the value
82 * @param key main preference key
83 * @param klass The struct class
84 * @return a list of objects of type T or an empty list if nothing was found
85 */
86 public static <T> List<T> getListOfStructs(IPreferences preferences, String key, Class<T> klass) {
87 return Optional.ofNullable(getListOfStructs(preferences, key, null, klass)).orElseGet(Collections::emptyList);
88 }
89
90 /**
91 * same as above, but returns def if nothing was found
92 * @param <T> klass type
93 * @param preferences preferences to look up the value
94 * @param key main preference key
95 * @param def default value
96 * @param klass The struct class
97 * @return a list of objects of type T or {@code def} if nothing was found
98 */
99 public static <T> List<T> getListOfStructs(IPreferences preferences, String key, Collection<T> def, Class<T> klass) {
100 List<Map<String, String>> prop =
101 preferences.getListOfMaps(key, def == null ? null : serializeListOfStructs(def, klass));
102 if (prop == null)
103 return def == null ? null : new ArrayList<>(def);
104 return prop.stream().map(p -> deserializeStruct(p, klass)).collect(Collectors.toList());
105 }
106
107 /**
108 * Convenience method that saves a MapListSetting which is provided as a collection of objects.
109 * <p>
110 * Each object is converted to a <code>Map&lt;String, String&gt;</code> using the fields with {@link StructEntry} annotation.
111 * The field name is the key and the value will be converted to a string.
112 * <p>
113 * Considers only fields that have the {@code @StructEntry} annotation.
114 * In addition it does not write fields with null values. (Thus they are cleared)
115 * Default values are given by the field values after default constructor has been called.
116 * Fields equal to the default value are not written unless the field has the {@link WriteExplicitly} annotation.
117 * @param <T> the class,
118 * @param preferences the preferences to save to
119 * @param key main preference key
120 * @param val the list that is supposed to be saved
121 * @param klass The struct class
122 * @return true if something has changed
123 */
124 public static <T> boolean putListOfStructs(IPreferences preferences, String key, Collection<T> val, Class<T> klass) {
125 return preferences.putListOfMaps(key, serializeListOfStructs(val, klass));
126 }
127
128 private static <T> List<Map<String, String>> serializeListOfStructs(Collection<T> l, Class<T> klass) {
129 if (l == null)
130 return null;
131 return l.stream().filter(Objects::nonNull)
132 .map(struct -> serializeStruct(struct, klass)).collect(Collectors.toList());
133 }
134
135 /**
136 * Options for {@link #serializeStruct}
137 */
138 public enum SerializeOptions {
139 /**
140 * Serialize {@code null} values
141 */
142 INCLUDE_NULL,
143 /**
144 * Serialize default values
145 */
146 INCLUDE_DEFAULT
147 }
148
149 /**
150 * Convert an object to a String Map, by using field names and values as map key and value.
151 * <p>
152 * The field value is converted to a String.
153 * <p>
154 * Only fields with annotation {@link StructEntry} are taken into account.
155 * <p>
156 * Fields will not be written to the map if the value is null or unchanged
157 * (compared to an object created with the no-arg-constructor).
158 * The {@link WriteExplicitly} annotation overrides this behavior, i.e. the default value will also be written.
159 *
160 * @param <T> the class of the object <code>struct</code>
161 * @param struct the object to be converted
162 * @param klass the class T
163 * @param options optional serialization options
164 * @return the resulting map (same data content as <code>struct</code>)
165 */
166 public static <T> Map<String, String> serializeStruct(T struct, Class<T> klass, SerializeOptions... options) {
167 List<SerializeOptions> optionsList = Arrays.asList(options);
168 T structPrototype;
169 try {
170 structPrototype = klass.getConstructor().newInstance();
171 } catch (ReflectiveOperationException ex) {
172 throw new IllegalArgumentException(ex);
173 }
174
175 Map<String, String> hash = new LinkedHashMap<>();
176 for (Field f : getDeclaredFieldsInClassOrSuperTypes(klass)) {
177 if (f.getAnnotation(StructEntry.class) == null) {
178 continue;
179 }
180 try {
181 ReflectionUtils.setObjectsAccessible(f);
182 Object fieldValue = f.get(struct);
183 Object defaultFieldValue = f.get(structPrototype);
184 boolean serializeNull = optionsList.contains(SerializeOptions.INCLUDE_NULL) || fieldValue != null;
185 boolean serializeDefault = optionsList.contains(SerializeOptions.INCLUDE_DEFAULT)
186 || f.getAnnotation(WriteExplicitly.class) != null
187 || !Objects.equals(fieldValue, defaultFieldValue);
188 if (serializeNull && serializeDefault) {
189 String key = f.getName().replace('_', '-');
190 if (fieldValue instanceof Map) {
191 hash.put(key, mapToJson((Map<?, ?>) fieldValue));
192 } else if (fieldValue instanceof MultiMap) {
193 hash.put(key, multiMapToJson((MultiMap<?, ?>) fieldValue));
194 } else if (fieldValue == null) {
195 hash.put(key, null);
196 } else {
197 hash.put(key, fieldValue.toString());
198 }
199 }
200 } catch (IllegalAccessException | SecurityException ex) {
201 throw new JosmRuntimeException(ex);
202 }
203 }
204 return hash;
205 }
206
207 /**
208 * Converts a String-Map to an object of a certain class, by comparing map keys to field names of the class and assigning
209 * map values to the corresponding fields.
210 * <p>
211 * The map value (a String) is converted to the field type. Supported types are: boolean, Boolean, int, Integer, double,
212 * Double, String, Map&lt;String, String&gt; and Map&lt;String, List&lt;String&gt;&gt;.
213 * <p>
214 * Only fields with annotation {@link StructEntry} are taken into account.
215 * @param <T> the class
216 * @param hash the string map with initial values
217 * @param klass the class T
218 * @return an object of class T, initialized as described above
219 */
220 public static <T> T deserializeStruct(Map<String, String> hash, Class<T> klass) {
221 T struct;
222 try {
223 struct = klass.getConstructor().newInstance();
224 } catch (ReflectiveOperationException ex) {
225 throw new IllegalArgumentException(ex);
226 }
227 for (Map.Entry<String, String> keyValue : hash.entrySet()) {
228 Field f = getDeclaredFieldInClassOrSuperTypes(klass, keyValue.getKey().replace('-', '_'));
229
230 if (f == null || f.getAnnotation(StructEntry.class) == null) {
231 continue;
232 }
233 ReflectionUtils.setObjectsAccessible(f);
234 Object value = STRING_PARSER.parse(f.getType(), keyValue.getValue());
235 try {
236 f.set(struct, value);
237 } catch (IllegalArgumentException ex) {
238 throw new AssertionError(ex);
239 } catch (IllegalAccessException ex) {
240 throw new JosmRuntimeException(ex);
241 }
242 }
243 return struct;
244 }
245
246 private static <T> Field getDeclaredFieldInClassOrSuperTypes(Class<T> clazz, String fieldName) {
247 Class<?> tClass = clazz;
248 do {
249 try {
250 return tClass.getDeclaredField(fieldName);
251 } catch (NoSuchFieldException ex) {
252 Logging.trace(ex);
253 }
254 tClass = tClass.getSuperclass();
255 } while (tClass != null);
256 return null;
257 }
258
259 private static <T> Field[] getDeclaredFieldsInClassOrSuperTypes(Class<T> clazz) {
260 List<Field> fields = new ArrayList<>();
261 Class<?> tclass = clazz;
262 do {
263 Collections.addAll(fields, tclass.getDeclaredFields());
264 tclass = tclass.getSuperclass();
265 } while (tclass != null);
266 return fields.toArray(new Field[] {});
267 }
268
269 @SuppressWarnings("rawtypes")
270 private static String mapToJson(Map map) {
271 StringWriter stringWriter = new StringWriter();
272 try (JsonWriter writer = Json.createWriter(stringWriter)) {
273 JsonObjectBuilder object = Json.createObjectBuilder();
274 for (Object o: map.entrySet()) {
275 Map.Entry e = (Map.Entry) o;
276 Object evalue = e.getValue();
277 object.add(e.getKey().toString(), evalue.toString());
278 }
279 writer.writeObject(object.build());
280 }
281 return stringWriter.toString();
282 }
283
284 @SuppressWarnings({ "rawtypes", "unchecked" })
285 private static Map mapFromJson(String s) {
286 Map ret;
287 try (JsonReader reader = Json.createReader(new StringReader(s))) {
288 JsonObject object = reader.readObject();
289 ret = new HashMap(Utils.hashMapInitialCapacity(object.size()));
290 for (Map.Entry<String, JsonValue> e: object.entrySet()) {
291 JsonValue value = e.getValue();
292 if (value instanceof JsonString) {
293 // in some cases, when JsonValue.toString() is called, then additional quotation marks are left in value
294 ret.put(e.getKey(), ((JsonString) value).getString());
295 } else {
296 ret.put(e.getKey(), e.getValue().toString());
297 }
298 }
299 }
300 return ret;
301 }
302
303 @SuppressWarnings("rawtypes")
304 private static String multiMapToJson(MultiMap map) {
305 StringWriter stringWriter = new StringWriter();
306 try (JsonWriter writer = Json.createWriter(stringWriter)) {
307 JsonObjectBuilder object = Json.createObjectBuilder();
308 for (Object o: map.entrySet()) {
309 Map.Entry e = (Map.Entry) o;
310 Set evalue = (Set) e.getValue();
311 JsonArrayBuilder a = Json.createArrayBuilder();
312 for (Object evo: evalue) {
313 a.add(evo.toString());
314 }
315 object.add(e.getKey().toString(), a.build());
316 }
317 writer.writeObject(object.build());
318 }
319 return stringWriter.toString();
320 }
321
322 @SuppressWarnings({ "rawtypes", "unchecked" })
323 private static MultiMap multiMapFromJson(String s) {
324 MultiMap ret;
325 try (JsonReader reader = Json.createReader(new StringReader(s))) {
326 JsonObject object = reader.readObject();
327 ret = new MultiMap(object.size());
328 for (Map.Entry<String, JsonValue> e: object.entrySet()) {
329 JsonValue value = e.getValue();
330 if (value instanceof JsonArray) {
331 for (JsonString js: ((JsonArray) value).getValuesAs(JsonString.class)) {
332 ret.put(e.getKey(), js.getString());
333 }
334 } else if (value instanceof JsonString) {
335 // in some cases, when JsonValue.toString() is called, then additional quotation marks are left in value
336 ret.put(e.getKey(), ((JsonString) value).getString());
337 } else {
338 ret.put(e.getKey(), e.getValue().toString());
339 }
340 }
341 }
342 return ret;
343 }
344}
Note: See TracBrowser for help on using the repository browser.