/*
* 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.operation.getfeature;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.bind.JAXBElement;
import javax.xml.namespace.QName;
import net.opengis.fes._2.AbstractQueryExpressionType;
import net.opengis.fes._2.FilterType;
import net.opengis.wfs._2.GetFeatureType;
import net.opengis.wfs._2.QueryType;
import net.opengis.wfs._2.ResolveValueType;
import net.opengis.wfs._2.StoredQueryType;
import org.citydb.api.registry.ObjectRegistry;
import org.citydb.config.Config;
import org.citydb.config.project.filter.FeatureClass;
import org.citydb.config.project.filter.FilterMode;
import org.citydb.config.project.filter.GmlId;
import org.citydb.log.Logger;
import org.citydb.modules.common.filter.feature.FeatureClassFilter;
import org.citydb.modules.common.filter.feature.GmlIdFilter;
import org.citygml4j.builder.jaxb.JAXBBuilder;
import org.citygml4j.model.module.Modules;
import org.citygml4j.model.module.citygml.CityGMLVersion;
import vcs.citydb.wfs.config.WFSConfig;
import vcs.citydb.wfs.exception.WFSException;
import vcs.citydb.wfs.exception.WFSExceptionCode;
import vcs.citydb.wfs.exception.WFSExceptionMessage;
import vcs.citydb.wfs.operation.BaseRequestHandler;
import vcs.citydb.wfs.operation.FeatureTypeHandler;
import vcs.citydb.wfs.operation.FilterHandler;
import vcs.citydb.wfs.operation.storedquery.StoredQuery;
import vcs.citydb.wfs.operation.storedquery.StoredQueryManager;
import vcs.citydb.wfs.util.LoggerUtil;
import vcs.citydb.wfs.xml.NamespaceFilter;
public class GetFeatureHandler {
private final Logger log = Logger.getInstance();
private final WFSConfig wfsConfig;
private final Config exporterConfig;
private final ExportController controller;
private final BaseRequestHandler baseRequestHandler;
private final FeatureTypeHandler featureTypeHandler;
private final FilterHandler filterHandler;
private final StoredQueryManager storedQueryManager;
public GetFeatureHandler(JAXBBuilder jaxbBuilder, WFSConfig wfsConfig, Config exporterConfig) {
this.wfsConfig = wfsConfig;
this.exporterConfig = exporterConfig;
controller = new ExportController(jaxbBuilder, wfsConfig, exporterConfig);
baseRequestHandler = new BaseRequestHandler();
featureTypeHandler = new FeatureTypeHandler();
filterHandler = new FilterHandler();
storedQueryManager = (StoredQueryManager)ObjectRegistry.getInstance().lookup(StoredQueryManager.class.getName());
}
public void doOperation(GetFeatureType wfsRequest,
NamespaceFilter namespaceFilter,
HttpServletRequest request,
HttpServletResponse response) throws WFSException {
log.info(LoggerUtil.getLogMessage(request, "Accepting GetFeature request."));
final List<QueryExpression> queryExpressions = new ArrayList<QueryExpression>();
final String operationHandle = wfsRequest.getHandle();
CityGMLVersion version = null;
// check base service parameters
baseRequestHandler.validate(wfsRequest);
// check output format
if (wfsRequest.isSetOutputFormat() && !wfsConfig.getOperations().getGetFeature().supportsOutputFormat(wfsRequest.getOutputFormat())) {
WFSExceptionMessage message = new WFSExceptionMessage(WFSExceptionCode.OPTION_NOT_SUPPORTED);
message.addExceptionText("The output format of a GetFeature request must match one of the following formats:");
message.addExceptionTexts(wfsConfig.getOperations().getGetFeature().getOutputFormatAsString());
message.setLocator(operationHandle);
throw new WFSException(message);
}
// TODO: add support for response paging
if (wfsRequest.isSetStartIndex())
throw new WFSException(WFSExceptionCode.OPTION_NOT_SUPPORTED, "Response paging is not supported.", operationHandle);
// TODO: add support for local resource resolving
if (wfsRequest.getResolve() != ResolveValueType.NONE)
throw new WFSException(WFSExceptionCode.OPTION_NOT_SUPPORTED, "Resource resolving is not supported.", operationHandle);
// check for queries to be present
if (wfsRequest.getAbstractQueryExpression().isEmpty())
throw new WFSException(WFSExceptionCode.OPERATION_PARSING_FAILED, "No query provided.", operationHandle);
// compile queries to be executed from ad hoc and stored queries
List<QueryTypeWrapper> queries = new ArrayList<QueryTypeWrapper>();
for (JAXBElement<? extends AbstractQueryExpressionType> queryElem: wfsRequest.getAbstractQueryExpression()) {
if (!(queryElem.getValue() instanceof StoredQueryType))
throw new WFSException(WFSExceptionCode.OPTION_NOT_SUPPORTED, "Only stored query expressions are supported in a GetFeature request.", operationHandle);
compileQuery(queryElem.getValue(), queries, namespaceFilter, operationHandle);
}
// iterate through queries
for (QueryTypeWrapper query : queries) {
// dummy fields to store parsing results
// TODO: must be replaced with FE layer
Set<QName> featureTypeNames = null;
Set<String> resourceIds = null;
String queryHandle = query.queryType.isSetHandle() ? query.queryType.getHandle() : operationHandle;
// TODO: aliases - and implicitly joins - are not supported
if (!query.queryType.getAliases().isEmpty())
throw new WFSException(WFSExceptionCode.OPTION_NOT_SUPPORTED, "Aliases for feature type names are not supported.", queryHandle);
if (query.queryType.isSetFeatureVersion())
throw new WFSException(WFSExceptionCode.OPTION_NOT_SUPPORTED, "Feature versioning is not supported.", queryHandle);
// TODO: add support for sorting
if (query.queryType.isSetAbstractSortingClause())
throw new WFSException(WFSExceptionCode.OPTION_NOT_SUPPORTED, "Sorting is not supported.", queryHandle);
// TODO: add support for coordinate transformation
if (query.queryType.isSetSrsName())
throw new WFSException(WFSExceptionCode.OPTION_NOT_SUPPORTED, "Coordinate transformation is not supported.", queryHandle);
// get feature type names and check for unique CityGML version
if (query.queryType.getTypeNames().size() > 1)
throw new WFSException(WFSExceptionCode.OPERATION_NOT_SUPPORTED, "Join queries are not supported.", queryHandle);
featureTypeNames = featureTypeHandler.getFeatureTypeNames(query.queryType.getTypeNames(), namespaceFilter, false, queryHandle);
CityGMLVersion featureVersion = CityGMLVersion.fromCityGMLModule(Modules.getCityGMLModule(featureTypeNames.iterator().next().getNamespaceURI()));
if (version == null)
version = featureVersion;
else if (version != featureVersion)
throw new WFSException(WFSExceptionCode.OPERATION_PROCESSING_FAILED, "Mixing feature types from different CityGML versions is not supported.", queryHandle);
// TODO: add support for projection attributes
if (query.queryType.isSetAbstractProjectionClause())
throw new WFSException(WFSExceptionCode.OPTION_NOT_SUPPORTED, "Property projection is not supported.", queryHandle);
// validate selection clause of query
if (query.queryType.isSetAbstractSelectionClause()) {
FilterType filter = validateSelectionClause(query.queryType.getAbstractSelectionClause(), queryHandle);
if (filter != null) {
resourceIds = filterHandler.getResourceIds(filter, queryHandle);
}
}
// map parsing result into query expression
// TODO: again, the query expression should simply hold
// objects from the intermediate FE layer
QueryExpression queryExpression = getQueryExpression(featureTypeNames, resourceIds, query.isGetFeatureById);
queryExpression.setHandle(queryHandle);
queryExpressions.add(queryExpression);
}
if (queryExpressions.size() == 0)
throw new WFSException(WFSExceptionCode.OPERATION_PARSING_FAILED, "No valid query expressions provided.", operationHandle);
controller.doExport(wfsRequest, queryExpressions, version, request, response);
log.info(LoggerUtil.getLogMessage(request, "GetFeature operation successfully finished."));
}
private void compileQuery(AbstractQueryExpressionType abstractQuery, List<QueryTypeWrapper> queries, NamespaceFilter namespaceFilter, String handle) throws WFSException {
if (abstractQuery instanceof QueryType) {
queries.add(new QueryTypeWrapper((QueryType)abstractQuery, false));
}
else if (abstractQuery instanceof StoredQueryType) {
StoredQueryType query = (StoredQueryType)abstractQuery;
StoredQuery storedQuery = storedQueryManager.getStoredQuery(query.getId(), handle);
if (storedQuery != null) {
if (storedQuery.getId().equals("http://www.opengis.net/def/query/OGC-WFS/0/GetFeatureById")) {
queries.add(new QueryTypeWrapper((QueryType)storedQuery.compile(query, namespaceFilter).iterator().next(), true));
} else {
for (AbstractQueryExpressionType compiled : storedQuery.compile(query, namespaceFilter))
compileQuery(compiled, queries, namespaceFilter, handle);
}
} else
throw new WFSException(WFSExceptionCode.INVALID_PARAMETER_VALUE, "No stored query with identifier '" + query.getId() + "' is offered by this server.", handle);
}
else
throw new WFSException(WFSExceptionCode.OPTION_NOT_SUPPORTED, "Only ad hoc and stored query expressions are supported in a GetFeature request.", handle);
}
private FilterType validateSelectionClause(JAXBElement<?> selectionClauseElement, String handle) throws WFSException {
if (!(selectionClauseElement.getValue() instanceof FilterType))
throw new WFSException(WFSExceptionCode.INVALID_PARAMETER_VALUE, "The element " + selectionClauseElement.getName() + " is not supported as selection clause of queries.", handle);
FilterType filter = (FilterType)selectionClauseElement.getValue();
if (filter.getSpatialOps() != null)
throw new WFSException(WFSExceptionCode.OPTION_NOT_SUPPORTED, "Spatial filter expressions are not supported.", handle);
if (filter.getTemporalOps() != null)
throw new WFSException(WFSExceptionCode.OPTION_NOT_SUPPORTED, "Temporal filter expressions are not supported.", handle);
if (filter.getLogicOps() != null)
throw new WFSException(WFSExceptionCode.OPTION_NOT_SUPPORTED, "Logical filter expressions are not supported.", handle);
if (filter.getComparisonOps() != null)
throw new WFSException(WFSExceptionCode.OPTION_NOT_SUPPORTED, "Arithmetic filter expressions are not supported.", handle);
if (filter.getExtensionOps() != null)
throw new WFSException(WFSExceptionCode.OPTION_NOT_SUPPORTED, "Filter extensions are not supported.", handle);
if (filter.getFunction() != null)
throw new WFSException(WFSExceptionCode.OPTION_NOT_SUPPORTED, "Filter functions are not supported.", handle);
return filter;
}
private QueryExpression getQueryExpression(Set<QName> featureTypeNames,
Set<String> resourceIds,
boolean isGetFeatureById) {
QueryExpression queryExpression = new QueryExpression();
// set exporter filter according to the parsing results
// TODO: In future, a FE layer should be populated instead
// of directly using the exporter filter
// populate feature type filter
if (featureTypeNames != null && !featureTypeNames.isEmpty()) {
queryExpression.setFeatureTypeNames(featureTypeNames);
FeatureClass featureClassFilter = new FeatureClass();
featureClassFilter.setActive(!featureTypeNames.isEmpty());
for (QName qName : featureTypeNames) {
String localPart = qName.getLocalPart();
if ("Building".equals(localPart))
featureClassFilter.setBuilding(false);
else if ("Bridge".equals(localPart))
featureClassFilter.setBridge(false);
else if ("Tunnel".equals(localPart))
featureClassFilter.setTunnel(false);
else if ("TransportationComplex".equals(localPart))
featureClassFilter.setTransportation(false);
else if ("Road".equals(localPart))
featureClassFilter.setRoad(false);
else if ("Track".equals(localPart))
featureClassFilter.setTrack(false);
else if ("Square".equals(localPart))
featureClassFilter.setSquare(false);
else if ("Railway".equals(localPart))
featureClassFilter.setRailway(false);
else if ("CityFurniture".equals(localPart))
featureClassFilter.setCityFurniture(false);
else if ("LandUse".equals(localPart))
featureClassFilter.setLandUse(false);
else if ("WaterBody".equals(localPart))
featureClassFilter.setWaterBody(false);
else if ("PlantCover".equals(localPart))
featureClassFilter.setPlantCover(false);
else if ("SolitaryVegetationObject".equals(localPart))
featureClassFilter.setSolitaryVegetationObject(false);
else if ("ReliefFeature".equals(localPart))
featureClassFilter.setReliefFeature(false);
else if ("GenericCityObject".equals(localPart))
featureClassFilter.setGenericCityObject(false);
else if ("CityObjectGroup".equals(localPart))
featureClassFilter.setCityObjectGroup(false);
}
// TODO: this is a hack to create a valid exporter filter
exporterConfig.getProject().getExporter().getFilter().setMode(FilterMode.COMPLEX);
exporterConfig.getProject().getExporter().getFilter().getComplexFilter().setFeatureClass(featureClassFilter);
queryExpression.setFeatureTypeFilter(new FeatureClassFilter(exporterConfig, org.citydb.modules.common.filter.FilterMode.EXPORT));
}
// populate resource id filter
if (resourceIds != null && !resourceIds.isEmpty()) {
GmlId gmlIdFilter = new GmlId();
for (String resourceId : resourceIds)
gmlIdFilter.addGmlId(resourceId);
exporterConfig.getProject().getExporter().getFilter().setMode(FilterMode.SIMPLE);
exporterConfig.getProject().getExporter().getFilter().getSimpleFilter().setGmlIdFilter(gmlIdFilter);
queryExpression.setGmlIdFilter(new GmlIdFilter(exporterConfig, org.citydb.modules.common.filter.FilterMode.EXPORT));
}
// is this the GetFeatureById query?
queryExpression.setGetFeatureById(isGetFeatureById);
return queryExpression;
}
private final class QueryTypeWrapper {
protected final QueryType queryType;
protected final boolean isGetFeatureById;
protected QueryTypeWrapper(QueryType queryType, boolean isGetFeatureById) {
this.queryType = queryType;
this.isGetFeatureById = isGetFeatureById;
}
}
}