org.geowebcache.service.wmts.WMTSService.java Source code

Java tutorial

Introduction

Here is the source code for org.geowebcache.service.wmts.WMTSService.java

Source

/**
 * This program 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.
 *
 * <p>This program 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 General Public License for more details.
 *
 * <p>You should have received a copy of the GNU Lesser General Public License along with this
 * program. If not, see <http://www.gnu.org/licenses/>.
 *
 * @author Arne Kepp, OpenGeo, Copyright 2009
 */
package org.geowebcache.service.wmts;

import com.google.common.base.Strings;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.geowebcache.GeoWebCacheDispatcher;
import org.geowebcache.GeoWebCacheException;
import org.geowebcache.GeoWebCacheExtensions;
import org.geowebcache.config.ServerConfiguration;
import org.geowebcache.conveyor.Conveyor;
import org.geowebcache.conveyor.ConveyorTile;
import org.geowebcache.filter.parameters.ParameterException;
import org.geowebcache.filter.security.SecurityDispatcher;
import org.geowebcache.grid.GridSetBroker;
import org.geowebcache.grid.GridSubset;
import org.geowebcache.grid.OutsideCoverageException;
import org.geowebcache.layer.TileLayer;
import org.geowebcache.layer.TileLayerDispatcher;
import org.geowebcache.mime.MimeException;
import org.geowebcache.mime.MimeType;
import org.geowebcache.service.HttpErrorCodeException;
import org.geowebcache.service.OWSException;
import org.geowebcache.service.Service;
import org.geowebcache.stats.RuntimeStats;
import org.geowebcache.storage.StorageBroker;
import org.geowebcache.util.NullURLMangler;
import org.geowebcache.util.ServletUtils;
import org.geowebcache.util.URLMangler;

public class WMTSService extends Service {

    public static final String SERVICE_WMTS = "wmts";
    public static final String SERVICE_PATH = "/" + GeoWebCacheDispatcher.TYPE_SERVICE + "/" + SERVICE_WMTS;
    public static final String REST_PATH = SERVICE_PATH + "/rest";
    public static final String GET_CAPABILITIES = "getcapabilities";
    public static final String GET_FEATUREINFO = "getfeatureinfo";
    public static final String GET_TILE = "gettile";

    enum RequestType {
        TILE, CAPABILITIES, FEATUREINFO
    }

    static final String buildRestPattern(int numPathElements, boolean hasStyle) {
        if (!hasStyle) {
            return ".*/service/wmts/rest" + Strings.repeat("/([^/]+)", numPathElements);
        } else {
            return ".*/service/wmts/rest/([^/]+)/([^/]*)" + Strings.repeat("/([^/]+)", numPathElements - 2);
        }
    }

    enum RestRequest {
        // "/{layer}/{tileMatrixSet}/{tileMatrix}/{tileRow}/{tileCol}"
        TILE(buildRestPattern(5, false), RequestType.TILE, false),
        // "/{layer}/{style}/{tileMatrixSet}/{tileMatrix}/{tileRow}/{tileCol}",
        TILE_STYLE(buildRestPattern(6, true), RequestType.TILE, true),
        // "/{layer}/{tileMatrixSet}/{tileMatrix}/{tileRow}/{tileCol}/{j}/{i}"
        FEATUREINFO(buildRestPattern(7, false), RequestType.FEATUREINFO, false),
        // "/{layer}/{style}/{tileMatrixSet}/{tileMatrix}/{tileRow}/{tileCol}/{j}/{i}",
        FEATUREINFO_STYLE(buildRestPattern(8, true), RequestType.FEATUREINFO, true);

        Pattern pattern;
        RequestType type;
        boolean hasStyle;

        RestRequest(String pattern, RequestType type, boolean hasStyle) {
            this.pattern = Pattern.compile(pattern);
            this.type = type;
            this.hasStyle = hasStyle;
        }

