Skip to content

Commit

Permalink
[Backport 2.25.x] [GEOS-11267] csw-iso: multiple mappings should have…
Browse files Browse the repository at this point in the history
… multiple queryabl… (#7518)

* [GEOS-11267] csw-iso: multiple mappings should have multiple queryables mappings

* [GEOS-11267] csw-iso: multiple mappings should have multiple queryables mappings (fixes)

---------

Co-authored-by: NielsCharlier <niels@scitus.be>
  • Loading branch information
aaime and NielsCharlier authored Mar 27, 2024
1 parent e0a1787 commit c980f1a
Show file tree
Hide file tree
Showing 18 changed files with 431 additions and 186 deletions.
11 changes: 10 additions & 1 deletion doc/en/user/source/extensions/csw-iso/queryables.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,13 @@ The ISO Metadata queryables mapping can be found in the file ``csw/MD_Metadata.q

Bounding Box
~~~~~~~~~~~~
The 'BoundingBox' queryable has an additional functionality. Changing it will not only alter the queryable as such, but also how the layers are mapped to ISO metadata records. The first XPath will be the place where the bounding box of the layers are encoded in each of the records.
The 'BoundingBox' queryable has an additional functionality. Changing it will not only alter the queryable as such, but also how the layers are mapped to ISO metadata records. The first XPath will be the place where the bounding box of the layers are encoded in each of the records.

Multiple Mappings
~~~~~~~~~~~~~~~~~
The CSW module supports mapping each layer to multiple records (see :ref:`csw_mapping_file`). In this case one might want to have separate queryables mappings associated with these distinct mappings as well. (One could avoid this and map queryables to multiple XPaths instead as explained above, but that would not make it possible to map the bounding box to separate XPaths). The syntax of the queryables mappings file names is analogue to the regular mappings. For instance, or instance, one could have the following files in the ``csw`` directory:

* ``csw/Record.properties``
* ``csw/Record-otherRecord.properties``
* ``csw/Record.queryables.properties``
* ``csw/Record-otherRecord.queryables.properties``
Original file line number Diff line number Diff line change
Expand Up @@ -262,18 +262,20 @@ public NamespaceSupport getNamespaceSupport() {

@Override
public Query adaptQuery(Query query) {
query = new Query(query);
Filter filter = query.getFilter();
if (filter != null && !Filter.INCLUDE.equals(filter)) {
Filter qualified = (Filter) filter.accept(NSS_QUALIFIER, null);
Filter extended = (Filter) qualified.accept(PATH_EXTENDER, null);
query.setFilter(extended);
}

SortBy[] sortBy = query.getSortBy();
if (sortBy != null && sortBy.length > 0) {
CSWPropertyPathExtender extender = new CSWPropertyPathExtender();
CSWPropertyPathExtender extender = new CSWPropertyPathExtender();

if (query.getSortBy() != null && query.getSortBy().length > 0) {
SortBy[] sortBy = new SortBy[query.getSortBy().length];
for (int i = 0; i < sortBy.length; i++) {
SortBy sb = sortBy[i];
SortBy sb = query.getSortBy()[i];
if (!SortBy.NATURAL_ORDER.equals(sb) && !SortBy.REVERSE_ORDER.equals(sb)) {
PropertyName name = sb.getPropertyName();
PropertyName extended = extender.extendProperty(name, FF, NAMESPACES);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public class GenericRecordBuilder implements RecordBuilder {
protected ComplexFeatureBuilder fb;
protected List<ReferencedEnvelope> boxes = new ArrayList<>();
protected RecordDescriptor recordDescriptor;
protected QueryablesMapping queryables;
protected Map<Name, Name> substitutionMap = new HashMap<>();

/**
Expand Down Expand Up @@ -127,7 +128,18 @@ public boolean cleanUp() {
* @param recordDescriptor The Record Descriptor
*/
public GenericRecordBuilder(RecordDescriptor recordDescriptor) {
this(recordDescriptor, recordDescriptor);
}

/**
* Start Generic Record Builder based on the Record Descriptor
*
* @param recordDescriptor The Record Descriptor
* @param queryables The queryables
*/
public GenericRecordBuilder(RecordDescriptor recordDescriptor, QueryablesMapping queryables) {
this.recordDescriptor = recordDescriptor;
this.queryables = queryables;
fb = new ComplexFeatureBuilder(recordDescriptor.getFeatureDescriptor());

for (PropertyDescriptor descriptor : recordDescriptor.getFeatureType().getDescriptors()) {
Expand Down Expand Up @@ -372,11 +384,11 @@ public Feature build(String id) {
geom = geom.getFactory().createMultiPolygon(new Polygon[] {(Polygon) geom});
}

if (recordDescriptor.getBoundingBoxPropertyName() != null) {
if (queryables.getBoundingBoxPropertyName() != null) {
Map<Object, Object> userData =
Collections.singletonMap(ORIGINAL_BBOXES, new ArrayList<>(boxes));
addElement(
recordDescriptor.getBoundingBoxPropertyName(),
queryables.getBoundingBoxPropertyName(),
Collections.singletonList(geom),
userData,
new int[0]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.csw.records;

import java.util.List;
import org.geotools.api.data.Query;
import org.geotools.api.feature.type.Name;
import org.geotools.api.filter.expression.PropertyName;

/**
* A mapping of CSW queryables to properties of the metadata record schema. Provides functionality
* for translating properties and adapting queries.
*/
public interface QueryablesMapping {

/**
* Allow the descriptor to adjust the query to the internal representation of records. For
* example, in the case of SimpleLiteral we have a complex type with simple content, something
* that we cannot readily represent in GeoTools
*
* <p>Must provide a copy, not change the original query.
*/
Query adaptQuery(Query query);

/**
* Return the property name (with dots) for the bounding box property
*
* @return the bounding box property name
*/
String getBoundingBoxPropertyName();

/**
* Translate a property from a queryable name to a propertyname, possibly converting to an
* x-path
*
* @return the property name
*/
List<PropertyName> translateProperty(Name name);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@
import java.util.LinkedHashSet;
import java.util.List;
import net.opengis.cat.csw20.ElementSetType;
import org.geotools.api.data.Query;
import org.geotools.api.feature.type.AttributeDescriptor;
import org.geotools.api.feature.type.FeatureType;
import org.geotools.api.feature.type.Name;
import org.geotools.api.filter.Filter;
import org.geotools.api.filter.expression.PropertyName;
import org.xml.sax.helpers.NamespaceSupport;

/**
Expand All @@ -22,7 +20,7 @@
* @author Andrea Aime - GeoSolutions
* @author Niels Charlier
*/
public interface RecordDescriptor {
public interface RecordDescriptor extends QueryablesMapping {

/** The GeoTools feature type representing this kind of record */
FeatureType getFeatureType();
Expand All @@ -47,18 +45,21 @@ public interface RecordDescriptor {
NamespaceSupport getNamespaceSupport();

/**
* Allow the descriptor to adjust the query to the internal representation of records. For
* example, in the case of SimpleLiteral we have a complex type with simple content, something
* that we cannot readily represent in GeoTools
* Checks that the spatial filters are actually referring to a spatial property. The {@link
* SpatialFilterChecker} utility class can be used against simple records (like CSW), but more
* complex record types will need a more sophisticated approach
*/
Query adaptQuery(Query query);
void verifySpatialFilters(Filter filter);

/**
* Return the property name (with dots) for the bounding box property
* Optional support for multiple queryables mappings
*
* @return the bounding box property name
* @param mappingName name of the queryables mapping
* @return the queryables
*/
String getBoundingBoxPropertyName();
default QueryablesMapping getQueryablesMapping(String mappingName) {
return this;
}

/**
* Return the queryables for this type of record (for getcapabilities)
Expand All @@ -73,19 +74,4 @@ public interface RecordDescriptor {
* @return the description string
*/
String getQueryablesDescription();

/**
* Translate a property from a queryable name to a propertyname, possibly converting to an
* x-path
*
* @return the property name
*/
List<PropertyName> translateProperty(Name name);

/**
* Checks that the spatial filters are actually referring to a spatial property. The {@link
* SpatialFilterChecker} utility class can be used against simple records (like CSW), but more
* complex record types will need a more sophisticated approach
*/
void verifySpatialFilters(Filter filter);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.geoserver.catalog.util.CloseableIteratorAdapter;
import org.geoserver.csw.records.AbstractRecordDescriptor;
import org.geoserver.csw.records.CSWRecordDescriptor;
import org.geoserver.csw.records.QueryablesMapping;
import org.geoserver.csw.records.RecordDescriptor;
import org.geotools.api.data.Query;
import org.geotools.api.data.Transaction;
Expand Down Expand Up @@ -104,6 +105,20 @@ protected boolean hasProperties(FeatureType featureType, List<PropertyName> prop
return hasProperty;
}

protected Query prepareQuery(Query q, RecordDescriptor rd, QueryablesMapping qm) {
if (Boolean.TRUE.equals(q.getHints().get(KEY_UNPREPARED))) {
q = qm.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());
}
}

return q;
}

@Override
public FeatureCollection<FeatureType, Feature> getRecords(
Query q, Transaction t, RecordDescriptor rdOutput) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.geotools.api.filter.expression.PropertyName;
import org.geotools.api.filter.identity.FeatureId;
import org.geotools.feature.FeatureCollection;
import org.geotools.util.factory.Hints;

/**
* Interfaces to a storage for CSW record objects. By default it has to provide support for CSW
Expand All @@ -29,6 +30,14 @@
*/
public interface CatalogStore {

/**
* This query hint specifies that the query has not yet been prepared (adapted and verified). It
* is necessary when we use multiple queryables mappings so that the query must be prepared
* separately for each mapping; but for backwards compatibility of the API this flag was
* created.
*/
public static final Hints.Key KEY_UNPREPARED = new Hints.Key(Boolean.class);

/** Returns the supported record types */
RecordDescriptor[] getRecordDescriptors() throws IOException;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,19 +141,10 @@ private List<GetRecords.WrappedQuery> toGtQueries(
Query q = new Query(typeName.getLocalPart());
q.setFilter(filter);

// 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(GetRecords.KEY_BASEURL, request.getBaseUrl());
q.getHints().put(GetRecords.KEY_BASEURL, request.getBaseUrl());

result.add(new GetRecords.WrappedQuery(adapted, rd));
result.add(new GetRecords.WrappedQuery(q, rd));
}

return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
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;
Expand Down Expand Up @@ -217,8 +216,6 @@ private List<WrappedQuery> toGtQueries(
"typeNames");
}

RecordDescriptor rd = getRecordDescriptor(typeName);

Query q = new Query(typeName.getLocalPart());
q.setFilter(filter);
q.setProperties(getPropertyNames(outputRd, query));
Expand All @@ -228,19 +225,13 @@ private List<WrappedQuery> toGtQueries(
} 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());
}
// prepare later for multiple queryables mappings support
q.getHints().put(CatalogStore.KEY_UNPREPARED, true);

// smuggle base url
adapted.getHints().put(KEY_BASEURL, request.getBaseUrl());
q.getHints().put(KEY_BASEURL, request.getBaseUrl());

result.add(new WrappedQuery(adapted, outputRd));
result.add(new WrappedQuery(q, outputRd));
}
}

Expand Down Expand Up @@ -295,25 +286,6 @@ private Set<Name> getSupportedTypes() throws IOException {
return result;
}

/**
* Search for the record descriptor maching the typename, throws a service exception in case
* none is found
*/
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ public CatalogStoreFeatureIterator(
nextInternal();

this.outputRecordDescriptor = outputRecordDescriptor;
builder = new GenericRecordBuilder(outputRecordDescriptor);
builder =
new GenericRecordBuilder(
outputRecordDescriptor,
outputRecordDescriptor.getQueryablesMapping(mapping.getMappingName()));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,11 @@ public CatalogStoreMapping subMapping(List<PropertyName> properties, RecordDescr
mapping.identifier = identifier;

mapping.includeEnvelope =
includeEnvelope && paths.contains(rd.getBoundingBoxPropertyName());
includeEnvelope
&& paths.contains(
rd.getQueryablesMapping(mappingName).getBoundingBoxPropertyName());

mapping.mappingName = mappingName;

return mapping;
}
Expand Down
Loading

0 comments on commit c980f1a

Please sign in to comment.