[12851] | 1 | // License: GPL. For details, see LICENSE file.
|
---|
| 2 | package org.openstreetmap.josm.data;
|
---|
| 3 |
|
---|
| 4 | import java.io.StringReader;
|
---|
| 5 | import java.io.StringWriter;
|
---|
| 6 | import java.lang.annotation.Retention;
|
---|
| 7 | import java.lang.annotation.RetentionPolicy;
|
---|
| 8 | import java.lang.reflect.Field;
|
---|
| 9 | import java.util.ArrayList;
|
---|
| 10 | import java.util.Collection;
|
---|
| 11 | import java.util.Collections;
|
---|
| 12 | import java.util.HashMap;
|
---|
| 13 | import java.util.LinkedHashMap;
|
---|
| 14 | import java.util.List;
|
---|
| 15 | import java.util.Map;
|
---|
| 16 | import java.util.Objects;
|
---|
| 17 | import java.util.Optional;
|
---|
| 18 | import java.util.Set;
|
---|
| 19 | import java.util.stream.Collectors;
|
---|
| 20 |
|
---|
| 21 | import javax.json.Json;
|
---|
| 22 | import javax.json.JsonArray;
|
---|
| 23 | import javax.json.JsonArrayBuilder;
|
---|
| 24 | import javax.json.JsonObject;
|
---|
| 25 | import javax.json.JsonObjectBuilder;
|
---|
| 26 | import javax.json.JsonReader;
|
---|
| 27 | import javax.json.JsonString;
|
---|
| 28 | import javax.json.JsonValue;
|
---|
| 29 | import javax.json.JsonWriter;
|
---|
| 30 |
|
---|
| 31 | import org.openstreetmap.josm.spi.preferences.IPreferences;
|
---|
| 32 | import org.openstreetmap.josm.tools.JosmRuntimeException;
|
---|
| 33 | import org.openstreetmap.josm.tools.Logging;
|
---|
| 34 | import org.openstreetmap.josm.tools.MultiMap;
|
---|
| 35 | import 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}.
|
---|
[13563] | 41 | * Those fields will be respected when converting an object to a {@link Map} and back.
|
---|
[12851] | 42 | * @since 12851
|
---|
| 43 | */
|
---|
| 44 | public 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.
|
---|
[13563] | 71 | * Possible properties are given by fields of the class klass that have the @StructEntry annotation.
|
---|
[12851] | 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<String, String></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()) {
|
---|
[13173] | 159 | if (f.getAnnotation(StructEntry.class) == null) {
|
---|
[12851] | 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<String, String> and Map<String, List<String>>.
|
---|
| 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 | }
|
---|
[13173] | 214 | if (f.getAnnotation(StructEntry.class) == null) {
|
---|
[12851] | 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 | }
|
---|