        /**
         * Returns the parsed KVP, or null if the path does not match the request pattern
         *
         * @param request
         * @return
         */
        public Map<String, String> toKVP(HttpServletRequest request) {
            final Matcher matcher = pattern.matcher(request.getPathInfo());
            if (!matcher.matches()) {
                return null;
            }
            Map<String, String> values = new HashMap<>();
            // go through the pattern and extract the actual request
            // leverage the predictable path structure to use a single parsing sequence for all
            // requests
            int i = 1;
            final boolean isFeatureInfo = type == RequestType.FEATUREINFO;
            values.put("request", isFeatureInfo ? GET_FEATUREINFO : GET_TILE);
            values.put("layer", matcher.group(i++));
            if (hasStyle) {
                values.put("style", matcher.group(i++));
            }
            values.put("tilematrixset", matcher.group(i++));
            values.put("tilematrix", matcher.group(i++));
            values.put("tilerow", matcher.group(i++));
            values.put("tilecol", matcher.group(i++));
            if (isFeatureInfo) {
                values.put("j", matcher.group(i++));
                values.put("i", matcher.group(i++));
            }
            if (request.getParameter("format") instanceof String) {
                if (isFeatureInfo) {
                    values.put("infoformat", request.getParameter("format"));
                } else {
                    values.put("format", request.getParameter("format"));
                }
            }
            return values;
        }
    }

    // private static Log log = LogFactory.getLog(org.geowebcache.service.wmts.WMTSService.class);

    private StorageBroker sb;

    private TileLayerDispatcher tld;

    private GridSetBroker gsb;

    private RuntimeStats stats;

    private URLMangler urlMangler = new NullURLMangler();

    private GeoWebCacheDispatcher controller = null;

    private ServerConfiguration mainConfiguration;

    // list of this service extensions ordered by their priority
    private final List<WMTSExtension> extensions = new ArrayList<>();

    private SecurityDispatcher securityDispatcher;

    /** Protected no-argument constructor to allow run-time instrumentation */
    protected WMTSService() {
        super(SERVICE_WMTS);
        extensions.addAll(GeoWebCacheExtensions.extensions(WMTSExtension.class));
    }

    public WMTSService(StorageBroker sb, TileLayerDispatcher tld, GridSetBroker gsb, RuntimeStats stats) {
        super(SERVICE_WMTS);

        this.sb = sb;
        this.tld = tld;
        this.gsb = gsb;
        this.stats = stats;
        extensions.addAll(GeoWebCacheExtensions.extensions(WMTSExtension.class));
    }

    public WMTSService(StorageBroker sb, TileLayerDispatcher tld, GridSetBroker gsb, RuntimeStats stats,
            URLMangler urlMangler, GeoWebCacheDispatcher controller) {
        super(SERVICE_WMTS);

        this.sb = sb;
        this.tld = tld;
        this.gsb = gsb;
        this.stats = stats;
        this.urlMangler = urlMangler;
        this.controller = controller;
        extensions.addAll(GeoWebCacheExtensions.extensions(WMTSExtension.class));
    }

    @Override
    public Conveyor getConveyor(HttpServletRequest request, HttpServletResponse response)
            throws GeoWebCacheException, OWSException {

        // let's see if we have any extension that wants to provide a conveyor for this request
        for (WMTSExtension extension : extensions) {
            Conveyor conveyor = extension.getConveyor(request, response, sb);
            if (conveyor != null) {
                // this extension provides a conveyor for this request, we are done
                return conveyor;
            }
        }

        if (request.getPathInfo() != null && request.getPathInfo().contains("service/wmts/rest")) {
            return getRestConveyor(request, response);
        }

        String[] keys = { "layer", "request", "style", "format", "infoformat", "tilematrixset", "tilematrix",
                "tilerow", "tilecol", "i", "j" };
        String encoding = request.getCharacterEncoding();
        Map<String, String> values = ServletUtils.selectedStringsFromMap(request.getParameterMap(), encoding, keys);
        return getKvpConveyor(request, response, values);
    }

