org.niord.web.api.ApiRestService.java Source code

Java tutorial

Introduction

Here is the source code for org.niord.web.api.ApiRestService.java

Source

/*
 * Copyright 2016 Danish Maritime Authority.
 *
 * 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 org.niord.web.api;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.apache.commons.lang.StringUtils;
import org.jboss.resteasy.annotations.GZIP;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.niord.core.NiordApp;
import org.niord.core.area.Area;
import org.niord.core.message.Message;
import org.niord.core.publication.Publication;
import org.niord.model.DataFilter;
import org.niord.model.message.AreaVo;
import org.niord.model.message.MainType;
import org.niord.model.message.MessageVo;
import org.niord.model.publication.PublicationVo;
import org.niord.model.search.PagedSearchResultVo;

import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.SchemaOutputResolver;
import javax.xml.transform.Result;
import javax.xml.transform.stream.StreamResult;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * A public REST API for accessing message and publications Niord data.
 * <p>
 * Also, defines the Swagger API using annotations.
 * <p>
 * NB: Swagger codegen-generated model classes cannot handle UNIX Epoch timestamps, which is the date format
 * used in JSON throughout Niord. Instead, these classes expects dates to be encoded as ISO-8601 strings
 * (e.g. "2016-10-12T13:21:22.000+0000").<br>
 * To facilitate both formats, use the "dateFormat" parameter.
 */
@Api(value = "/public/v1", description = "Public API for accessing message and publication data from the Niord NW-NM system", tags = {
        "messages", "publications" })
@Path("/public/v1")
@Stateless
@SuppressWarnings("unused")
public class ApiRestService extends AbstractApiService {

    /**
     * The format to use for dates in generated JSON
     **/
    public enum JsonDateFormat {
        UNIX_EPOCH, ISO_8601
    }

    @Inject
    NiordApp app;

    /***************************
     * Message end-points
     ***************************/

    /**
     * {@inheritDoc}
     */
    @ApiOperation(value = "Returns the published NW and NM messages", response = MessageVo.class, responseContainer = "List", tags = {
            "messages" })
    @GET
    @Path("/messages")
    @Produces({ "application/json;charset=UTF-8" })
    @GZIP
    public Response searchMessages(
            @ApiParam(value = "Two-letter ISO 639-1 language code", example = "en") @QueryParam("lang") String language,

            @ApiParam(value = "The IDs of the domains to select messages from", example = "niord-client-nw") @QueryParam("domain") Set<String> domainIds,

            @ApiParam(value = "Specific message series to select messages from", example = "dma-nw") @QueryParam("messageSeries") Set<String> messageSeries,

            @ApiParam(value = "The IDs of the publications to select message from") @QueryParam("publication") Set<String> publicationIds,

            @ApiParam(value = "The IDs of the areas to select messages from", example = "urn:mrn:iho:country:dk") @QueryParam("areaId") Set<String> areaIds,

            @ApiParam(value = "Either NW (navigational warnings) or NM (notices to mariners)", example = "NW") @QueryParam("mainType") Set<MainType> mainTypes,

            @ApiParam(value = "Well-Known Text for geographical extent", example = "POLYGON((7 54, 7 57, 13 56, 13 57, 7 54))") @QueryParam("wkt") String wkt,

            @ApiParam(value = "Whether to rewrite all embedded links and paths to be absolute URL's", example = "true") @QueryParam("externalize") @DefaultValue("true") boolean externalize,

            @ApiParam(value = "The date format to use for JSON date-time encoding. Either 'UNIX_EPOCH' or 'ISO_8601'", example = "UNIX_EPOCH") @QueryParam("dateFormat") @DefaultValue("UNIX_EPOCH") JsonDateFormat dateFormat

    ) throws Exception {

        // Perform the search
        PagedSearchResultVo<Message> searchResult = super.searchMessages(language, domainIds, messageSeries,
                publicationIds, areaIds, mainTypes, wkt);

        // Convert messages to value objects and externalize message links, if requested
        List<MessageVo> messages = searchResult.map(m -> toMessageVo(m, language, externalize)).getData();

        // Depending on the dateFormat param, either use UNIX epoch or ISO-8601
        StreamingOutput stream = os -> objectMapperForDateFormat(dateFormat).writeValue(os, messages);

        return Response.ok(stream, MediaType.APPLICATION_JSON_TYPE.withCharset("utf-8")).build();

    }

