/* * 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.math.BigInteger; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.List; import javax.xml.bind.JAXBElement; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeConstants; import javax.xml.datatype.DatatypeFactory; import javax.xml.datatype.XMLGregorianCalendar; import javax.xml.namespace.QName; import org.citydb.api.concurrent.SingleWorkerPool; import org.citydb.api.concurrent.WorkerPool; import org.citydb.config.Config; import org.citydb.database.DatabaseConnectionPool; import org.citydb.modules.citygml.exporter.database.content.DBSplittingResult; import org.citydb.modules.common.filter.ExportFilter; import org.citydb.modules.common.filter.feature.GmlIdFilter; import org.citydb.util.Util; import org.citygml4j.builder.jaxb.JAXBBuilder; import org.citygml4j.model.citygml.CityGMLClass; import org.citygml4j.util.xml.SAXEventBuffer; import org.citygml4j.util.xml.SAXFragmentWriter; import org.citygml4j.util.xml.SAXFragmentWriter.WriteMode; import org.xml.sax.SAXException; import net.opengis.wfs._2.FeatureCollectionType; import net.opengis.wfs._2.GetFeatureType; import net.opengis.wfs._2.MemberPropertyType; import net.opengis.wfs._2.ObjectFactory; import net.opengis.wfs._2.ResultTypeType; import vcs.citydb.wfs.config.Constants; import vcs.citydb.wfs.config.WFSConfig; import vcs.citydb.wfs.exception.WFSException; import vcs.citydb.wfs.exception.WFSExceptionCode; public class QueryExecuter { private final List<QueryExpression> queryExpressions; private final FeatureMemberWriterFactory writerFactory; private final long count; private final ResultTypeType resultType; private final WorkerPool<DBSplittingResult> databaseWorkerPool; private final SingleWorkerPool<SAXEventBuffer> writerPool; private final DatabaseConnectionPool connectionPool; private final Marshaller marshaller; private final ExportFilter exportFilter; private final Config exporterConfig; private final QueryBuilder queryBuilder; private final ObjectFactory wfsFactory; private final DatatypeFactory datatypeFactory; public QueryExecuter(GetFeatureType wfsRequest, List<QueryExpression> queryExpressions, FeatureMemberWriterFactory writerFactory, WorkerPool<DBSplittingResult> databaseWorkerPool, SingleWorkerPool<SAXEventBuffer> writerPool, DatabaseConnectionPool connectionPool, JAXBBuilder jaxbBuilder, ExportFilter exportFilter, WFSConfig wfsConfig, Config exporterConfig) throws JAXBException, DatatypeConfigurationException { this.queryExpressions = queryExpressions; this.writerFactory = writerFactory; this.databaseWorkerPool = databaseWorkerPool; this.writerPool = writerPool; this.connectionPool = connectionPool; this.exportFilter = exportFilter; this.exporterConfig = exporterConfig; // get standard request parameters long maxFeatureCount = wfsConfig.getSecurity().getMaxFeatureCount(); count = wfsRequest.isSetCount() && wfsRequest.getCount().longValue() < maxFeatureCount ? wfsRequest.getCount().longValue() : maxFeatureCount; resultType = wfsRequest.getResultType(); queryBuilder = new QueryBuilder(); marshaller = jaxbBuilder.getJAXBContext().createMarshaller(); datatypeFactory = DatatypeFactory.newInstance(); wfsFactory = new ObjectFactory(); } public void executeQuery() throws WFSException { String query = queryBuilder.buildQuery(queryExpressions); boolean isMultipleQueryRequest = queryExpressions.size() > 1; boolean countBreak = false; long returnedFeature = 0; int currentQuery = -1; boolean purgeConnectionPool = false; boolean isWriteBareFeature = !isMultipleQueryRequest && queryExpressions.get(0).isGetFeatureById() && resultType == ResultTypeType.RESULTS; writerFactory.setWriteMemberProperty(!isWriteBareFeature); Connection connection = null; PreparedStatement stmt = null; ResultSet rs = null; try { connection = initConnection(); stmt = connection.prepareStatement(query.toString()); fillPlaceHolders(queryExpressions, stmt); rs = stmt.executeQuery(); if (rs.next()) { long matchAll = rs.getLong("match_all"); long returnAll = resultType == ResultTypeType.RESULTS ? Math.min(matchAll, count) : 0; if (isWriteBareFeature && returnAll != 1) { isWriteBareFeature = false; writerFactory.setWriteMemberProperty(true); } if (!isWriteBareFeature) startFeatureCollection(matchAll, returnAll, false); setExporterContext(queryExpressions.get(0)); if (resultType == ResultTypeType.RESULTS) { do { if (isMultipleQueryRequest) { int queryNo = rs.getInt("query_no"); if (queryNo != currentQuery) { if (currentQuery != -1) { // join database workers databaseWorkerPool.join(); endFeatureCollection(true); } // add feature collections for intermediate queries without matches while (currentQuery < queryNo - 1) { writeFeatureCollection(0, true); currentQuery++; } currentQuery = queryNo; long matchQuery = rs.getInt("match_query"); long returnQuery = returnedFeature + matchQuery <= returnAll ? matchQuery : returnAll - returnedFeature; startFeatureCollection(matchQuery, returnQuery, true); if (currentQuery > 0) setExporterContext(queryExpressions.get(currentQuery)); } } if (returnedFeature != count) { long cityObjectId = rs.getLong("id"); int classId = rs.getInt("objectclass_id"); CityGMLClass featureType = Util.classId2cityObject(classId); // put feature on worker queue DBSplittingResult splitter = new DBSplittingResult(cityObjectId, featureType); databaseWorkerPool.addWork(splitter); returnedFeature++; } else { countBreak = true; break; } } while (rs.next()); // shutdown database worker pool databaseWorkerPool.shutdownAndWait(); if (isMultipleQueryRequest) { endFeatureCollection(true); // add feature collections for queries without matches or returns while (++currentQuery < queryExpressions.size()) { long matchQuery = 0; if (countBreak) { rs.close(); stmt.close(); QueryBuilder builder = new QueryBuilder(); stmt = connection.prepareStatement(builder.buildHitsQuery(queryExpressions.get(currentQuery))); rs = stmt.executeQuery(); if (rs.next()) matchQuery = rs.getLong(1); } writeFeatureCollection(matchQuery, true); } } } if (!isWriteBareFeature) endFeatureCollection(false); } else { // no results returned if (!isWriteBareFeature) { if (isMultipleQueryRequest) { startFeatureCollection(0, 0, false); for (int i = 0; i < queryExpressions.size(); i++) writeFeatureCollection(0, true); endFeatureCollection(false); } else writeFeatureCollection(0, false); } else { QueryExpression getFeatureById = queryExpressions.get(0); String identifier = getFeatureById.getGmlIdFilter().getFilterState().get(0); throw new WFSException(WFSExceptionCode.NOT_FOUND, "The specified feature identified by '" + identifier + "' was not found.", identifier); } } } catch (SQLException e) { purgeConnectionPool = true; throw new WFSException(WFSExceptionCode.INTERNAL_SERVER_ERROR, "A fatal SQL error occurred whilst querying the database.", e); } catch (JAXBException e) { throw new WFSException(WFSExceptionCode.INTERNAL_SERVER_ERROR, "A fatal JAXB error occurred whilst marshalling the response document.", e); } catch (SAXException e) { throw new WFSException(WFSExceptionCode.INTERNAL_SERVER_ERROR, "A fatal SAX error occurred whilst marshalling the response document.", e); } catch (InterruptedException e) { throw new WFSException(WFSExceptionCode.INTERNAL_SERVER_ERROR, "A fatal error occurred whilst marshalling the response document.", e); } finally { if (rs != null) { try { rs.close(); } catch (SQLException e) { throw new WFSException(WFSExceptionCode.INTERNAL_SERVER_ERROR, "Failed to close database resource", e); } } if (stmt != null) { try { stmt.close(); } catch (SQLException e) { throw new WFSException(WFSExceptionCode.INTERNAL_SERVER_ERROR, "Failed to close database resource", e); } } if (connection != null) { try { connection.close(); } catch (SQLException e) { throw new WFSException(WFSExceptionCode.INTERNAL_SERVER_ERROR, "Failed to close database resource", e); } } // purge connection pool to remove possibly defect connections if (purgeConnectionPool) connectionPool.purge(); } } private void fillPlaceHolders(List<QueryExpression> queryExpressions, PreparedStatement stmt) throws SQLException { int i = 1; for (QueryExpression queryExpression : queryExpressions) { GmlIdFilter filter = queryExpression.getGmlIdFilter(); if (filter != null) { List<String> ids = filter.getFilterState(); if (ids != null && !ids.isEmpty()) { for (String id : ids) stmt.setString(i++, id); } } } } private void setExporterContext(QueryExpression queryExpression) { // enable xlink references in multiple query responses exporterConfig.getInternal().setRegisterGmlIdInCache(queryExpressions.size() > 1); // set target reference system for export exporterConfig.getInternal().setExportTargetSRS(connectionPool.getActiveDatabaseAdapter().getConnectionMetaData().getReferenceSystem()); exporterConfig.getInternal().setTransformCoordinates(false); // update filter configuration // TODO: replace with filter layer exportFilter.setFeatureClassFilter(queryExpression.getFeatureTypeFilter()); exportFilter.setGmlIdFilter(queryExpression.getGmlIdFilter()); } private void writeFeatureCollection(long matchNo, boolean withMemberProperty) throws JAXBException, SAXException { JAXBElement<?> output = null; FeatureCollectionType featureCollection = new FeatureCollectionType(); featureCollection.setTimeStamp(getTimeStamp()); featureCollection.setNumberMatched(String.valueOf(matchNo)); featureCollection.setNumberReturned(BigInteger.valueOf(0)); if (withMemberProperty) { MemberPropertyType member = new MemberPropertyType(); member.getContent().add(wfsFactory.createFeatureCollection(featureCollection)); output = wfsFactory.createMember(member); } else output = wfsFactory.createFeatureCollection(featureCollection); SAXEventBuffer buffer = new SAXEventBuffer(); marshaller.marshal(output, buffer); writerPool.addWork(buffer); } private void startFeatureCollection(long matchNo, long returnNo, boolean withMemberProperty) throws JAXBException, SAXException { writeFeatureCollection(matchNo, returnNo, withMemberProperty, WriteMode.HEAD); } private void endFeatureCollection(boolean withMemberProperty) throws JAXBException, SAXException { writeFeatureCollection(0, 0, withMemberProperty, WriteMode.TAIL); } private void writeFeatureCollection(long matchNo, long returnNo, boolean withMemberProperty, WriteMode writeMode) throws JAXBException, SAXException { JAXBElement<?> output = null; FeatureCollectionType featureCollection = new FeatureCollectionType(); featureCollection.setTimeStamp(getTimeStamp()); featureCollection.setNumberMatched(String.valueOf(matchNo)); featureCollection.setNumberReturned(BigInteger.valueOf(returnNo)); if (withMemberProperty) { MemberPropertyType member = new MemberPropertyType(); member.getContent().add(wfsFactory.createFeatureCollection(featureCollection)); output = wfsFactory.createMember(member); } else output = wfsFactory.createFeatureCollection(featureCollection); SAXEventBuffer buffer = new SAXEventBuffer(); SAXFragmentWriter fragmentWriter = new SAXFragmentWriter(new QName(Constants.WFS_NAMESPACE_URI, "FeatureCollection"), buffer, writeMode); marshaller.marshal(output, fragmentWriter); writerPool.addWork(buffer); } private XMLGregorianCalendar getTimeStamp() { GregorianCalendar date = new GregorianCalendar(); return datatypeFactory.newXMLGregorianCalendar( date.get(Calendar.YEAR), date.get(Calendar.MONTH) + 1, date.get(Calendar.DAY_OF_MONTH), date.get(Calendar.HOUR_OF_DAY), date.get(Calendar.MINUTE), date.get(Calendar.SECOND), DatatypeConstants.FIELD_UNDEFINED, DatatypeConstants.FIELD_UNDEFINED); } private Connection initConnection() throws SQLException { Connection connection = connectionPool.getConnection(); connection.setAutoCommit(false); // try and change workspace for connection if (connectionPool.getActiveDatabaseAdapter().hasVersioningSupport()) { connectionPool.getActiveDatabaseAdapter().getWorkspaceManager().gotoWorkspace( connection, exporterConfig.getProject().getDatabase().getWorkspaces().getExportWorkspace()); } // TODO: create temporary table for global appearances if needed return connection; } }