com.mapcode.services.implementation.RootResourceImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.mapcode.services.implementation.RootResourceImpl.java

Source

/*
 * Copyright (C) 2016-2019, Stichting Mapcode Foundation (http://www.mapcode.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mapcode.services.implementation;

import com.mapcode.services.MapcodeResource;
import com.mapcode.services.RootResource;
import com.mapcode.services.dto.MapcodeDTO;
import com.mapcode.services.dto.PointDTO;
import com.mapcode.services.dto.VersionDTO;
import com.mapcode.services.metrics.SystemMetrics;
import com.tomtom.speedtools.json.Json;
import com.tomtom.speedtools.maven.MavenProperties;
import com.tomtom.speedtools.time.UTCTime;
import com.tomtom.speedtools.utils.MathUtils;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.inject.Inject;
import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.container.Suspended;
import javax.ws.rs.core.Response;

/**
 * This class implements the REST API that deals with the root resource for the Mapcode REST API.
 * This includes methods to get HTML help, the service version and server status.
 */
public class RootResourceImpl implements RootResource {
    private static final Logger LOG = LoggerFactory.getLogger(RootResourceImpl.class);

    @Nonnull
    private static final String HELP_TEXT = ""
            + "IMPORTANT NOTICE: The Mapcode Foundation provides an implementation of the Mapcode REST API at:\n"
            + "----------------- https://api.mapcode.com/mapcode\n\n" +

            "                  This free online service is provided for demonstration purposes only and the Mapcode\n"
            + "                  Foundation accepts no claims on its availability or reliability, although we'll try hard\n"
            + "                  to provide a stable and decent service. Note that usage or log data may be collected, only\n"
            + "                  to further improve service (never for commercial purposes). If you are interested in using\n"
            + "                  the service for professional purposes, or in high-availability or high-demands contexts,\n"
            + "                  you may wish to consider self-hosting this service. The source code is open-source and\n"
            + "                  available at: https://github.com/mapcode-foundation/mapcode-rest-service\n\n" +

            "The REST API Methods\n" + "--------------------\n\n" +

            "All REST services (except 'metrics') are able to return both JSON and XML. Use the HTTP\n"
            + "'Accept:' header to specify the expected format: application/json or application/xml\n"
            + "If the 'Accept:' header is omitted, JSON is assumed.\n\n" +

            "GET /mapcode         Returns this help page.\n"
            + "GET /mapcode/version Returns the software version.\n"
            + "GET /mapcode/metrics Returns some system metrics (JSON-only, also available from JMX).\n"
            + "GET /mapcode/status  Returns 200 if the service OK.\n\n" +

            "GET /mapcode/codes/{lat},{lon}[/[mapcodes|local|international]]\n"
            + "     [?precision=[0..8] & territory={restrictToTerritory} & country={restrictToCountry}\n"
            + "     alphabet={alphabet} & include={offset|territory|alphabet|rectangle}]\n\n" +

            "   Convert latitude/longitude to one or more mapcodes. The response always contains the 'international' mapcode and\n"
            + "   only contains a 'local' mapcode if there are any non-international mapcode AND they are all of the same territory.\n\n"
            +

            "   The 'country' parameter always specifies a country, by a 2 or 3 character ISO-3166 code, like 'US' or 'USA'\n"
            + "   (for the USA), and 'NL' or 'NLD' (for the Netherlands). In a web environment, the country code is often available\n"
            + "   as a 2-character code. That code can be used for this parameter.\n\n" +

            "   The 'territory' parameter is a 2, 3 or 5 (XX-YY) character code. These code can be countries or states within countries.\n"
            + "   Some 2 character state codes are the same as country codes. In that case, the territory implies the state, not the country.\n"
            + "   For example, the territory code 'US' is unambiguous and means USA, but 'NL' means 'IN-NL' (Nagaland, India) rather than\n"
            + "   the Netherlands. You cannot use the standard 2-character country codes in web applications for this parameter.\n\n"
            +

            "   Path parameters:\n"
            + "     lat             : Latitude, range [-90, 90] (automatically limited to this range).\n"
            + "     lon             : Longitude, range [-180, 180] (automatically wrapped to this range).\n\n" +

            "   An additional filter can be specified to limit the results:\n"
            + "     mapcodes        : Same as without specifying a filter, returns all mapcodes.\n"
            + "     local           : Return the shortest local mapcode (not an international code). Note that multiple local\n"
            + "                       mapcodes may exist for a location, with different territories. This method returns the\n"
            + "                       shortest code. It does not check if the territory is the 'geographically correct' one\n"
            + "                       for the coordinates. To get the shortest code for a specific territory, you need to explicitly\n"
            + "                       specify the territory with 'territory=' parameter in the query.\n"
            + "     international   : Return the international mapcode.\n\n" +