    public Conveyor getRestConveyor(HttpServletRequest request, HttpServletResponse response)
            throws GeoWebCacheException, OWSException {
        final String path = request.getPathInfo();

        // special simpler case for GetCapabilities
        if (path.endsWith("/service/wmts/rest/WMTSCapabilities.xml")) {
            ConveyorTile tile = new ConveyorTile(sb, null, request, response);
            tile.setHint(GET_CAPABILITIES);
            tile.setRequestHandler(ConveyorTile.RequestHandler.SERVICE);
            return tile;
        }

        // all other paths are handled via the RestRequest enumeration, matching patterns and
        // extracting variables
        for (RestRequest restRequest : RestRequest.values()) {
            Map<String, String> values = restRequest.toKVP(request);
            if (values != null) {
                return getKvpConveyor(request, response, values);
            }
        }

        // we implement all WMTS supported request, this means that the provided request name is
        // invalid
        throw new HttpErrorCodeException(404, "Unknown resource " + request.getPathInfo());
    }

    public Conveyor getKvpConveyor(HttpServletRequest request, HttpServletResponse response,
            Map<String, String> values) throws GeoWebCacheException, OWSException {
        // let's see if we have any extension that wants to provide a conveyor for this request
        for (WMTSExtension extension : extensions) {
            Conveyor conveyor = extension.getConveyor(request, response, sb);
            if (conveyor != null) {
                // this extension provides a conveyor for this request, we are done
                return conveyor;
            }
        }

        // check if we need to be CITE strictly compliant
        boolean isCitecompliant = isCiteCompliant();
        if (isCitecompliant) {
            performCiteValidation(request);
        }

        String req = values.get("request");
        if (req == null) {
            // OWSException(httpCode, exceptionCode, locator, exceptionText);
            throw new OWSException(400, "MissingParameterValue", "request", "Missing Request parameter");
        } else {
            req = req.toLowerCase();
        }

        if (isCitecompliant) {
            String acceptedVersions = getParameterValue("AcceptVersions", request);
            // if provided handle accepted versions parameter
            if (acceptedVersions != null) {
                // we only support version 1.0.0, so make sure that's one of the accepted versions
                String[] versions = acceptedVersions.split("\\s*,\\s*");
                int foundIndex = Arrays.binarySearch(versions, "1.0.0");
                if (foundIndex < 0) {
                    // no supported version is accepted
                    throw new OWSException(400, "VersionNegotiationFailed", null,
                            "List of versions in AcceptVersions parameter value, in GetCapabilities "
                                    + "operation request, did not include any version supported by this server.");
                }
            }
        }

        if (req.equals(GET_TILE)) {
            if (isCitecompliant) {
                boolean isRestRequest = isRestRequest(request);
                // we need to make sure that a style was provided, otherwise GWC will just assume
                // the default one
                if (!isRestRequest && getParameterValue("Style", request) == null) {
                    // mandatory STYLE query parameter is missing
                    throw new OWSException(400, "MissingParameterValue", "Style",
                            "Mandatory Style query parameter not provided.");
                }
            }
            ConveyorTile tile = getTile(values, request, response, RequestType.TILE);
            return tile;
        } else if (req.equals("getcapabilities")) {
            ConveyorTile tile = new ConveyorTile(sb, values.get("layer"), request, response);
            tile.setHint(req);
            tile.setRequestHandler(ConveyorTile.RequestHandler.SERVICE);
            return tile;
        } else if (req.equals("getfeatureinfo")) {
            ConveyorTile tile = getTile(values, request, response, RequestType.FEATUREINFO);
            tile.setHint(req);
            tile.setRequestHandler(Conveyor.RequestHandler.SERVICE);
            return tile;
        } else {
            // we implement all WMTS supported request, this means that the provided request name is
            // invalid
            throw new OWSException(400, "InvalidParameterValue", "request",
                    String.format("Invalid request name '%s'.", req));
        }
    }

