/* (c) 2014 Open Source Geospatial Foundation - all rights reserved * (c) 2001 - 2013 OpenPlans * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.csw; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.xml.namespace.QName; import net.opengis.cat.csw20.ElementSetType; import net.opengis.cat.csw20.GetRecordByIdType; import net.opengis.cat.csw20.GetRecordsType; import net.opengis.cat.csw20.QueryType; import net.opengis.cat.csw20.ResultType; import org.geoserver.csw.records.CSWRecordDescriptor; import org.geoserver.csw.records.RecordDescriptor; import org.geoserver.csw.response.CSWRecordsResult; import org.geoserver.csw.store.CatalogStore; import org.geoserver.feature.CompositeFeatureCollection; import org.geoserver.ows.URLMangler.URLType; import org.geoserver.ows.util.ResponseUtils; import org.geoserver.platform.ServiceException; import org.geotools.csw.CSW; import org.geotools.data.Query; import org.geotools.data.Transaction; import org.geotools.factory.CommonFactoryFinder; import org.geotools.factory.Hints; import org.geotools.feature.FeatureCollection; import org.geotools.feature.NameImpl; import org.geotools.feature.type.Types; import org.opengis.feature.type.Name; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory2; import org.opengis.filter.expression.PropertyName; /** * Runs the GetRecords request * * @author Andrea Aime - GeoSolutions */ public class GetRecords { static final FilterFactory2 FF = CommonFactoryFinder.getFilterFactory2(); static final public Hints.Key KEY_BASEURL = new Hints.Key(String.class); CSWInfo csw; CatalogStore store; private List<RecordDescriptor> recordDescriptors; public GetRecords(CSWInfo csw, CatalogStore store, List<RecordDescriptor> recordDescriptors) { this.csw = csw; this.store = store; this.recordDescriptors = recordDescriptors; } public CSWRecordsResult run(GetRecordsType request) { // mark the time the request started Date timestamp = new Date(); try { // build the queries RecordDescriptor outputRd = getRecordDescriptor(request); QueryType cswQuery = (QueryType) request.getQuery(); List<Query> queries = toGtQueries(outputRd, cswQuery, request); // see how many records we have to return int maxRecords; if(request.getMaxRecords() == null) { maxRecords = 10; } else { maxRecords = request.getMaxRecords(); } // get and check the offset (which is 1 based, but our API is 0 based) int offset = request.getStartPosition() == null ? 0 : request.getStartPosition() - 1; if(offset < 0) { throw new ServiceException("startPosition must be a positive number", ServiceException.INVALID_PARAMETER_VALUE, "startPosition"); } // and check what kind of result is desired ResultType resultType = request.getResultType(); if(maxRecords == 0 && resultType == ResultType.RESULTS) { resultType = ResultType.HITS; } // compute the number of records matched (in validate mode this is also a quick way // to check the request) int numberOfRecordsMatched = 0; int[] counts = new int[queries.size()]; for (int i = 0; i < queries.size(); i++) { counts[i] = store.getRecordsCount(queries.get(i), Transaction.AUTO_COMMIT); numberOfRecordsMatched += counts[i]; } ElementSetType elementSet = getElementSet(cswQuery); int numberOfRecordsReturned = 0; int nextRecord = 0; FeatureCollection records = null; if(resultType != ResultType.VALIDATE) { // compute the number of records we're returning and the next record if (offset > numberOfRecordsMatched) { numberOfRecordsReturned = 0; nextRecord = 0; } else if (numberOfRecordsMatched - offset <= maxRecords) { numberOfRecordsReturned = numberOfRecordsMatched - offset; nextRecord = 0; } else { numberOfRecordsReturned = maxRecords; // mind, nextRecord is 1 based too nextRecord = offset + numberOfRecordsReturned + 1; } // time to run the queries if we are not in hits mode if(resultType == ResultType.RESULTS) { if(resultType != ResultType.HITS) { List<FeatureCollection> results = new ArrayList<FeatureCollection>(); for (int i = 0; i < queries.size() && maxRecords > 0; i++) { Query q = queries.get(i); if(offset > 0) { if(offset > counts[i]) { // skip the query altogheter offset -= counts[i]; continue; } else { q.setStartIndex(offset); offset = 0; } } if(maxRecords > 0) { q.setMaxFeatures(maxRecords); maxRecords -= counts[i]; } else { // skip the query, we already have enough results continue; } results.add(store.getRecords(q, Transaction.AUTO_COMMIT, request.getOutputSchema())); } if(results.size() == 1) { records = results.get(0); } else if(results.size() > 1) { records = new CompositeFeatureCollection(results); } } } } // in case this is a hits request we are actually not returning any record if(resultType == ResultType.HITS) { numberOfRecordsReturned = 0; } CSWRecordsResult result = new CSWRecordsResult(elementSet, request.getOutputSchema(), numberOfRecordsMatched, numberOfRecordsReturned, nextRecord, timestamp, records); return result; } catch(IOException e) { throw new ServiceException("Request failed due to: " + e.getMessage(), e); } } private List<Query> toGtQueries(RecordDescriptor outputRd, QueryType query, GetRecordsType request) throws IOException { // prepare to build the queries Filter filter = query.getConstraint() != null ? query.getConstraint().getFilter() : null; Set<Name> supportedTypes = getSupportedTypes(); // the CSW specification expects like filters to be case insensitive (by CITE tests) // but we default to have filters case sensitive instead if(filter != null) { filter = (Filter) filter.accept(new CaseInsenstiveFilterTransformer(), null); } // build one query per type name, forgetting about paging for the time being List<Query> result = new ArrayList<Query>(); for (QName qName : query.getTypeNames()) { Name typeName = new NameImpl(qName); if (!supportedTypes.contains(typeName)) { throw new ServiceException("Unsupported record type " + typeName, ServiceException.INVALID_PARAMETER_VALUE, "typeNames"); } RecordDescriptor rd = getRecordDescriptor(typeName); Query q = new Query(typeName.getLocalPart()); q.setFilter(filter); q.setProperties(getPropertyNames(outputRd, query)); q.setSortBy(query.getSortBy()); try { q.setNamespace(new URI(typeName.getNamespaceURI())); } catch (URISyntaxException e) { } // perform some necessary query adjustments Query adapted = rd.adaptQuery(q); // the specification demands that we throw an error if a spatial operator // is used against a non spatial property if(q.getFilter() != null) { rd.verifySpatialFilters(q.getFilter()); } //smuggle base url adapted.getHints().put(KEY_BASEURL, request.getBaseUrl()); result.add(adapted); } return result; } private List<PropertyName> getPropertyNames(RecordDescriptor rd, QueryType query) { if(query.getElementName() != null && !query.getElementName().isEmpty()) { // turn the QName into PropertyName. We don't do any verification cause the // elements in the actual feature could be parts of substitution groups // of the elements in the feature's schema List<PropertyName> result = new ArrayList<PropertyName>(); for (QName qn : query.getElementName()) { result.add(store.translateProperty(rd, Types.toTypeName(qn))); } return result; } else { ElementSetType elementSet = getElementSet(query); List<Name> properties = rd.getPropertiesForElementSet(elementSet); if(properties != null) { List<PropertyName> result = new ArrayList<PropertyName>(); for (Name pn : properties) { result.add(store.translateProperty(rd, pn)); } return result; } else { // the profile is the full one return null; } } } private ElementSetType getElementSet(QueryType query) { if(query.getElementName() != null && query.getElementName().size() > 0) { return ElementSetType.FULL; } ElementSetType elementSet = query.getElementSetName() != null ? query.getElementSetName().getValue() : null; if(elementSet == null) { // the default is "summary" elementSet = ElementSetType.SUMMARY; } return elementSet; } private Set<Name> getSupportedTypes() throws IOException { Set<Name> result = new HashSet<Name>(); for (RecordDescriptor rd : store.getRecordDescriptors()) { result.add(rd.getFeatureDescriptor().getName()); } return result; } /** * Search for the record descriptor maching the typename, throws a service exception in case none * is found * * @param request * */ private RecordDescriptor getRecordDescriptor(Name typeName) { if (typeName == null) { return CSWRecordDescriptor.getInstance(); } for (RecordDescriptor rd : recordDescriptors) { if (typeName.equals(rd.getFeatureDescriptor().getName())) { return rd; } } throw new ServiceException("Unknown type: " + typeName, ServiceException.INVALID_PARAMETER_VALUE, "typeNames"); } /** * Search for the record descriptor maching the request, throws a service exception in case none * is found * * @param request * */ private RecordDescriptor getRecordDescriptor(GetRecordsType request) { String outputSchema = request.getOutputSchema(); if (outputSchema == null) { outputSchema = CSW.NAMESPACE; request.setOutputFormat(CSW.NAMESPACE); } for (RecordDescriptor rd : recordDescriptors) { if (outputSchema.equals(rd.getOutputSchema())) { return rd; } } throw new ServiceException("Cannot encode records in output schema " + outputSchema, ServiceException.INVALID_PARAMETER_VALUE, "outputSchema"); } }