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

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

see #15310 - fix unit tests, warnings

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