Skip to content

Commit 6f3d10c

Browse files
Component to allow checking duplicated metadata values for title, alternative title and resource identifier in the metadata editor and display a message to the user (#6984)
* Components to allow to check duplicated metadata titles in the metadata editor and display a message to the user: * Automatic formatting * Simplify duplicate titles search * Update the component to check duplicates in the following fields: - Metadata title - Metadata alternative title - Metadata resource identifier --------- Co-authored-by: Juan Luis Rodríguez <juanluisrp@gmail.com>
1 parent 60014cd commit 6f3d10c

File tree

6 files changed

+231
-4
lines changed

6 files changed

+231
-4
lines changed

schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/layout/config-editor.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@
9898

9999
<for name="mrs:referenceSystemIdentifier" addDirective="data-gn-crs-selector"/>
100100

101+
<!-- Example configuration to check duplicated metadata alternate title -->
102+
<!--<for name="cit:alternateTitle"
103+
xpath="/mdb:MD_Metadata/mdb:identificationInfo/mri:MD_DataIdentification/mri:citation/cit:CI_Citation/cit:alternateTitle"
104+
use="data-gn-duplicated-metadata-value-checker">
105+
<directiveAttributes
106+
data-field-key="altTitle" />
107+
</for>-->
108+
101109
<for name="mdb:contact" addDirective="data-gn-directory-entry-selector">
102110
<directiveAttributes
103111
data-template-add-snippet="&lt;cit:CI_Responsibility

schemas/iso19139/src/main/plugin/iso19139/layout/config-editor.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@
108108
</for>
109109
-->
110110

111+
<!-- Check if metadata title is defined in other metadata record
112+
<for name="gmd:title" use="data-gn-duplicated-metadata-title-checker" />
113+
-->
114+
111115
<for name="gts:TM_PeriodDuration" use="data-gn-field-duration-div"/>
112116
<for name="gml:duration" use="data-gn-field-duration-div"/>
113117

services/src/main/java/org/fao/geonet/api/records/MetadataApi.java

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2001-2023 Food and Agriculture Organization of the
2+
* Copyright (C) 2001-2024 Food and Agriculture Organization of the
33
* United Nations (FAO-UN), United Nations World Food Programme (WFP)
44
* and United Nations Environment Programme (UNEP)
55
*
@@ -51,6 +51,7 @@
5151
import org.fao.geonet.kernel.SchemaManager;
5252
import org.fao.geonet.kernel.datamanager.IMetadataUtils;
5353
import org.fao.geonet.kernel.mef.MEFLib;
54+
import org.fao.geonet.kernel.search.EsSearchManager;
5455
import org.fao.geonet.lib.Lib;
5556
import org.fao.geonet.repository.MetadataRepository;
5657
import org.fao.geonet.utils.Log;
@@ -63,6 +64,7 @@
6364
import org.springframework.http.HttpStatus;
6465
import org.springframework.http.MediaType;
6566
import org.springframework.http.ResponseEntity;
67+
import org.springframework.security.access.prepost.PreAuthorize;
6668
import org.springframework.stereotype.Controller;
6769
import org.springframework.web.bind.annotation.*;
6870

@@ -108,6 +110,9 @@ public class MetadataApi {
108110

109111
private ApplicationContext context;
110112

113+
@Autowired
114+
EsSearchManager esSearchManager;
115+
111116
public static RelatedResponse getRelatedResources(
112117
String language, ServiceContext context,
113118
AbstractMetadata md, RelatedItemType[] type, int start, int rows) throws Exception {
@@ -780,6 +785,47 @@ public FeatureResponse getFeatureCatalog(
780785

781786
}
782787

788+
@io.swagger.v3.oas.annotations.Operation(summary = "Check if metadata field value is duplicated in another metadata",
789+
description = "Verifies if a metadata field value is in use. Fields supported: title (title), " +
790+
"alternate title (altTitle) or resource identifier (identifier)")
791+
@PostMapping(value = "/{metadataUuid:.+}/checkDuplicatedFieldValue",
792+
produces = {MediaType.APPLICATION_JSON_VALUE})
793+
@PreAuthorize("hasAuthority('Editor')")
794+
@ApiResponses(value = {
795+
@ApiResponse(responseCode = "200", description = "Return true if the field value is duplicated in another metadata or false in other case."),
796+
@ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_CAN_VIEW)
797+
})
798+
public ResponseEntity<Boolean> checkDuplicatedFieldValue(
799+
@Parameter(description = API_PARAM_RECORD_UUID,
800+
required = true)
801+
@PathVariable
802+
String metadataUuid,
803+
@Parameter(description = "Metadata field information to check",
804+
required = true)
805+
@RequestBody DuplicatedValueDto duplicatedValueDto,
806+
HttpServletRequest request
807+
) throws Exception {
808+
try {
809+
ApiUtils.canViewRecord(metadataUuid, request);
810+
} catch (SecurityException e) {
811+
Log.debug(API.LOG_MODULE_NAME, e.getMessage(), e);
812+
throw new NotAllowedException(ApiParams.API_RESPONSE_NOT_ALLOWED_CAN_VIEW);
813+
}
814+
815+
List<String> validFields = Arrays.asList("title", "altTitle", "identifier");
816+
817+
if (!validFields.contains(duplicatedValueDto.getField())) {
818+
throw new IllegalArgumentException(String.format("A valid field name is required:", String.join(",", validFields)));
819+
}
820+
821+
if (StringUtils.isEmpty(duplicatedValueDto.getValue())) {
822+
throw new IllegalArgumentException("A non-empty value is required.");
823+
}
824+
825+
826+
boolean uuidsWithSameTitle = MetadataUtils.isMetadataFieldValueExistingInOtherRecords(duplicatedValueDto.getValue(), duplicatedValueDto.getField(), metadataUuid);
827+
return ResponseEntity.ok(uuidsWithSameTitle);
828+
}
783829

784830
private boolean isIncludedAttributeTable(RelatedResponse.Fcat fcat) {
785831
return fcat != null
@@ -789,4 +835,25 @@ private boolean isIncludedAttributeTable(RelatedResponse.Fcat fcat) {
789835
&& fcat.getItem().get(0).getFeatureType().getAttributeTable() != null
790836
&& fcat.getItem().get(0).getFeatureType().getAttributeTable().getElement() != null;
791837
}
838+
839+
private static class DuplicatedValueDto {
840+
private String field;
841+
private String value;
842+
843+
public String getField() {
844+
return field;
845+
}
846+
847+
public void setField(String field) {
848+
this.field = field;
849+
}
850+
851+
public String getValue() {
852+
return value;
853+
}
854+
855+
public void setValue(String value) {
856+
this.value = value;
857+
}
858+
}
792859
}

services/src/main/java/org/fao/geonet/api/records/MetadataUtils.java

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2001-2023 Food and Agriculture Organization of the
2+
* Copyright (C) 2001-2024 Food and Agriculture Organization of the
33
* United Nations (FAO-UN), United Nations World Food Programme (WFP)
44
* and United Nations Environment Programme (UNEP)
55
*
@@ -37,6 +37,7 @@
3737
import org.fao.geonet.ApplicationContextHolder;
3838
import org.fao.geonet.GeonetContext;
3939
import org.fao.geonet.NodeInfo;
40+
import org.fao.geonet.api.API;
4041
import org.fao.geonet.api.es.EsHTTPProxy;
4142
import org.fao.geonet.api.records.model.related.AssociatedRecord;
4243
import org.fao.geonet.api.records.model.related.RelatedItemOrigin;
@@ -306,7 +307,7 @@ public static Map<RelatedItemType, List<AssociatedRecord>> getAssociated(
306307
if (!e.fields().isEmpty()) {
307308
FIELDLIST_RELATED_SCRIPTED.keySet().forEach(f -> {
308309
JsonData dc = (JsonData) e.fields().get(f);
309-
310+
310311
if (dc != null) {
311312
if (associatedRecord.getProperties() == null) {
312313
associatedRecord.setProperties(new HashMap<>());
@@ -774,6 +775,48 @@ public static boolean retrieveMetadataValidationStatus(AbstractMetadata metadata
774775
return isInvalid;
775776
}
776777

778+
/**
779+
* Check if other metadata records exist apart from the one with {code}metadataUuidToExclude{code} with the same
780+
* {code}metadataValue{code} for the field {code}metadataField{code}.
781+
*
782+
* @param metadataValue Metadata value to check.
783+
* @param metadataField Metadata field to check the value.
784+
* @param metadataUuidToExclude Metadata identifier to exclude from the search.
785+
* @return A list of metadata uuids that have the same value for the field provided.
786+
*/
787+
public static boolean isMetadataFieldValueExistingInOtherRecords(String metadataValue, String metadataField, String metadataUuidToExclude) {
788+
ApplicationContext applicationContext = ApplicationContextHolder.get();
789+
EsSearchManager searchMan = applicationContext.getBean(EsSearchManager.class);
790+
791+
String esFieldName = "resourceTitleObject.\\\\*.keyword";
792+
if (metadataField.equals("altTitle")) {
793+
esFieldName = "resourceAltTitleObject.\\\\*.keyword";
794+
} else if (metadataField.equals("identifier")) {
795+
esFieldName = "resourceIdentifier.code";
796+
}
797+
798+
boolean duplicatedMetadataValue = false;
799+
String jsonQuery = " {" +
800+
" \"query_string\": {" +
801+
" \"query\": \"+" + esFieldName + ":\\\"%s\\\" -uuid:\\\"%s\\\"\"" +
802+
" }" +
803+
"}";
804+
805+
ObjectMapper objectMapper = new ObjectMapper();
806+
try {
807+
JsonNode esJsonQuery = objectMapper.readTree(String.format(jsonQuery, metadataValue, metadataUuidToExclude));
808+
809+
final SearchResponse queryResult = searchMan.query(
810+
esJsonQuery,
811+
FIELDLIST_UUID,
812+
0, 5);
813+
814+
duplicatedMetadataValue = !queryResult.hits().hits().isEmpty();
815+
} catch (Exception ex) {
816+
Log.error(API.LOG_MODULE_NAME, ex.getMessage(), ex);
817+
}
818+
return duplicatedMetadataValue;
819+
}
777820

778821
/**
779822
* Checks if a result for a search query has results.

web-ui/src/main/resources/catalog/components/edit/FieldsDirective.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,4 +542,106 @@
542542
};
543543
}
544544
]);
545+
546+
/**
547+
* @ngdoc directive
548+
* @name gn_fields.directive:gnDuplicatedMetadataValueChecker
549+
*
550+
* @description
551+
* Checks if the associated control value exists in another metadata record.
552+
* Valid field keys:
553+
* - title: Metadata title.
554+
* - altTitle: Metadata alternative title.
555+
* - identifier: Metadata resource identifier.
556+
* Configure in your metadata schema config-editor.xml the usage of this directive
557+
* for the title element. For example, for iso19139:
558+
* <fields>
559+
* ...
560+
* <for name="gmd:alternateTitle" use="data-gn-duplicated-metadata-value-checker">
561+
* <directiveAttributes
562+
* data-field-name="ResourceName"
563+
* data-field-key="altTitle" />
564+
*/
565+
module.directive("gnDuplicatedMetadataValueChecker", [
566+
"gnCurrentEdit",
567+
"$http",
568+
"$compile",
569+
"$translate",
570+
function (gnCurrentEdit, $http, $compile, $translate) {
571+
return {
572+
restrict: "A",
573+
scope: {
574+
fieldKey: "@" // Elasticsearch field name. Allowed values: title (Metadata title), altTitle (Metadata alternate title), identifier (Resource identifier)
575+
},
576+
link: function (scope, element, attrs) {
577+
var duplicatedFieldNameMessage = $translate.instant(
578+
"metadataDuplicatedField-" + scope.fieldKey
579+
);
580+
581+
var messageTemplate =
582+
"<p class='help-block' " +
583+
"style='color: #d9534f' " +
584+
"data-ng-show='duplicatedValue && !hiddenControl' " +
585+
"data-translate>" +
586+
duplicatedFieldNameMessage +
587+
"</p>";
588+
var messageTemplateCompiled = $compile(messageTemplate)(scope);
589+
590+
var messageTarget = document.getElementById(element[0].id);
591+
element.blur(function () {
592+
if (messageTarget.value !== scope.metadataFieldValue) {
593+
scope.metadataFieldValue = messageTarget.value;
594+
scope.checkField(scope.metadataFieldValue, scope.metadataUuid);
595+
}
596+
});
597+
598+
scope.metadataUuid = gnCurrentEdit.uuid;
599+
scope.metadataFieldValue = messageTarget.value;
600+
scope.duplicatedValue = false;
601+
scope.hiddenControl = false;
602+
603+
element.after(messageTemplateCompiled);
604+
605+
// For multilingual title directive to hide the messages when displaying each language individually
606+
var observer = new MutationObserver(function (event) {
607+
if (event.length > 0) {
608+
scope.hiddenControl = event[0].target.className.indexOf("hidden") > -1;
609+
// Force a refresh, otherwise takes a delay to show / hide the message
610+
scope.$apply();
611+
}
612+
});
613+
614+
observer.observe(messageTarget, {
615+
attributes: true,
616+
attributeFilter: ["class"],
617+
childList: false,
618+
characterData: false
619+
});
620+
621+
scope.checkField = function (fieldValue, metadataUuid) {
622+
if (fieldValue === "") {
623+
scope.duplicatedValue = false;
624+
return;
625+
}
626+
627+
var postBody = {
628+
field: scope.fieldKey,
629+
value: fieldValue
630+
};
631+
632+
$http
633+
.post(
634+
"../api/records/" + metadataUuid + "/checkDuplicatedFieldValue",
635+
postBody
636+
)
637+
.then(function (response) {
638+
scope.duplicatedValue = response.data === true;
639+
});
640+
};
641+
642+
scope.checkField(scope.metadataFieldValue, scope.metadataUuid);
643+
}
644+
};
645+
}
646+
]);
545647
})();

web-ui/src/main/resources/catalog/locales/en-editor.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,5 +459,8 @@
459459
"associated-hasfeaturecats": "Using this feature catalog",
460460
"associatedResourcesPanel": "Associated resources",
461461
"validationSuccessLabel": "success",
462-
"validationErrorLabel": "errors"
462+
"validationErrorLabel": "errors",
463+
"metadataDuplicatedField-title": "The metadata title is used in another metadata record.",
464+
"metadataDuplicatedField-altTitle": "The metadata alternate title is used in another metadata record.",
465+
"metadataDuplicatedField-identifier": "The metadata resource identifier is used in another metadata record."
463466
}

0 commit comments

Comments
 (0)