Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
entries as `collection_id (Title)`.
- The HTML collection page links to the collection schema and shows the spatial and temporal extent when available (#185).
- HTML templates: Bootstrap 5.0.0-beta3 -> 5.3.8, Leaflet 1.7.1 -> 1.9.4, proj4 2.7.5 -> 2.22.0 and swagger-ui 3.45.1 -> 5.29.1 (still loaded from CDNs, with updated SRI hashes). The Leaflet and proj4 imports now live in `mapLibs.ftl`, shared by `featureCollection.ftl` and `feature.ftl`; a custom template directory that overrides these two templates keeps its own versions (#199).

- New config key `api.collections.links.*` adds links to the top-level `links` array of the `/collections` response, in both JSON and HTML. Parallel to the existing `api.links.*` (landing page) and `default.collections.links.*` / `collections.<id>.links.*` (per-collection entries).

## 2.0.0

Expand Down
28 changes: 28 additions & 0 deletions docker/cfg/addresses.properties
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,34 @@ api.description=Example implementation of OGC API Services with hakunapi. INSPIR
api.license.name=Creative Commons Attribution 4.0 International (CC BY 4.0) - Syke (Finnish Environment Institute)
api.license.url=https://www.syke.fi/fi/ymparistotieto/kayttolupa-ja-vastuut

# Additional links on the landing page
api.links=syke
api.links.syke.href=https://www.syke.fi/en-US
api.links.syke.rel=about
api.links.syke.type=text/html
api.links.syke.title=Finnish Environment Institute

# Additional links on the /collections response.
# INSPIRE /req/pre-defined/spatial-data-set-metadata requires /collections to link the
# data set's metadata record as rel=describedby + type=application/xml, resolving to an
# ISO 19115 record; /rec/pre-defined/spatial-data-set-metadata-html recommends the same
# record as text/html. Both are configured below, pointing at the record for the Ryhti
# building address data in the national catalogue. Note the record id is brace-wrapped
# and the braces are part of it, hence %7B / %7D.
api.collections.links=metadata-xml,metadata-html,license
api.collections.links.metadata-xml.href=https://www.paikkatietohakemisto.fi/geonetwork/srv/fin/csw?service=CSW&version=2.0.2&request=GetRecordById&elementSetName=full&outputSchema=http%3A%2F%2Fwww.isotc211.org%2F2005%2Fgmd&id=%7BDBD610F4-3392-44CD-B601-BAE8FA547A57%7D
api.collections.links.metadata-xml.rel=describedby
api.collections.links.metadata-xml.type=application/xml
api.collections.links.metadata-xml.title=ISO 19115 metadata record for the building address data
api.collections.links.metadata-html.href=https://ckan.ymparisto.fi/dataset/%7BDBD610F4-3392-44CD-B601-BAE8FA547A57%7D
api.collections.links.metadata-html.rel=describedby
api.collections.links.metadata-html.type=text/html
api.collections.links.metadata-html.title=Metadata record for the building address data
api.collections.links.license.href=https://www.syke.fi/fi/ymparistotieto/kayttolupa-ja-vastuut
api.collections.links.license.rel=license
api.collections.links.license.type=text/html
api.collections.links.license.title=CC BY 4.0 licence terms

# List of servers
## Required, comma-separated list of server the API is
servers=dev
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public abstract class FeatureServiceConfig {
protected List<SRIDCode> knownSrids;
protected ServiceTelemetry telemetry = ServiceTelemetry.NOP;
protected List<Link> additionalLinks;
protected List<Link> collectionsAdditionalLinks;
protected Localization localization;

private final Map<String, FeatureServiceConfig> localizedCache = new ConcurrentHashMap<>();
Expand Down Expand Up @@ -244,6 +245,14 @@ public void setAdditionalLinks(List<Link> additionalLinks) {
this.additionalLinks = additionalLinks;
}

public List<Link> getCollectionsAdditionalLinks() {
return collectionsAdditionalLinks == null ? Collections.emptyList() : collectionsAdditionalLinks;
}

public void setCollectionsAdditionalLinks(List<Link> collectionsAdditionalLinks) {
this.collectionsAdditionalLinks = collectionsAdditionalLinks;
}

public Localization getLocalization() {
return localization == null ? Localization.EMPTY : localization;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ public List<Link> readAdditionalLinks() {
return readLinksWithPrefix("api.links");
}

public List<Link> readCollectionsAdditionalLinks() {
return readLinksWithPrefix("api.collections.links");
}

private List<Link> readLinksWithPrefix(String keyPrefix) {
String[] names = getMultiple(keyPrefix);
List<Link> links = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,4 +300,14 @@ public void setAdditionalLinks(List<Link> additionalLinks) {
service.setAdditionalLinks(additionalLinks);
}

@Override
public List<Link> getCollectionsAdditionalLinks() {
return service.getCollectionsAdditionalLinks();
}

@Override
public void setCollectionsAdditionalLinks(List<Link> collectionsAdditionalLinks) {
service.setCollectionsAdditionalLinks(collectionsAdditionalLinks);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

import java.util.List;
import java.util.Properties;

import org.junit.Test;

import fi.nls.hakunapi.core.schemas.Link;

public class HakunaConfigParserTest {

@Test
Expand Down Expand Up @@ -57,4 +62,60 @@ public void testReadCollectionIds() {
assertArrayEquals(new String[] { "a", "b" }, new HakunaConfigParser(cfg).readCollectionIds());
}

@Test
public void testReadCollectionsAdditionalLinks() {
Properties cfg = new Properties();
cfg.setProperty("api.links", "api");
cfg.setProperty("api.links.api.href", "https://example.org/api");
cfg.setProperty("api.links.api.rel", "describedby");
cfg.setProperty("api.links.api.type", "application/xml");
cfg.setProperty("api.links.api.title", "Service metadata");
cfg.setProperty("api.collections.links", "metadata,license");
cfg.setProperty("api.collections.links.metadata.href", "https://example.org/metadata");
cfg.setProperty("api.collections.links.metadata.rel", "describedby");
cfg.setProperty("api.collections.links.metadata.type", "application/xml");
cfg.setProperty("api.collections.links.metadata.title", "Data set metadata");
cfg.setProperty("api.collections.links.license.href", "https://example.org/license");
cfg.setProperty("api.collections.links.license.rel", "license");
cfg.setProperty("api.collections.links.license.type", "text/html");
cfg.setProperty("api.collections.links.license.title", "Licence");

HakunaConfigParser parser = new HakunaConfigParser(cfg);

List<Link> links = parser.readCollectionsAdditionalLinks();
assertEquals(2, links.size());
assertEquals("https://example.org/metadata", links.get(0).getHref());
assertEquals("describedby", links.get(0).getRel());
assertEquals("application/xml", links.get(0).getType());
assertEquals("Data set metadata", links.get(0).getTitle());
assertEquals("https://example.org/license", links.get(1).getHref());
assertEquals("license", links.get(1).getRel());
assertEquals("text/html", links.get(1).getType());
assertEquals("Licence", links.get(1).getTitle());

// The two sources are independent
List<Link> apiLinks = parser.readAdditionalLinks();
assertEquals(1, apiLinks.size());
assertEquals("https://example.org/api", apiLinks.get(0).getHref());
}

@Test
public void testReadCollectionsAdditionalLinksUnconfigured() {
HakunaConfigParser parser = new HakunaConfigParser(new Properties());
assertTrue(parser.readCollectionsAdditionalLinks().isEmpty());
}

@Test
public void testReadCollectionsAdditionalLinksMissingProperty() {
Properties cfg = new Properties();
cfg.setProperty("api.collections.links", "metadata");
cfg.setProperty("api.collections.links.metadata.href", "https://example.org/metadata");
cfg.setProperty("api.collections.links.metadata.rel", "describedby");
cfg.setProperty("api.collections.links.metadata.type", "application/xml");

HakunaConfigParser parser = new HakunaConfigParser(cfg);

assertThrows(IllegalArgumentException.class, () -> parser.readCollectionsAdditionalLinks());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@
<li><a href="collections/${collection.id}${langQuery}">${collection.id}<#if collection.title??> (${collection.title})</#if></a></li>
</#list>
</ul>

<#-- self and alternate are the only links this page generates itself -->
<#assign additionalLinks = model.links?filter(link -> link.rel != "self" && link.rel != "alternate") />
<#if additionalLinks?size gt 0>
<div class="row">
<h2>Additional Resources</h2>
<#list additionalLinks as link>
<p><a href="${link.href}" target="_blank">${link.title}</a></p>
</#list>
</div>
</#if>

<footer class="pt-3 mt-4 text-muted border-top">Powered by hakunapi</footer>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package fi.nls.hakunapi.html;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.StringWriter;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;

import org.junit.Test;

import freemarker.cache.ClassTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import io.swagger.v3.oas.models.info.Info;

import fi.nls.hakunapi.core.FeatureServiceConfig;
import fi.nls.hakunapi.core.FeatureType;
import fi.nls.hakunapi.core.OutputFormat;
import fi.nls.hakunapi.core.schemas.CollectionsContent;
import fi.nls.hakunapi.core.schemas.Link;
import fi.nls.hakunapi.html.model.HTMLContext;

public class CollectionsTemplateTest {

@Test
public void testAdditionalLinks() throws Exception {
List<Link> links = List.of(
new Link("https://example.org/collections", "self", "text/html", "This document"),
new Link("https://example.org/collections", "alternate", "application/json", "This document"),
new Link("https://example.org/metadata", "describedby", "application/xml", "Data set metadata"),
new Link("https://example.org/license", "license", "text/html", "Licence"));

String html = render(new CollectionsContent(links, Collections.emptyList()));

assertTrue(html.contains("Additional Resources"));
assertTrue(html.contains("https://example.org/metadata"));
assertTrue(html.contains("Data set metadata"));
assertTrue(html.contains("https://example.org/license"));
assertTrue(html.contains("Licence"));
// self and alternate are page navigation, not additional resources
assertFalse(html.contains("This document"));
}

@Test
public void testNoAdditionalLinks() throws Exception {
List<Link> links = List.of(new Link("https://example.org/collections", "self", "text/html", "This document"));

String html = render(new CollectionsContent(links, Collections.emptyList()));

assertFalse(html.contains("Additional Resources"));
}

private String render(CollectionsContent content) throws Exception {
Configuration cfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
cfg.setDefaultEncoding("UTF-8");
cfg.setLocale(Locale.US);
cfg.setTemplateLoader(new ClassTemplateLoader(OutputFormatFactoryHTML.class, ""));

Template template = cfg.getTemplate("collections.ftl");
TestServiceConfig service = new TestServiceConfig();
service.setInfo(new Info().title("Test service"));
HTMLContext<CollectionsContent> ctx = new HTMLContext<>(service, "https://example.org", content);

StringWriter out = new StringWriter();
template.process(ctx, out);
return out.toString();
}

private static class TestServiceConfig extends FeatureServiceConfig {

@Override
public Collection<FeatureType> getCollections() {
return Collections.emptyList();
}

@Override
public FeatureType getCollection(String name) {
return null;
}

@Override
public OutputFormat getOutputFormat(String f) {
return null;
}

@Override
public Collection<OutputFormat> getOutputFormats() {
return Collections.emptyList();
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ private Localized localize(String langParam, UriInfo uriInfo, HttpHeaders header
links.add(Links.getSelfLink(path, queryParams, contentType, lang));
links.addAll(getAlternateLinks(path, queryParams, contentType));
links.addAll(Links.getAlternateLangLinks(path, queryParams, contentType, available, lang));
links.addAll(service.getCollectionsAdditionalLinks());

List<CollectionInfo> collections = new ArrayList<>();
for (FeatureType ft : localizedService.getCollections()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ public void contextInitialized(ServletContextEvent sce) {
service.setServers(servers);
service.setAdditionalLinks(parser.readAdditionalLinks());
service.setLocalization(localization);
service.setCollectionsAdditionalLinks(parser.readCollectionsAdditionalLinks());
service.setLimitDefault(PropertyUtil.getInt(config, "getfeatures.limit.default", 1000));
service.setLimitMaximum(PropertyUtil.getInt(config, "getfeatures.limit.max", 10000));
service.setConformanceClasses(conformsTo);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,46 @@ public void testCollectionsOrder() {
.assertThat("$.collections[1].id", equalTo("aallonmurtaja"));
}

@SuppressWarnings("unchecked")
@Test
public void testCollectionsAdditionalLinks() {
final String response = target("/collections").request().get(String.class);
LOG.info(response);

with(response)
//
.assertThat("$.links[0].rel", equalTo("self"))
.assertThat("$.links[?(@.rel == 'describedby')]", is(collectionWithSize(equalTo(1))))
.assertThat("$.links[?(@.rel == 'describedby')].href",
hasItems(equalTo("https://example.org/dataset-metadata")))
.assertThat("$.links[?(@.rel == 'describedby')].title",
hasItems(equalTo("Data set metadata record")))
.assertThat("$.links[?(@.rel == 'license')].href", hasItems(equalTo("https://example.org/license")))
.assertThat("$.links[?(@.rel == 'license')].type", hasItems(equalTo("text/html")));

// The landing page link source must not leak into /collections
with(response)
.assertThat("$.links[?(@.href == 'https://example.org/api-metadata')]",
is(collectionWithSize(equalTo(0))));

// Nor must the /collections links leak into the collection entries
with(response)
.assertThat("$.collections[*].links[?(@.rel == 'license')]", is(collectionWithSize(equalTo(0))));
}

@Test
public void testLandingPageAdditionalLinks() {
final String response = target("/").request().get(String.class);
LOG.info(response);

with(response)
//
.assertThat("$.links[?(@.href == 'https://example.org/api-metadata')]",
is(collectionWithSize(equalTo(1))))
.assertThat("$.links[?(@.href == 'https://example.org/dataset-metadata')]",
is(collectionWithSize(equalTo(0))));
}

@SuppressWarnings("unchecked")
@Test
public void testCollectionsAallonmurtaja() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ api.contact.url=https://www.maanmittauslaitos.fi/tietoa-maanmittauslaitoksesta/y
api.license.name=Data accessed from the API is licensed by the National Land Survey open data CC 4.0 licence.
api.license.url=https://www.maanmittauslaitos.fi/en/opendata-licence-cc40

api.links=metadata
api.links.metadata.href=https://example.org/api-metadata
api.links.metadata.rel=describedby
api.links.metadata.type=application/xml
api.links.metadata.title=Service metadata record

api.collections.links=metadata,license
api.collections.links.metadata.href=https://example.org/dataset-metadata
api.collections.links.metadata.rel=describedby
api.collections.links.metadata.type=application/xml
api.collections.links.metadata.title=Data set metadata record
api.collections.links.license.href=https://example.org/license
api.collections.links.license.rel=license
api.collections.links.license.type=text/html
api.collections.links.license.title=Licence of the data sets

servers=dev
servers.dev.url=https://localhost/hakuna
servers.dev.description=Pre-Production
Expand Down
Loading