            "   Query parameters:\n" + "     precision       : Precision, range [0..8] (default=0).\n"
            + "     territory       : Territory (country or state) to restrict results to (name or alphacode).\n"
            + "     country         : Country to restrict results to (name or alphacode).\n"
            + "     alphabet        : Alphabet to return results in.\n"
            + "     include         : Multiple options may be set, separated by comma's:\n"
            + "                         offset    = Include offset from mapcode center to lat/lon (in meters).\n"
            + "                         territory = Always include territory in result, also for territory 'AAA'.\n"
            + "                         alphabet  = Always the mapcodeInAlphabet, also if same as mapcode.\n"
            + "                         rectangle = Include the encompassing rectangle of a mapcode.\n\n" +

            "                       Note that you can use 'include=territory,alphabet' to ensure the territory code\n"
            + "                       is always present, as well as the translated territory and mapcode codes.\n"
            + "                       This can make processing the records easier in scripts, for example.\n\n" +

            "GET /mapcode/coords/{code} [?context={territory} & include={include}]\n"
            + "   Convert a mapcode into a latitude/longitude pair.\n\n" +

            "   Path parameters:\n"
            + "     code            : Mapcode code (local or international). You can specify the territory in the code itself,\n"
            + "                       like 'NLD%20XX.XX' (note that the space is URL-encoded to '%20'), or you specifty the\n"
            + "                       territory separately in the 'context=' parameter, like 'XX.XX?context-NLD'.\n"
            +

            "   Query parameters:\n"
            + "     context         : Optional mapcode territory context (name or alphacode). The context is only used if the\n\n"
            + "                       code is ambiguous without it, otherwise it is ignored. For example, the context is ignored\n"
            + "                       when converting an international code (but it is not considered an error to provide it).\n"
            + "     include         : An additional option may be set:\n"
            + "                         rectangle = Include the encompassing rectangle of a mapcode.\n\n" +

            "GET /mapcode/territories [?offset={offset}&count={count}]\n"
            + "   Return a list of all territories.\n\n" +

            "GET /mapcode/territories/{territory} [?context={territory}]\n"
            + "   Return information for a single territory code.\n\n" +

            "   Path parameters:\n" + "     territory       : Territory to get info for (name or alphacode).\n\n" +

            "   Query parameters:\n"
            + "     context         : Territory context (optional, for disambiguation, name or alphacode).\n"
            + "                       The context can only be: USA IND CAN AUS MEX BRA RUS CHN ATA\n\n" +

            "GET /mapcode/alphabets [?offset={offset}&count={count}]\n"
            + "   Return a list of all alphabet codes.\n\n" +

            "GET /mapcode/alphabets/{alphabet}\n" + "   Return information for a specific alphabet.\n\n" +

            "   Path parameters:\n" + "     alphabet        : Alphabet to get info for.\n\n" +

            "General query parameters for methods which return a list of results:\n\n"
            + "   offset            : Return list from 'offset' (negative value start counting from end).\n"
            + "   count             : Return 'count' items at most.\n\n" +

            "JSON and XML Responses\n" + "----------------------\n\n" +

            "The REST API methods defined above obey the HTTP \"Accept:\" header. To retrieve JSON responses,\n"
            + "use \"Accept:application/json\", to retrieve XML responses, use \"Accept:application/xml\".\n\n" +

            "The default response type is JSON, if no \"Accept:\" header is specified.\n\n" +

            "Note that some browsers (such as FireFox) may silently insert an 'Accept:' header when using a\n"
            + "XMLHttpRequest in Javascript. This may lead to returning JSON in some browsers and XML in others.\n"
            + "It is advised to explicitly set the appropriate header in Javascript in such cases.\n\n" +

            "Alternatively, to retrieve XML or JSON responses if no \"Accept:\" header is specified, you can add\n"
            + "\"/xml\" or \"/json\" in the URL, directly after \"/mapcode\".\n\n" +

            "So, the following methods are supported as well and return XML or JSON by default:\n\n" +

            "    GET /mapcode/xml/version           GET /mapcode/json/version\n"
            + "    GET /mapcode/xml/status            GET /mapcode/json/status\n"
            + "    GET /mapcode/xml/codes             GET /mapcode/json/codes\n"
            + "    GET /mapcode/xml/coords            GET /mapcode/json/coords\n"
            + "    GET /mapcode/xml/territories       GET /mapcode/json/territories\n"
            + "    GET /mapcode/xml/alphabets         GET /mapcode/json/alphabets\n";

    private final MapcodeResource mapcodeResource;
    private final MavenProperties mavenProperties;
    private final SystemMetrics metrics;

