dk.dma.msinm.web.rest.LocationRestService.java Source code

Java tutorial

Introduction

Here is the source code for dk.dma.msinm.web.rest.LocationRestService.java

Source

/* Copyright (c) 2011 Danish Maritime Authority
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this library.  If not, see <http://www.gnu.org/licenses/>.
 */
package dk.dma.msinm.web.rest;

import dk.dma.msinm.common.repo.RepositoryService;
import dk.dma.msinm.model.Location;
import dk.dma.msinm.model.Point;
import dk.dma.msinm.vo.LocationVo;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import javax.inject.Inject;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.xml.namespace.NamespaceContext;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * Location support
 */
@Path("/location")
public class LocationRestService {

    @Inject
    Logger log;

    @Context
    ServletContext servletContext;

    /**
     * Parses the KML and returns a JSON list of locations.
     *
     * TDOD: Handle MultiGeometry.
     * Example: http://www.microformats.dk/2008/11/02/kommunegrnserne-til-de-98-danske-kommuner/
     *
     * @param kml the KML to parse
     * @return the corresponding list of locations
     */
    @POST
    @Path("/parse-kml")
    @Produces("application/json")
    public List<LocationVo> parseKml(String kml) throws UnsupportedEncodingException {

        // Strip BOM from UTF-8 with BOM
        if (kml.startsWith("\uFEFF")) {
            kml = kml.replace("\uFEFF", "");
        }

        // Extract the default namespace
        String namespace = extractDefaultNamespace(kml);

        List<LocationVo> result = new ArrayList<>();
        XPath xpath = XPathFactory.newInstance().newXPath();
        xpath.setNamespaceContext(new KmlNamespaceContext(namespace));
        InputSource inputSource = new InputSource(new StringReader(kml));

        try {
            // Fetch all "Placemark" elements
            NodeList placemarks = (NodeList) xpath.evaluate("//kml:Placemark", inputSource, XPathConstants.NODESET);

            for (int i = 0; i < placemarks.getLength(); i++) {

                // Fetch all "Point" coordinates
                NodeList coordinates = (NodeList) xpath.evaluate("//kml:Point/kml:coordinates", placemarks.item(i),
                        XPathConstants.NODESET);
                extractLocations(result, coordinates, Location.LocationType.POINT);

                // Fetch all "Polyline" coordinates
                coordinates = (NodeList) xpath.evaluate("//kml:LineString/kml:coordinates", placemarks.item(i),
                        XPathConstants.NODESET);
                extractLocations(result, coordinates, Location.LocationType.POLYLINE);

                // Fetch all "Polygon" coordinates
                coordinates = (NodeList) xpath.evaluate(
                        "//kml:Polygon/kml:outerBoundaryIs/kml:LinearRing/kml:coordinates", placemarks.item(i),
                        XPathConstants.NODESET);
                extractLocations(result, coordinates, Location.LocationType.POLYGON);
            }

        } catch (Exception e) {
            log.error("Error parsing kml", e);
        }

        return result;
    }

    /**
     * Converts a list of coordinate nodes into a locations and adds them to the result
     * @param result the result to update with coordinates
     * @param coordinates the coordinates nodes
     * @param type the type of location
     */
    void extractLocations(List<LocationVo> result, NodeList coordinates, Location.LocationType type) {
        if (coordinates != null && coordinates.getLength() > 0) {
            for (int i = 0; i < coordinates.getLength(); i++) {
                LocationVo loc = extractLocation(coordinates.item(i), type);
                if (loc != null) {
                    result.add(loc);
                }
            }
        }
    }

