001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.plugins.streetside.utils.api; 003 004import java.text.ParseException; 005import java.text.SimpleDateFormat; 006import java.util.Collection; 007import java.util.HashSet; 008import java.util.Locale; 009import java.util.function.Function; 010 011import javax.json.JsonArray; 012import javax.json.JsonNumber; 013import javax.json.JsonObject; 014import javax.json.JsonValue; 015 016import org.apache.log4j.Logger; 017import org.openstreetmap.josm.data.coor.LatLon; 018import org.openstreetmap.josm.tools.I18n; 019 020public final class JsonDecoder { 021 022 final static Logger logger = Logger.getLogger(JsonDecoder.class); 023 024 private JsonDecoder() { 025 // Private constructor to avoid instantiation 026 } 027 028 /** 029 * Parses a given {@link JsonObject} as a GeoJSON FeatureCollection into a {@link Collection} 030 * of the desired Java objects. The method, which converts the GeoJSON features into Java objects 031 * is given as a parameter to this method. 032 * @param <T> feature type 033 * @param json the {@link JsonObject} to be parsed 034 * @param featureDecoder feature decoder which transforms JSON objects to Java objects 035 * @return a {@link Collection} which is parsed from the given {@link JsonObject}, which contains GeoJSON. 036 * Currently a {@link HashSet} is used, but please don't rely on it, this could change at any time without 037 * prior notice. The return value will not be <code>null</code>. 038 */ 039 public static <T> Collection<T> decodeFeatureCollection(final JsonObject json, Function<JsonObject, T> featureDecoder) { 040 final Collection<T> result = new HashSet<>(); 041 if ( 042 json != null && "FeatureCollection".equals(json.getString("type", null)) && json.containsKey("features") 043 ) { 044 final JsonValue features = json.get("features"); 045 for (int i = 0; features instanceof JsonArray && i < ((JsonArray) features).size(); i++) { 046 final JsonValue val = ((JsonArray) features).get(i); 047 if (val instanceof JsonObject) { 048 final T feature = featureDecoder.apply((JsonObject) val); 049 if (feature != null) { 050 result.add(feature); 051 } 052 } 053 } 054 } 055 return result; 056 } 057 058 /** 059 * Decodes a {@link JsonArray} of exactly size 2 to a {@link LatLon} instance. 060 * The first value in the {@link JsonArray} is treated as longitude, the second one as latitude. 061 * @param json the {@link JsonArray} containing the two numbers 062 * @return the decoded {@link LatLon} instance, or <code>null</code> if the parameter is 063 * not a {@link JsonArray} of exactly size 2 containing two {@link JsonNumber}s. 064 */ 065 static LatLon decodeLatLon(final JsonArray json) { 066 final double[] result = decodeDoublePair(json); 067 if (result != null) { 068 return new LatLon(result[1], result[0]); 069 } 070 return null; 071 } 072 073 /** 074 * Decodes a pair of double values, which are stored in a {@link JsonArray} of exactly size 2. 075 * @param json the {@link JsonArray} containing the two values 076 * @return a double array which contains the two values in the same order, or <code>null</code> 077 * if the parameter was not a {@link JsonArray} of exactly size 2 containing two {@link JsonNumber}s 078 */ 079 @SuppressWarnings("PMD.ReturnEmptyArrayRatherThanNull") 080 static double[] decodeDoublePair(final JsonArray json) { 081 if ( 082 json != null && 083 json.size() == 2 && 084 json.get(0) instanceof JsonNumber && 085 json.get(1) instanceof JsonNumber 086 ) { 087 return new double[]{json.getJsonNumber(0).doubleValue(), json.getJsonNumber(1).doubleValue()}; 088 } 089 return null; 090 } 091 092 /** 093 * Decodes a timestamp formatted as a {@link String} to the equivalent UNIX epoch timestamp 094 * (number of milliseconds since 1970-01-01T00:00:00.000+0000). 095 * @param timestamp the timestamp formatted according to the format <code>yyyy-MM-dd'T'HH:mm:ss.SSSX</code> 096 * @return the point in time as a {@link Long} value representing the UNIX epoch time, or <code>null</code> if the 097 * parameter does not match the required format (this also triggers a warning via 098 * {@link logger#warn(Throwable)}), or the parameter is <code>null</code>. 099 */ 100 static Long decodeTimestamp(final String timestamp) { 101 if (timestamp != null) { 102 try { 103 return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX", Locale.UK).parse(timestamp).getTime(); 104 } catch (ParseException e) { 105 StackTraceElement calledBy = e.getStackTrace()[Math.min(e.getStackTrace().length - 1, 2)]; 106 logger.warn(I18n.tr(String.format( 107 "Could not decode time from the timestamp `%s` (called by %s.%s:%d)", 108 timestamp, calledBy.getClassName(), calledBy.getMethodName(), calledBy.getLineNumber() 109 ), e)); 110 } 111 } 112 return null; 113 } 114}