    /**
     * {@inheritDoc}
     */
    @ApiOperation(value = "Returns the public (published, cancelled or expired) NW or NM message details", response = MessageVo.class, tags = {
            "messages" })
    @GET
    @Path("/message/{messageId}")
    @Produces({ "application/json;charset=UTF-8" })
    @GZIP
    public Response messageDetails(
            @ApiParam(value = "The message UID or short ID", example = "NM-1275-16") @PathParam("messageId") String messageId,

            @ApiParam(value = "Two-letter ISO 639-1 language code", example = "en") @QueryParam("lang") String language,

            @ApiParam(value = "Whether to rewrite all embedded links and paths to be absolute URL's", example = "true") @QueryParam("externalize") @DefaultValue("true") boolean externalize,

            @ApiParam(value = "The date format to use for JSON date-time encoding. Either 'UNIX_EPOCH' or 'ISO_8601'", example = "UNIX_EPOCH") @QueryParam("dateFormat") @DefaultValue("UNIX_EPOCH") JsonDateFormat dateFormat

    ) throws Exception {

        // Perform the search
        Message message = super.getMessage(messageId);

        if (message == null) {
            return Response.status(Response.Status.NOT_FOUND).entity("No message found with ID: " + messageId)
                    .build();
        } else {

            // Convert message to value objects and externalize message links, if requested
            MessageVo result = toMessageVo(message, language, externalize);

            // Depending on the dateFormat param, either use UNIX epoch or ISO-8601
            StreamingOutput stream = os -> objectMapperForDateFormat(dateFormat).writeValue(os, result);

            return Response.ok(stream, MediaType.APPLICATION_JSON_TYPE.withCharset("utf-8")).build();
        }
    }

    /**
     * Returns the XSD for the Message class.
     * Two XSDs are produced, "schema1.xsd" and "schema2.xsd". The latter is the main schema for
     * Message and will import the former.
     * @return the XSD for the Message class
     */
    @ApiOperation(value = "Returns XSD model of the Message class", tags = { "messages" })
    @GET
    @Path("/xsd/{schemaFile}")
    @Produces("application/xml;charset=UTF-8")
    @GZIP
    @NoCache
    public String getMessageXsd(
            @ApiParam(value = "The schema file, either schema1.xsd or schema2.xsd", example = "schema2.xsd") @PathParam("schemaFile") String schemaFile)
            throws Exception {

        if (!schemaFile.equals("schema1.xsd") && !schemaFile.equals("schema2.xsd")) {
            throw new Exception("Only 'schema1.xsd' and 'schema2.xsd' allowed");
        }

        JAXBContext jaxbContext = JAXBContext.newInstance(MessageVo.class);

        Map<String, StringWriter> result = new HashMap<>();
        SchemaOutputResolver sor = new SchemaOutputResolver() {
            @Override
            public Result createOutput(String namespaceURI, String suggestedFileName) throws IOException {
                StringWriter out = new StringWriter();
                result.put(suggestedFileName, out);
                StreamResult result = new StreamResult(out);
                result.setSystemId(app.getBaseUri() + "/rest/public/v1/xsd/" + suggestedFileName);
                return result;
            }

        };

        // Generate the schemas
        jaxbContext.generateSchema(sor);

        return result.get(schemaFile).toString();
    }