    private ConveyorTile getTile(Map<String, String> values, HttpServletRequest request,
            HttpServletResponse response, RequestType reqType) throws OWSException {
        String encoding = request.getCharacterEncoding();

        String layer = values.get("layer");
        if (layer == null) {
            throw new OWSException(400, "MissingParameterValue", "LAYER", "Missing LAYER parameter");
        }

        TileLayer tileLayer = null;

        try {
            tileLayer = tld.getTileLayer(layer);
        } catch (GeoWebCacheException e) {
            throw new OWSException(400, "InvalidParameterValue", "LAYER", "LAYER " + layer + " is not known.");
        }

        Map<String, String[]> rawParameters = new HashMap<>(request.getParameterMap());
        Map<String, String> filteringParameters;
        try {

            /*
             * Merge values with request parameter
             */
            for (Entry<String, String> e : values.entrySet()) {
                rawParameters.put(e.getKey(), new String[] { e.getValue() });
            }

            // WMTS uses the "STYLE" instead of "STYLES"
            for (Entry<String, String[]> e : rawParameters.entrySet()) {
                if (e.getKey().equalsIgnoreCase("STYLE")) {
                    rawParameters.put("STYLES", e.getValue());
                    break;
                }
            }
            filteringParameters = tileLayer.getModifiableParameters(rawParameters, encoding);

        } catch (ParameterException e) {
            throw new OWSException(e.getHttpCode(), e.getExceptionCode(), e.getLocator(), e.getMessage());
        } catch (GeoWebCacheException e) {
            throw new OWSException(500, "NoApplicableCode", "",
                    e.getMessage() + " while fetching modifiable parameters for LAYER " + layer);
        }

        MimeType mimeType = null;
        if (reqType == RequestType.TILE) {
            String format = values.get("format");
            if (format == null) {
                throw new OWSException(400, "MissingParameterValue", "FORMAT",
                        "Unable to determine requested FORMAT, " + format);
            }
            try {
                mimeType = MimeType.createFromFormat(format);
            } catch (MimeException me) {
                throw new OWSException(400, "InvalidParameterValue", "FORMAT",
                        "Unable to determine requested FORMAT, " + format);
            }
        } else {
            String infoFormat = values.get("infoformat");

            if (infoFormat == null) {
                throw new OWSException(400, "MissingParameterValue", "INFOFORMAT",
                        "Parameter INFOFORMAT was not provided");
            }
            try {
                mimeType = MimeType.createFromFormat(infoFormat);
            } catch (MimeException me) {
                throw new OWSException(400, "InvalidParameterValue", "INFOFORMAT",
                        "Unable to determine requested INFOFORMAT, " + infoFormat);
            }
        }

        final String tilematrixset = values.get("tilematrixset");
        if (tilematrixset == null) {
            throw new OWSException(400, "MissingParameterValue", "TILEMATRIXSET", "No TILEMATRIXSET specified");
        }

        GridSubset gridSubset = tileLayer.getGridSubset(tilematrixset);
        if (gridSubset == null) {
            throw new OWSException(400, "InvalidParameterValue", "TILEMATRIXSET",
                    "Unable to match requested TILEMATRIXSET " + tilematrixset + " to those supported by layer");
        }

        final String tileMatrix = values.get("tilematrix");
        if (tileMatrix == null) {
            throw new OWSException(400, "MissingParameterValue", "TILEMATRIX", "No TILEMATRIX specified");
        }
        long z = gridSubset.getGridIndex(tileMatrix);

        if (z < 0) {
            throw new OWSException(400, "InvalidParameterValue", "TILEMATRIX", "Unknown TILEMATRIX " + tileMatrix);
        }

        // WMTS has 0 in the top left corner -> flip y value
        final String tileRow = values.get("tilerow");
        if (tileRow == null) {
            throw new OWSException(400, "MissingParameterValue", "TILEROW", "No TILEROW specified");
        }

        final long tilesHigh = gridSubset.getNumTilesHigh((int) z);

        long y = tilesHigh - Long.parseLong(tileRow) - 1;

        String tileCol = values.get("tilecol");
        if (tileCol == null) {
            throw new OWSException(400, "MissingParameterValue", "TILECOL", "No TILECOL specified");
        }
        long x = Long.parseLong(tileCol);

        long[] gridCov = gridSubset.getCoverage((int) z);

        if (x < gridCov[0] || x > gridCov[2]) {
            throw new OWSException(400, "TileOutOfRange", "TILECOLUMN",
                    "Column " + x + " is out of range, min: " + gridCov[0] + " max:" + gridCov[2]);
        }

        if (y < gridCov[1] || y > gridCov[3]) {
            long minRow = tilesHigh - gridCov[3] - 1;
            long maxRow = tilesHigh - gridCov[1] - 1;

            throw new OWSException(400, "TileOutOfRange", "TILEROW",
                    "Row " + tileRow + " is out of range, min: " + minRow + " max:" + maxRow);
        }

        long[] tileIndex = { x, y, z };

        try {
            gridSubset.checkCoverage(tileIndex);
        } catch (OutsideCoverageException e) {

        }

        ConveyorTile convTile = new ConveyorTile(sb, layer, gridSubset.getName(), tileIndex, mimeType,
                rawParameters, filteringParameters, request, response);

        convTile.setTileLayer(tileLayer);

        return convTile;
    }