    @Inject
    public RootResourceImpl(@Nonnull final MapcodeResource mapcodeResource,
            @Nonnull final MavenProperties mavenProperties, @Nonnull final SystemMetrics metrics) {
        assert mapcodeResource != null;
        assert mavenProperties != null;
        assert metrics != null;

        // Store the injected values.
        this.mapcodeResource = mapcodeResource;
        this.mavenProperties = mavenProperties;
        this.metrics = metrics;
    }

    @Override
    @Nonnull
    public String getHelpHTML() {
        LOG.info("getHelpHTML: show help page, version {}", mavenProperties.getPomVersion());
        return "<html><pre>\n" + "MAPCODE API (" + mavenProperties.getPomVersion() + ")\n" + "-----------\n\n"
                + HELP_TEXT + "</pre></html>\n";
    }

    @Override
    public void getVersion(@Suspended @Nonnull final AsyncResponse response) {
        assert response != null;

        // No input validation required. Just return version number.
        final String pomVersion = mavenProperties.getPomVersion();
        LOG.info("getVersion: POM version={}", pomVersion);

        // Create the response binder and validate it (returned objects must also be validated!).
        // Validation errors are automatically caught as exceptions and returned by the framework.
        final VersionDTO result = new VersionDTO(pomVersion); // Create a binder.
        result.validate(); // You must validate it before using it.

        // Build the response and return it.
        response.resume(Response.ok(result).build());
    }

    @Override
    public void getStatus(@Suspended @Nonnull final AsyncResponse response) {
        assert response != null;
        LOG.info("getStatus: get status");

        try {
            // Execute a lat/lon to mapcode conversion
            final double latDeg = 52.158974;
            final double lonDeg = 4.492479;
            final String mapcode = "QJM.1G";
            final String territory = "NLD";
            final int precision = 0;
            final String include = "";
            final String client = "";
            final String allowLog = "false";
            final TestAsyncResponse asyncResponse1 = new TestAsyncResponse();
            mapcodeResource.convertLatLonToMapcode(String.valueOf(latDeg), String.valueOf(lonDeg), "local",
                    String.valueOf(precision), territory, null, null, null, include, client, allowLog,
                    asyncResponse1);
            waitForResponse(asyncResponse1);

            // Check if conversion was OK.
            if (asyncResponse1.getResponse() instanceof Response) {
                final Response response1 = (Response) asyncResponse1.getResponse();
                if (response1.getEntity() instanceof MapcodeDTO) {
                    final MapcodeDTO result1 = (MapcodeDTO) response1.getEntity();
                    final MapcodeDTO expected1 = new MapcodeDTO(mapcode, null, territory, null);
                    //noinspection ConstantConditions
                    if (expected1.getMapcode().equals(result1.getMapcode())
                            && expected1.getTerritory().equals(result1.getTerritory())) {

                        // Now execute a mapcode to lat/lon conversion.
                        final TestAsyncResponse asyncResponse2 = new TestAsyncResponse();
                        mapcodeResource.convertMapcodeToLatLon(mapcode, territory, null, include, client, allowLog,
                                asyncResponse2);
                        waitForResponse(asyncResponse2);

                        if (asyncResponse2.getResponse() instanceof Response) {
                            final Response response2 = (Response) asyncResponse2.getResponse();
                            if (response2.getEntity() instanceof PointDTO) {
                                final PointDTO result2 = (PointDTO) response2.getEntity();
                                final PointDTO expected2 = new PointDTO(latDeg, lonDeg);
                                if (MathUtils.isAlmostEqual(expected2.getLatDeg(), result2.getLatDeg())
                                        && MathUtils.isAlmostEqual(expected2.getLonDeg(), result2.getLonDeg())) {

                                    // Yes, all is OK.
                                    response.resume(Response.ok().build());
                                    return;
                                }
                            }
                        }
                        response.resume(Response.ok().build());
                        return;
                    }
                }
            }
        } catch (final InterruptedException ignored) {
            // Empty.
        }

        // This are not OK.
        response.resume(Response.serverError().build());
    }

    @Override
    public void getMetrics(@Suspended @Nonnull final AsyncResponse response) {
        assert response != null;
        LOG.info("getMetrics");

        // No input validation required. Just return metrics as a plain JSON string.
        final String json = Json.toJson(metrics);
        response.resume(Response.ok(json).build());
    }

    private static void waitForResponse(@Nonnull final TestAsyncResponse asyncResponse)
            throws InterruptedException {
        final int sleepMillis = 100; // No more than 10 reqs/sec.
        final DateTime now = UTCTime.now();
        final DateTime until = now.plusSeconds(10); // Wait at most 10 seconds.
        while (now.isBefore(until)) {
            if (asyncResponse.isReady()) {
                if (asyncResponse.getResponse() != null) { // If the object is null, retry.

                    // All OK.
                    return;
                }
            }
            //noinspection BusyWait
            Thread.sleep(sleepMillis);
        }
        throw new IllegalStateException("Did not receive a response");
    }
}