    /**
     * Convert the message to a value object representation.
     * If requested, rewrite all links to make them external URLs.
     * @param msg the message to convert to a value object
     * @param externalize whether to rewrite all links to make them external URLs
     * @return the message value object representation
     **/
    private MessageVo toMessageVo(Message msg, String language, boolean externalize) {

        // Sanity check
        if (msg == null) {
            return null;
        }

        // Convert the message to a value object
        DataFilter filter = Message.MESSAGE_DETAILS_FILTER.lang(language);
        MessageVo message = msg.toVo(MessageVo.class, filter);

        // If "externalize" is set, rewrite all links to make them external
        if (externalize) {
            String baseUri = app.getBaseUri();

            if (message.getParts() != null) {
                String from = concat("/rest/repo/file", msg.getRepoPath());
                String to = concat(baseUri, from);
                message.getParts().forEach(mp -> mp.rewriteRepoPath(from, to));
            }
            if (message.getAttachments() != null) {
                String to = concat(baseUri, "rest/repo/file", msg.getRepoPath());
                message.getAttachments().forEach(att -> att.rewriteRepoPath(msg.getRepoPath(), to));
            }
        }

        return message;
    }

    /***************************
     * Publication end-points
     ***************************/

    /**
     * Searches for publications
     */
    @ApiOperation(value = "Returns the publications", response = PublicationVo.class, responseContainer = "List", tags = {
            "publications" })
    @GET
    @Path("/publications")
    @Produces({ "application/json;charset=UTF-8" })
    @GZIP
    public Response searchPublications(

            @ApiParam(value = "Two-letter ISO 639-1 language code", example = "en") @QueryParam("lang") String language,

            @ApiParam(value = "Timestamp (Unix epoch) for the start date of the publications") @QueryParam("from") Long from,

            @ApiParam(value = "Timestamp (Unix epoch) for the end date of the publications") @QueryParam("to") Long to,

            @ApiParam(value = "Whether to rewrite all embedded links and paths to be absolute URL's", example = "true") @QueryParam("externalize") @DefaultValue("true") boolean externalize,

            @ApiParam(value = "The date format to use for JSON date-time encoding. Either 'UNIX_EPOCH' or 'ISO_8601'", example = "UNIX_EPOCH") @QueryParam("dateFormat") @DefaultValue("UNIX_EPOCH") JsonDateFormat dateFormat) {

        // If from and to-dates are unspecified, return the publications currently active
        if (from == null && to == null) {
            from = to = System.currentTimeMillis();
        }

        List<PublicationVo> publications = super.searchPublications(language, from, to).stream()
                .map(p -> toPublicationVo(p, language, externalize)).collect(Collectors.toList());

        // Depending on the dateFormat param, either use UNIX epoch or ISO-8601
        StreamingOutput stream = os -> objectMapperForDateFormat(dateFormat).writeValue(os, publications);

        return Response.ok(stream, MediaType.APPLICATION_JSON_TYPE.withCharset("utf-8")).build();
    }

    /**
     * Returns the details for the given publications
     */
    @ApiOperation(value = "Returns the publication with the given ID", response = PublicationVo.class, tags = {
            "publications" })
    @GET
    @Path("/publication/{publicationId}")
    @Produces({ "application/json;charset=UTF-8" })
    @GZIP
    public Response publicationDetails(

            @ApiParam(value = "The publication ID", example = "5eab7f50-d890-42d9-8f0a-d30e078d3d5a") @PathParam("publicationId") String publicationId,

            @ApiParam(value = "Two-letter ISO 639-1 language code", example = "en") @QueryParam("lang") String language,

            @ApiParam(value = "Whether to rewrite all embedded links and paths to be absolute URL's", example = "true") @QueryParam("externalize") @DefaultValue("true") boolean externalize,

            @ApiParam(value = "The date format to use for JSON date-time encoding. Either 'UNIX_EPOCH' or 'ISO_8601'", example = "UNIX_EPOCH") @QueryParam("dateFormat") @DefaultValue("UNIX_EPOCH") JsonDateFormat dateFormat) {

        Publication publication = super.getPublication(publicationId);

        if (publication == null) {
            return Response.status(Response.Status.NOT_FOUND)
                    .entity("No publication found with ID: " + publicationId).build();
        } else {

            // Convert publication to value objects and externalize publication links, if requested
            PublicationVo result = toPublicationVo(publication, language, externalize);

            // Depending on the dateFormat param, either use UNIX epoch or ISO-8601
            StreamingOutput stream = os -> objectMapperForDateFormat(dateFormat).writeValue(os, result);

            return Response.ok(stream, MediaType.APPLICATION_JSON_TYPE.withCharset("utf-8")).build();
        }
    }

