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

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

see #15310 - fix javadoc

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 Utils.setObjectsAccessible(f);
163 try {
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 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.