Skip to content

Commit

Permalink
[GEOS-11328] STAC: expose collection layers in products too, allow su…
Browse files Browse the repository at this point in the history
…pporting the wms web-map-links extension
  • Loading branch information
aaime committed Mar 11, 2024
1 parent 2615e7e commit 3aa698b
Show file tree
Hide file tree
Showing 9 changed files with 290 additions and 78 deletions.
13 changes: 13 additions & 0 deletions doc/en/user/source/community/ogc-api/features/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,16 @@ As an example customize how collections are listed:
<#include "common-footer.ftl">
#. For details on how to write templates see :ref:`tutorial_freemarkertemplate` tutorial.
The following functions are specific to OGC API templates:
* ``serviceLink(path*, format)`` generates a link back to the same service.
The first argument, mandatory, is the extra path after the service landing page, the second argument, optional, is the format to use for the link.
* ``genericServiceLink(path*, k1, v1, k2, v2, ....)`` generates a link back to any GeoServer OGC service, with additional query parameters.
The first argument, mandatory, is the extra path after the GeoServer context path (usually ``/geoserver``),
the following arguments are key-value pairs to be added as query parameters to the link.
* ``resourceLink(path)`` links to a static resource, such as a CSS file or an image.
The argument is the path to the resource, relative to the GeoServer context path (usually ``/geoserver``).
*
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
*/
package org.geoserver.ogcapi;

import static org.geoserver.ows.URLMangler.URLType.RESOURCE;

import com.fasterxml.jackson.databind.ObjectMapper;
import freemarker.template.TemplateMethodModelEx;
import freemarker.template.TemplateModel;
Expand Down Expand Up @@ -98,52 +100,70 @@ protected HashMap<String, Object> setupModel(Object value) {
*/
@SuppressWarnings("unchecked") // TemplateMethodModelEx is not generified
protected void addLinkFunctions(String baseURL, Map<String, Object> model) {
model.put("serviceLink", (TemplateMethodModelEx) arguments -> serviceLink(arguments));
model.put(
"serviceLink",
(TemplateMethodModelEx)
arguments -> {
APIRequestInfo requestInfo = APIRequestInfo.get();
return ResponseUtils.buildURL(
requestInfo.getBaseURL(),
ResponseUtils.appendPath(
requestInfo.getServiceLandingPage(),
(String) unwrapArgument(arguments.get(0))),
arguments.size() > 1
? Collections.singletonMap(
"f", (String) unwrapArgument(arguments.get(1)))
: null,
URLMangler.URLType.SERVICE);
});
"genericServiceLink",
(TemplateMethodModelEx) arguments -> genericServiceLink(arguments));
model.put(
"resourceLink",
(TemplateMethodModelEx)
arguments ->
ResponseUtils.buildURL(
baseURL,
(String) unwrapArgument(arguments.get(0)),
null,
URLMangler.URLType.RESOURCE));
model.put(
"externalLink",
(TemplateMethodModelEx)
arguments ->
ResponseUtils.buildURL(
baseURL,
(String) unwrapArgument(arguments.get(0)),
null,
URLMangler.URLType.EXTERNAL));
arguments -> simpleLinkFunction(baseURL, arguments, RESOURCE));
model.put(
"htmlExtensions",
(TemplateMethodModelEx)
arguments -> {
if (arguments != null) {
arguments = unwrapArguments(arguments);
}
return processHtmlExtensions(model, arguments);
});
arguments -> processHtmlExtensions(model, unwrapArguments(arguments)));
model.put("loadJSON", parseJSON());
}

private String simpleLinkFunction(String baseURL, List arguments, URLMangler.URLType urlType) {
return ResponseUtils.buildURL(
baseURL, (String) unwrapArgument(arguments.get(0)), null, urlType);
}

/** Builds a service link back to the same service. Used for backlinks. */
private String serviceLink(List arguments) {
APIRequestInfo requestInfo = APIRequestInfo.get();
return ResponseUtils.buildURL(
requestInfo.getBaseURL(),
ResponseUtils.appendPath(
requestInfo.getServiceLandingPage(),
(String) unwrapArgument(arguments.get(0))),
arguments.size() > 1
? Collections.singletonMap("f", (String) unwrapArgument(arguments.get(1)))
: null,
URLMangler.URLType.SERVICE);
}

/** Builds a service link with the provided path, does not inject the current service path */
private String genericServiceLink(List arguments) {
APIRequestInfo requestInfo = APIRequestInfo.get();
return ResponseUtils.buildURL(
requestInfo.getBaseURL(),
(String) unwrapArgument(arguments.get(0)),
arguments.size() > 1
? argumentsToKVP(arguments.subList(1, arguments.size()))
: null,
URLMangler.URLType.SERVICE);
}

