GeoPointDeserializer.java

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.search.documents.implementation;

import com.azure.search.documents.models.GeoPoint;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonTokenId;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * Custom deserializer to detect GeoJSON structures in dynamic results and deserialize as instances of {@link GeoPoint}
 */
final class GeoPointDeserializer extends UntypedObjectDeserializer {
    private static final long serialVersionUID = 1L;
    private final UntypedObjectDeserializer defaultDeserializer;

    /**
     * Constructor
     *
     * @param defaultDeserializer the deserializer to use when a GeoJSON match is not found
     */
    GeoPointDeserializer(UntypedObjectDeserializer defaultDeserializer) {
        super(null, null);
        this.defaultDeserializer = defaultDeserializer;
    }

    @Override
    public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
        if (jp.currentTokenId() == JsonTokenId.ID_START_OBJECT) {
            Object obj = defaultDeserializer.deserialize(jp, ctxt);
            return parseGeoPoint(obj);
        } else if (jp.currentTokenId() == JsonTokenId.ID_START_ARRAY) {
            List<?> list = (List) defaultDeserializer.deserialize(jp, ctxt);
            return list.stream()
                .map(this::parseGeoPoint)
                .collect(Collectors.toList());
        } else {
            return defaultDeserializer.deserialize(jp, ctxt);
        }
    }

    /**
     * Converts an object to a GeoPoint if it is valid GeoJSON, otherwise returns the original object.
     *
     * @param obj the object to parse
     * @return an instance of {@link GeoPoint} if valid GeoJSON, otherwise obj.
     */
    @SuppressWarnings("unchecked")
    private Object parseGeoPoint(Object obj) {
        if (isGeoJsonPoint(obj)) {
            // We already know the class is a Map<String, Object> - validated in isGeoJsonPoint.
            Map<String, Object> map = (Map<String, Object>) obj;
            List<?> coordinates = (List) map.get("coordinates");

            Double latitude = coordinates.get(1).getClass() == Double.class
                ? (Double) coordinates.get(1)
                : Double.valueOf((Integer) coordinates.get(1));
            Double longitude = coordinates.get(0).getClass() == Double.class
                ? (Double) coordinates.get(0)
                : Double.valueOf((Integer) coordinates.get(0));

            return GeoPoint.create(latitude, longitude);
        } else {
            return obj;
        }
    }

    /**
     * Determines whether an object is valid GeoJSON object.
     *
     * @param obj the object to test
     * @return true if the object is valid GeoJSON, false otherwise.
     */
    @SuppressWarnings("unchecked")
    private boolean isGeoJsonPoint(Object obj) {
        try {
            if (obj instanceof Map) {
                Map<String, Object> map = (Map<String, Object>) obj;

                return isValidPoint(map)
                    && isValidCoordinates(map)
                    && isValidCrs(map);
            }

            return false;
        } catch (RuntimeException ex) {
            // somehow we got an object which isn't a Map<String, Object>
            return false;
        }
    }

    private boolean isValidPoint(Map<String, Object> map) {
        if (map != null && map.containsKey("type")) {
            return map.get("type").equals("Point");
        }

        return false;
    }

    @SuppressWarnings("unchecked")
    private boolean isValidCrs(Map<String, Object> map) {
        // crs is not required to deserialize, but must be valid if present
        boolean isValidCrs;
        if (map != null && map.containsKey("crs")) {
            Map<String, Object> crs = (Map<String, Object>) map.get("crs");
            boolean isValidType = crs.get("type").equals("name");

            Map<String, Object> properties = (Map<String, Object>) crs.get("properties");
            boolean isValidProperties = properties.get("name").equals("EPSG:4326");

            isValidCrs = isValidType && isValidProperties;
        } else {
            isValidCrs = true;
        }
        return isValidCrs;
    }

    private boolean isValidCoordinates(Map<String, Object> map) {
        boolean isValidCoordinates = false;
        List<?> coordinates = (List) map.get("coordinates");
        if (coordinates.size() == 2) {
            Class<?> longitudeClass = coordinates.get(0).getClass();
            boolean isValidLongitude = longitudeClass == Integer.class
                || longitudeClass == Double.class;

            Class<?> latitudeClass = coordinates.get(1).getClass();
            boolean isValidLatitude = latitudeClass == Integer.class
                || latitudeClass == Double.class;

            isValidCoordinates = isValidLongitude && isValidLatitude;
        }
        return isValidCoordinates;
    }
}