/*
* Constellation - An open source and standard compliant SDI
* http://www.constellation-sdi.org
*
* Copyright 2014 Geomatys.
*
* 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.constellation.wmts.ws.rs;
import org.constellation.ServiceDef;
import org.constellation.ServiceDef.Specification;
import org.constellation.wmts.ws.DefaultWMTSWorker;
import org.constellation.wmts.ws.WMTSWorker;
import org.constellation.ws.CstlServiceException;
import org.constellation.ws.MimeType;
import org.constellation.ws.WSEngine;
import org.constellation.ws.Worker;
import org.constellation.ws.rs.GridWebService;
import org.geotoolkit.ows.xml.RequestBase;
import org.geotoolkit.ows.xml.v110.AcceptFormatsType;
import org.geotoolkit.ows.xml.v110.AcceptVersionsType;
import org.geotoolkit.ows.xml.v110.ExceptionReport;
import org.geotoolkit.ows.xml.v110.SectionsType;
import org.geotoolkit.util.ImageIOUtilities;
import org.geotoolkit.wmts.xml.WMTSMarshallerPool;
import org.geotoolkit.wmts.xml.v100.DimensionNameValue;
import org.geotoolkit.wmts.xml.v100.GetCapabilities;
import org.geotoolkit.wmts.xml.v100.GetFeatureInfo;
import org.geotoolkit.wmts.xml.v100.GetTile;
import javax.imageio.IIOException;
import javax.inject.Singleton;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.logging.Level;
import static org.constellation.api.QueryConstants.ACCEPT_FORMATS_PARAMETER;
import static org.constellation.api.QueryConstants.ACCEPT_VERSIONS_PARAMETER;
import static org.constellation.api.QueryConstants.REQUEST_PARAMETER;
import static org.constellation.api.QueryConstants.SECTIONS_PARAMETER;
import static org.constellation.api.QueryConstants.SERVICE_PARAMETER;
import static org.constellation.api.QueryConstants.UPDATESEQUENCE_PARAMETER;
import static org.constellation.api.QueryConstants.VERSION_PARAMETER;
import static org.constellation.ws.ExceptionCode.INVALID_PARAMETER_VALUE;
import static org.constellation.ws.ExceptionCode.MISSING_PARAMETER_VALUE;
import static org.constellation.ws.ExceptionCode.NO_APPLICABLE_CODE;
import static org.constellation.ws.ExceptionCode.OPERATION_NOT_SUPPORTED;
// Jersey dependencies
/**
* The REST facade to an OGC Web Map Tile Service, implementing the 1.0.0 version.
*
* @version $Id$
*
* @author Cédric Briançon (Geomatys)
* @author Guilhem Legal (Geomatys)
* @since 0.3
*/
@Path("wmts/{serviceId}")
@Singleton
public class WMTSService extends GridWebService<WMTSWorker> {
private static final String NOT_WORKING = "The WMTS service is not running";
/**
* Builds a new WMTS service REST (both REST Kvp and RESTFUL). This service only
* provides the version 1.0.0 of OGC WMTS standard, for the moment.
*/
public WMTSService() {
super(Specification.WMTS);
setXMLContext(WMTSMarshallerPool.getInstance());
setFullRequestLog(true);
LOGGER.log(Level.INFO, "WMTS REST service running ({0} instances)", getWorkerMapSize());
}
/**
* {@inheritDoc}
*/
@Override
protected Class getWorkerClass() {
return DefaultWMTSWorker.class;
}
/**
* {@inheritDoc}
*/
@Override
public Response treatIncomingRequest(final Object objectRequest, final WMTSWorker worker) {
ServiceDef serviceDef = null;
try {
// if the request is not an xml request we fill the request parameter.
final RequestBase request;
if (objectRequest == null) {
request = adaptQuery(getParameter(REQUEST_PARAMETER, true));
} else if (objectRequest instanceof RequestBase) {
request = (RequestBase) objectRequest;
} else {
throw new CstlServiceException("The operation " + objectRequest.getClass().getName() + " is not supported by the service",
INVALID_PARAMETER_VALUE, "request");
}
serviceDef = worker.getVersionFromNumber(request.getVersion());
if (request instanceof GetCapabilities) {
final GetCapabilities gc = (GetCapabilities) request;
return Response.ok(worker.getCapabilities(gc), MimeType.TEXT_XML).build();
}
if (request instanceof GetTile) {
final GetTile gt = (GetTile) request;
return Response.ok(worker.getTile(gt), gt.getFormat()).build();
}
if (request instanceof GetFeatureInfo) {
final GetFeatureInfo gf = (GetFeatureInfo) request;
final Map.Entry<String, Object> result = worker.getFeatureInfo(gf);
if (result != null) {
return Response.ok(result.getValue(), result.getKey()).build();
}
//throw an exception if result of GetFeatureInfo visitor is null
throw new CstlServiceException("An error occurred during GetFeatureInfo response building.");
}
throw new CstlServiceException("The operation " + request.getClass().getName() +
" is not supported by the service", OPERATION_NOT_SUPPORTED, "request");
} catch (CstlServiceException ex) {
return processExceptionResponse(ex, serviceDef, worker);
}
}
/**
* Build request object fom KVP parameters.
*
* @param request
* @return
* @throws CstlServiceException
*/
private RequestBase adaptQuery(final String request) throws CstlServiceException {
if ("GetCapabilities".equalsIgnoreCase(request)) {
return createNewGetCapabilitiesRequest();
} else if ("GetTile".equalsIgnoreCase(request)) {
return createNewGetTileRequest();
} else if ("GetFeatureInfo".equalsIgnoreCase(request)) {
return createNewGetFeatureInfoRequest();
}
throw new CstlServiceException("The operation " + request + " is not supported by the service",
INVALID_PARAMETER_VALUE, "request");
}
/**
* Builds a new {@link GetCapabilities} request from a REST Kvp request.
*
* @return The {@link GetCapabilities} request.
* @throws CstlServiceException if a required parameter is not present in the request.
*/
private GetCapabilities createNewGetCapabilitiesRequest() throws CstlServiceException {
String version = getParameter(ACCEPT_VERSIONS_PARAMETER, false);
AcceptVersionsType versions;
if (version != null) {
if (version.indexOf(',') != -1) {
version = version.substring(0, version.indexOf(','));
}
versions = new AcceptVersionsType(version);
} else {
versions = new AcceptVersionsType("1.0.0");
}
final AcceptFormatsType formats = new AcceptFormatsType(getParameter(ACCEPT_FORMATS_PARAMETER, false));
//We transform the String of sections in a list.
//In the same time we verify that the requested sections are valid.
final String section = getParameter(SECTIONS_PARAMETER, false);
List<String> requestedSections = new ArrayList<String>();
if (section != null && !section.equalsIgnoreCase("All")) {
final StringTokenizer tokens = new StringTokenizer(section, ",;");
while (tokens.hasMoreTokens()) {
final String token = tokens.nextToken().trim();
if (SectionsType.getExistingSections("1.1.1").contains(token)){
requestedSections.add(token);
} else {
throw new CstlServiceException("The section " + token + " does not exist",
INVALID_PARAMETER_VALUE, "Sections");
}
}
} else {
//if there is no requested Sections we add all the sections
requestedSections = SectionsType.getExistingSections("1.1.1");
}
final SectionsType sections = new SectionsType(requestedSections);
final String updateSequence = getParameter(UPDATESEQUENCE_PARAMETER, false);
return new GetCapabilities(versions,
sections,
formats,
updateSequence,
getParameter(SERVICE_PARAMETER, true));
}
/**
* Builds a new {@link GetCapabilities} request from a RESTFUL request.
*
* @return The {@link GetCapabilities} request.
* @throws CstlServiceException if a required parameter is not present in the request.
*/
private GetCapabilities createNewGetCapabilitiesRequestRestful(final String version) throws CstlServiceException {
final AcceptVersionsType versions;
if (version != null) {
versions = new AcceptVersionsType(version);
} else {
versions = new AcceptVersionsType("1.0.0");
}
return new GetCapabilities(versions, null, null, null, "WMTS");
}
/**
* Builds a new {@link GetFeatureInfo} request from a REST Kvp request.
*
* @return The {@link GetFeatureInfo} request.
* @throws CstlServiceException if a required parameter is not present in the request.
*/
private GetFeatureInfo createNewGetFeatureInfoRequest() throws CstlServiceException {
final GetFeatureInfo gfi = new GetFeatureInfo();
gfi.setGetTile(createNewGetTileRequest());
gfi.setI(Integer.valueOf(getParameter("I", true)));
gfi.setJ(Integer.valueOf(getParameter("J", true)));
gfi.setInfoFormat(getParameter("infoformat", true));
gfi.setService(getParameter(SERVICE_PARAMETER, true));
gfi.setVersion(getParameter(VERSION_PARAMETER, true));
return gfi;
}
/**
* Builds a new {@link GetTile} request from a REST Kvp request.
*
* @return The {@link GetTile} request.
* @throws CstlServiceException if a required parameter is not present in the request.
*/
private GetTile createNewGetTileRequest() throws CstlServiceException {
final GetTile getTile = new GetTile();
final MultivaluedMap<String, String> parameters = getParameters();
parameters.remove(REQUEST_PARAMETER);
// Mandatory parameters
getTile.setFormat(getParameter("format", true));
parameters.remove("format");
getTile.setLayer(getParameter("layer", true));
parameters.remove("layer");
getTile.setService(getParameter(SERVICE_PARAMETER, true));
parameters.remove(SERVICE_PARAMETER);
getTile.setVersion(getParameter(VERSION_PARAMETER, true));
parameters.remove(VERSION_PARAMETER);
getTile.setTileCol(Integer.valueOf(getParameter("TileCol", true)));
parameters.remove("TileCol");
getTile.setTileRow(Integer.valueOf(getParameter("TileRow", true)));
parameters.remove("TileRow");
getTile.setTileMatrix(getParameter("TileMatrix", true));
parameters.remove("TileMatrix");
getTile.setTileMatrixSet(getParameter("TileMatrixSet", true));
parameters.remove("TileMatrixSet");
// Optional parameters
getTile.setStyle(getParameter("style", false));
parameters.remove("style");
/*
* HACK : Remaining parameters will be considered as extra dimension of the layer. We don't check layer
* capabilities because it could be resource consuming operation. Filtering will be done by worker when it will
* recompose a multi-dimensional envelope.
*/
if (!parameters.isEmpty()) {
final List<DimensionNameValue> dims = getTile.getDimensionNameValue();
for (Map.Entry<String, List<String>> entry : parameters.entrySet()) {
if (entry.getValue() != null && !entry.getValue().isEmpty()) {
final DimensionNameValue dnv = new DimensionNameValue();
dnv.setName(entry.getKey());
dnv.setValue(entry.getValue().get(0));
dims.add(dnv);
}
}
}
return getTile;
}
/**
* Builds a new {@link GetTile} request from a RESTFUL request.
*
* @return The {@link GetTile} request.
* @throws CstlServiceException if a required parameter is not present in the request.
*/
private GetTile createNewGetTileRequestRestful(final String layer, final String tileMatrixSet,
final String tileMatrix, final String tileRow,
final String tileCol, final String format, final String style)
throws CstlServiceException
{
final GetTile getTile = new GetTile();
// Mandatory parameters
if (format == null) {
throw new CstlServiceException("The parameter FORMAT must be specified",
MISSING_PARAMETER_VALUE);
}
getTile.setFormat(format);
getTile.setLayer(layer);
if (layer == null) {
throw new CstlServiceException("The parameter LAYER must be specified",
MISSING_PARAMETER_VALUE);
}
getTile.setService("WMTS");
getTile.setVersion("1.0.0");
if (tileCol == null) {
throw new CstlServiceException("The parameter TILECOL must be specified",
MISSING_PARAMETER_VALUE);
}
getTile.setTileCol(Integer.valueOf(tileCol));
if (tileRow == null) {
throw new CstlServiceException("The parameter TILEROW must be specified",
MISSING_PARAMETER_VALUE);
}
getTile.setTileRow(Integer.valueOf(tileRow));
if (tileMatrix == null) {
throw new CstlServiceException("The parameter TILEMATRIX must be specified",
MISSING_PARAMETER_VALUE);
}
getTile.setTileMatrix(tileMatrix);
if (tileMatrixSet == null) {
throw new CstlServiceException("The parameter TILEMATRIXSET must be specified",
MISSING_PARAMETER_VALUE);
}
getTile.setTileMatrixSet(tileMatrixSet);
// Optionnal parameters
getTile.setStyle(style);
return getTile;
}
/**
* Handle {@code GetCapabilities request} in RESTFUL mode.
*
* @param version The version of the GetCapabilities request.
* @param resourcename The name of the resource file.
*
* @return The XML formatted response, for an OWS GetCapabilities of the WMTS standard.
*/
@GET
@Path("{version}/{caps}")
public Response processGetCapabilitiesRestful(@PathParam("version") final String version,
@PathParam("caps") final String resourcename) {
try {
final GetCapabilities gc = createNewGetCapabilitiesRequestRestful(version);
return treatIncomingRequest(gc);
} catch (CstlServiceException ex) {
final Worker w = WSEngine.getInstance("WMTS", getSafeParameter("serviceId"));
return processExceptionResponse(ex, null, w);
}
}
/**
* Handle {@code GetTile request} in RESTFUL mode.
*
* @param layer The layer to request.
* @param tileMatrixSet The matrix set of the tile.
* @param tileMatrix The matrix tile.
* @param tileRow The row of the tile in the matrix.
* @param tileCol The column of the tile in the matrix.
* @param format The format extension, like png.
*
* @return The response containing the tile.
*/
@GET
@Path("{layer}/{tileMatrixSet}/{tileMatrix}/{tileRow}/{tileCol}.{format}")
public Response processGetTileRestful(@PathParam("layer") final String layer,
@PathParam("tileMatrixSet") final String tileMatrixSet,
@PathParam("tileMatrix") final String tileMatrix,
@PathParam("tileRow") final String tileRow,
@PathParam("tileCol") final String tileCol,
@PathParam("format") final String format) {
try {
final String mimeType;
try {
mimeType = ImageIOUtilities.formatNameToMimeType(format);
} catch (IIOException ex) {
throw new CstlServiceException(ex, NO_APPLICABLE_CODE);
}
final GetTile gt = createNewGetTileRequestRestful(layer, tileMatrixSet, tileMatrix, tileRow, tileCol, mimeType, null);
return treatIncomingRequest(gt);
} catch (CstlServiceException ex) {
final Worker w = WSEngine.getInstance("WMTS", getSafeParameter("serviceId"));
return processExceptionResponse(ex, null, w);
}
}
/**
* Handle all exceptions returned by a web service operation in two ways:
* <ul>
* <li>if the exception code indicates a mistake done by the user, just display a single
* line message in logs.</li>
* <li>otherwise logs the full stack trace in logs, because it is something interesting for
* a developer</li>
* </ul>
* In both ways, the exception is then marshalled and returned to the client.
*
* @param ex The exception that has been generated during the web-service operation requested.
* @return An XML representing the exception.
*
*/
@Override
protected Response processExceptionResponse(final CstlServiceException ex, ServiceDef serviceDef, final Worker worker) {
logException(ex);
if (serviceDef == null) {
serviceDef = worker.getBestVersion(null);
}
final String codeName = getOWSExceptionCodeRepresentation(ex.getExceptionCode());
final ExceptionReport report = new ExceptionReport(ex.getMessage(), codeName,
ex.getLocator(), serviceDef.exceptionVersion.toString());
return Response.ok(report, MimeType.TEXT_XML).build();
}
}