    /**
     * Convert the publication to a value object representation.
     * If requested, rewrite all links to make them external URLs.
     * @param pub the publication to convert to a value object
     * @param externalize whether to rewrite all links to make them external URLs
     * @return the publication value object representation
     **/
    private PublicationVo toPublicationVo(Publication pub, String language, boolean externalize) {

        // Sanity check
        if (pub == null) {
            return null;
        }

        // Convert the publication to a value object
        DataFilter filter = DataFilter.get().lang(language);
        PublicationVo publication = pub.toVo(PublicationVo.class, filter);

        // If "externalize" is set, rewrite all links to make them external
        if (externalize) {
            String baseUri = app.getBaseUri();

            if (publication.getDescs() != null) {
                publication.getDescs().stream().filter(d -> StringUtils.isNotBlank(d.getLink()))
                        .filter(d -> !d.getLink().matches("^(?i)(https?)://.*$"))
                        .forEach(d -> d.setLink(concat(baseUri, d.getLink())));
            }
        }

        return publication;
    }

    /***************************
     * Area end-points
     ***************************/

    /**
     * Returns the details for the given area
     */
    @ApiOperation(value = "Returns the area with the given ID", response = AreaVo.class, tags = { "areas" })
    @GET
    @Path("/area/{areaId}")
    @Produces({ "application/json;charset=UTF-8" })
    @GZIP
    public Response areaDetails(

            @ApiParam(value = "The area ID", example = "urn:mrn:iho:country:dk") @PathParam("areaId") String areaId,

            @ApiParam(value = "Two-letter ISO 639-1 language code", example = "en") @QueryParam("lang") String language) {

        Area area = super.getArea(areaId);

        if (area == null) {
            return Response.status(Response.Status.NOT_FOUND).entity("No area found with ID: " + areaId).build();
        } else {

            // Convert area to value objects
            AreaVo result = area.toVo(AreaVo.class, DataFilter.get().fields(DataFilter.PARENT).lang(language));

            return Response.ok(result, MediaType.APPLICATION_JSON_TYPE.withCharset("utf-8")).build();
        }
    }

    /**
     * Returns the details for the given area
     */
    @ApiOperation(value = "Returns the sub-areas of the area with the given ID", response = AreaVo.class, responseContainer = "List", tags = {
            "areas" })
    @GET
    @Path("/area/{areaId}/sub-areas")
    @Produces({ "application/json;charset=UTF-8" })
    @GZIP
    public Response subAreas(

            @ApiParam(value = "The area ID", example = "urn:mrn:iho:country:dk") @PathParam("areaId") String areaId,

            @ApiParam(value = "Two-letter ISO 639-1 language code", example = "en") @QueryParam("lang") String language) {

        Area area = super.getArea(areaId);

        if (area == null) {
            return Response.status(Response.Status.NOT_FOUND).entity("No area found with ID: " + areaId).build();
        } else {

            // NB: Area.getChildren() will return sub-areas in sibling sort order
            List<AreaVo> result = area.getChildren().stream().filter(Area::isActive)
                    .map(a -> a.toVo(AreaVo.class, DataFilter.get().lang(language))).collect(Collectors.toList());

            return Response.ok(result, MediaType.APPLICATION_JSON_TYPE.withCharset("utf-8")).build();
        }
    }

    /***************************
     * Utility functions
     ***************************/

    /** Returns an ObjectMapper for the given date format **/
    private ObjectMapper objectMapperForDateFormat(JsonDateFormat dateFormat) {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, dateFormat == JsonDateFormat.UNIX_EPOCH);
        return mapper;
    }

    /** Concatenates the URI components **/
    private String concat(String... paths) {
        StringBuilder result = new StringBuilder();
        if (paths != null) {
            Arrays.stream(paths).filter(StringUtils::isNotBlank).forEach(p -> {
                if (result.length() > 0 && !result.toString().endsWith("/") && !p.startsWith("/")) {
                    result.append("/");
                } else if (result.toString().endsWith("/") && p.startsWith("/")) {
                    p = p.substring(1);
                }
                result.append(p);
            });
        }
        return result.toString();
    }

}