    public void handleRequest(Conveyor conv) throws OWSException, GeoWebCacheException {

        // let's see if any extension wants to handle this request
        for (WMTSExtension extension : extensions) {
            if (extension.handleRequest(conv)) {
                // the request was handled by this extension
                return;
            }
        }

        // no extension wants to handle this request, so let's proceed with a normal execution
        ConveyorTile tile = (ConveyorTile) conv;

        String servletPrefix = null;
        if (controller != null)
            servletPrefix = controller.getServletPrefix();

        String servletBase = ServletUtils.getServletBaseURL(conv.servletReq, servletPrefix);
        String context = ServletUtils.getServletContextPath(conv.servletReq,
                new String[] { SERVICE_PATH, REST_PATH }, servletPrefix);

        if (tile.getHint() != null) {
            if (tile.getHint().equals(GET_CAPABILITIES)) {
                WMTSGetCapabilities wmsGC = new WMTSGetCapabilities(tld, gsb, tile.servletReq, servletBase, context,
                        urlMangler, extensions);
                wmsGC.writeResponse(tile.servletResp, stats);

            } else if (tile.getHint().equals(GET_FEATUREINFO)) {
                getSecurityDispatcher().checkSecurity(tile);
                ConveyorTile convTile = (ConveyorTile) conv;
                WMTSGetFeatureInfo wmsGFI = new WMTSGetFeatureInfo(convTile);
                wmsGFI.writeResponse(stats);
            }
        }
    }

    void addExtension(WMTSExtension extension) {
        extensions.add(extension);
    }

    public Collection<WMTSExtension> getExtensions() {
        return Collections.unmodifiableCollection(extensions);
    }

    public void setSecurityDispatcher(SecurityDispatcher secDisp) {
        this.securityDispatcher = secDisp;
    }

    protected SecurityDispatcher getSecurityDispatcher() {
        return securityDispatcher;
    }

    /**
     * Sets GWC main configuration.
     *
     * @param mainConfiguration GWC main configuration
     */
    public void setMainConfiguration(ServerConfiguration mainConfiguration) {
        this.mainConfiguration = mainConfiguration;
    }

    /**
     * Return the GWC configuration used by this WMTS service instance.
     *
     * @return GWC main configuration
     */
    ServerConfiguration getMainConfiguration() {
        return mainConfiguration;
    }

