    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}