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

Last change on this file since 13678 was 13647, checked in by Don-vip, 6 years ago

see #16204 - Allow to start and close JOSM in WebStart sandbox mode (where every external access is denied). This was very useful to reproduce some very tricky bugs that occured in real life but were almost impossible to diagnose.

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