/** Turns a list of keys alternating with values into a map */
@SuppressWarnings("unchecked")
private Map<String, String> argumentsToKVP(List kvp) {
if (kvp.size() % 2 != 0)
throw new IllegalArgumentException(
"Arguments beyond the first must be a list of key value pairs");

List<Object> unwrapped = unwrapArguments(kvp);
Map<String, String> map = new HashMap<>();
for (int i = 0; i < unwrapped.size(); i += 2) {
String key = (String) unwrapped.get(i);
String value = (String) unwrapped.get(i + 1);
map.put(key, value);
}

return map;
}

private TemplateMethodModelEx parseJSON() {
return arguments -> loadJSON(arguments.get(0).toString());
}
Expand All @@ -170,6 +190,7 @@ private String loadJSON(String filePath) {
}

public List<Object> unwrapArguments(List<Object> arguments) {
if (arguments == null) return null;
return arguments.stream().map(v -> unwrapArgument(v)).collect(Collectors.toList());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
Expand Down Expand Up @@ -128,13 +130,12 @@ public interface IOBiFunction<T, U, R> {

private Transaction transaction;

public AbstractMappingStore(
JDBCOpenSearchAccess openSearchAccess, FeatureType collectionFeatureType)
public AbstractMappingStore(JDBCOpenSearchAccess openSearchAccess, FeatureType schema)
throws IOException {
this.openSearchAccess = openSearchAccess;
this.schema = collectionFeatureType;
this.propertyMapper = new SourcePropertyMapper(schema);
this.defaultSort = buildDefaultSort(schema);
this.schema = schema;
this.propertyMapper = new SourcePropertyMapper(this.schema);
this.defaultSort = buildDefaultSort(this.schema);
this.linkFeatureType = buildLinkFeatureType();
this.styleType = buildStyleType(openSearchAccess);
this.collectionLayerSchema = buildCollectionLayerFeatureType(openSearchAccess);
Expand Down Expand Up @@ -549,17 +550,21 @@ public FeatureCollection<FeatureType, Feature> getFeatures(Query query) throws I
fc = getDelegateSource().getFeatures(dataQuery);
}

return new MappingFeatureCollection(schema, fc, this::mapToComplexFeature);
// the mapper state allows the simple to complex map funcion to retain state across
// feature mappings (e.g. for caching)
HashMap<String, Object> mapperState = new HashMap<>();
return new MappingFeatureCollection(schema, fc, it -> mapToComplexFeature(it, mapperState));
}

/** Maps the underlying features (eventually joined) to the output complex feature */
protected Feature mapToComplexFeature(PushbackFeatureIterator<SimpleFeature> it) {
protected Feature mapToComplexFeature(
PushbackFeatureIterator<SimpleFeature> it, Map<String, Object> mapperState) {
SimpleFeature fi = it.next();

ComplexFeatureBuilder builder = new ComplexFeatureBuilder(schema, FEATURE_FACTORY);

// allow subclasses to perform custom mappings while reusing the common ones
mapPropertiesToComplex(builder, fi);
mapPropertiesToComplex(builder, fi, mapperState);

// the OGC links can be more than one
Set<SimpleFeature> links = new LinkedHashSet<>();
Expand Down Expand Up @@ -609,7 +614,8 @@ protected Feature mapToComplexFeature(PushbackFeatureIterator<SimpleFeature> it)
}

/** Performs the common mappings, subclasses can override to add more */
protected void mapPropertiesToComplex(ComplexFeatureBuilder builder, SimpleFeature fi) {
protected void mapPropertiesToComplex(
ComplexFeatureBuilder builder, SimpleFeature fi, Map<String, Object> mapperState) {
AttributeBuilder ab = new AttributeBuilder(FEATURE_FACTORY);
FeatureType schema = builder.getFeatureType();
for (PropertyDescriptor pd : schema.getDescriptors()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
package org.geoserver.opensearch.eo.store;

import static org.geoserver.opensearch.eo.store.JDBCOpenSearchAccess.FF;
import static org.geoserver.opensearch.eo.store.OpenSearchAccess.COLLECTION_PROPERTY_NAME;
import static org.geoserver.opensearch.eo.store.OpenSearchAccess.EO_IDENTIFIER;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import java.util.stream.Collectors;
Expand All @@ -22,11 +25,13 @@
import org.geotools.api.data.SimpleFeatureStore;
import org.geotools.api.feature.Attribute;
import org.geotools.api.feature.Feature;
import org.geotools.api.feature.Property;
import org.geotools.api.feature.simple.SimpleFeature;
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.data.DataUtilities;
import org.geotools.data.collection.ListFeatureCollection;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.factory.CommonFactoryFinder;
Expand Down Expand Up @@ -99,29 +104,18 @@ protected Query mapToSimpleCollectionQuery(Query query, boolean addJoins) throws
result.getJoins().add(join);
}

if (addJoins
&& hasOutputProperty(query, OpenSearchAccess.COLLECTION_PROPERTY_NAME, false)) {
Filter filter =
FF.equal(
FF.property("eoParentIdentifier"),
FF.property("collection.eoIdentifier"),
true);
Join join = new Join("collection", filter);
join.setAlias("collection");
result.getJoins().add(join);
}

return result;
}

@Override
protected void mapPropertiesToComplex(ComplexFeatureBuilder builder, SimpleFeature fi) {
protected void mapPropertiesToComplex(
ComplexFeatureBuilder builder, SimpleFeature fi, Map<String, Object> mapperState) {
// JSONB Keys are Unsorted, so we sort them here
jsonBProperties.stream()
.filter(n -> fi.getAttribute(n) != null && fi.getAttribute(n) instanceof String)
.forEach(n -> sortJSONBKeys(fi, n));
// basic mappings
super.mapPropertiesToComplex(builder, fi);
super.mapPropertiesToComplex(builder, fi, mapperState);

// quicklook extraction
Object metadataValue = fi.getAttribute("quicklook");
Expand All @@ -136,29 +130,55 @@ protected void mapPropertiesToComplex(ComplexFeatureBuilder builder, SimpleFeatu
}

// collection extraction
Object collection = fi.getAttribute("collection");
if (collection instanceof SimpleFeature) {
String parentIdentifier = (String) fi.getAttribute("eoParentIdentifier");
if (parentIdentifier != null) {
Feature collectionFeature = getCollectionFeature(mapperState, parentIdentifier);
builder.append(COLLECTION_PROPERTY_NAME, collectionFeature);
}
}

private Feature getCollectionFeature(Map<String, Object> mapperState, String parentIdentifier) {
Feature collectionFeature = (Feature) mapperState.get(parentIdentifier);
if (collectionFeature == null) {
try {
FeatureType collectionType =
(FeatureType)
getSchema()
.getDescriptor(
JDBCOpenSearchAccess.COLLECTION_PROPERTY_NAME)
.getType();
JDBCCollectionFeatureStore collectionSource =
(JDBCCollectionFeatureStore)
((JDBCOpenSearchAccess) getDataStore()).getCollectionSource();
ComplexFeatureBuilder cb =
new ComplexFeatureBuilder(collectionType, FEATURE_FACTORY);
SimpleFeature sf = (SimpleFeature) collection;
collectionSource.mapPropertiesToComplex(cb, sf);
Feature collectionFeature =
cb.buildFeature((String) sf.getAttribute("eoIdentifier"));
builder.append(OpenSearchAccess.COLLECTION_PROPERTY_NAME, collectionFeature);
Query q = new Query(Query.ALL);
q.setFilter(FF.equals(FF.property(EO_IDENTIFIER), FF.literal(parentIdentifier)));
Feature first = DataUtilities.first(collectionSource.getFeatures(q));

// remap the feature, it's using the wrong namespace
collectionFeature = remapCollectionToEONamespace(parentIdentifier, first);

mapperState.put(parentIdentifier, collectionFeature);
} catch (IOException e) {
throw new RuntimeException("Failed to access collection schema", e);
// not impossible, but unexpected
throw new RuntimeException(e);
}
}
return collectionFeature;
}

/**
* The collection feature is generated in the store provided namespaceURI, but the collection
* property in the product uses the EO namespace instead (to have a stable namespace for usage
* in JSON templates). So we need to remap, cannot have a feature with a namespaceURI different
* from the one of its type descriptor....
*/
private Feature remapCollectionToEONamespace(String parentIdentifier, Feature first) {
FeatureType collectionType =
(FeatureType) getSchema().getDescriptor(COLLECTION_PROPERTY_NAME).getType();
ComplexFeatureBuilder cb = new ComplexFeatureBuilder(collectionType, FEATURE_FACTORY);
for (Property p : first.getProperties()) {
if (p instanceof Feature) {
cb.append(p.getName(), p);
} else {
cb.append(p.getName().getLocalPart(), p.getValue());
}
}
Feature feature = cb.buildFeature(parentIdentifier);
return feature;
}

private static void sortJSONBKeys(SimpleFeature fi, Name n) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -930,7 +930,7 @@ public void testJDBCProducteFeatureStoreSortJSONB() throws Exception {
Double.valueOf(3.3)
},
"fid.3");
jdbcProductFeatureStore.mapPropertiesToComplex(complexFeatureBuilder, f);
jdbcProductFeatureStore.mapPropertiesToComplex(complexFeatureBuilder, f, new HashMap<>());
assertEquals(
"{\"a\":{\"archive\":7,\"hello\":6,\"meh\":{\"aver\":9,\"working\":8}},\"c\":5,\"f\":3,\"g\":1,\"h\":4,\"m\":2,\"opt:cloudCover\":34}",
f.getAttribute("string").toString());
Expand Down
Loading

0 comments on commit 3aa698b

Please sign in to comment.