    /**
     * Helper method that checks if WMTS implementation should be CITE strictly compliant.
     *
     * @return TRUE if GWC main configuration or at least one of the WMTS extensions forces CITE
     *     compliance
     */
    private boolean isCiteCompliant() {
        // let's see if main GWC configuration forces WMTS implementation to be CITE compliant
        if (mainConfiguration != null && mainConfiguration.isWmtsCiteCompliant()) {
            return true;
        }
        // let's see if at least one of the extensions forces CITE compliant mode
        for (WMTSExtension extension : extensions) {
            if (extension.getServiceInformation() != null && extension.getServiceInformation().isCiteCompliant()) {
                return true;
            }
        }
        // we are not in CITE compliant mode
        return false;
    }

    /** Helper method that performs CITE tests mandatory validations. */
    private static void performCiteValidation(HttpServletRequest request) throws OWSException {
        // paths validation are not done for WMTS REST API
        if (isRestRequest(request)) {
            return;
        }
        // base path should end with WMTS
        String basePath = request.getPathInfo();
        String[] paths = basePath.split("/");
        String lastPath = paths[paths.length - 1];
        if (!lastPath.equalsIgnoreCase("WMTS")) {
            // invalid base path, not found should be returned
            throw new OWSException(404, "NoApplicableCode", "request", "Service or request not found");
        }
        // service query parameter is mandatory and should be equal to WMTS
        validateWmtsServiceName("wmts", request);
    }

    /**
     * Helper method that just checks if current WMTS request is in the context of a REST API call,
     * certain OGC validations don't make sense in that context.
     */
    private static boolean isRestRequest(HttpServletRequest request) {
        // rest/wmts is always lowercase
        return request.getPathInfo().contains("service/wmts/rest");
    }

    /**
     * Checks if the URL base path extracted service name matches the HTTP request SERVICE query
     * parameter value. If the HTTP request doesn't contains any SERVICE query parameter an OWS
     * exception will be returned.
     *
     * <p>This validation only happens for WMTS service and if CITE strict compliance is activated.
     *
     * @param pathServiceName service name extracted from the URL base path
     * @param request the original HTTP request
     * @throws OWSException if the URL path extracted service name and the HTTP request service name
     *     don't match
     */
    private static void validateWmtsServiceName(String pathServiceName, HttpServletRequest request)
            throws OWSException {
        if (pathServiceName == null || !pathServiceName.equalsIgnoreCase("WMTS")) {
            // not an OGC service, so nothing to do
            return;
        }
        // let's see if the service path and requested service match
        String requestedServiceName = getParameterValue("SERVICE", request);
        if (requestedServiceName == null) {
            // mandatory service query parameter not provided
            throw new OWSException(400, "MissingParameterValue", "service",
                    "Mandatory SERVICE query parameter not provided.");
        }
        if (!pathServiceName.equalsIgnoreCase(requestedServiceName)) {
            // bad request, the URL path service and the requested service don't match
            throw new OWSException(400, "InvalidParameterValue", "service",
                    String.format("URL path service '%s' don't match the requested service '%s'.", pathServiceName,
                            requestedServiceName));
        }
    }

    /**
     * Search in a non case sensitive way for a query parameter in the provided HTTP request. If the
     * query parameter is found is first value is returned otherwise NULL is returned.
     *
     * @param parameterName query parameter name to search
     * @param request HTTP request
     * @return the first value of the query parameter if it exists otherwise NULL
     */
    private static String getParameterValue(String parameterName, HttpServletRequest request) {
        if (parameterName == null) {
            // nothing to do
            return null;
        }
        for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
            if (entry.getKey() != null && entry.getKey().equalsIgnoreCase(parameterName)) {
                // we found our parameter
                String[] values = entry.getValue();
                return values != null ? values[0] : null;
            }
        }
        // parameter not found
        return null;
    }
}