diff --git a/core/src/main/java/org/fao/geonet/api/records/attachments/AbstractStore.java b/core/src/main/java/org/fao/geonet/api/records/attachments/AbstractStore.java index 2400164f116a..b57c67199e4d 100644 --- a/core/src/main/java/org/fao/geonet/api/records/attachments/AbstractStore.java +++ b/core/src/main/java/org/fao/geonet/api/records/attachments/AbstractStore.java @@ -79,16 +79,28 @@ public List getResources(final ServiceContext context, final S return getResources(context, metadataUuid, metadataResourceVisibility, filter, true); } + @Override + public List getResources(ServiceContext context, String metadataUuid, MetadataResourceVisibility metadataResourceVisibility, String filter, Boolean approved) + throws Exception { + return getResources(context, metadataUuid, metadataResourceVisibility, filter, approved, false); + } + @Override public List getResources(ServiceContext context, String metadataUuid, Sort sort, String filter, Boolean approved) throws Exception { + return getResources(context, metadataUuid, sort, filter, approved, false); + } + + @Override + public List getResources(ServiceContext context, String metadataUuid, Sort sort, String filter, Boolean approved, boolean includeAdditionalIndexedProperties) + throws Exception { int metadataId = getAndCheckMetadataId(metadataUuid, approved); boolean canEdit = getAccessManager(context).canEdit(context, String.valueOf(metadataId)); List resourceList = new ArrayList<>( - getResources(context, metadataUuid, MetadataResourceVisibility.PUBLIC, filter, approved)); + getResources(context, metadataUuid, MetadataResourceVisibility.PUBLIC, filter, approved, includeAdditionalIndexedProperties)); if (canEdit) { - resourceList.addAll(getResources(context, metadataUuid, MetadataResourceVisibility.PRIVATE, filter, approved)); + resourceList.addAll(getResources(context, metadataUuid, MetadataResourceVisibility.PRIVATE, filter, approved, includeAdditionalIndexedProperties)); } if (sort == Sort.name) { @@ -289,7 +301,7 @@ public MetadataResource patchResourceStatus(final ServiceContext context, final @Override public void copyResources(ServiceContext context, String sourceUuid, String targetUuid, MetadataResourceVisibility metadataResourceVisibility, boolean sourceApproved, boolean targetApproved) throws Exception { - final List resources = getResources(context, sourceUuid, metadataResourceVisibility, null, sourceApproved); + final List resources = getResources(context, sourceUuid, metadataResourceVisibility, null, sourceApproved, false); for (MetadataResource resource: resources) { try (Store.ResourceHolder holder = getResource(context, sourceUuid, metadataResourceVisibility, resource.getFilename(), sourceApproved)) { putResource(context, targetUuid, holder.getResource(), metadataResourceVisibility, targetApproved); diff --git a/core/src/main/java/org/fao/geonet/api/records/attachments/FilesystemStore.java b/core/src/main/java/org/fao/geonet/api/records/attachments/FilesystemStore.java index a54c5dde1e88..ddf7b3753126 100644 --- a/core/src/main/java/org/fao/geonet/api/records/attachments/FilesystemStore.java +++ b/core/src/main/java/org/fao/geonet/api/records/attachments/FilesystemStore.java @@ -80,7 +80,7 @@ public FilesystemStore() { @Override public List getResources(ServiceContext context, String metadataUuid, MetadataResourceVisibility visibility, - String filter, Boolean approved) throws Exception { + String filter, Boolean approved, boolean includeAdditionalIndexedProperties) throws Exception { int metadataId = canDownload(context, metadataUuid, visibility, approved); Path metadataDir = Lib.resource.getMetadataDir(getDataDirectory(context), metadataId); diff --git a/core/src/main/java/org/fao/geonet/api/records/attachments/ResourceLoggerStore.java b/core/src/main/java/org/fao/geonet/api/records/attachments/ResourceLoggerStore.java index 6da9ded24cba..086f14880f6d 100644 --- a/core/src/main/java/org/fao/geonet/api/records/attachments/ResourceLoggerStore.java +++ b/core/src/main/java/org/fao/geonet/api/records/attachments/ResourceLoggerStore.java @@ -74,6 +74,16 @@ public List getResources(ServiceContext context, String metada return null; } + @Override + public List getResources(ServiceContext context, String metadataUuid, + MetadataResourceVisibility metadataResourceVisibility, String filter, Boolean approved, boolean includeAdditionalIndexedProperties) + throws Exception { + if (decoratedStore != null) { + return decoratedStore.getResources(context, metadataUuid, metadataResourceVisibility, filter, approved, includeAdditionalIndexedProperties); + } + return null; + } + @Override public ResourceHolder getResource(final ServiceContext context, final String metadataUuid, final MetadataResourceVisibility visibility, final String resourceId, Boolean approved) throws Exception { diff --git a/core/src/main/java/org/fao/geonet/api/records/attachments/Store.java b/core/src/main/java/org/fao/geonet/api/records/attachments/Store.java index 8438de351646..1397d178cc11 100644 --- a/core/src/main/java/org/fao/geonet/api/records/attachments/Store.java +++ b/core/src/main/java/org/fao/geonet/api/records/attachments/Store.java @@ -101,6 +101,57 @@ public interface Store { */ List getResources(ServiceContext context, String metadataUuid, MetadataResourceVisibility metadataResourceVisibility, String filter, Boolean approved) throws Exception; + /** + * Retrieve all resources for a metadata. The list of resources depends on current user + * privileges. + * + *

This overload allows callers to control whether additional indexed properties + * are populated on each returned {@link MetadataResource}. Callers should set + * {@code includeAdditionalIndexedProperties} to {@code true} only when they actually + * need those extra indexed values (for example, when preparing data for search or + * indexing purposes). For simple listing or download operations where these extra + * properties are not required, it is recommended to set this flag to {@code false}.

+ * + * @param context the service context + * @param metadataUuid the metadata UUID + * @param sort sort by resource name or sharing policy {@link Sort} + * @param filter a {@link java.nio.file.Files#newDirectoryStream(Path) GLOB + * expression} to filter resources eg. *.{png|jpg} + * @param approved return the approved version or not + * @param includeAdditionalIndexedProperties whether to populate additional indexed + * properties on returned resources ({@code true}) or to skip them + * for better performance when they are not needed ({@code false}) + * @return A list of resources + * @throws Exception if an error occurs while retrieving the resources + */ + List getResources(ServiceContext context, String metadataUuid, Sort sort, String filter, Boolean approved, boolean includeAdditionalIndexedProperties) throws Exception; + + /** + * Retrieve all resources for a metadata having a specific sharing policy. + * + *

This overload allows callers to control whether additional indexed properties + * are populated on each returned {@link MetadataResource}. Callers should set + * {@code includeAdditionalIndexedProperties} to {@code true} only when they actually + * need those extra indexed values (for example, when preparing data for search or + * indexing purposes). For simple listing or download operations where these extra + * properties are not required, it is recommended to set this flag to {@code false}.

+ * + * @param context the service context + * @param metadataUuid the metadata UUID + * @param metadataResourceVisibility the type of sharing policy + * {@link MetadataResourceVisibility} + * @param filter a {@link java.nio.file.Files#newDirectoryStream(Path) GLOB + * expression} to filter resources eg. *.{png|jpg} + * @param approved return the approved version or not + * @param includeAdditionalIndexedProperties whether to populate additional indexed + * properties on returned resources ({@code true}) + * or to skip them for better performance when they + * are not needed ({@code false}) + * @return A list of resources + * @throws Exception if an error occurs while retrieving the resources + */ + List getResources(ServiceContext context, String metadataUuid, MetadataResourceVisibility metadataResourceVisibility, String filter, Boolean approved, boolean includeAdditionalIndexedProperties) throws Exception; + /** * Retrieve a resource. * diff --git a/core/src/main/java/org/fao/geonet/kernel/datamanager/base/BaseMetadataIndexer.java b/core/src/main/java/org/fao/geonet/kernel/datamanager/base/BaseMetadataIndexer.java index 50fff6228fce..51a0f8d7d43d 100644 --- a/core/src/main/java/org/fao/geonet/kernel/datamanager/base/BaseMetadataIndexer.java +++ b/core/src/main/java/org/fao/geonet/kernel/datamanager/base/BaseMetadataIndexer.java @@ -705,7 +705,8 @@ public Multimap indexMetadataFileStore(AbstractMetadata fullMd) fullMd.getUuid(), (org.fao.geonet.api.records.attachments.Sort) null, null, - !(fullMd instanceof MetadataDraft)); + !(fullMd instanceof MetadataDraft), + true); if (metadataResources != null && !metadataResources.isEmpty()) { JsonNode jsonNode = indexObjectMapper.valueToTree(metadataResources); diff --git a/datastorages/cmis/src/main/java/org/fao/geonet/api/records/attachments/CMISStore.java b/datastorages/cmis/src/main/java/org/fao/geonet/api/records/attachments/CMISStore.java index 65e276cd53b6..1308c9f453d8 100644 --- a/datastorages/cmis/src/main/java/org/fao/geonet/api/records/attachments/CMISStore.java +++ b/datastorages/cmis/src/main/java/org/fao/geonet/api/records/attachments/CMISStore.java @@ -81,7 +81,7 @@ public class CMISStore extends AbstractStore { @Override public List getResources(final ServiceContext context, final String metadataUuid, - final MetadataResourceVisibility visibility, String filter, Boolean approved) throws Exception { + final MetadataResourceVisibility visibility, String filter, Boolean approved, boolean includeAdditionalIndexedProperties) throws Exception { final int metadataId = canDownload(context, metadataUuid, visibility, approved); final String resourceTypeDir = getMetadataDir(context, metadataId) + cmisConfiguration.getFolderDelimiter() + visibility.toString(); diff --git a/datastorages/jcloud/src/main/java/org/fao/geonet/api/records/attachments/JCloudStore.java b/datastorages/jcloud/src/main/java/org/fao/geonet/api/records/attachments/JCloudStore.java index b8df440857a2..aee773e7127d 100644 --- a/datastorages/jcloud/src/main/java/org/fao/geonet/api/records/attachments/JCloudStore.java +++ b/datastorages/jcloud/src/main/java/org/fao/geonet/api/records/attachments/JCloudStore.java @@ -33,10 +33,7 @@ import org.fao.geonet.api.exception.InputStreamLimitExceededException; import org.fao.geonet.api.exception.ResourceNotFoundException; import org.fao.geonet.constants.Geonet; -import org.fao.geonet.domain.MetadataResource; -import org.fao.geonet.domain.MetadataResourceContainer; -import org.fao.geonet.domain.MetadataResourceExternalManagementProperties; -import org.fao.geonet.domain.MetadataResourceVisibility; +import org.fao.geonet.domain.*; import org.fao.geonet.kernel.GeonetworkDataDirectory; import org.fao.geonet.kernel.setting.SettingManager; import org.fao.geonet.languages.IsoLanguagesMapper; @@ -102,7 +99,7 @@ public class JCloudStore extends AbstractStore { @Override public List getResources(final ServiceContext context, final String metadataUuid, - final MetadataResourceVisibility visibility, String filter, Boolean approved) throws Exception { + final MetadataResourceVisibility visibility, String filter, Boolean approved, boolean includeAdditionalIndexedProperties) throws Exception { final int metadataId = canDownload(context, metadataUuid, visibility, approved); final String resourceTypeDir = getMetadataDir(context, metadataId) + jCloudConfiguration.getFolderDelimiter() + visibility.toString() + jCloudConfiguration.getFolderDelimiter(); @@ -132,7 +129,7 @@ public List getResources(final ServiceContext context, final S Path keyPath = new File(storageMetadata.getName()).toPath().getFileName(); if (storageMetadata.getType() == StorageType.BLOB && matcher.matches(keyPath)){ final String filename = getFilename(storageMetadata.getName()); - MetadataResource resource = createResourceDescription(context, metadataUuid, visibility, filename, storageMetadata, metadataId, approved); + MetadataResource resource = createResourceDescription(context, metadataUuid, visibility, filename, storageMetadata, metadataId, approved, includeAdditionalIndexedProperties); resourceList.add(resource); } } @@ -147,7 +144,7 @@ public List getResources(final ServiceContext context, final S private MetadataResource createResourceDescription(final ServiceContext context, final String metadataUuid, final MetadataResourceVisibility visibility, final String resourceId, - StorageMetadata storageMetadata, int metadataId, boolean approved) { + StorageMetadata storageMetadata, int metadataId, boolean approved, boolean includeAdditionalIndexedProperties) { String filename = getFilename(metadataUuid, resourceId); Date changedDate; @@ -194,9 +191,28 @@ private MetadataResource createResourceDescription(final ServiceContext context, } } - MetadataResourceExternalManagementProperties metadataResourceExternalManagementProperties = - getMetadataResourceExternalManagementProperties(context, metadataId, metadataUuid, visibility, resourceId, filename, storageMetadata.getETag(), storageMetadata.getType(), - validationStatus); + MetadataResourceExternalManagementProperties metadataResourceExternalManagementProperties; + if (includeAdditionalIndexedProperties) { + Map additionalIndexedProperties = new HashMap<>(); + if (jCloudConfiguration.getAdditionalIndexedProperties() != null && !jCloudConfiguration.getAdditionalIndexedProperties().isEmpty()) { + for (String propertyName : jCloudConfiguration.getAdditionalIndexedProperties()) { + String propertyValue = null; + if (storageMetadata.getUserMetadata().containsKey(propertyName)) { + propertyValue = storageMetadata.getUserMetadata().get(propertyName); + } + if (StringUtils.hasLength(propertyValue)) { + additionalIndexedProperties.put(propertyName, propertyValue); + } + } + } + metadataResourceExternalManagementProperties = + getIndexedMetadataResourceExternalManagementProperties(context, metadataId, metadataUuid, visibility, resourceId, filename, storageMetadata.getETag(), storageMetadata.getType(), + validationStatus, additionalIndexedProperties); + } else { + metadataResourceExternalManagementProperties = + getMetadataResourceExternalManagementProperties(context, metadataId, metadataUuid, visibility, resourceId, filename, storageMetadata.getETag(), storageMetadata.getType(), + validationStatus); + } return new FilesystemStoreResource(metadataUuid, metadataId, filename, settingManager.getNodeURL() + "api/records/", visibility, storageMetadata.getSize(), changedDate, versionValue, metadataResourceExternalManagementProperties, approved); @@ -222,7 +238,7 @@ public ResourceHolder getResource(final ServiceContext context, final String met .withDescriptionKey("exception.resourceNotFound.resource.description", new String[]{resourceId, metadataUuid}); } return new JCloudResourceHolder(object, createResourceDescription(context, metadataUuid, visibility, resourceId, - object.getMetadata(), metadataId, approved)); + object.getMetadata(), metadataId, approved, false)); } catch (ContainerNotFoundException e) { throw new ResourceNotFoundException( String.format("Metadata container for resource '%s' not found for metadata '%s'", resourceId, metadataUuid)) @@ -246,7 +262,7 @@ public MetadataResource getResourceMetadata(final ServiceContext context, final .withDescriptionKey("exception.resourceNotFound.resource.description", new String[]{resourceId, metadataUuid}); } return createResourceDescription(context, metadataUuid, visibility, resourceId, - metadata, metadataId, approved); + metadata, metadataId, approved, false); } catch (ContainerNotFoundException e) { throw new ResourceNotFoundException( String.format("Metadata container for resource '%s' not found for metadata '%s'", resourceId, metadataUuid)) @@ -271,7 +287,7 @@ public ResourceHolder getResourceWithRange(ServiceContext context, String metada .withDescriptionKey("exception.resourceNotFound.resource.description", new String[]{resourceId, metadataUuid}); } return new JCloudResourceHolder(object, createResourceDescription(context, metadataUuid, metadataResourceVisibility, resourceId, - object.getMetadata(), metadataId, approved)); + object.getMetadata(), metadataId, approved, false)); } catch (ContainerNotFoundException e) { throw new ResourceNotFoundException( String.format("Metadata container for resource '%s' not found for metadata '%s'", resourceId, metadataUuid)) @@ -290,7 +306,7 @@ public ResourceHolder getResourceInternal(String metadataUuid, MetadataResourceV final Blob object = jCloudConfiguration.getClient().getBlobStore().getBlob( jCloudConfiguration.getContainerName(), getKey(context, metadataUuid, metadataId, visibility, resourceId)); return new JCloudResourceHolder(object, createResourceDescription(context, metadataUuid, visibility, resourceId, - object.getMetadata(), metadataId, approved)); + object.getMetadata(), metadataId, approved, false)); } catch (ContainerNotFoundException e) { throw new ResourceNotFoundException( String.format("Metadata resource '%s' not found for metadata '%s'", resourceId, metadataUuid)) @@ -381,7 +397,7 @@ protected MetadataResource putResource(final ServiceContext context, final Strin } Blob blobResults = jCloudConfiguration.getClient().getBlobStore().getBlob(jCloudConfiguration.getContainerName(), key); - return createResourceDescription(context, metadataUuid, visibility, filename, blobResults.getMetadata(), metadataId, approved); + return createResourceDescription(context, metadataUuid, visibility, filename, blobResults.getMetadata(), metadataId, approved, false); } finally { locks.remove(key); } @@ -547,7 +563,7 @@ public MetadataResource patchResourceStatus(final ServiceContext context, final break; } else { // already the good visibility - return createResourceDescription(context, metadataUuid, visibility, resourceId, storageMetadata, metadataId, approved); + return createResourceDescription(context, metadataUuid, visibility, resourceId, storageMetadata, metadataId, approved, false); } } } catch (ContainerNotFoundException ignored) { @@ -562,7 +578,7 @@ public MetadataResource patchResourceStatus(final ServiceContext context, final Blob blobResults = jCloudConfiguration.getClient().getBlobStore().getBlob(jCloudConfiguration.getContainerName(), targetKey); - return createResourceDescription(context, metadataUuid, visibility, resourceId, blobResults.getMetadata(), metadataId, approved); + return createResourceDescription(context, metadataUuid, visibility, resourceId, blobResults.getMetadata(), metadataId, approved, false); } else { Log.warning(Geonet.RESOURCES, String.format("Could not update permissions. Metadata resource '%s' not found for metadata '%s'", resourceId, metadataUuid)); @@ -772,7 +788,7 @@ public MetadataResource getResourceDescription(final ServiceContext context, fin return null; } else { final StorageMetadata metadata = object.getMetadata(); - return createResourceDescription(context, metadataUuid, visibility, filename, metadata, metadataId, approved); + return createResourceDescription(context, metadataUuid, visibility, filename, metadata, metadataId, approved, false); } } catch (ContainerNotFoundException e) { return null; @@ -854,23 +870,23 @@ private boolean isFolder(StorageMetadata storageMetadata) { } /** - * get external resource management for the supplied resource. - * Replace the following - * {objectId} type:visibility:metadataId:version:resourceId in base64 encoding - * {id} resource id - * {type} // If the type is folder then type "folder" will be displayed else if document then "document" will be displayed - * {uuid} metadatauuid - * {metadataid} metadataid - * {visibility} visibility - * {filename} filename - * {version} version - * {lang} ISO639-1 2 char language - * {iso3lang} ISO 639-2/T language - *

- * Sample url for custom app - * http://localhost:8080/artifact?filename={filename}&version={version}&lang={lang} + * Creates external resource management properties for the specified resource. + * + *

This method generates an object ID and constructs an external management URL + * for a metadata resource, encapsulating them in a {@link MetadataResourceExternalManagementProperties} object.

+ * + * @param context the service context providing access to application services + * @param metadataId the unique identifier of the metadata record + * @param metadataUuid the UUID of the metadata record + * @param visibility the visibility level of the resource (e.g., public, private) + * @param resourceId the unique identifier of the resource + * @param filename the name of the file resource (may be null) + * @param version the version identifier of the resource (may be null) + * @param type the storage type (FOLDER or document); null defaults to document + * @param validationStatus the validation status of the resource + * @return a new {@link MetadataResourceExternalManagementProperties} instance containing + * the object ID, URL, and validation status */ - private MetadataResourceExternalManagementProperties getMetadataResourceExternalManagementProperties(ServiceContext context, int metadataId, final String metadataUuid, @@ -881,72 +897,170 @@ private MetadataResourceExternalManagementProperties getMetadataResourceExternal StorageType type, MetadataResourceExternalManagementProperties.ValidationStatus validationStatus ) { - String metadataResourceExternalManagementPropertiesUrl = jCloudConfiguration.getExternalResourceManagementUrl(); String objectId = getResourceManagementExternalPropertiesObjectId((type == null ? "document" : (StorageType.FOLDER.equals(type) ? "folder" : "document")), visibility, metadataId, version, resourceId); - if (StringUtils.hasLength(metadataResourceExternalManagementPropertiesUrl)) { - // {objectid} objectId // It will be the type:visibility:metadataId:version:resourceId in base64 - // i.e. folder::100::100 # Folder in resource 100 - // i.e. document:public:100:v1:sample.jpg # public document 100 version v1 name sample.jpg - if (metadataResourceExternalManagementPropertiesUrl.contains("{objectid}")) { - metadataResourceExternalManagementPropertiesUrl = metadataResourceExternalManagementPropertiesUrl.replaceAll("(\\{objectid\\})", objectId); - } - // {id} id - if (metadataResourceExternalManagementPropertiesUrl.contains("{id}")) { - metadataResourceExternalManagementPropertiesUrl = metadataResourceExternalManagementPropertiesUrl.replaceAll("(\\{id\\})", resourceId); - } - // {type} // If the type is folder then type "folder" will be displayed else if document then "document" will be displayed - if (metadataResourceExternalManagementPropertiesUrl.contains("{type}")) { - metadataResourceExternalManagementPropertiesUrl = metadataResourceExternalManagementPropertiesUrl.replaceAll("(\\{type\\})", - (type == null ? "document" : (StorageType.FOLDER.equals(type) ? "folder" : "document"))); - } - // {uuid} metadata uuid - if (metadataResourceExternalManagementPropertiesUrl.contains("{uuid}")) { - metadataResourceExternalManagementPropertiesUrl = metadataResourceExternalManagementPropertiesUrl.replaceAll("(\\{uuid\\})", (metadataUuid == null ? "" : metadataUuid)); - } - // {metadataid} metadataId - if (metadataResourceExternalManagementPropertiesUrl.contains("{metadataid}")) { - metadataResourceExternalManagementPropertiesUrl = metadataResourceExternalManagementPropertiesUrl.replaceAll("(\\{metadataid\\})", String.valueOf(metadataId)); - } - // {visibility} visibility - if (metadataResourceExternalManagementPropertiesUrl.contains("{visibility}")) { - metadataResourceExternalManagementPropertiesUrl = - metadataResourceExternalManagementPropertiesUrl.replaceAll("(\\{visibility\\})", (visibility == null ? "" : visibility.toString().toLowerCase())); - } - // {filename} filename - if (metadataResourceExternalManagementPropertiesUrl.contains("{filename}")) { - metadataResourceExternalManagementPropertiesUrl = metadataResourceExternalManagementPropertiesUrl.replaceAll("(\\{filename\\})", (filename == null ? "" : filename)); - } - // {version} version - if (metadataResourceExternalManagementPropertiesUrl.contains("{version}")) { - metadataResourceExternalManagementPropertiesUrl = metadataResourceExternalManagementPropertiesUrl.replaceAll("(\\{version\\})", (version == null ? "" : version)); - } + String url = buildExternalManagementUrl(context, metadataId, metadataUuid, visibility, resourceId, filename, version, type); + return new MetadataResourceExternalManagementProperties(objectId, url, validationStatus); + } + + /** + * Creates indexed external resource management properties for the specified resource. + * + *

This method extends the functionality of {@link #getMetadataResourceExternalManagementProperties} + * by including additional custom properties that can be indexed. It generates an object ID + * and constructs an external management URL for a metadata resource, along with custom + * metadata properties.

+ * + * @param context the service context providing access to application services + * @param metadataId the unique identifier of the metadata record + * @param metadataUuid the UUID of the metadata record + * @param visibility the visibility level of the resource (e.g., public, private) + * @param resourceId the unique identifier of the resource + * @param filename the name of the file resource (may be null) + * @param version the version identifier of the resource (may be null) + * @param type the storage type (FOLDER or document); null defaults to document + * @param validationStatus the validation status of the resource + * @param additionalProperties a map of custom properties to be included with the resource metadata + * @return a new {@link IndexedMetadataResourceExternalManagementProperties} instance containing + * the object ID, URL, validation status, and additional indexed properties + */ + private IndexedMetadataResourceExternalManagementProperties getIndexedMetadataResourceExternalManagementProperties(ServiceContext context, + int metadataId, + final String metadataUuid, + final MetadataResourceVisibility visibility, + final String resourceId, + String filename, + String version, + StorageType type, + MetadataResourceExternalManagementProperties.ValidationStatus validationStatus, + Map additionalProperties + ) { + String objectId = getResourceManagementExternalPropertiesObjectId((type == null ? "document" : (StorageType.FOLDER.equals(type) ? "folder" : "document")), visibility, metadataId, version, + resourceId); + String url = buildExternalManagementUrl(context, metadataId, metadataUuid, visibility, resourceId, filename, version, type); + return new IndexedMetadataResourceExternalManagementProperties(objectId, url, validationStatus, additionalProperties); + } - if (metadataResourceExternalManagementPropertiesUrl.contains("{lang}") || metadataResourceExternalManagementPropertiesUrl.contains("{ISO3lang}")) { - final IsoLanguagesMapper mapper = context.getBean(IsoLanguagesMapper.class); - String contextLang = context.getLanguage() == null ? Geonet.DEFAULT_LANGUAGE : context.getLanguage(); - String lang; - String iso3Lang; + /** + * Builds the external management URL by replacing template placeholders with actual values. + * + *

This method constructs a URL from a configured template by performing token substitution + * for various metadata and resource properties. If no external resource management URL is + * configured, an empty string is returned.

+ * + *

Supported Placeholders:

+ *
    + *
  • {objectid} - Base64-encoded string in format: {@code type:visibility:metadataId:version:resourceId} + *
    Examples: + *
      + *
    • {@code folder::100::100} - Folder in resource 100
    • + *
    • {@code document:public:100:v1:sample.jpg} - Public document 100, version v1, name sample.jpg
    • + *
    + *
  • + *
  • {id} - The resource identifier
  • + *
  • {type} - Resource type: "folder" for folders, "document" for documents
  • + *
  • {uuid} - The metadata UUID
  • + *
  • {metadataid} - The numeric metadata identifier
  • + *
  • {visibility} - The resource visibility level (lowercase)
  • + *
  • {filename} - The resource filename
  • + *
  • {version} - The resource version identifier
  • + *
  • {lang} - ISO 639-1 two-character language code
  • + *
  • {iso3lang} - ISO 639-2/T three-character language code
  • + *
+ * + *

Example URL Templates:

+ *
+     * http://localhost:8080/artifact?filename={filename}&version={version}&lang={lang}
+     * https://example.com/resources/{uuid}/{id}?type={type}&visibility={visibility}
+     * 
+ * + * @param context the service context providing access to application services and language settings + * @param metadataId the unique identifier of the metadata record + * @param metadataUuid the UUID of the metadata record + * @param visibility the visibility level of the resource (e.g., public, private) + * @param resourceId the unique identifier of the resource + * @param filename the name of the file resource (may be null, replaced with empty string) + * @param version the version identifier of the resource (may be null, replaced with empty string) + * @param type the storage type (FOLDER or document); null defaults to document + * @return the constructed URL with all placeholders replaced, or an empty string if no URL template is configured + */ + private String buildExternalManagementUrl(ServiceContext context, + int metadataId, + final String metadataUuid, + final MetadataResourceVisibility visibility, + final String resourceId, + String filename, + String version, + StorageType type) { + String url = jCloudConfiguration.getExternalResourceManagementUrl(); + + if (!StringUtils.hasLength(url)) { + return url; + } - if (contextLang.length() == 2) { - lang = contextLang; - iso3Lang = mapper.iso639_1_to_iso639_2(contextLang); - } else { - lang = mapper.iso639_2_to_iso639_1(contextLang); - iso3Lang = contextLang; - } - // {lang} ISO639-1 2 char language - if (metadataResourceExternalManagementPropertiesUrl.contains("{lang}")) { - metadataResourceExternalManagementPropertiesUrl = metadataResourceExternalManagementPropertiesUrl.replaceAll("(\\{lang\\})", lang); - } - // {iso3lang} ISO 639-2/T language - if (metadataResourceExternalManagementPropertiesUrl.contains("{iso3lang}")) { - metadataResourceExternalManagementPropertiesUrl = metadataResourceExternalManagementPropertiesUrl.replaceAll("(\\{iso3lang\\})", iso3Lang); - } + String typeString = type == null ? "document" : (StorageType.FOLDER.equals(type) ? "folder" : "document"); + String objectId = getResourceManagementExternalPropertiesObjectId((typeString), visibility, metadataId, version, + resourceId); + + // {objectid} objectId // It will be the type:visibility:metadataId:version:resourceId in base64 + // i.e. folder::100::100 # Folder in resource 100 + // i.e. document:public:100:v1:sample.jpg # public document 100 version v1 name sample.jpg + if (url.contains("{objectid}")) { + url = url.replaceAll("(\\{objectid\\})", objectId); + } + // {id} id + if (url.contains("{id}")) { + url = url.replaceAll("(\\{id\\})", resourceId); + } + // {type} // If the type is folder then type "folder" will be displayed else if document then "document" will be displayed + if (url.contains("{type}")) { + url = url.replaceAll("(\\{type\\})", typeString); + } + // {uuid} metadata uuid + if (url.contains("{uuid}")) { + url = url.replaceAll("(\\{uuid\\})", (metadataUuid == null ? "" : metadataUuid)); + } + // {metadataid} metadataId + if (url.contains("{metadataid}")) { + url = url.replaceAll("(\\{metadataid\\})", String.valueOf(metadataId)); + } + // {visibility} visibility + if (url.contains("{visibility}")) { + url = url.replaceAll("(\\{visibility\\})", (visibility == null ? "" : visibility.toString().toLowerCase())); + } + // {filename} filename + if (url.contains("{filename}")) { + url = url.replaceAll("(\\{filename\\})", (filename == null ? "" : filename)); + } + // {version} version + if (url.contains("{version}")) { + url = url.replaceAll("(\\{version\\})", (version == null ? "" : version)); + } + + if (url.contains("{lang}") || url.contains("{ISO3lang}")) { + final IsoLanguagesMapper mapper = context.getBean(IsoLanguagesMapper.class); + String contextLang = context.getLanguage() == null ? Geonet.DEFAULT_LANGUAGE : context.getLanguage(); + String lang; + String iso3Lang; + + if (contextLang.length() == 2) { + lang = contextLang; + iso3Lang = mapper.iso639_1_to_iso639_2(contextLang); + } else { + lang = mapper.iso639_2_to_iso639_1(contextLang); + iso3Lang = contextLang; + } + // {lang} ISO639-1 2 char language + if (url.contains("{lang}")) { + url = url.replaceAll("(\\{lang\\})", lang); + } + // {iso3lang} ISO 639-2/T language + if (url.contains("{iso3lang}")) { + url = url.replaceAll("(\\{iso3lang\\})", iso3Lang); } } - return new MetadataResourceExternalManagementProperties(objectId, metadataResourceExternalManagementPropertiesUrl, validationStatus); + return url; } public ResourceManagementExternalProperties getResourceManagementExternalProperties() { diff --git a/datastorages/jcloud/src/main/java/org/fao/geonet/resources/JCloudConfiguration.java b/datastorages/jcloud/src/main/java/org/fao/geonet/resources/JCloudConfiguration.java index 236ef078375c..a4b774083de7 100644 --- a/datastorages/jcloud/src/main/java/org/fao/geonet/resources/JCloudConfiguration.java +++ b/datastorages/jcloud/src/main/java/org/fao/geonet/resources/JCloudConfiguration.java @@ -31,6 +31,7 @@ import javax.annotation.Nonnull; import javax.annotation.PostConstruct; +import java.util.List; public class JCloudConfiguration { @@ -54,6 +55,11 @@ public class JCloudConfiguration { * Property name for storing the metadata uuid that is expected to be a String */ private String metadataUUIDPropertyName; + /** + * List of field names that are copied from the storage metadata into the geonetwork index. + * Can be set as a comma-separated string which will be automatically parsed into a List. + */ + private List additionalIndexedProperties; /** * Url used for managing enhanced resource properties related to the metadata. */ @@ -288,6 +294,32 @@ public void setMetadataUUIDPropertyName(String metadataUUIDPropertyName) { this.metadataUUIDPropertyName = metadataUUIDPropertyName; } + /** + * Gets the additional indexed properties as a list of field names. + * + * @return List of field names + */ + public List getAdditionalIndexedProperties() { + return this.additionalIndexedProperties; + } + + /** + * Sets the additional indexed properties from a comma-separated string. + * Empty values are filtered out. + * + * @param additionalIndexedProperties Comma-separated list of field names + */ + public void setAdditionalIndexedProperties(String additionalIndexedProperties) { + if (StringUtils.hasLength(additionalIndexedProperties)) { + this.additionalIndexedProperties = java.util.Arrays.stream(additionalIndexedProperties.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(java.util.stream.Collectors.toList()); + } else { + this.additionalIndexedProperties = null; + } + } + public String getExternalResourceManagementChangedDatePropertyName() { return externalResourceManagementChangedDatePropertyName; } diff --git a/datastorages/jcloud/src/main/resources/config-store/config-jcloud-overrides.properties b/datastorages/jcloud/src/main/resources/config-store/config-jcloud-overrides.properties index bbb966a00af4..fb963f8b8f66 100644 --- a/datastorages/jcloud/src/main/resources/config-store/config-jcloud-overrides.properties +++ b/datastorages/jcloud/src/main/resources/config-store/config-jcloud-overrides.properties @@ -23,3 +23,6 @@ jcloud.versioning.strategy=${JCLOUD_VERSIONING_STRATEGY:#{null}} jcloud.external.resource.management.version.property.name=${JCLOUD_EXTERNAL_RESOURCE_MANAGEMENT_VERSION_PROPERTY_NAME:#{null}} jcloud.metadata.uuid.property.name=${JCLOUD_METADATA_UUID_PROPERTY_NAME:#{null}} + +# Comma-separated list of field names to copy from storage metadata into the geonetwork index +jcloud.additional.indexed.properties=${JCLOUD_ADDITIONAL_INDEXED_PROPERTIES:#{null}} diff --git a/datastorages/jcloud/src/main/resources/config-store/config-jcloud.xml b/datastorages/jcloud/src/main/resources/config-store/config-jcloud.xml index 427437dd29e4..fbce090f8d43 100644 --- a/datastorages/jcloud/src/main/resources/config-store/config-jcloud.xml +++ b/datastorages/jcloud/src/main/resources/config-store/config-jcloud.xml @@ -61,6 +61,9 @@ + + + getResources(final ServiceContext context, final String metadataUuid, - final MetadataResourceVisibility visibility, String filter, Boolean approved) throws Exception { + final MetadataResourceVisibility visibility, String filter, Boolean approved, boolean includeAdditionalIndexedProperties) throws Exception { final int metadataId = canEdit(context, metadataUuid, approved); final String resourceTypeDir = getMetadataDir(metadataId) + "/" + visibility.toString(); diff --git a/docs/manual/docs/customizing-application/index.md b/docs/manual/docs/customizing-application/index.md index 97a5f4d9b266..aaecd01984c8 100644 --- a/docs/manual/docs/customizing-application/index.md +++ b/docs/manual/docs/customizing-application/index.md @@ -11,5 +11,6 @@ - [Advanced configuration](advanced-configuration.md) - [Adding static pages](adding-static-pages.md) - [Implementing schema plugins](implementing-a-schema-plugin.md) +- [Integrating External Resource Properties](integrating-external-resource-properties.md) - [Characterset](characterset.md) - [Miscellaneous](misc.md) diff --git a/docs/manual/docs/customizing-application/integrating-external-resource-properties.md b/docs/manual/docs/customizing-application/integrating-external-resource-properties.md new file mode 100644 index 000000000000..1904361febe0 --- /dev/null +++ b/docs/manual/docs/customizing-application/integrating-external-resource-properties.md @@ -0,0 +1,80 @@ +# Integrating External Resource Properties + +Add custom metadata properties to resources stored in JCloud-based storage. These properties are stored as metadata in the cloud storage and are automatically indexed into GeoNetwork's catalog. + +## Quick Start + +### 1. Configure GeoNetwork + +Add this property or environment variable to your GeoNetwork configuration to specify which metadata fields from cloud storage should be included in the catalog index: + +```properties +jcloud.additional.indexed.properties=field1,field2,field3 +``` + +```bash +export JCLOUD_ADDITIONAL_INDEXED_PROPERTIES=field1,field2,field3 +``` + +**Property Explanation:** + +- **`jcloud.additional.indexed.properties`** - Comma-separated list of metadata field names to copy from cloud storage into the catalog index + - Each field name must correspond to a metadata property that you store with your resources in cloud storage + - Example: `jcloud.additional.indexed.properties=department,owner,budget,status` + - Environment variable: `JCLOUD_ADDITIONAL_INDEXED_PROPERTIES` + - Properties are optional - if not configured, only standard resource metadata is indexed + +### 2. Store Metadata with Your Resources + +When uploading resources to cloud storage, attach metadata properties with the names you specified in the configuration: + +**Example using blob metadata:** +``` +Blob Name: documents/report.pdf + +Metadata Properties: + - department: "Planning" + - owner: "John Doe" + - budget: "$50,000" + - status: "Active" +``` + +**Metadata Requirements:** +- Property names must match exactly those configured in `jcloud.additional.indexed.properties` +- Property values should be simple types (strings, numbers, booleans) +- Properties are optional - resources without configured properties will simply not include those fields + +### 3. Verify in Catalog + +After indexing, resources will contain the custom properties in the `additionalProperties` field of the `metadataResourceExternalManagementProperties` object: + +```json +{ + "lastModification": "2025-10-28T15:43:03.000+00:00", + "metadataResourceExternalManagementProperties": { + "id": "resource-001", + "url": "http://example.com/resource/resource-001", + "validationStatus": "INCOMPLETE", + "additionalProperties": { + "department": "Planning", + "owner": "John Doe", + "budget": "$50,000", + "status": "Active" + } + }, + "size": 112339, + "url": "http://localhost:8084/catalogue/srv/api/records/37aecae5-7783-4274-b595-df02aa003ac3/attachments/Sample1.pdf", + "version": "1", + "visibility": "PUBLIC" +} +``` + +## How It Works + +1. When resources are retrieved for indexing, GeoNetwork reads the metadata properties stored with each blob in cloud storage +2. For any properties that match the configured `jcloud.additional.indexed.properties` names, the values are extracted +3. These properties are stored in the `additionalProperties` map within the `MetadataResourceExternalManagementProperties` object +4. The enriched resource data is indexed and becomes searchable in the catalog + +**Properties are extracted automatically during metadata indexing** - no additional setup required once configured and metadata is stored with your resources. + diff --git a/docs/manual/mkdocs.yml b/docs/manual/mkdocs.yml index 3dcdf228ef12..77d78ba80318 100644 --- a/docs/manual/mkdocs.yml +++ b/docs/manual/mkdocs.yml @@ -396,8 +396,9 @@ nav: - customizing-application/configuring-faceted-search.md - customizing-application/advanced-configuration.md - customizing-application/adding-static-pages.md - - customizing-application/characterset.md - customizing-application/implementing-a-schema-plugin.md + - customizing-application/integrating-external-resource-properties.md + - customizing-application/characterset.md - customizing-application/misc.md - 'Contributing': - contributing/index.md diff --git a/domain/src/main/java/org/fao/geonet/domain/IndexedMetadataResourceExternalManagementProperties.java b/domain/src/main/java/org/fao/geonet/domain/IndexedMetadataResourceExternalManagementProperties.java new file mode 100644 index 000000000000..1239b890f28c --- /dev/null +++ b/domain/src/main/java/org/fao/geonet/domain/IndexedMetadataResourceExternalManagementProperties.java @@ -0,0 +1,72 @@ +/* + * ============================================================================= + * === Copyright (C) 2001-2026 Food and Agriculture Organization of the + * === United Nations (FAO-UN), United Nations World Food Programme (WFP) + * === and United Nations Environment Programme (UNEP) + * === + * === This program is free software; you can redistribute it and/or modify + * === it under the terms of the GNU General Public License as published by + * === the Free Software Foundation; either version 2 of the License, or (at + * === your option) any later version. + * === + * === This program is distributed in the hope that it will be useful, but + * === WITHOUT ANY WARRANTY; without even the implied warranty of + * === MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * === General Public License for more details. + * === + * === You should have received a copy of the GNU General Public License + * === along with this program; if not, write to the Free Software + * === Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + * === + * === Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2, + * === Rome - Italy. email: geonetwork@osgeo.org + * ============================================================================== + */ + +package org.fao.geonet.domain; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.HashMap; +import java.util.Map; + +/** + * Extends MetadataResourceExternalManagementProperties to add additional properties for indexing purposes. + */ +@XmlRootElement(name = "metadataResourceExternalManagementProperties") +@XmlAccessorType(XmlAccessType.FIELD) +public class IndexedMetadataResourceExternalManagementProperties extends MetadataResourceExternalManagementProperties { + /** + * Additional properties for indexing. + */ + private Map additionalProperties = new HashMap<>(); + + /** + * Constructor for IndexedMetadataResourceExternalManagementProperties. + * + * @param id The identifier of the metadata resource. + * @param url The URL of the metadata resource. + * @param validationStatus The validation status of the metadata resource. + * @param additionalProperties Additional properties for indexing (can be null). + */ + public IndexedMetadataResourceExternalManagementProperties(@Nonnull String id, @Nonnull String url, @Nonnull ValidationStatus validationStatus, @Nullable Map additionalProperties) { + super(id, url, validationStatus); + if (additionalProperties != null) { + this.additionalProperties = additionalProperties; + } + } + + /** + * Gets the additional properties for indexing. + * + * @return A map of additional properties. + */ + public Map getAdditionalProperties() { + return additionalProperties; + } +} + +