    /**
     * Converts a list of coordinates into a location. Returns null if invalid
     * @param coordinates the coordinates node
     * @param type the type of location
     * @return the location or null
     */
    LocationVo extractLocation(Node coordinates, Location.LocationType type) {
        if (coordinates != null) {
            Location loc = new Location();
            loc.setType(type);

            // Parse the coordinates which consist of white-space separated tuples with one of the following formats:
            // * "longitude,latitude,altitude" (used by Google Earth)
            // * "longitude,latitude"
            String txt = coordinates.getTextContent().trim();
            for (String coord : txt.split("\\s+")) {
                String[] lonLatAlt = coord.split(",");
                if (lonLatAlt.length == 2 || lonLatAlt.length == 3) {
                    Point pt = new Point();
                    pt.setLon(Double.parseDouble(lonLatAlt[0]));
                    pt.setLat(Double.parseDouble(lonLatAlt[1]));
                    pt.setLocation(loc);
                    loc.addPoint(pt);
                }
            }

            if (loc.getPoints().size() > 0) {
                // For polygons, skip the last point since it is identical to the first
                if (loc.getType() == Location.LocationType.POLYGON && loc.getPoints().size() > 1) {
                    loc.getPoints().remove(loc.getPoints().size() - 1);
                }

                return new LocationVo(loc);
            }
        }
        return null;
    }

    /**
     * Annoyingly, different versions of KML use different default namespaces.
     * Hence, attempt to extract the default namespace
     * @param kml the xml
     * @return the default KML namespace
     */
    private String extractDefaultNamespace(String kml) {
        Pattern p = Pattern.compile(".*<kml xmlns=\"([^\"]*)\".*",
                Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE);
        Matcher m = p.matcher(kml);
        if (m.matches()) {
            return m.group(1);
        }
        return "http://www.opengis.net/kml/2.2";
    }

    /**
     * Parse the KML file of the uploaded .kmz or .kml file and returns a JSON list of locations
     *
     * @param request the servlet request
     * @return the corresponding list of locations
     */
    @POST
    @javax.ws.rs.Path("/upload-kml")
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @Produces("application/json;charset=UTF-8")
    public List<LocationVo> uploadKml(@Context HttpServletRequest request) throws FileUploadException, IOException {
        FileItemFactory factory = RepositoryService.newDiskFileItemFactory(servletContext);
        ServletFileUpload upload = new ServletFileUpload(factory);
        List<FileItem> items = upload.parseRequest(request);

        for (FileItem item : items) {
            if (!item.isFormField()) {
                try {
                    // .kml file
                    if (item.getName().toLowerCase().endsWith(".kml")) {
                        // Parse the KML and return the corresponding locations
                        return parseKml(IOUtils.toString(item.getInputStream()));
                    }

                    // .kmz file
                    else if (item.getName().toLowerCase().endsWith(".kmz")) {
                        // Parse the .kmz file as a zip file
                        ZipInputStream zis = new ZipInputStream(new BufferedInputStream(item.getInputStream()));
                        ZipEntry entry;

                        // Look for the first zip entry with a .kml extension
                        while ((entry = zis.getNextEntry()) != null) {
                            if (!entry.getName().toLowerCase().endsWith(".kml")) {
                                continue;
                            }

                            log.info("Unzipping: " + entry.getName());
                            int size;
                            byte[] buffer = new byte[2048];
                            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
                            while ((size = zis.read(buffer, 0, buffer.length)) != -1) {
                                bytes.write(buffer, 0, size);
                            }
                            bytes.flush();
                            zis.close();

                            // Parse the KML and return the corresponding locations
                            return parseKml(new String(bytes.toByteArray(), "UTF-8"));
                        }
                    }

                } catch (Exception ex) {
                    log.error("Error extracting kmz", ex);
                }
            }
        }

        // Return an empty result
        return new ArrayList<>();
    }

    /**
     * Defines the KML namespace context
     */
    private static class KmlNamespaceContext implements NamespaceContext {

        String namespace;

        public KmlNamespaceContext(String namespace) {
            this.namespace = namespace;
        }

        public String getNamespaceURI(String prefix) {
            if ("kml".equals(prefix)) {
                return namespace;
            }
            return null;
        }

        public String getPrefix(String namespaceURI) {
            return null;
        }

        public Iterator getPrefixes(String namespaceURI) {
            return null;
        }

    }
}