/*
* 3D City Database Web Feature Service
* http://www.3dcitydb.org/
*
* Copyright 2014 - 2016
* virtualcitySYSTEMS GmbH
* Tauentzienstrasse 7b/c
* 10789 Berlin, Germany
* http://www.virtualcitysystems.de/
*
* 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 vcs.citydb.wfs;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.XMLConstants;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.UnmarshallerHandler;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import net.opengis.ows._1.AcceptVersionsType;
import net.opengis.wfs._2.DescribeFeatureTypeType;
import net.opengis.wfs._2.DescribeStoredQueriesType;
import net.opengis.wfs._2.GetCapabilitiesType;
import net.opengis.wfs._2.GetFeatureType;
import net.opengis.wfs._2.ListStoredQueriesType;
import org.citydb.api.concurrent.SingleWorkerPool;
import org.citydb.api.concurrent.Worker;
import org.citydb.api.concurrent.WorkerFactory;
import org.citydb.api.registry.ObjectRegistry;
import org.citydb.config.Config;
import org.citydb.log.Logger;
import org.citydb.modules.citygml.common.database.cache.CacheTableManager;
import org.citygml4j.builder.jaxb.JAXBBuilder;
import org.citygml4j.xml.schema.SchemaHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import vcs.citydb.wfs.config.Constants;
import vcs.citydb.wfs.config.WFSConfig;
import vcs.citydb.wfs.exception.WFSException;
import vcs.citydb.wfs.exception.WFSExceptionCode;
import vcs.citydb.wfs.exception.WFSExceptionReportHandler;
import vcs.citydb.wfs.operation.describefeaturetype.DescribeFeatureTypeHandler;
import vcs.citydb.wfs.operation.getcapabilities.GetCapabilitiesHandler;
import vcs.citydb.wfs.operation.getfeature.GetFeatureHandler;
import vcs.citydb.wfs.operation.storedquery.DescribeStoredQueriesHandler;
import vcs.citydb.wfs.operation.storedquery.ListStoredQueriesHandler;
import vcs.citydb.wfs.operation.storedquery.StoredQueryManager;
import vcs.citydb.wfs.util.CacheTableCleanerWorker;
import vcs.citydb.wfs.xml.NamespaceFilter;
import vcs.citydb.wfs.xml.ValidationEventHandlerImpl;
@WebServlet(Constants.WFS_SERVICE_PATH)
public class WFSService extends HttpServlet {
private static final long serialVersionUID = 1L;
private final Logger log = Logger.getInstance();
private WFSConfig wfsConfig;
private Config exporterConfig;
private JAXBBuilder jaxbBuilder;
private SAXParserFactory saxParserFactory;
private Schema wfsSchema;
private ArrayBlockingQueue<String> requestQueue;
private SingleWorkerPool<CacheTableManager> cacheTableCleanerPool;
private WFSExceptionReportHandler exceptionReportHandler;
@Override
public void init() throws ServletException {
// check whether servlet initialization threw an error
Object error = getServletContext().getAttribute(Constants.INIT_ERROR_ATTRNAME);
if (error instanceof ServletException)
throw (ServletException)error;
log.info("WFS service is loaded by servlet container.");
// service specific initialization
ObjectRegistry registry = ObjectRegistry.getInstance();
jaxbBuilder = (JAXBBuilder)registry.lookup(JAXBBuilder.class.getName());
wfsConfig = (WFSConfig)registry.lookup(WFSConfig.class.getName());
exporterConfig = (Config)registry.lookup(Config.class.getName());
requestQueue = new ArrayBlockingQueue<String>(wfsConfig.getServer().getMaxParallelRequests(), true);
exceptionReportHandler = new WFSExceptionReportHandler(jaxbBuilder);
saxParserFactory = SAXParserFactory.newInstance();
saxParserFactory.setNamespaceAware(true);
try {
StoredQueryManager storedQueryManager = new StoredQueryManager(jaxbBuilder, wfsConfig);
registry.register(StoredQueryManager.class.getName(), storedQueryManager);
} catch (ParserConfigurationException e) {
String message = "Failed to initialize stored query manager.";
log.error(message);
log.error(e.getMessage());
throw new ServletException(message, e);
} catch (SAXException e) {
String message = "Failed to initialize stored query manager.";
log.error(message);
log.error(e.getMessage());
throw new ServletException(message, e);
}
// read WFS 2.0 schema to validate requests
if (wfsConfig.getOperations().isUseXMLValidation()) {
try {
SchemaHandler schemaHandler = (SchemaHandler)registry.lookup(SchemaHandler.class.getName());
schemaHandler.parseSchema(new File(getServletContext().getRealPath(Constants.XML_SCHEMAS_PATH + "/wfs/2.0.2/wfs.xsd")));
SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
wfsSchema = schemaFactory.newSchema(schemaHandler.getSchemaSources());
} catch (SAXException e) {
String message = "Failed to read WFS XML Schema from " + Constants.XML_SCHEMAS_PATH + "/wfs/2.0.2/wfs.xsd.";
log.error(message);
log.error(e.getMessage());
throw new ServletException(message, e);
}
}
// register cache table cleaner pool
cacheTableCleanerPool = new SingleWorkerPool<CacheTableManager>(
"cache_table_cleaner",
new WorkerFactory<CacheTableManager>() {
public Worker<CacheTableManager> createWorker() {
return new CacheTableCleanerWorker();
}
},
1);
cacheTableCleanerPool.prestartCoreWorker();
registry.register(CacheTableCleanerWorker.class.getName(), cacheTableCleanerPool);
}
@Override
public void destroy() {
log.info("WFS service is destroyed by servlet container.");
// destroy resources which may otherwise cause memory leaks
try {
cacheTableCleanerPool.shutdownAndWait();
} catch (InterruptedException e) {
String message = "Failed to clean temporay tables.";
log.error(message);
log.error(e.getMessage());
}
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Map<String, String[]> tmp = request.getParameterMap();
Map<String, String[]> parameterMap = new HashMap<String, String[]>();
for (Entry<String, String[]> entry : tmp.entrySet())
parameterMap.put(entry.getKey().toUpperCase(), entry.getValue());
try {
if (parameterMap.containsKey("REQUEST")) {
if (parameterMap.get("REQUEST").length > 1)
throw new WFSException(WFSExceptionCode.OPERATION_PROCESSING_FAILED, "Multiple REQUEST keywords are not allowed.");
String requestValue = parameterMap.get("REQUEST")[0];
if (requestValue.equals("GetCapabilities")) {
// translate into JAXB object
// TODO: improve mapping
GetCapabilitiesType wfsRequest = new GetCapabilitiesType();
wfsRequest.setService(parameterMap.containsKey("SERVICE") ? parameterMap.get("SERVICE")[0] : null);
if (parameterMap.containsKey("ACCEPTVERSIONS")) {
AcceptVersionsType acceptVersions = new AcceptVersionsType();
for (String version : parameterMap.get("ACCEPTVERSIONS")[0].split(",")) {
if (!version.isEmpty())
acceptVersions.getVersion().add(version.trim());
}
wfsRequest.setAcceptVersions(acceptVersions);
}
// handle GetCapabilities request
GetCapabilitiesHandler getCapabilitiesHandler = new GetCapabilitiesHandler(jaxbBuilder, wfsConfig);
getCapabilitiesHandler.doOperation(wfsRequest, getServletContext(), request, response);
return;
}
}
// TODO: KVP binding over HTTP GET is not supported so far...
throw new WFSException(WFSExceptionCode.NO_APPLICABLE_CODE, "HTTP GET is not supported by this WFS implementation.");
} catch (JAXBException e) {
exceptionReportHandler.sendErrorResponse(new WFSException(WFSExceptionCode.INTERNAL_SERVER_ERROR, "Failed to unmarshal the XML message.", e), request, response);
}catch (WFSException e) {
exceptionReportHandler.sendErrorResponse(e, request, response);
}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
// set CORS http headers
if (wfsConfig.getServer().isEnableCORS())
response.addHeader("Access-Control-Allow-Origin", "*");
Unmarshaller unmarshaller = jaxbBuilder.getJAXBContext().createUnmarshaller();
ValidationEventHandlerImpl validationEventHandler = null;
// support XML validation
if (wfsConfig.getOperations().isUseXMLValidation()) {
unmarshaller.setSchema(wfsSchema);
validationEventHandler = new ValidationEventHandlerImpl();
unmarshaller.setEventHandler(validationEventHandler);
}
UnmarshallerHandler unmarshallerHandler = unmarshaller.getUnmarshallerHandler();
// use SAX parser to keep track of namespace declarations
SAXParser parser = saxParserFactory.newSAXParser();
XMLReader reader = parser.getXMLReader();
NamespaceFilter namespaceFilter = new NamespaceFilter(reader);
namespaceFilter.setContentHandler(unmarshallerHandler);
try {
namespaceFilter.parse(new InputSource(request.getInputStream()));
} catch (SAXException e) {
if (validationEventHandler != null && !validationEventHandler.isValid())
throw new WFSException(WFSExceptionCode.OPERATION_PARSING_FAILED, validationEventHandler.getCause());
else throw e;
}
// unmarshal WFS request
// TODO: add native support for GML 3.2
Object object = unmarshallerHandler.getResult();
if (!(object instanceof JAXBElement<?>))
throw new WFSException(WFSExceptionCode.OPERATION_PARSING_FAILED, "Failed to parse XML document received through HTTP POST.");
JAXBElement<?> jaxbElement = (JAXBElement<?>)object;
Object wfsRequest = ((JAXBElement<?>)object).getValue();
if (wfsRequest instanceof GetFeatureType) {
try {
// make sure we only serve a maximum number of requests in parallel
if (!requestQueue.offer(request.getRemoteAddr(), wfsConfig.getServer().getWaitTimeout(), TimeUnit.SECONDS))
throw new WFSException(WFSExceptionCode.SERVICE_UNAVAILABLE, "The service is currently unavailable because it is overloaded. " +
"Generally, this is a temporary state. Please retry later.");
// handle GetFeature request
GetFeatureHandler getFeatureHandler = new GetFeatureHandler(jaxbBuilder, wfsConfig, exporterConfig);
getFeatureHandler.doOperation((GetFeatureType)wfsRequest, namespaceFilter, request, response);
} catch (InterruptedException e) {
throw new WFSException(WFSExceptionCode.INTERNAL_SERVER_ERROR, "The service has internally interrupted the request: " + e.getMessage());
} finally {
// free one slot from the request queue
requestQueue.remove(request.getRemoteAddr());
}
}
else if (wfsRequest instanceof DescribeFeatureTypeType) {
// handle DescribeFeatureType request
DescribeFeatureTypeHandler describeFeatureTypeHandler = new DescribeFeatureTypeHandler(wfsConfig);
describeFeatureTypeHandler.doOperation((DescribeFeatureTypeType)wfsRequest, getServletContext(), request, response);
}
else if (wfsRequest instanceof GetCapabilitiesType) {
// handle GetCapabilities request
GetCapabilitiesHandler getCapabilitiesHandler = new GetCapabilitiesHandler(jaxbBuilder, wfsConfig);
getCapabilitiesHandler.doOperation((GetCapabilitiesType)wfsRequest, getServletContext(), request, response);
}
else if (wfsRequest instanceof ListStoredQueriesType) {
// handle ListStoredQueries request
ListStoredQueriesHandler listStoredQueriesHandler = new ListStoredQueriesHandler(jaxbBuilder);
listStoredQueriesHandler.doOperation((ListStoredQueriesType)wfsRequest, request, response);
}
else if (wfsRequest instanceof DescribeStoredQueriesType) {
// handle ListStoredQueries request
DescribeStoredQueriesHandler describeStoredQueriesHandler = new DescribeStoredQueriesHandler(jaxbBuilder);
describeStoredQueriesHandler.doOperation((DescribeStoredQueriesType)wfsRequest, request, response);
}
else if (wfsRequest != null)
throw new WFSException(WFSExceptionCode.OPERATION_NOT_SUPPORTED, "The operation " + jaxbElement.getName().toString() + " is not supported by this WFS implementation.");
else
throw new WFSException(WFSExceptionCode.OPERATION_PARSING_FAILED, "Failed to parse the requested operation.");
} catch (JAXBException e) {
exceptionReportHandler.sendErrorResponse(new WFSException(WFSExceptionCode.INTERNAL_SERVER_ERROR, "Failed to unmarshal the XML message.", e), request, response);
} catch (SAXException e) {
exceptionReportHandler.sendErrorResponse(new WFSException(WFSExceptionCode.OPERATION_PARSING_FAILED, "Failed to parse the XML message.", e), request, response);
} catch (ParserConfigurationException e) {
exceptionReportHandler.sendErrorResponse(new WFSException(WFSExceptionCode.INTERNAL_SERVER_ERROR, "Failed to initialize a SAX parser for parsing the XML message.", e), request, response);
} catch (WFSException e) {
if (!response.isCommitted())
response.reset();
exceptionReportHandler.sendErrorResponse(e, request, response);
}
}
@Override
protected void doOptions(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// support CORS preflight requests
if (wfsConfig.getServer().isEnableCORS()) {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "GET, POST");
response.setHeader("Access-Control-Max-Age", "86400");
String requestCORSHeaders = request.getHeader("Access-Control-Request-Headers");
if (requestCORSHeaders != null)
response.setHeader("Access-Control-Allow-Headers", requestCORSHeaders);
}
}
}