Skip to content
Open
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
1 change: 1 addition & 0 deletions core/src/main/java/org/fao/geonet/constants/Geonet.java
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,7 @@ public static class IndexFieldNames {
public static final String IS_PUBLISHED_TO_ALL = "isPublishedToAll";
public static final String IS_PUBLISHED_TO_INTRANET = "isPublishedToIntranet";
public static final String IS_PUBLISHED_TO_GUEST = "isPublishedToGuest";
public static final String AGG_ASSOCIATED_REVISION_OF = "agg_associated_revisionOf";
public static final String FEEDBACKCOUNT = "feedbackCount";
public static final String DRAFT = "draft";
public static final String DRAFT_ID = "draftId";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/*
* Copyright (C) 2001-2025 Food and Agriculture Organization of the
* United Nations (FAO-UN), United Nations World Food Programme (WFP)
Expand Down Expand Up @@ -133,6 +133,8 @@
.add("link")
.add("format")
.add("resourceType")
.add("resourceEdition")
.add("resourceDate")
.add("cl_status.key")
.add(Geonet.IndexFieldNames.OP_PREFIX + "*")
.add(Geonet.IndexFieldNames.GROUP_OWNER)
Expand Down Expand Up @@ -796,6 +798,40 @@
return client.getDocument(defaultIndex, uuid);
}

public static Set<String> extractFieldValues(Object fieldValue) {
Set<String> values = new LinkedHashSet<>();
if (fieldValue == null) {
return values;
}

if (fieldValue instanceof Collection<?>) {
for (Object value : (Collection<?>) fieldValue) {
addFieldValue(values, value);
}
} else if (fieldValue.getClass().isArray()) {
int length = java.lang.reflect.Array.getLength(fieldValue);
for (int i = 0; i < length; i++) {
addFieldValue(values, java.lang.reflect.Array.get(fieldValue, i));
}
} else {
addFieldValue(values, fieldValue);
}

return values;
}

private static void addFieldValue(Set<String> values, Object value) {
if (value == null) {
return;
}

String trimmed = String.valueOf(value).trim();
if (StringUtils.isNotEmpty(trimmed)) {
values.add(trimmed);
}
}


public SearchResponse query(String luceneQuery, String filterQuery, int startPosition, int maxRecords) throws Exception {
return client.query(defaultIndex, luceneQuery, filterQuery, new HashSet<>(), new HashMap<>(), startPosition, maxRecords);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Other types of resources (eg. sensor, publication) {#linking-others}
# Other types of resources (eg. sensor, publication, revision) {#linking-others}

In ISO, associated resource allows to link to record defining the type of relation with:

* association type (mandatory)
* initiative type

Expand All @@ -16,8 +17,45 @@ Codelist values for association types are:
* Dependency
* Revision Of

In ISO19115-3, by default parent relations are defined with association type set to `partOfSeamlessDatabase`. This can be customized with [the panel configuration](linking-panel-configuration.md).

![](img/iso-associated-resources.png)

## Grouping records together

When having a series of records related together, it may make sense to group them under a same parent record.

In ISO19115-3, by default parent relations are defined with association type set to `partOfSeamlessDatabase` to indicate that a set of datasets are stored in same database.
This can be customized with [the panel configuration](linking-panel-configuration.md).

To promote some kind of "data products" or "collections", it may be interesting to create parent records with association type pointing to the child records with association type `isComposedOf`. This is the approach used at EEA (https://sdi.eea.europa.eu/catalogue) where all datasets are grouped in series. The series provide general information and quick access to all child records (eg. temporal series, current and archived versions).



## Linking to previous version

When a record is updated, it may be interesting to link the new version to the previous version.
This can be done with the association type `revisionOf` and linking to the previous version of the record:

```xml
<mri:associatedResource>
<mri:MD_AssociatedResource>
<mri:associationType>
<mri:DS_AssociationTypeCode codeListValue="revisionOf"/>
</mri:associationType>
<mri:metadataReference uuidref="481df889-4f3d-4290-bd89-b7f3ad11a2f1"
xlink:href="http://localhost:8080/geonetwork/srv/api/records/481df889-4f3d-4290-bd89-b7f3ad11a2f1"/>
</mri:MD_AssociatedResource>
</mri:associatedResource>
```

This will allow users to navigate between versions of the record in the record view or using the related record API.

It is recommended to use the same privileges for all versions of the record to be able to navigate between all versions. Usually when a revision is created, the previous version status is updated to "archived" or "superseded".



## Linking to scientific publications

When a dataset is linked to a scientific publication, an option is to use a link with the association type `cross reference` and initiative type set to `study`.
Those type of links usually point to a DOI or a remote URL (See [link to remote URL](linking-remote-records.md) and [using DataCite and Crossref for DOI](linking-panel-configuration.md)).

Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Example:
## Associated resources

For associated resources, configuration has the following properties:

* `type` is the type of association (eg. parent, child, sibling)
* `label` is the label key of the resource type (defined in JSON loc file or in database translations)
* `config` defines the configuration for the association type, which has the following properties:
Expand Down Expand Up @@ -98,6 +99,7 @@ When `metadataStore` is defined as a source, the user can search for metadata re
}
```
In another plugin:

* `catalog-*` association type will only search for records with a `catalog` resource type
* `nextResource-*` association type will only search for `dataset` or `service`

Expand All @@ -120,6 +122,7 @@ In another plugin:
#### DOI

Allows to select metadata from a DOI endpoint:

* DataCite
* Crossref

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@ In some situations, a dataset is part of a temporal or spatial collections of re
- Corine Land Cover 2012
- \...

Parent/child relations can be set on ISO19139 and Dublin Core records. In ISO19139, the link to a parent record is encoded in the child record using the following:
Parent/child relations can be set on ISO and Dublin Core records.

In ISO19139, the link to a parent record is encoded in the child record using the following:

``` xml
<gmd:parentIdentifier>
<gco:CharacterString>78f93047-74f8-4419-ac3d-fc62e4b0477b</gco:CharacterString>
</gmd:parentIdentifier>
```

Parent/Child relation in ISO19139 may also be encoded using aggregates (see [Other types of resources (eg. sensor, publication)](linking-others.md)).
Parent/Child relation in ISO may also be encoded using aggregates to clarify the type of the association (see [Other types of resources (eg. sensor, publication)](linking-others.md)). By default, ISO19115-3 is using the `associatedResource` element with association type set to `partOfSeamlessDatabase` for parent/child relation.


When creating such relationship, users will be able to navigate between the records in the search and record view.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ public synchronized void setApplicationContext(ApplicationContext context) {

@io.swagger.v3.oas.annotations.Operation(
summary = "Get record associated resources",
description = "Retrieve related services, datasets, sources, ... " +
"to this records.<br/>" +
description = "Retrieve related resources for a record. " +
"There are 2 options to access related resources. One is using this operation, the other is to use the `_search` endpoint and using the `relatedType` parameter.\n\n" +
"<a href='https://docs.geonetwork-opensource.org/latest/user-guide/associating-resources/'>More info</a>")
@RequestMapping(value = "/{metadataUuid:.+}/associated",
method = RequestMethod.GET,
Expand All @@ -111,7 +111,26 @@ public Map<RelatedItemType, List<AssociatedRecord>> getAssociatedResources(
required = true)
@PathVariable
String metadataUuid,
@Parameter(description = "Type of related resource. If none, all resources are returned.",
@Parameter(description = "Type of related resource. If none, all types are returned.\n\n" +
"Allowed values:\n" +
"- `parent`: parent records declared by schema associations.\n" +
"- `children`: records having parent relation pointing to this record.\n" +
"- `brothersAndSisters`: records sharing the same parent(s).\n" +
"- `siblings`: aggregated/associated resources linked by this record.\n" +
"- `associated`: reverse siblings relation (records referencing this record).\n" +
"- `versions`: full revision chain for this record (does not support remote records). If the current record is the only version, empty list is returned.\n" +
"- `nextVersion`: immediate next item in version ordering.\n" +
"- `previousVersion`: immediate previous item in version ordering.\n" +
"- `services`: services publishing this dataset record.\n" +
"- `datasets`: datasets published (operated) by a service record.\n" +
"- `fcats`: feature catalog references.\n" +
"- `hasfeaturecats`: records referencing this record as feature catalog.\n" +
"- `sources`: this record is derived from those.\n" +
"- `hassources`: records derived from this record.\n\n" +
"Deprecated values:\n" +
"- `related`: legacy relation table links.\n" +
"- `onlines`: online links extracted from metadata. Use record links property instead.\n" +
"- `thumbnails`: thumbnails extracted from metadata. Use record overview property instead.",
required = false
)
@RequestParam(defaultValue = "")
Expand Down
101 changes: 77 additions & 24 deletions services/src/main/java/org/fao/geonet/api/records/MetadataUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Joiner;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import jeeves.server.context.ServiceContext;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
Expand All @@ -43,6 +54,9 @@
import org.fao.geonet.api.records.model.related.RelatedItemOrigin;
import org.fao.geonet.api.records.model.related.RelatedItemType;
import org.fao.geonet.constants.Geonet;

import static org.fao.geonet.api.records.MetadataVersionsUtils.*;

import org.fao.geonet.domain.AbstractMetadata;
import org.fao.geonet.domain.ReservedOperation;
import org.fao.geonet.domain.Source;
Expand All @@ -53,7 +67,13 @@
import org.fao.geonet.kernel.schema.AssociatedResource;
import org.fao.geonet.kernel.schema.AssociatedResourcesSchemaPlugin;
import org.fao.geonet.kernel.schema.SchemaPlugin;
import static org.fao.geonet.kernel.search.EsFilterBuilder.buildPermissionsFilter;
import org.fao.geonet.kernel.search.EsSearchManager;
import static org.fao.geonet.kernel.search.EsSearchManager.FIELDLIST_CORE;
import static org.fao.geonet.kernel.search.EsSearchManager.FIELDLIST_RELATED;
import static org.fao.geonet.kernel.search.EsSearchManager.FIELDLIST_RELATED_SCRIPTED;
import static org.fao.geonet.kernel.search.EsSearchManager.FIELDLIST_UUID;
import static org.fao.geonet.kernel.search.EsSearchManager.RELATED_INDEX_FIELDS;
import org.fao.geonet.kernel.setting.SettingInfo;
import org.fao.geonet.kernel.setting.SettingManager;
import org.fao.geonet.lib.Lib;
Expand All @@ -71,12 +91,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;

import java.util.*;
import java.util.stream.Collectors;

import static org.fao.geonet.kernel.search.EsFilterBuilder.buildPermissionsFilter;
import static org.fao.geonet.kernel.search.EsSearchManager.*;
import org.w3c.dom.Node;


Expand All @@ -95,6 +109,7 @@
private Set<String> expectedRecords = new HashSet<>();
private Set<String> remoteRecords = new HashSet<>();
private Map<String, Map<String, String>> recordsProperties = new HashMap<>();
private List<String> orderedRecords = new ArrayList<>();

public RelatedTypeDetails(String query) {
this.query = query;
Expand All @@ -116,6 +131,15 @@
this.remoteRecords = remoteRecords;
}

public RelatedTypeDetails(String query, Set<String> expectedRecords, Map<String, Map<String, String>> recordsProperties,
Set<String> remoteRecords, List<String> orderedRecords) {
this.query = query;
this.expectedRecords = expectedRecords;
this.recordsProperties = recordsProperties;
this.remoteRecords = remoteRecords;
this.orderedRecords = orderedRecords;
}

public String getQuery() {
return query;
}
Expand Down Expand Up @@ -143,6 +167,10 @@
public Set<String> getRemoteRecords() {
return remoteRecords;
}

public List<String> getOrderedRecords() {
return orderedRecords;
}
}


Expand All @@ -157,42 +185,46 @@
try {
Map<RelatedItemType, List<AssociatedRecord>> associated = MetadataUtils.getAssociated(context, metadataEntity, RelatedItemType.values(), 0, 100);
for (Map.Entry<RelatedItemType, List<AssociatedRecord>> entry : associated.entrySet()) {
for (AssociatedRecord record : entry.getValue()) {
for (AssociatedRecord associatedRecord : entry.getValue()) {
Element relation = new Element(entry.getKey().name());
relation.setAttribute("uuid", record.getUuid());
relation.setAttribute("origin", record.getOrigin());
if (record.getProperties() != null) {
if (record.getProperties().get("associationType") != null) {
relation.setAttribute("associationType", record.getProperties().get("associationType"));
relation.setAttribute("uuid", associatedRecord.getUuid());
relation.setAttribute("origin", associatedRecord.getOrigin());

Check failure on line 191 in services/src/main/java/org/fao/geonet/api/records/MetadataUtils.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "origin" 7 times.

See more on https://sonarcloud.io/project/issues?id=geonetwork_core-geonetwork&issues=AZ5yeBLP-df0Au6ruJZW&open=AZ5yeBLP-df0Au6ruJZW&pullRequest=9227
if (associatedRecord.getProperties() != null) {
if (associatedRecord.getProperties().get("associationType") != null) {

Check failure on line 193 in services/src/main/java/org/fao/geonet/api/records/MetadataUtils.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "associationType" 6 times.

See more on https://sonarcloud.io/project/issues?id=geonetwork_core-geonetwork&issues=AZ5yeBLP-df0Au6ruJZX&open=AZ5yeBLP-df0Au6ruJZX&pullRequest=9227
relation.setAttribute("associationType", associatedRecord.getProperties().get("associationType"));
}
if (record.getProperties().get("initiativeType") != null) {
relation.setAttribute("initiativeType", record.getProperties().get("initiativeType"));
if (associatedRecord.getProperties().get("initiativeType") != null) {

Check failure on line 196 in services/src/main/java/org/fao/geonet/api/records/MetadataUtils.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "initiativeType" 6 times.

See more on https://sonarcloud.io/project/issues?id=geonetwork_core-geonetwork&issues=AZ5yeBLP-df0Au6ruJZV&open=AZ5yeBLP-df0Au6ruJZV&pullRequest=9227
relation.setAttribute("initiativeType", associatedRecord.getProperties().get("initiativeType"));
}
if (record.getProperties().get("resourceTitle") != null) {
relation.setAttribute("resourceTitle", record.getProperties().get("resourceTitle"));
if (associatedRecord.getProperties().get("resourceTitle") != null) {

Check failure on line 199 in services/src/main/java/org/fao/geonet/api/records/MetadataUtils.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "resourceTitle" 6 times.

See more on https://sonarcloud.io/project/issues?id=geonetwork_core-geonetwork&issues=AZ5yeBLP-df0Au6ruJZU&open=AZ5yeBLP-df0Au6ruJZU&pullRequest=9227
relation.setAttribute("resourceTitle", associatedRecord.getProperties().get("resourceTitle"));
}
if (record.getProperties().get("url") != null) {
relation.setAttribute("url", record.getProperties().get("url"));
if (associatedRecord.getProperties().get("url") != null) {
relation.setAttribute("url", associatedRecord.getProperties().get("url"));
}
}
relation.addContent(Xml.getXmlFromJSON(record.getRecord().toPrettyString()));
relation.addContent(Xml.getXmlFromJSON(associatedRecord.getRecord().toPrettyString()));
relations.addContent(relation);
}
}
} catch (Exception e) {
throw new RuntimeException(e);
LOGGER.warn(String.format("An error occurred when getting associated records for metadata with uuid %s: %s",
metadataUuid, e.getMessage()), e);
return null;
}

DOMOutputter outputter = new DOMOutputter();
try {
return outputter.output(new Document(relations));
} catch (JDOMException e) {
throw new RuntimeException(e);
LOGGER.warn(String.format("An error occurred when converting associated records as XML for metadata with uuid %s: %s",
metadataUuid, e.getMessage()), e);
return null;
}
}


public static Map<RelatedItemType, List<AssociatedRecord>> getAssociated(

Check warning on line 227 in services/src/main/java/org/fao/geonet/api/records/MetadataUtils.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

A "Brain Method" was detected. Refactor it to reduce at least one of the following metrics: LOC from 184 to 64, Complexity from 44 to 14, Nesting Level from 7 to 2, Number of Variables from 44 to 6.

See more on https://sonarcloud.io/project/issues?id=geonetwork_core-geonetwork&issues=AZ5yeBLP-df0Au6ruJZY&open=AZ5yeBLP-df0Au6ruJZY&pullRequest=9227
ServiceContext context,
AbstractMetadata md, RelatedItemType[] types, int start, int size)
throws Exception {
Expand All @@ -213,6 +245,7 @@
// For each type, store a query and expected list of uuids.
Map<RelatedItemType, RelatedTypeDetails> queries = new HashMap<>();
Set<String> allSearchedUuids = new HashSet<>();
RelatedTypeDetails versionsDetails = null;


// We have 3 types of links
Expand All @@ -227,8 +260,19 @@
// brothers&sisters
//
// * All of them could be remote records
Arrays.stream(types).forEach(type -> {
if (type == RelatedItemType.associated
for (RelatedItemType type : types) {
if (type == RelatedItemType.versions) {
if (versionsDetails == null) {
versionsDetails = getAllVersions(searchMan, md.getUuid());
}
queries.put(type, versionsDetails);
} else if (type == RelatedItemType.nextVersion || type == RelatedItemType.previousVersion) {
if (versionsDetails == null) {
versionsDetails = getAllVersions(searchMan, md.getUuid());
}
queries.put(type, getNextOrPrevious(
versionsDetails, md.getUuid(), type == RelatedItemType.nextVersion));
} else if (type == RelatedItemType.associated
|| type == RelatedItemType.hasfeaturecats
|| type == RelatedItemType.services
|| type == RelatedItemType.hassources) {
Expand Down Expand Up @@ -325,7 +369,7 @@
));
allSearchedUuids.addAll(isComposedOfList);
}
});
}


Map<RelatedItemType, List<AssociatedRecord>> associated =
Expand Down Expand Up @@ -395,6 +439,15 @@
}

buildRemoteRecords(mapper, relatedTypeDetails, records);
records = reorderIndexDocBasedOnOrderedRecords(records, relatedTypeDetails.getOrderedRecords());

// If the current record is the only version, empty list is returned.
if (entry.getKey() == RelatedItemType.versions
&& records.size() == 1
&& md.getUuid().equals(records.get(0).getUuid())) {
records = new ArrayList<>();
}

associated.put(entry.getKey(), records);
}

Expand Down
Loading
Loading