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 168e4e2a63c..35d1b2867a7 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 @@ -286,7 +286,7 @@ public String toString() { private String escapeResourceManagementExternalProperties(String value) { return value.replace(RESOURCE_MANAGEMENT_EXTERNAL_PROPERTIES_SEPARATOR, RESOURCE_MANAGEMENT_EXTERNAL_PROPERTIES_ESCAPED_SEPARATOR); -} + } /** * Create an encoded base 64 object id contains the following fields to uniquely identify the resource diff --git a/core/src/main/java/org/fao/geonet/kernel/SelectionManager.java b/core/src/main/java/org/fao/geonet/kernel/SelectionManager.java index a42a9e982e9..230bf390ff6 100644 --- a/core/src/main/java/org/fao/geonet/kernel/SelectionManager.java +++ b/core/src/main/java/org/fao/geonet/kernel/SelectionManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2016 Food and Agriculture Organization of the + * Copyright (C) 2001-2024 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -51,9 +51,9 @@ * Manage objects selection for a user session. */ public class SelectionManager { - public static final String SELECTION_METADATA = "metadata"; - public static final String SELECTION_BUCKET = "bucket"; + // Bucket name used in the search UI to store the selected the metadata + public static final String SELECTION_BUCKET = "s101"; // used to limit select all if get system setting maxrecords fails or contains value we can't parse public static final int DEFAULT_MAXHITS = 1000; public static final String ADD_ALL_SELECTED = "add-all"; @@ -61,20 +61,20 @@ public class SelectionManager { public static final String ADD_SELECTED = "add"; public static final String REMOVE_SELECTED = "remove"; public static final String CLEAR_ADD_SELECTED = "clear-add"; - private Hashtable> selections = null; + private Hashtable> selections; private SelectionManager() { - selections = new Hashtable>(0); + selections = new Hashtable<>(0); Set MDSelection = Collections - .synchronizedSet(new HashSet(0)); + .synchronizedSet(new HashSet<>(0)); selections.put(SELECTION_METADATA, MDSelection); } public Map getSelectionsAndSize() { return selections.entrySet().stream().collect(Collectors.toMap( - e -> e.getKey(), + Map.Entry::getKey, e -> e.getValue().size() )); } @@ -183,7 +183,7 @@ public int updateSelection(String type, // Get the selection manager or create it Set selection = this.getSelection(type); if (selection == null) { - selection = Collections.synchronizedSet(new HashSet()); + selection = Collections.synchronizedSet(new HashSet<>()); this.selections.put(type, selection); } @@ -192,30 +192,21 @@ public int updateSelection(String type, this.selectAll(type, context, session); else if (selected.equals(REMOVE_ALL_SELECTED)) this.close(type); - else if (selected.equals(ADD_SELECTED) && listOfIdentifiers.size() > 0) { + else if (selected.equals(ADD_SELECTED) && !listOfIdentifiers.isEmpty()) { // TODO ? Should we check that the element exist first ? - for (String paramid : listOfIdentifiers) { - selection.add(paramid); - } - } else if (selected.equals(REMOVE_SELECTED) && listOfIdentifiers.size() > 0) { + selection.addAll(listOfIdentifiers); + } else if (selected.equals(REMOVE_SELECTED) && !listOfIdentifiers.isEmpty()) { for (String paramid : listOfIdentifiers) { selection.remove(paramid); } - } else if (selected.equals(CLEAR_ADD_SELECTED) && listOfIdentifiers.size() > 0) { + } else if (selected.equals(CLEAR_ADD_SELECTED) && !listOfIdentifiers.isEmpty()) { this.close(type); - for (String paramid : listOfIdentifiers) { - selection.add(paramid); - } + selection.addAll(listOfIdentifiers); } } // Remove empty/null element from the selection - Iterator iter = selection.iterator(); - while (iter.hasNext()) { - Object element = iter.next(); - if (element == null) - iter.remove(); - } + selection.removeIf(Objects::isNull); return selection.size(); } @@ -241,14 +232,12 @@ public void selectAll(String type, ServiceContext context, UserSession session) if (StringUtils.isNotEmpty(type)) { JsonNode request = (JsonNode) session.getProperty(Geonet.Session.SEARCH_REQUEST + type); - if (request == null) { - return; - } else { + if (request != null) { final SearchResponse searchResponse; try { EsSearchManager searchManager = context.getBean(EsSearchManager.class); searchResponse = searchManager.query(request.get("query"), FIELDLIST_UUID, 0, maxhits); - List uuidList = new ArrayList(); + List uuidList = new ArrayList<>(); ObjectMapper objectMapper = new ObjectMapper(); for (Hit h : (List) searchResponse.hits().hits()) { uuidList.add((String) objectMapper.convertValue(h.source(), Map.class).get(Geonet.IndexFieldNames.UUID)); @@ -293,7 +282,7 @@ public Set getSelection(String type) { Set sel = selections.get(type); if (sel == null) { Set MDSelection = Collections - .synchronizedSet(new HashSet(0)); + .synchronizedSet(new HashSet<>(0)); selections.put(type, MDSelection); } return selections.get(type); 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 989dc719fc4..5957cc4c3c8 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 @@ -779,14 +779,14 @@ public String toString() { } protected static class ResourceHolderImpl implements ResourceHolder { - private CmisObject cmisObject; + private final CmisObject cmisObject; private Path tempFolderPath; private Path path; private final MetadataResource metadataResource; public ResourceHolderImpl(final CmisObject cmisObject, MetadataResource metadataResource) throws IOException { // Preserve filename by putting the files into a temporary folder and using the same filename. - tempFolderPath = Files.createTempDirectory("gn-meta-res-" + String.valueOf(metadataResource.getMetadataId() + "-")); + tempFolderPath = Files.createTempDirectory("gn-meta-res-" + metadataResource.getMetadataId() + "-"); tempFolderPath.toFile().deleteOnExit(); path = tempFolderPath.resolve(getFilename(cmisObject.getName())); this.metadataResource = metadataResource; @@ -817,11 +817,5 @@ public void close() throws IOException { path=null; tempFolderPath = null; } - - @Override - protected void finalize() throws Throwable { - close(); - super.finalize(); - } } } 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 e4016a72e37..579704aff45 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 @@ -24,13 +24,11 @@ */ package org.fao.geonet.api.records.attachments; - import static org.jclouds.blobstore.options.PutOptions.Builder.multipart; import jeeves.server.context.ServiceContext; import org.apache.commons.collections.MapUtils; -import org.apache.commons.lang.StringUtils; import org.fao.geonet.ApplicationContextHolder; import org.fao.geonet.api.exception.ResourceNotFoundException; import org.fao.geonet.constants.Geonet; @@ -50,6 +48,7 @@ import org.jclouds.blobstore.options.CopyOptions; import org.jclouds.blobstore.options.ListContainerOptions; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.StringUtils; import java.io.File; import java.io.IOException; @@ -60,6 +59,7 @@ import java.nio.file.PathMatcher; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -67,6 +67,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nullable; @@ -75,8 +77,17 @@ public class JCloudStore extends AbstractStore { + private static final ConcurrentHashMap locks = new ConcurrentHashMap<>(); + + private static final String FIRST_VERSION = "1"; + + private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + static { + DATE_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC")); + } + // For azure Blob ADSL hdi_isfolder property name used to identify folders - private final static String AZURE_BLOB_IS_FOLDER_PROPERTY_NAME="hdi_isfolder"; + private static final String AZURE_BLOB_IS_FOLDER_PROPERTY_NAME="hdi_isfolder"; private Path baseMetadataDir = null; @@ -102,7 +113,7 @@ public List getResources(final ServiceContext context, final S FileSystems.getDefault().getPathMatcher("glob:" + filter); ListContainerOptions opts = new ListContainerOptions(); - opts.delimiter(jCloudConfiguration.getFolderDelimiter()).prefix(resourceTypeDir);; + opts.delimiter(jCloudConfiguration.getFolderDelimiter()).prefix(resourceTypeDir); // Page through the data String marker = null; @@ -114,7 +125,7 @@ public List getResources(final ServiceContext context, final S PageSet page = jCloudConfiguration.getClient().getBlobStore().list(jCloudConfiguration.getContainerName(), opts); for (StorageMetadata storageMetadata : page) { - // Only add to the list if it is a blob and it matches the filter. + // Only add to the list if it is a blob, and it matches the filter. Path keyPath = new File(storageMetadata.getName()).toPath().getFileName(); if (storageMetadata.getType() == StorageType.BLOB && matcher.matches(keyPath)){ final String filename = getFilename(storageMetadata.getName()); @@ -136,28 +147,56 @@ private MetadataResource createResourceDescription(final ServiceContext context, StorageMetadata storageMetadata, int metadataId, boolean approved) { String filename = getFilename(metadataUuid, resourceId); + Date changedDate; + String changedDatePropertyName = jCloudConfiguration.getExternalResourceManagementChangedDatePropertyName(); + if (storageMetadata.getUserMetadata().containsKey(changedDatePropertyName)) { + String changedDateValue = storageMetadata.getUserMetadata().get(changedDatePropertyName); + try { + changedDate = DATE_FORMATTER.parse(changedDateValue); + } catch (ParseException e) { + Log.warning(Geonet.RESOURCES, String.format("Unable to parse date '%s' into format pattern '%s' on resource '%s' for metadata %d(%s). Will use resource last modified date", + changedDateValue, DATE_FORMATTER.toPattern(), resourceId, metadataId, metadataUuid), e); + changedDate = storageMetadata.getLastModified(); + } + } else { + changedDate = storageMetadata.getLastModified(); + } + + String versionValue = null; if (jCloudConfiguration.isVersioningEnabled()) { - versionValue = storageMetadata.getETag(); // ETAG is cryptic may need some other value? + String versionPropertyName = jCloudConfiguration.getExternalResourceManagementVersionPropertyName(); + if (StringUtils.hasLength(versionPropertyName)) { + if (storageMetadata.getUserMetadata().containsKey(versionPropertyName)) { + versionValue = storageMetadata.getUserMetadata().get(versionPropertyName); + } else { + Log.warning(Geonet.RESOURCES, String.format("Expecting property '%s' on resource '%s' for metadata %d(%s) but the property was not found.", + versionPropertyName, resourceId, metadataId, metadataUuid)); + versionValue = ""; + } + } else { + versionValue = storageMetadata.getETag(); + } } MetadataResourceExternalManagementProperties.ValidationStatus validationStatus = MetadataResourceExternalManagementProperties.ValidationStatus.UNKNOWN; - if (!StringUtils.isEmpty(jCloudConfiguration.getExternalResourceManagementValidationStatusPropertyName())) { + if (StringUtils.hasLength(jCloudConfiguration.getExternalResourceManagementValidationStatusPropertyName())) { String validationStatusPropertyName = jCloudConfiguration.getExternalResourceManagementValidationStatusPropertyName(); String propertyValue = null; if (storageMetadata.getUserMetadata().containsKey(validationStatusPropertyName)) { propertyValue = storageMetadata.getUserMetadata().get(validationStatusPropertyName); } - if (StringUtils.isNotEmpty(propertyValue)) { + if (StringUtils.hasLength(propertyValue)) { validationStatus = MetadataResourceExternalManagementProperties.ValidationStatus.fromValue(Integer.parseInt(propertyValue)); } } MetadataResourceExternalManagementProperties metadataResourceExternalManagementProperties = - getMetadataResourceExternalManagementProperties(context, metadataId, metadataUuid, visibility, resourceId, filename, storageMetadata.getETag(), storageMetadata.getType(), validationStatus); + 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(), storageMetadata.getLastModified(), versionValue, metadataResourceExternalManagementProperties, approved); + settingManager.getNodeURL() + "api/records/", visibility, storageMetadata.getSize(), changedDate, versionValue, metadataResourceExternalManagementProperties, approved); } protected static String getFilename(final String key) { @@ -217,50 +256,70 @@ protected String getKey(final ServiceContext context, String metadataUuid, int m @Override public MetadataResource putResource(final ServiceContext context, final String metadataUuid, final String filename, - final InputStream is, @Nullable final Date changeDate, final MetadataResourceVisibility visibility, Boolean approved) + final InputStream is, @Nullable final Date changeDate, final MetadataResourceVisibility visibility, final Boolean approved) throws Exception { return putResource(context, metadataUuid, filename, is, changeDate, visibility, approved, null); } protected MetadataResource putResource(final ServiceContext context, final String metadataUuid, final String filename, - final InputStream is, @Nullable final Date changeDate, final MetadataResourceVisibility visibility, Boolean approved, Map additionalProperties) + final InputStream is, @Nullable final Date changeDate, final MetadataResourceVisibility visibility, final Boolean approved, + Map additionalProperties) throws Exception { final int metadataId = canEdit(context, metadataUuid, approved); String key = getKey(context, metadataUuid, metadataId, visibility, filename); - Map properties = null; + // Get or create a lock object + Object lock = locks.computeIfAbsent(key, k -> new Object()); - try { - StorageMetadata storageMetadata = jCloudConfiguration.getClient().getBlobStore().blobMetadata(jCloudConfiguration.getContainerName(), key); - if (storageMetadata != null) { - properties = storageMetadata.getUserMetadata(); - } - } catch (ContainerNotFoundException ignored) { - // ignored - } + // Avoid multiple updates on the same file at the same time. otherwise the properties could get messed up. + // Especially the version number. + synchronized (lock) { + try { + Map properties = null; + boolean isNewResource = true; + try { + StorageMetadata storageMetadata = jCloudConfiguration.getClient().getBlobStore().blobMetadata(jCloudConfiguration.getContainerName(), key); + if (storageMetadata != null) { + isNewResource = false; - if (properties == null) { - properties = new HashMap<>(); - } + // Copy existing properties + properties = new HashMap<>(storageMetadata.getUserMetadata()); + } + } catch (ContainerNotFoundException ignored) { + // ignored + } + + if (properties == null) { + properties = new HashMap<>(); + } - addProperties(metadataUuid, properties, changeDate, additionalProperties); + setProperties(properties, metadataUuid, changeDate, additionalProperties); - Blob blob = jCloudConfiguration.getClient().getBlobStore().blobBuilder(key) - .payload(is) - .contentLength(is.available()) - .userMetadata(properties) - .build(); - // Upload the Blob in multiple chunks to supports large files. - jCloudConfiguration.getClient().getBlobStore().putBlob(jCloudConfiguration.getContainerName(), blob, multipart()); - Blob blobResults = jCloudConfiguration.getClient().getBlobStore().getBlob(jCloudConfiguration.getContainerName(), key); + // Update/set version + setPropertiesVersion(context, properties, isNewResource, metadataUuid, metadataId, visibility, approved, filename); + Blob blob = jCloudConfiguration.getClient().getBlobStore().blobBuilder(key) + .payload(is) + .contentLength(is.available()) + .userMetadata(properties) + .build(); - return createResourceDescription(context, metadataUuid, visibility, filename, blobResults.getMetadata(), metadataId, approved); + Log.info(Geonet.RESOURCES, + String.format("Put(2) blob '%s' with version label '%s'.", key, properties.get(jCloudConfiguration.getExternalResourceManagementVersionPropertyName()))); + // Upload the Blob in multiple chunks to supports large files. + jCloudConfiguration.getClient().getBlobStore().putBlob(jCloudConfiguration.getContainerName(), blob, multipart()); + Blob blobResults = jCloudConfiguration.getClient().getBlobStore().getBlob(jCloudConfiguration.getContainerName(), key); + + return createResourceDescription(context, metadataUuid, visibility, filename, blobResults.getMetadata(), metadataId, approved); + } finally { + locks.remove(key); + } + } } - protected void addProperties(String metadataUuid, Map properties, Date changeDate, Map additionalProperties) { + protected void setProperties(Map properties, String metadataUuid, Date changeDate, Map additionalProperties) { // Add additional properties if exists. if (MapUtils.isNotEmpty(additionalProperties)) { @@ -268,42 +327,137 @@ protected void addProperties(String metadataUuid, Map properties } // now update metadata uuid and status and change date . - setMetadataUUID(properties, metadataUuid); - // JCloud does not allow changing the last modified date. So the change date will be put in defined changed date field if supplied. - setExternalResourceManagementChangedDate(properties, changeDate); + // JCloud does not allow changing the last modified date or creation date. So the change date/created date will be put in defined changed date/created date field if supplied. + setExternalResourceManagementDates(properties, changeDate); // If it is a new record so set the default status value property if it does not already exist as an additional property. - if (!StringUtils.isEmpty(jCloudConfiguration.getExternalResourceManagementValidationStatusPropertyName()) && + if (StringUtils.hasLength(jCloudConfiguration.getExternalResourceManagementValidationStatusPropertyName()) && !properties.containsKey(jCloudConfiguration.getExternalResourceManagementValidationStatusPropertyName())) { setExternalManagementResourceValidationStatus(properties, jCloudConfiguration.getValidationStatusDefaultValue()); } - } protected void setMetadataUUID(Map properties, String metadataUuid) { // Don't allow users metadata uuid to be supplied as a property so let's overwrite any value that may exist. - if (!StringUtils.isEmpty(jCloudConfiguration.getMetadataUUIDPropertyName())) { - setProperty(properties, jCloudConfiguration.getMetadataUUIDPropertyName(), metadataUuid); + if (StringUtils.hasLength(jCloudConfiguration.getMetadataUUIDPropertyName())) { + setPropertyValue(properties, jCloudConfiguration.getMetadataUUIDPropertyName(), metadataUuid); } } - protected void setExternalResourceManagementChangedDate(Map properties, Date changeDate) { - // Don't allow change date to be supplied as a property so let's overwrite any value that may exist. - if (changeDate != null && !StringUtils.isEmpty(jCloudConfiguration.getExternalResourceManagementChangedDatePropertyName())) { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - properties.put(jCloudConfiguration.getExternalResourceManagementChangedDatePropertyName(), dateFormat.format(changeDate)); + protected void setExternalResourceManagementDates(Map properties, Date changeDate) { + // If changeDate was not supplied then default to now. + if (changeDate == null) { + changeDate = new Date(); + } + + // JCloud does not allow created date to be set so we may supply the value we want as a property so assign the value. + // Only assign the value if we currently don't have a creation date, and we don't have a version assigned either because if either of these exists then + // it will indicate that this is not the first version. + String createdDatePropertyName = jCloudConfiguration.getExternalResourceManagementCreatedDatePropertyName(); + String versionPropertyName = jCloudConfiguration.getExternalResourceManagementVersionPropertyName(); + if (StringUtils.hasLength(createdDatePropertyName) && + !properties.containsKey(createdDatePropertyName) && + (!StringUtils.hasLength(versionPropertyName) || (!properties.containsKey(versionPropertyName))) + ) { + properties.put(jCloudConfiguration.getExternalResourceManagementCreatedDatePropertyName(), DATE_FORMATTER.format(changeDate)); + } + + // JCloud does not allow last modified date to be changed so we may supply the value we want as a property so let's overwrite any value that may exist. + if (StringUtils.hasLength(jCloudConfiguration.getExternalResourceManagementChangedDatePropertyName())) { + properties.put(jCloudConfiguration.getExternalResourceManagementChangedDatePropertyName(), DATE_FORMATTER.format(changeDate)); } } protected void setExternalManagementResourceValidationStatus(Map properties, MetadataResourceExternalManagementProperties.ValidationStatus status) { - if (!StringUtils.isEmpty(jCloudConfiguration.getExternalResourceManagementValidationStatusPropertyName())) { - setProperty(properties, jCloudConfiguration.getExternalResourceManagementValidationStatusPropertyName(), String.valueOf(status.getValue())); + if (StringUtils.hasLength(jCloudConfiguration.getExternalResourceManagementValidationStatusPropertyName())) { + setPropertyValue(properties, jCloudConfiguration.getExternalResourceManagementValidationStatusPropertyName(), String.valueOf(status.getValue())); } } - protected void setProperty(Map properties, String propertyName, String value) { - if (!StringUtils.isEmpty(propertyName)) { + /** + * Set the new version if this is a new record and if updating then bump the version up by 1. + * @param context need to get metadata if metadata id is a working copy. + * @param properties containing all the properties. The version field should be in the properties map. + * @param isNewResource flag to indicate that this is a new resource of if updating existing resource. + * @param metadataUuid uuid of the related metadata record that contains the resource being versioned. + * @param metadataId id of the related metadata record that contains the resource being versioned. + * @param visibility of the resource being versioned. + * @param approved status of the approved record. + * @param filename or resource of the resource being versioned. + * @throws Exception if there are errors. + */ + protected void setPropertiesVersion(final ServiceContext context, final Map properties, boolean isNewResource, String metadataUuid, int metadataId, + final MetadataResourceVisibility visibility, final Boolean approved, final String filename) throws Exception { + if (StringUtils.hasLength(jCloudConfiguration.getExternalResourceManagementVersionPropertyName())) { + String versionPropertyName = jCloudConfiguration.getExternalResourceManagementVersionPropertyName(); + + final int approvedMetadataId = Boolean.TRUE.equals(approved) ? metadataId : canEdit(context, metadataUuid, true); + // if the current record id equal to the approved record id then it has not been approved and is a draft otherwise we are editing a working copy + final boolean draft = (metadataId == approvedMetadataId); + + String newVersionLabel = null; + if (!isNewResource && !draft && + (jCloudConfiguration.getVersioningStrategy().equals(JCloudConfiguration.VersioningStrategy.DRAFT) || + jCloudConfiguration.getVersioningStrategy().equals(JCloudConfiguration.VersioningStrategy.APPROVED))) { + String approveKey = getKey(context, metadataUuid, approvedMetadataId, visibility, filename); + + try { + StorageMetadata storageMetadata = jCloudConfiguration.getClient().getBlobStore().blobMetadata(jCloudConfiguration.getContainerName(), approveKey); + if (storageMetadata != null) { + if (storageMetadata.getUserMetadata().containsKey(versionPropertyName)) { + newVersionLabel = bumpVersion(storageMetadata.getUserMetadata().get(versionPropertyName)); + } + } + } catch (ContainerNotFoundException ignored) { + // ignored + } + if (newVersionLabel == null) { + newVersionLabel = FIRST_VERSION; + } + } + + if (properties.containsKey(versionPropertyName)) { + if (isNewResource) { + throw new RuntimeException(String.format("Found property '%s' while adding new resource '%s' for metadata %d(%s). This is unexpected.", + versionPropertyName, filename, metadataId, metadataUuid)); + } + if (newVersionLabel == null) { + if (jCloudConfiguration.getVersioningStrategy().equals(JCloudConfiguration.VersioningStrategy.DRAFT) || + jCloudConfiguration.getVersioningStrategy().equals(JCloudConfiguration.VersioningStrategy.ALL)) { + newVersionLabel = bumpVersion(properties.get(versionPropertyName)); + } else { + newVersionLabel = properties.get(versionPropertyName); + } + } + } else { + if (!isNewResource) { + // If the version was not found then it means that it will be starting from version 1 when there could be previous versions. + // This could be a data problem and should be investigated. + Log.error(Geonet.RESOURCES, + String.format("Expecting property '%s' while modifying existing resource '%s' for metadata %d(%s) but the property was not found. Version being set to '%s'", + versionPropertyName, filename, metadataId, metadataUuid, FIRST_VERSION)); + } + newVersionLabel = FIRST_VERSION; + } + + setPropertyValue(properties, versionPropertyName, newVersionLabel); + } + } + + /** + * Bump the version string up one version. + * @param currentVersionLabel to be increased + * @return new version label + */ + protected String bumpVersion(String currentVersionLabel) { + int majorVersion = Integer.parseInt(currentVersionLabel); + majorVersion++; + return String.valueOf(majorVersion); + } + + protected void setPropertyValue(Map properties, String propertyName, String value) { + if (StringUtils.hasLength(propertyName)) { properties.put(propertyName, value); } } @@ -313,7 +467,7 @@ public MetadataResource patchResourceStatus(final ServiceContext context, final int metadataId = canEdit(context, metadataUuid, approved); String sourceKey = null; - StorageMetadata storageMetadata = null; + StorageMetadata storageMetadata; for (MetadataResourceVisibility sourceVisibility : MetadataResourceVisibility.values()) { final String key = getKey(context, metadataUuid, metadataId, sourceVisibility, resourceId); try { @@ -332,12 +486,12 @@ public MetadataResource patchResourceStatus(final ServiceContext context, final } } if (sourceKey != null) { - final String destKey = getKey(context, metadataUuid, metadataId, visibility, resourceId); + final String targetKey = getKey(context, metadataUuid, metadataId, visibility, resourceId); - jCloudConfiguration.getClient().getBlobStore().copyBlob(jCloudConfiguration.getContainerName(), sourceKey, jCloudConfiguration.getContainerName(), destKey, CopyOptions.NONE); + jCloudConfiguration.getClient().getBlobStore().copyBlob(jCloudConfiguration.getContainerName(), sourceKey, jCloudConfiguration.getContainerName(), targetKey, CopyOptions.NONE); jCloudConfiguration.getClient().getBlobStore().removeBlob(jCloudConfiguration.getContainerName(), sourceKey); - Blob blobResults = jCloudConfiguration.getClient().getBlobStore().getBlob(jCloudConfiguration.getContainerName(), destKey); + Blob blobResults = jCloudConfiguration.getClient().getBlobStore().getBlob(jCloudConfiguration.getContainerName(), targetKey); return createResourceDescription(context, metadataUuid, visibility, resourceId, blobResults.getMetadata(), metadataId, approved); } else { @@ -391,6 +545,8 @@ public String delResource(final ServiceContext context, final String metadataUui return String.format("Metadata resource '%s' removed.", resourceId); } } + Log.info(Geonet.RESOURCES, + String.format("Unable to remove resource '%s'.", resourceId)); return String.format("Unable to remove resource '%s'.", resourceId); } @@ -401,6 +557,8 @@ public String delResource(final ServiceContext context, final String metadataUui if (tryDelResource(context, metadataUuid, metadataId, visibility, resourceId)) { return String.format("Metadata resource '%s' removed.", resourceId); } + Log.info(Geonet.RESOURCES, + String.format("Unable to remove resource '%s'.", resourceId)); return String.format("Unable to remove resource '%s'.", resourceId); } @@ -419,6 +577,117 @@ protected boolean tryDelResource(final ServiceContext context, final String meta return false; } + @Override + public void copyResources(ServiceContext context, String sourceUuid, String targetUuid, MetadataResourceVisibility metadataResourceVisibility, boolean sourceApproved, boolean targetApproved) throws Exception { + final int sourceMetadataId = canEdit(context, sourceUuid, metadataResourceVisibility, sourceApproved); + final int targetMetadataId = canEdit(context, targetUuid, metadataResourceVisibility, targetApproved); + final String sourceResourceTypeDir = getMetadataDir(context, sourceMetadataId) + jCloudConfiguration.getFolderDelimiter() + metadataResourceVisibility + jCloudConfiguration.getFolderDelimiter(); + final String targetResourceTypeDir = getMetadataDir(context, targetMetadataId) + jCloudConfiguration.getFolderDelimiter() + metadataResourceVisibility + jCloudConfiguration.getFolderDelimiter(); + + Log.debug(Geonet.RESOURCES, String.format("Copying resources from '%s' (approved=%s) to '%s' (approved=%s)", + sourceResourceTypeDir, sourceApproved, targetResourceTypeDir, targetApproved)); + + String versionPropertyName = null; + if (jCloudConfiguration.isVersioningEnabled()) { + versionPropertyName = jCloudConfiguration.getExternalResourceManagementVersionPropertyName(); + } + + try { + ListContainerOptions opts = new ListContainerOptions(); + opts.prefix(sourceResourceTypeDir).recursive(); + + // Page through the data + String marker = null; + do { + if (marker != null) { + opts.afterMarker(marker); + } + + PageSet page = jCloudConfiguration.getClient().getBlobStore().list(jCloudConfiguration.getContainerName(), opts); + + for (StorageMetadata sourceStorageMetadata : page) { + if (!isFolder(sourceStorageMetadata)) { + String sourceBlobName = sourceStorageMetadata.getName(); + String targetBlobName = targetResourceTypeDir + sourceBlobName.substring(sourceResourceTypeDir.length()); + + Blob sourceBlob = jCloudConfiguration.getClient().getBlobStore().getBlob(jCloudConfiguration.getContainerName(), sourceBlobName); + + // Copy existing properties. + Map targetProperties = new HashMap<>(sourceBlob.getMetadata().getUserMetadata()); + + // Check if target exists. + StorageMetadata targetStorageMetadata = null; + + try { + targetStorageMetadata = jCloudConfiguration.getClient().getBlobStore().blobMetadata(jCloudConfiguration.getContainerName(), targetBlobName); + + } catch (ContainerNotFoundException ignored) { + // ignored + } + + Log.debug(Geonet.RESOURCES, String.format("Copying resource from '%s' to '%s' (new=%s)", sourceBlobName, targetBlobName, targetStorageMetadata==null)); + + if (jCloudConfiguration.isVersioningEnabled() && StringUtils.hasLength(versionPropertyName)) { + if (targetStorageMetadata != null && + targetProperties.containsKey(versionPropertyName) && + targetStorageMetadata.getUserMetadata().containsKey(versionPropertyName) && + !targetProperties.get(versionPropertyName).equals(targetStorageMetadata.getUserMetadata().get(versionPropertyName))) { + + String targetVersionCurrentLabel; + if (jCloudConfiguration.getVersioningStrategy().equals(JCloudConfiguration.VersioningStrategy.DRAFT) || + jCloudConfiguration.getVersioningStrategy().equals(JCloudConfiguration.VersioningStrategy.APPROVED)) { + // If draft or approved, then we only bump the target version up by one version only. + targetVersionCurrentLabel = targetStorageMetadata.getUserMetadata().get(versionPropertyName); + if (StringUtils.hasLength(targetVersionCurrentLabel)) { + targetVersionCurrentLabel = bumpVersion(targetVersionCurrentLabel); + } else { + targetVersionCurrentLabel = FIRST_VERSION; + // Log warning as this could be an issue if the version property is being lost. + Log.warning(Geonet.RESOURCES, String.format("Target version for resource '%s' was empty. Setting version to '%s'", targetBlobName, targetVersionCurrentLabel)); + } + } else { + // If versioning all then we will use the current version. + targetVersionCurrentLabel = targetProperties.get(versionPropertyName); + Log.debug(Geonet.RESOURCES, String.format("Keeping version '%s' for source for resource '%s'", targetVersionCurrentLabel, targetBlobName)); + if (!StringUtils.hasLength(targetVersionCurrentLabel)) { + targetVersionCurrentLabel = FIRST_VERSION; + // Log warning as this could be an issue if the version property is being lost. + Log.warning(Geonet.RESOURCES, String.format("Version resource '%s' was empty. Setting version to '%s'", targetBlobName, targetVersionCurrentLabel)); + } + } + targetProperties.put(versionPropertyName, targetVersionCurrentLabel); + } else if (targetApproved && (targetStorageMetadata == null || !targetStorageMetadata.getUserMetadata().containsKey(versionPropertyName))) { + // If the targetApproved is true then it is a new draft so if target resource did not exist + // then this will be added as a first version item. Otherwise, we keep the version unchanged from the approved copy. + targetProperties.put(versionPropertyName, FIRST_VERSION); + } + + // If version is still not set then lets set it. + if (!targetProperties.containsKey(versionPropertyName) || !StringUtils.hasLength(targetProperties.get(versionPropertyName))) { + targetProperties.put(versionPropertyName, FIRST_VERSION); + // There seems to have been an issue detecting the version so log a warning + Log.warning(Geonet.RESOURCES, String.format("Version was not set for resource '%s'. Setting version to '%s'", targetBlobName, + targetProperties.get(versionPropertyName))); + } + } + Blob targetblob = jCloudConfiguration.getClient().getBlobStore().blobBuilder(targetBlobName) + .payload(sourceBlob.getPayload()) + .contentLength(sourceBlob.getMetadata().getContentMetadata().getContentLength()) + .userMetadata(targetProperties) + .build(); + + // Upload the Blob in multiple chunks to supports large files. + jCloudConfiguration.getClient().getBlobStore().putBlob(jCloudConfiguration.getContainerName(), targetblob, multipart()); + } + } + marker = page.getNextMarker(); + } while (marker != null); + } catch (ContainerNotFoundException e) { + Log.warning(Geonet.RESOURCES, + String.format("Unable to located metadata '%s' directory to be copied.", sourceMetadataId)); + } + } + @Override public MetadataResource getResourceDescription(final ServiceContext context, final String metadataUuid, final MetadataResourceVisibility visibility, final String filename, Boolean approved) throws Exception { @@ -441,13 +710,6 @@ public MetadataResource getResourceDescription(final ServiceContext context, fin public MetadataResourceContainer getResourceContainerDescription(final ServiceContext context, final String metadataUuid, Boolean approved) throws Exception { int metadataId = getAndCheckMetadataId(metadataUuid, approved); - final String key = getMetadataDir(context, metadataId); - - - String folderRoot = jCloudConfiguration.getExternalResourceManagementFolderRoot(); - if (folderRoot == null) { - folderRoot = ""; - } MetadataResourceExternalManagementProperties metadataResourceExternalManagementProperties = getMetadataResourceExternalManagementProperties(context, metadataId, metadataUuid, null, String.valueOf(metadataId), null, null, StorageType.FOLDER, MetadataResourceExternalManagementProperties.ValidationStatus.UNKNOWN); @@ -468,7 +730,7 @@ private String getMetadataDir(ServiceContext context, final int metadataId) { } String key; - // For windows it may be "\" in which case we need to change it to folderDelimiter which is normally "/" + // For windows, it may be "\" in which case we need to change it to folderDelimiter which is normally "/" if (metadataDir.getFileSystem().getSeparator().equals(jCloudConfiguration.getFolderDelimiter())) { key = metadataDir.toString(); } else { @@ -490,23 +752,23 @@ private String getMetadataDir(ServiceContext context, final int metadataId) { protected Path getBaseMetadataDir(ServiceContext context, Path metadataFullDir) { //If we not already figured out the base metadata dir then lets figure it out. - if (baseMetadataDir == null) { + if (this.baseMetadataDir == null) { Path systemFullDir = getDataDirectory(context).getSystemDataDir(); // If the metadata full dir is relative from the system dir then use system dir as the base dir. if (metadataFullDir.toString().startsWith(systemFullDir.toString())) { - baseMetadataDir = systemFullDir; + this.baseMetadataDir = systemFullDir; } else { // If the metadata full dir is an absolute folder then use that as the base dir. if (getDataDirectory(context).getMetadataDataDir().isAbsolute()) { - baseMetadataDir = metadataFullDir.getRoot(); + this.baseMetadataDir = metadataFullDir.getRoot(); } else { // use it as a relative url. - baseMetadataDir = Paths.get("."); + this.baseMetadataDir = Paths.get("."); } } } - return baseMetadataDir; + return this.baseMetadataDir; } private GeonetworkDataDirectory getDataDirectory(ServiceContext context) { @@ -549,7 +811,7 @@ private MetadataResourceExternalManagementProperties getMetadataResourceExternal String metadataResourceExternalManagementPropertiesUrl = jCloudConfiguration.getExternalResourceManagementUrl(); String objectId = getResourceManagementExternalPropertiesObjectId((type == null ? "document" : (StorageType.FOLDER.equals(type) ? "folder" : "document")), visibility, metadataId, version, resourceId); - if (!StringUtils.isEmpty(metadataResourceExternalManagementPropertiesUrl)) { + 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 @@ -611,10 +873,7 @@ private MetadataResourceExternalManagementProperties getMetadataResourceExternal } } - MetadataResourceExternalManagementProperties metadataResourceExternalManagementProperties - = new MetadataResourceExternalManagementProperties(objectId, metadataResourceExternalManagementPropertiesUrl, validationStatus); - - return metadataResourceExternalManagementProperties; + return new MetadataResourceExternalManagementProperties(objectId, metadataResourceExternalManagementPropertiesUrl, validationStatus); } public ResourceManagementExternalProperties getResourceManagementExternalProperties() { @@ -622,7 +881,7 @@ public ResourceManagementExternalProperties getResourceManagementExternalPropert @Override public boolean isEnabled() { // Return true if we have an external management url - return !StringUtils.isEmpty(jCloudConfiguration.getExternalResourceManagementUrl()); + return StringUtils.hasLength(jCloudConfiguration.getExternalResourceManagementUrl()); } @Override @@ -658,7 +917,7 @@ protected static class ResourceHolderImpl implements ResourceHolder { public ResourceHolderImpl(final Blob object, MetadataResource metadataResource) throws IOException { // Preserve filename by putting the files into a temporary folder and using the same filename. - tempFolderPath = Files.createTempDirectory("gn-meta-res-" + String.valueOf(metadataResource.getMetadataId() + "-")); + tempFolderPath = Files.createTempDirectory("gn-meta-res-" + metadataResource.getMetadataId() + "-"); tempFolderPath.toFile().deleteOnExit(); path = tempFolderPath.resolve(getFilename(object.getMetadata().getName())); this.metadataResource = metadataResource; @@ -681,14 +940,8 @@ public MetadataResource getMetadata() { public void close() throws IOException { // Delete temporary file and folder. IO.deleteFileOrDirectory(tempFolderPath, true); - path=null; + path = null; tempFolderPath = null; } - - @Override - protected void finalize() throws Throwable { - close(); - super.finalize(); - } } } 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 5971855c8a2..236ef078375 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 @@ -68,6 +68,11 @@ public class JCloudConfiguration { */ private String externalResourceManagementChangedDatePropertyName; + /** + * Property name for storing the creation date of the record. + */ + private String externalResourceManagementCreatedDatePropertyName; + /** * Property name for validation status that is expected to be an integer with values of null, 0, 1, 2 * (See MetadataResourceExternalManagementProperties.ValidationStatus for code meaning) @@ -86,7 +91,39 @@ public class JCloudConfiguration { * Enable option to add versioning in the link to the resource. */ private Boolean versioningEnabled; + /** + * Property name for storing the version information JCloud does not support versioning. + */ + private String externalResourceManagementVersionPropertyName; + /** + * Property to identify the version strategy to be used. + */ + public enum VersioningStrategy { + /** + * Each new resource change should generate a new version + * i.e. All new uploads will increase the version including draft and working copy. + * For workflow, this could cause confusion on working copies which would increase the version in the working copy + * but when merged only the last version would be merged and could make it look like there are missing versions. + */ + ALL, + /** + * Each new resource change should generate a new version, But working copies will only increase by one version. + * This will avoid working copy version increases more than one to avoid the issues from ALL (lost versions on merge) + * This option may be preferred to ALL when workflow is enabled. + */ + DRAFT, + /** + * Add a new version each time a metadata is approved. + * i.e. draft will remain as version 1 until approved and working copy will only increase by 1 which is what would be used once approved. + */ + APPROVED + } + + /** + * Version strategy to use when generating new versions + */ + private VersioningStrategy versioningStrategy = VersioningStrategy.ALL; public void setProvider(String provider) { this.provider = provider; @@ -225,6 +262,24 @@ public void setVersioningEnabled(String versioningEnabled) { this.versioningEnabled = BooleanUtils.toBooleanObject(versioningEnabled); } + public String getExternalResourceManagementVersionPropertyName() { + return externalResourceManagementVersionPropertyName; + } + + public void setExternalResourceManagementVersionPropertyName(String externalResourceManagementVersionPropertyName) { + this.externalResourceManagementVersionPropertyName = externalResourceManagementVersionPropertyName; + } + + public VersioningStrategy getVersioningStrategy() { + return versioningStrategy; + } + + public void setVersioningStrategy(String versioningStrategy) { + if (StringUtils.hasLength(versioningStrategy)) { + this.versioningStrategy = VersioningStrategy.valueOf(versioningStrategy); + } + } + public String getMetadataUUIDPropertyName() { return metadataUUIDPropertyName; } @@ -240,6 +295,15 @@ public String getExternalResourceManagementChangedDatePropertyName() { public void setExternalResourceManagementChangedDatePropertyName(String externalResourceManagementChangedDatePropertyName) { this.externalResourceManagementChangedDatePropertyName = externalResourceManagementChangedDatePropertyName; } + + public String getExternalResourceManagementCreatedDatePropertyName() { + return externalResourceManagementCreatedDatePropertyName; + } + + public void setExternalResourceManagementCreatedDatePropertyName(String externalResourceManagementCreatedDatePropertyName) { + this.externalResourceManagementCreatedDatePropertyName = externalResourceManagementCreatedDatePropertyName; + } + public String getExternalResourceManagementValidationStatusPropertyName() { return externalResourceManagementValidationStatusPropertyName; } @@ -311,7 +375,9 @@ private void validateMetadataPropertyNames() throws IllegalArgumentException { String[] names = { getMetadataUUIDPropertyName(), getExternalResourceManagementChangedDatePropertyName(), - getExternalResourceManagementValidationStatusPropertyName() + getExternalResourceManagementValidationStatusPropertyName(), + getExternalResourceManagementCreatedDatePropertyName(), + getExternalResourceManagementVersionPropertyName() }; JCloudMetadataNameValidator.validateMetadataNamesForProvider(provider, names); 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 4a2bafe5b34..bbb966a00af 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 @@ -16,7 +16,10 @@ jcloud.external.resource.management.validation.status.property.name=${JCLOUD_EXT jcloud.external.resource.management.validation.status.default.value=${JCLOUD_EXTERNAL_RESOURCE_MANAGEMENT_VALIDATION_STATUS_DEFAULT_VALUE:#{null}} jcloud.external.resource.management.changed.date.property.name=${JCLOUD_EXTERNAL_RESOURCE_MANAGEMENT_CHANGE_DATE_PROPERTY_NAME:#{null}} +jcloud.external.resource.management.created.date.property.name=${JCLOUD_EXTERNAL_RESOURCE_MANAGEMENT_CREATED_DATE_PROPERTY_NAME:#{null}} jcloud.versioning.enabled=${JCLOUD_VERSIONING_ENABLED:#{null}} +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}} 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 e6a43b63a16..427437dd29e 100644 --- a/datastorages/jcloud/src/main/resources/config-store/config-jcloud.xml +++ b/datastorages/jcloud/src/main/resources/config-store/config-jcloud.xml @@ -53,8 +53,12 @@ + + + + diff --git a/datastorages/s3/src/main/java/org/fao/geonet/api/records/attachments/S3Store.java b/datastorages/s3/src/main/java/org/fao/geonet/api/records/attachments/S3Store.java index 061f566aab3..2114f8f5d20 100644 --- a/datastorages/s3/src/main/java/org/fao/geonet/api/records/attachments/S3Store.java +++ b/datastorages/s3/src/main/java/org/fao/geonet/api/records/attachments/S3Store.java @@ -295,11 +295,5 @@ public void close() throws IOException { path = null; } } - - @Override - protected void finalize() throws Throwable { - close(); - super.finalize(); - } } } diff --git a/docs/manual/docs/administrator-guide/managing-users-and-groups/creating-user.md b/docs/manual/docs/administrator-guide/managing-users-and-groups/creating-user.md index 240fac6944b..e1d35ba75eb 100644 --- a/docs/manual/docs/administrator-guide/managing-users-and-groups/creating-user.md +++ b/docs/manual/docs/administrator-guide/managing-users-and-groups/creating-user.md @@ -3,8 +3,11 @@ To add a new user to the GeoNetwork system, please do the following: 1. Select the *Administration* button in the menu. On the Administration page, select *User management*. -2. Click the button *Add a new user*; -3. Provide the *information* required for the new user; -4. Assign the correct *profile* (see [Users, Groups and Roles](index.md#user_profiles)); -5. Assign the user to a *group* (see [Creating group](creating-group.md)); +2. Click the button *Add a new user*. +3. Provide the *information* required for the new user. +4. Assign the correct *profile* (see [Users, Groups and Roles](index.md#user_profiles)). +5. Assign the user to a *group* (see [Creating group](creating-group.md)). 6. Click *Save*. + +!!! note + Usernames are not case sensitive. The application does not allow to create different users with the same username in different cases. diff --git a/docs/manual/docs/administrator-guide/managing-users-and-groups/img/password-forgot.png b/docs/manual/docs/administrator-guide/managing-users-and-groups/img/password-forgot.png index d1bc512667d..bdccc9830b2 100644 Binary files a/docs/manual/docs/administrator-guide/managing-users-and-groups/img/password-forgot.png and b/docs/manual/docs/administrator-guide/managing-users-and-groups/img/password-forgot.png differ diff --git a/docs/manual/docs/administrator-guide/managing-users-and-groups/img/selfregistration-start.png b/docs/manual/docs/administrator-guide/managing-users-and-groups/img/selfregistration-start.png index 7e9a6f8084f..1c617a5d007 100644 Binary files a/docs/manual/docs/administrator-guide/managing-users-and-groups/img/selfregistration-start.png and b/docs/manual/docs/administrator-guide/managing-users-and-groups/img/selfregistration-start.png differ diff --git a/docs/manual/docs/administrator-guide/managing-users-and-groups/index.md b/docs/manual/docs/administrator-guide/managing-users-and-groups/index.md index aa0408ce3f4..c35bb17f71b 100644 --- a/docs/manual/docs/administrator-guide/managing-users-and-groups/index.md +++ b/docs/manual/docs/administrator-guide/managing-users-and-groups/index.md @@ -3,6 +3,7 @@ - [Creating group](creating-group.md) - [Creating user](creating-user.md) - [User Self-Registration](user-self-registration.md) +- [User reset password](user-reset-password.md) - [Authentication mode](authentication-mode.md) ## Default user {#user-defaults} diff --git a/docs/manual/docs/administrator-guide/managing-users-and-groups/user-reset-password.md b/docs/manual/docs/administrator-guide/managing-users-and-groups/user-reset-password.md new file mode 100644 index 00000000000..2eb887c85d5 --- /dev/null +++ b/docs/manual/docs/administrator-guide/managing-users-and-groups/user-reset-password.md @@ -0,0 +1,36 @@ +# User 'Forgot your password?' function {#user_forgot_password} + +!!! note + This function requires an email server configured. See [System configuration](../configuring-the-catalog/system-configuration.md#system-config-feedback). + +This function allows users who have forgotten their password to request a new one. Go to the sign in page to access the form: + +![](img/password-forgot.png) + +If a user takes this option they will receive an email inviting them to change their password as follows: + + You have requested to change your Greenhouse GeoNetwork Site password. + + You can change your password using the following link: + + http://localhost:8080/geonetwork/srv/en/password.change.form?username=dubya.shrub@greenhouse.gov&changeKey=635d6c84ddda782a9b6ca9dda0f568b011bb7733 + + This link is valid for today only. + + Greenhouse GeoNetwork Site + +The catalog has generated a changeKey from the forgotten password and the current date and emailed that to the user as part of a link to a change password form. + +If you want to change the content of this email, you should modify `xslt/service/account/password-forgotten-email.xsl`. + +When the user clicks on the link, a change password form is displayed in their browser and a new password can be entered. When that form is submitted, the changeKey is regenerated and checked with the changeKey supplied in the link, if they match then the password is changed to the new password supplied by the user. + +The final step in this process is a verification email sent to the email address of the user confirming that a change of password has taken place: + + Your Greenhouse GeoNetwork Site password has been changed. + + If you did not change this password contact the Greenhouse GeoNetwork Site helpdesk + + The Greenhouse GeoNetwork Site team + +If you want to change the content of this email, you should modify `xslt/service/account/password-changed-email.xsl`. diff --git a/docs/manual/docs/administrator-guide/managing-users-and-groups/user-self-registration.md b/docs/manual/docs/administrator-guide/managing-users-and-groups/user-self-registration.md index fe3cb2d0142..aa7fdbb254b 100644 --- a/docs/manual/docs/administrator-guide/managing-users-and-groups/user-self-registration.md +++ b/docs/manual/docs/administrator-guide/managing-users-and-groups/user-self-registration.md @@ -1,5 +1,9 @@ # User Self-Registration {#user_self_registration} +!!! note + This function requires an email server configured. See [System configuration](../configuring-the-catalog/system-configuration.md#system-config-feedback). + + To enable the self-registration functions, see [System configuration](../configuring-the-catalog/system-configuration.md). When self-registration is enabled, for users that are not logged in, an additional link is shown on the login page: ![](img/selfregistration-start.png) @@ -15,8 +19,8 @@ The fields in this form are self-explanatory except for the following: - the user will still be given the `Registered User` profile - an email will be sent to the Email address nominated in the Feedback section of the 'System Administration' menu, informing them of the request for a more privileged profile - **Requested group**: By default, self-registered users are not assigned to any group. If a group is selected: - - the user will still not be assigned to any group - - an email will be sent to the Email address nominated in the Feedback section of the 'System Administration' menu, informing them of the requested group. + - the user will still not be assigned to any group + - an email will be sent to the Email address nominated in the Feedback section of the 'System Administration' menu, informing them of the requested group. ## What happens when a user self-registers? @@ -72,39 +76,3 @@ If you want to change the content of this email, you should modify `xslt/service The Greenhouse GeoNetwork Site If you want to change the content of this email, you should modify `xslt/service/account/registration-prof-email.xsl`. - -## The 'Forgot your password?' function - -This function allows users who have forgotten their password to request a new one. Go to the sign in page to access the form: - -![](img/password-forgot.png) - -For security reasons, only users that have the `Registered User` profile can request a new password. - -If a user takes this option they will receive an email inviting them to change their password as follows: - - You have requested to change your Greenhouse GeoNetwork Site password. - - You can change your password using the following link: - - http://localhost:8080/geonetwork/srv/en/password.change.form?username=dubya.shrub@greenhouse.gov&changeKey=635d6c84ddda782a9b6ca9dda0f568b011bb7733 - - This link is valid for today only. - - Greenhouse GeoNetwork Site - -The catalog has generated a changeKey from the forgotten password and the current date and emailed that to the user as part of a link to a change password form. - -If you want to change the content of this email, you should modify `xslt/service/account/password-forgotten-email.xsl`. - -When the user clicks on the link, a change password form is displayed in their browser and a new password can be entered. When that form is submitted, the changeKey is regenerated and checked with the changeKey supplied in the link, if they match then the password is changed to the new password supplied by the user. - -The final step in this process is a verification email sent to the email address of the user confirming that a change of password has taken place: - - Your Greenhouse GeoNetwork Site password has been changed. - - If you did not change this password contact the Greenhouse GeoNetwork Site helpdesk - - The Greenhouse GeoNetwork Site team - -If you want to change the content of this email, you should modify `xslt/service/account/password-changed-email.xsl`. diff --git a/docs/manual/docs/overview/authors.md b/docs/manual/docs/overview/authors.md index 302e339d85e..106fba51a02 100644 --- a/docs/manual/docs/overview/authors.md +++ b/docs/manual/docs/overview/authors.md @@ -9,7 +9,6 @@ In brief the committee votes on proposals on the geonetwork-dev mailinglist. Pro ### Members of the Project Steering Committee - Jeroen Ticheler (jeroen ticheler * geocat net) [GeoCat](https://www.geocat.net) - Chair -- Francois Prunayre [Titellus](https://titellus.net) - Simon Pigot [CSIRO](https://www.csiro.au) - Florent Gravin [CamptoCamp](https://camptocamp.com) - Jose Garcia [GeoCat](https://www.geocat.net) @@ -20,6 +19,7 @@ In brief the committee votes on proposals on the geonetwork-dev mailinglist. Pro - Jo Cook [Astun Technology](https://www.astuntechnology.com) - Patrizia Monteduro (Patrizia Monteduro * fao org) [FAO-UN](https://www.fao.org) - Emanuele Tajariol (e tajariol * mclink it - GeoSolutions) +- Francois Prunayre - Jesse Eichar - Andrea Carboni (acarboni * crisalis-tech com - Independent consultant) - Archie Warnock (warnock * awcubed com) [A/WWW Enterprises](https://www.awcubed.com) diff --git a/docs/manual/mkdocs.yml b/docs/manual/mkdocs.yml index f25d6a2ca22..cc3c1920116 100644 --- a/docs/manual/mkdocs.yml +++ b/docs/manual/mkdocs.yml @@ -323,6 +323,7 @@ nav: - administrator-guide/managing-users-and-groups/creating-group.md - administrator-guide/managing-users-and-groups/creating-user.md - administrator-guide/managing-users-and-groups/user-self-registration.md + - administrator-guide/managing-users-and-groups/user-reset-password.md - 'Classification Systems': - administrator-guide/managing-classification-systems/index.md - administrator-guide/managing-classification-systems/managing-categories.md diff --git a/domain/src/main/java/org/fao/geonet/repository/UserRepository.java b/domain/src/main/java/org/fao/geonet/repository/UserRepository.java index feaf720afb6..b5ac5138653 100644 --- a/domain/src/main/java/org/fao/geonet/repository/UserRepository.java +++ b/domain/src/main/java/org/fao/geonet/repository/UserRepository.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2016 Food and Agriculture Organization of the + * Copyright (C) 2001-2024 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -45,6 +45,10 @@ public interface UserRepository extends GeonetRepository, JpaSpec /** * Find all users identified by the provided username ignoring the case. + * + * Old versions allowed to create users with the same username with different case. + * New versions do not allow this. + * * @param username the username. * @return all users with username equals ignore case the provided username. */ diff --git a/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustom.java b/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustom.java index 65e3162a22e..21148980e14 100644 --- a/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustom.java +++ b/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustom.java @@ -61,7 +61,7 @@ public interface UserRepositoryCustom { */ @Nonnull List> findAllByGroupOwnerNameAndProfile(@Nonnull Collection metadataIds, - @Nullable Profile profil); + @Nullable Profile profile); /** * Find all the users that own at least one metadata element. diff --git a/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustomImpl.java b/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustomImpl.java index e5f1efa1166..4585548d9fe 100644 --- a/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustomImpl.java +++ b/domain/src/main/java/org/fao/geonet/repository/UserRepositoryCustomImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2016 Food and Agriculture Organization of the + * Copyright (C) 2001-2024 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -25,7 +25,6 @@ import org.fao.geonet.domain.*; import org.fao.geonet.utils.Log; -import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; import javax.annotation.Nonnull; @@ -48,66 +47,83 @@ public class UserRepositoryCustomImpl implements UserRepositoryCustom { @PersistenceContext - private EntityManager _entityManager; + private EntityManager entityManager; @Override public User findOne(final String userId) { - return _entityManager.find(User.class, Integer.valueOf(userId)); + return entityManager.find(User.class, Integer.valueOf(userId)); } @Override - public User findOneByEmail(final String email) { - CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); + public User findOneByEmail(@Nonnull final String email) { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(User.class); Root root = query.from(User.class); + Join joinedEmailAddresses = root.join(User_.emailAddresses); - query.where(cb.isMember(email, root.get(User_.emailAddresses))); - final List resultList = _entityManager.createQuery(query).getResultList(); + // Case in-sensitive email search + query.where(cb.equal(cb.lower(joinedEmailAddresses), email.toLowerCase())); + query.orderBy(cb.asc(root.get(User_.username))); + final List resultList = entityManager.createQuery(query).getResultList(); if (resultList.isEmpty()) { return null; } if (resultList.size() > 1) { - Log.error(Constants.DOMAIN_LOG_MODULE, "The database is inconsistent. There are multiple users with the email address: " + - email); + Log.error(Constants.DOMAIN_LOG_MODULE, String.format("The database is inconsistent. There are multiple users with the email address: %s", + email)); } return resultList.get(0); } @Override - public User findOneByEmailAndSecurityAuthTypeIsNullOrEmpty(final String email) { - CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); + public User findOneByEmailAndSecurityAuthTypeIsNullOrEmpty(@Nonnull final String email) { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(User.class); Root root = query.from(User.class); + Join joinedEmailAddresses = root.join(User_.emailAddresses); final Path authTypePath = root.get(User_.security).get(UserSecurity_.authType); query.where(cb.and( - cb.isMember(email, root.get(User_.emailAddresses)), - cb.or(cb.isNull(authTypePath), cb.equal(cb.trim(authTypePath), "")))); - List results = _entityManager.createQuery(query).getResultList(); + // Case in-sensitive email search + cb.equal(cb.lower(joinedEmailAddresses), email.toLowerCase()), + cb.or(cb.isNull(authTypePath), cb.equal(cb.trim(authTypePath), ""))) + ).orderBy(cb.asc(root.get(User_.username))); + List results = entityManager.createQuery(query).getResultList(); if (results.isEmpty()) { return null; } else { + if (results.size() > 1) { + Log.error(Constants.DOMAIN_LOG_MODULE, String.format("The database is inconsistent. There are multiple users with the email address: %s", + email)); + } return results.get(0); } } @Override - public User findOneByUsernameAndSecurityAuthTypeIsNullOrEmpty(final String username) { - CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); + public User findOneByUsernameAndSecurityAuthTypeIsNullOrEmpty(@Nonnull final String username) { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(User.class); Root root = query.from(User.class); final Path authTypePath = root.get(User_.security).get(UserSecurity_.authType); final Path usernamePath = root.get(User_.username); - query.where(cb.and(cb.equal(usernamePath, username), cb.or(cb.isNull(authTypePath), cb.equal(cb.trim(authTypePath), "")))); - List results = _entityManager.createQuery(query).getResultList(); - + // Case in-sensitive username search + query.where(cb.and( + cb.equal(cb.lower(usernamePath), username.toLowerCase()), + cb.or(cb.isNull(authTypePath), cb.equal(cb.trim(authTypePath), ""))) + ).orderBy(cb.asc(root.get(User_.username))); + List results = entityManager.createQuery(query).getResultList(); if (results.isEmpty()) { return null; } else { + if (results.size() > 1) { + Log.error(Constants.DOMAIN_LOG_MODULE, String.format("The database is inconsistent. There are multiple users with username: %s", + username)); + } return results.get(0); } } @@ -115,7 +131,7 @@ public User findOneByUsernameAndSecurityAuthTypeIsNullOrEmpty(final String usern @Nonnull @Override public List findDuplicatedUsernamesCaseInsensitive() { - CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(String.class); Root userRoot = query.from(User.class); @@ -123,14 +139,14 @@ public List findDuplicatedUsernamesCaseInsensitive() { query.groupBy(cb.lower(userRoot.get(User_.username))); query.having(cb.gt(cb.count(userRoot), 1)); - return _entityManager.createQuery(query).getResultList(); + return entityManager.createQuery(query).getResultList(); } @Override @Nonnull public List> findAllByGroupOwnerNameAndProfile(@Nonnull final Collection metadataIds, @Nullable final Profile profile) { - List> results = new ArrayList>(); + List> results = new ArrayList<>(); results.addAll(findAllByGroupOwnerNameAndProfileInternal(metadataIds, profile, false)); results.addAll(findAllByGroupOwnerNameAndProfileInternal(metadataIds, profile, true)); @@ -139,8 +155,8 @@ public List> findAllByGroupOwnerNameAndProfile(@Nonnull fina } private List> findAllByGroupOwnerNameAndProfileInternal(@Nonnull final Collection metadataIds, - @Nullable final Profile profile, boolean draft) { - CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); + @Nullable final Profile profile, boolean draft) { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(Tuple.class); Root userRoot = query.from(User.class); @@ -148,22 +164,20 @@ private List> findAllByGroupOwnerNameAndProfileInternal(@Non Predicate metadataPredicate; Predicate ownerPredicate; - Root metadataRoot = null; - Root metadataDraftRoot = null; if (!draft) { - metadataRoot = query.from(Metadata.class); + Root metadataRoot = query.from(Metadata.class); query.multiselect(metadataRoot.get(Metadata_.id), userRoot); metadataPredicate = metadataRoot.get(Metadata_.id).in(metadataIds); ownerPredicate = cb.equal(metadataRoot.get(Metadata_.sourceInfo).get(MetadataSourceInfo_.groupOwner), userGroupRoot.get(UserGroup_.id).get(UserGroupId_.groupId)); } else { - metadataDraftRoot = query.from(MetadataDraft.class); - query.multiselect(metadataDraftRoot.get(MetadataDraft_.id), userRoot); - metadataPredicate = metadataDraftRoot.get(Metadata_.id).in(metadataIds); + Root metadataRoot = query.from(MetadataDraft.class); + query.multiselect(metadataRoot.get(MetadataDraft_.id), userRoot); + metadataPredicate = metadataRoot.get(MetadataDraft_.id).in(metadataIds); - ownerPredicate = cb.equal(metadataDraftRoot.get(Metadata_.sourceInfo).get(MetadataSourceInfo_.groupOwner), + ownerPredicate = cb.equal(metadataRoot.get(MetadataDraft_.sourceInfo).get(MetadataSourceInfo_.groupOwner), userGroupRoot.get(UserGroup_.id).get(UserGroupId_.groupId)); } @@ -180,9 +194,9 @@ private List> findAllByGroupOwnerNameAndProfileInternal(@Non query.distinct(true); - List> results = new ArrayList>(); + List> results = new ArrayList<>(); - for (Tuple result : _entityManager.createQuery(query).getResultList()) { + for (Tuple result : entityManager.createQuery(query).getResultList()) { Integer mdId = (Integer) result.get(0); User user = (User) result.get(1); results.add(Pair.read(mdId, user)); @@ -193,7 +207,7 @@ private List> findAllByGroupOwnerNameAndProfileInternal(@Non @Nonnull @Override public List findAllUsersThatOwnMetadata() { - final CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); final CriteriaQuery query = cb.createQuery(User.class); final Root metadataRoot = query.from(Metadata.class); @@ -206,13 +220,13 @@ public List findAllUsersThatOwnMetadata() { query.where(ownerExpression); query.distinct(true); - return _entityManager.createQuery(query).getResultList(); + return entityManager.createQuery(query).getResultList(); } @Nonnull @Override public List findAllUsersInUserGroups(@Nonnull final Specification userGroupSpec) { - final CriteriaBuilder cb = _entityManager.getCriteriaBuilder(); + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); final CriteriaQuery query = cb.createQuery(User.class); final Root userGroupRoot = query.from(UserGroup.class); @@ -225,7 +239,7 @@ public List findAllUsersInUserGroups(@Nonnull final Specification> found = _userRepo.findAllByGroupOwnerNameAndProfile(Arrays.asList(md1.getId()), null); - Collections.sort(found, Comparator.comparing(s -> s.two().getName())); + List> found = userRepo.findAllByGroupOwnerNameAndProfile(List.of(md1.getId()), null); + found.sort(Comparator.comparing(s -> s.two().getName())); assertEquals(2, found.size()); assertEquals(md1.getId(), found.get(0).one().intValue()); @@ -203,9 +248,9 @@ public void testFindAllByGroupOwnerNameAndProfile() { assertEquals(editUser, found.get(0).two()); assertEquals(reviewerUser, found.get(1).two()); - found = _userRepo.findAllByGroupOwnerNameAndProfile(Arrays.asList(md1.getId()), null); + found = userRepo.findAllByGroupOwnerNameAndProfile(List.of(md1.getId()), null); // Sort by user name descending - Collections.sort(found, Comparator.comparing(s -> s.two().getName(), Comparator.reverseOrder())); + found.sort(Comparator.comparing(s -> s.two().getName(), Comparator.reverseOrder())); assertEquals(2, found.size()); assertEquals(md1.getId(), found.get(0).one().intValue()); @@ -214,13 +259,13 @@ public void testFindAllByGroupOwnerNameAndProfile() { assertEquals(reviewerUser, found.get(0).two()); - found = _userRepo.findAllByGroupOwnerNameAndProfile(Arrays.asList(md1.getId(), md2.getId()), null); + found = userRepo.findAllByGroupOwnerNameAndProfile(Arrays.asList(md1.getId(), md2.getId()), null); assertEquals(4, found.size()); int md1Found = 0; int md2Found = 0; - for (Pair record : found) { - if (record.one() == md1.getId()) { + for (Pair info : found) { + if (info.one() == md1.getId()) { md1Found++; } else { md2Found++; @@ -232,21 +277,21 @@ public void testFindAllByGroupOwnerNameAndProfile() { @Test public void testFindAllUsersInUserGroups() { - Group group1 = _groupRepo.save(GroupRepositoryTest.newGroup(_inc)); - Group group2 = _groupRepo.save(GroupRepositoryTest.newGroup(_inc)); + Group group1 = groupRepo.save(GroupRepositoryTest.newGroup(_inc)); + Group group2 = groupRepo.save(GroupRepositoryTest.newGroup(_inc)); - User editUser = _userRepo.save(newUser().setProfile(Profile.Editor)); - User reviewerUser = _userRepo.save(newUser().setProfile(Profile.Reviewer)); - User registeredUser = _userRepo.save(newUser().setProfile(Profile.RegisteredUser)); - _userRepo.save(newUser().setProfile(Profile.Administrator)); + User editUser = userRepo.save(newUser().setProfile(Profile.Editor)); + User reviewerUser = userRepo.save(newUser().setProfile(Profile.Reviewer)); + User registeredUser = userRepo.save(newUser().setProfile(Profile.RegisteredUser)); + userRepo.save(newUser().setProfile(Profile.Administrator)); - _userGroupRepository.save(new UserGroup().setGroup(group1).setUser(editUser).setProfile(Profile.Editor)); - _userGroupRepository.save(new UserGroup().setGroup(group2).setUser(registeredUser).setProfile(Profile.RegisteredUser)); - _userGroupRepository.save(new UserGroup().setGroup(group2).setUser(reviewerUser).setProfile(Profile.Editor)); - _userGroupRepository.save(new UserGroup().setGroup(group1).setUser(reviewerUser).setProfile(Profile.Reviewer)); + userGroupRepository.save(new UserGroup().setGroup(group1).setUser(editUser).setProfile(Profile.Editor)); + userGroupRepository.save(new UserGroup().setGroup(group2).setUser(registeredUser).setProfile(Profile.RegisteredUser)); + userGroupRepository.save(new UserGroup().setGroup(group2).setUser(reviewerUser).setProfile(Profile.Editor)); + userGroupRepository.save(new UserGroup().setGroup(group1).setUser(reviewerUser).setProfile(Profile.Reviewer)); - List found = Lists.transform(_userRepo.findAllUsersInUserGroups(UserGroupSpecs.hasGroupId(group1.getId())), - new Function() { + List found = Lists.transform(userRepo.findAllUsersInUserGroups(UserGroupSpecs.hasGroupId(group1.getId())), + new Function<>() { @Nullable @Override @@ -259,7 +304,7 @@ public Integer apply(@Nullable User input) { assertTrue(found.contains(editUser.getId())); assertTrue(found.contains(reviewerUser.getId())); - found = Lists.transform(_userRepo.findAllUsersInUserGroups(Specification.not(UserGroupSpecs.hasProfile(Profile.RegisteredUser) + found = Lists.transform(userRepo.findAllUsersInUserGroups(Specification.not(UserGroupSpecs.hasProfile(Profile.RegisteredUser) )), new Function() { @Nullable @@ -278,21 +323,20 @@ public Integer apply(@Nullable User input) { @Test public void testFindAllUsersThatOwnMetadata() { - - User editUser = _userRepo.save(newUser().setProfile(Profile.Editor)); - User reviewerUser = _userRepo.save(newUser().setProfile(Profile.Reviewer)); - _userRepo.save(newUser().setProfile(Profile.RegisteredUser)); - _userRepo.save(newUser().setProfile(Profile.Administrator)); + User editUser = userRepo.save(newUser().setProfile(Profile.Editor)); + User reviewerUser = userRepo.save(newUser().setProfile(Profile.Reviewer)); + userRepo.save(newUser().setProfile(Profile.RegisteredUser)); + userRepo.save(newUser().setProfile(Profile.Administrator)); Metadata md1 = MetadataRepositoryTest.newMetadata(_inc); md1.getSourceInfo().setOwner(editUser.getId()); - _metadataRepo.save(md1); + metadataRepo.save(md1); Metadata md2 = MetadataRepositoryTest.newMetadata(_inc); md2.getSourceInfo().setOwner(reviewerUser.getId()); - _metadataRepo.save(md2); + metadataRepo.save(md2); - List found = _userRepo.findAllUsersThatOwnMetadata(); + List found = userRepo.findAllUsersThatOwnMetadata(); assertEquals(2, found.size()); boolean editUserFound = false; @@ -318,20 +362,18 @@ public void testFindDuplicatedUsernamesCaseInsensitive() { User userNonDuplicated1 = newUser(); usernameDuplicated1.setUsername("userNamE1"); usernameDuplicated2.setUsername("usERNAME1"); - _userRepo.save(usernameDuplicated1); - _userRepo.save(usernameDuplicated2); - _userRepo.save(userNonDuplicated1); + userRepo.save(usernameDuplicated1); + userRepo.save(usernameDuplicated2); + userRepo.save(userNonDuplicated1); - List duplicatedUsernames = _userRepo.findDuplicatedUsernamesCaseInsensitive(); - assertThat("Duplicated usernames don't match the expected ones", + List duplicatedUsernames = userRepo.findDuplicatedUsernamesCaseInsensitive(); + MatcherAssert.assertThat("Duplicated usernames don't match the expected ones", duplicatedUsernames, CoreMatchers.is(Lists.newArrayList("username1"))); assertEquals(1, duplicatedUsernames.size()); } private User newUser() { - User user = newUser(_inc); - return user; + return newUser(_inc); } - } diff --git a/schemas/iso19139/src/main/plugin/iso19139/loc/por/codelists.xml b/schemas/iso19139/src/main/plugin/iso19139/loc/por/codelists.xml index dac2b2c7bb4..843364122ba 100644 --- a/schemas/iso19139/src/main/plugin/iso19139/loc/por/codelists.xml +++ b/schemas/iso19139/src/main/plugin/iso19139/loc/por/codelists.xml @@ -1,4 +1,4 @@ - + publication - + A data identifica quando o recurso foi emitido revision - + A data identifica quando o recurso foi examinado ou re-examinado e melhorado ou retificado @@ -51,20 +51,20 @@ download - Instruções online para transferir dados duma memória ou sistema a uma outra + Instruções online para transferir dados duma memória ou sistema a uma outra information - - Informação online sobre o recurso + + Informação online sobre o recurso offlineAccess - Instruções online para solicitar o recurso do provider + Instruções online para solicitar o recurso do provider @@ -76,7 +76,7 @@ search - Interface online de pesquisa para encontrar informação sobre o recurso + Interface online de pesquisa para encontrar informação sobre o recurso @@ -86,41 +86,41 @@ documentDigital - Visualização digital de um item principalmente escrito (também pode conter - ilustrações) + Visualização digital de um item principalmente escrito (também pode conter + ilustrações) imageDigital - Imagem de características, objectos e actividades naturais ou artificiais, + Imagem de características, objectos e actividades naturais ou artificiais, adquirido - pelo sensoriamento de espectro eletromagnético (tanto o segmento visível como os outros + pelo sensoriamento de espectro eletromagnético (tanto o segmento visível como os outros segmentos) - por sensores, tais como de infravermelho, e radar de alta resolução e afinal gravado num + por sensores, tais como de infravermelho, e radar de alta resolução e afinal gravado num formato digital documentHardcopy - - Visualização dum item principalmente escrito (também possa conter ilustrações) - em papel, material fotográfico ou outras mídias + + Visualização dum item principalmente escrito (também possa conter ilustrações) + em papel, material fotográfico ou outras mídias imageHardcopy - - Imagem de características, objectos e actividades naturais ou artificiais, + + Imagem de características, objectos e actividades naturais ou artificiais, adquirido - pelo sensoriamento de espectro eletromagnético (tanto o segmento visível como os outros + pelo sensoriamento de espectro eletromagnético (tanto o segmento visível como os outros segmentos) - por sensores, tais como de infravermelho, e radar de alta resolução e afinal reproduzido em + por sensores, tais como de infravermelho, e radar de alta resolução e afinal reproduzido em papel, - material fotográfico ou outras mídias para o uso imediato pelo utilizador humano + material fotográfico ou outras mídias para o uso imediato pelo utilizador humano @@ -132,8 +132,8 @@ mapHardcopy - - Mapa impresso em papel, material fotográfico ou outras mídias para + + Mapa impresso em papel, material fotográfico ou outras mídias para o uso imediato pelo utilizador humano @@ -141,54 +141,54 @@ modelDigital - Representação digital e multidimensional duma característica, dum processo, + Representação digital e multidimensional duma característica, dum processo, etc. modelHardcopy - - Modelo físico de três dimensões + + Modelo físico de três dimensões profileDigital - Secção plana em forma digital + Secção plana em forma digital profileHardcopy - - Secção plana impressa em papel, etc. + + Secção plana impressa em papel, etc. tableDigital - Apresentação sistematica (em colunas) de factos ou números em forma digital + Apresentação sistematica (em colunas) de factos ou números em forma digital tableHardcopy - - Apresentação sistematica (em colunas) de factos ou números - impressos em papel, material fotográfico ou outras mídias + + Apresentação sistematica (em colunas) de factos ou números + impressos em papel, material fotográfico ou outras mídias videoDigital - - Gravação de vídeo digital + + Gravação de vídeo digital videoHardcopy - - Gravação de vídeo em filme + + Gravação de vídeo em filme @@ -202,9 +202,9 @@ custodian - - Parte que assume a prestação de contas e a responsabilidade dos dados - e assegura o cuidado adequado, assim como a manutenção do recurso + + Parte que assume a prestação de contas e a responsabilidade dos dados + e assegura o cuidado adequado, assim como a manutenção do recurso @@ -235,7 +235,7 @@ pointOfContact - Parte que pode ser contactado para obter conhecimento sobre (a aquisição) de + Parte que pode ser contactado para obter conhecimento sobre (a aquisição) de recurso @@ -243,7 +243,7 @@ principalInvestigator - Parte-chave que é responsável para colher informação e dirigir a pesquisa + Parte-chave que é responsável para colher informação e dirigir a pesquisa @@ -262,7 +262,7 @@ author - Parte que é o autor do recurso + Parte que é o autor do recurso @@ -270,28 +270,28 @@ directInternal - - Método de avaliar a qualidade dum conjunto de dados, baseado em inspecções de + + Método de avaliar a qualidade dum conjunto de dados, baseado em inspecções de pormenores dentro - do conjunto de dados, de modo que não é necessário considerar dados de fora do conjunto + do conjunto de dados, de modo que não é necessário considerar dados de fora do conjunto avaliado directExternal - - Método de avaliar a qualidade dum conjunto de dados, baseado em inspecções de + + Método de avaliar a qualidade dum conjunto de dados, baseado em inspecções de pormenores dentro - do conjunto de dados, aqueles são avaliado através uma comparação com dados de referência + do conjunto de dados, aqueles são avaliado através uma comparação com dados de referência externos indirect - - Método de avaliação da qualidade dum conjunto de dados, baseado em conhecimento + + Método de avaliação da qualidade dum conjunto de dados, baseado em conhecimento externo @@ -301,26 +301,26 @@ crossReference - - Referência dum conjunto de dados a um outro + + Referência dum conjunto de dados a um outro largerWorkCitation - - Referência a um conjunto de dados principal, naquele este faz parte + + Referência a um conjunto de dados principal, naquele este faz parte partOfSeamlessDatabase - + Parte do mesmo banco de dados estruturado, mantido num computador source - Informacões cartográficas e tabelares de quais o conteúdo de conjunto de dados + Informacões cartográficas e tabelares de quais o conteúdo de conjunto de dados origina-se @@ -339,39 +339,39 @@ campaign - Série de acções organizadas e planejadas + Série de acções organizadas e planejadas collection - - Acumulação de conjuntos de dados, compostos para um propósito específico + + Acumulação de conjuntos de dados, compostos para um propósito específico exercise - - Performance específica duma função ou dum grupo de funções + + Performance específica duma função ou dum grupo de funções experiment - Processo desenhado para descobrir se alguma coisa é efectiva ou válida + Processo desenhado para descobrir se alguma coisa é efectiva ou válida investigation - - Pesquisa ou inquérito sistemático + + Pesquisa ou inquérito sistemático mission - - Operação específica dum sistema de colecção dos dados + + Operação específica dum sistema de colecção dos dados @@ -382,49 +382,49 @@ operation - - Acção que faz parte numa série das acções + + Acção que faz parte numa série das acções platform - Veículo ou outra base de apoio que dispõe dum sensor + Veículo ou outra base de apoio que dispõe dum sensor process - Método de fazer alguma coisa envolvendo uma série de passos + Método de fazer alguma coisa envolvendo uma série de passos program - Actividade específica e planejada + Actividade específica e planejada project - Empreendimento, investigação ou desenvolvimento organizado/a + Empreendimento, investigação ou desenvolvimento organizado/a study - Examinação ou investigação + Examinação ou investigação task - Peça de trabalho + Peça de trabalho trial - + Processo de teste para descobrir ou demonstrar alguma coisa @@ -434,13 +434,13 @@ point - Cada célula representa um ponto + Cada célula representa um ponto area - - Cada célula representa uma área + + Cada célula representa uma área @@ -465,21 +465,21 @@ utf7 - 7-bit UCS Transfer Format de tamanho variável, baseado em ISO/IEC 10646 + 7-bit UCS Transfer Format de tamanho variável, baseado em ISO/IEC 10646 utf8 - 8-bit UCS Transfer Format de tamanho variável, baseado em ISO/IEC 10646 + 8-bit UCS Transfer Format de tamanho variável, baseado em ISO/IEC 10646 utf16 - 16-bit UCS Transfer Format de tamanho variável, baseado em ISO/IEC 10646 + 16-bit UCS Transfer Format de tamanho variável, baseado em ISO/IEC 10646 @@ -658,34 +658,34 @@ unclassified - - Disponível para a divulgação geral + + Disponível para a divulgação geral restricted - Não para a divulgação geral + Não para a divulgação geral confidential - Disponível para alguém, a quem informação pode ser confiada + Disponível para alguém, a quem informação pode ser confiada secret Mantido ou destinado a ser mantido privado, desconhecido ou escondido de todos, - além dum grupo de pessoas seleccionadas + além dum grupo de pessoas seleccionadas topSecret - De altíssimo sigilo + De altíssimo sigilo @@ -694,23 +694,23 @@ image - Representação númerica elucidativa dum parâmetro físico, - que não é o valor actual do parâmetro físico + Representação númerica elucidativa dum parâmetro físico, + que não é o valor actual do parâmetro físico thematicClassification - - Valor de código sem significado quantitativo, usado para representar uma - quantidade física + + Valor de código sem significado quantitativo, usado para representar uma + quantidade física physicalMeasurement - - Valor em unidades físicas da quantidade medida + + Valor em unidades físicas da quantidade medida @@ -720,84 +720,84 @@ class Descriptor dum conjunto de objctivos que partilham os mesmos - atributos, operações, métodos, relações e comportamento + atributos, operações, métodos, relações e comportamento codelist - + Descriptor dum conjunto de objctivos que partilham os mesmos - atributos, operações, métodos, relações e comportamento + atributos, operações, métodos, relações e comportamento enumeration - - Tipo de dados cujos casos formam uma lista de valores nomeados e literais, não - extensíveis + + Tipo de dados cujos casos formam uma lista de valores nomeados e literais, não + extensíveis codelistElement - - Valor permissível para uma lista de códigos ou uma numeração + + Valor permissível para uma lista de códigos ou uma numeração abstractClass - Classe que não pode ser instanciado directamente + Classe que não pode ser instanciado directamente aggregateClass - Classe composta por classes, a quais é ligado por uma relação de agregação + Classe composta por classes, a quais é ligado por uma relação de agregação specifiedClass - + Subclasse que possa substituir a sua superclasse datatypeClass - Classe com poucas ou sem operações, cujas função principal é manter o estado + Classe com poucas ou sem operações, cujas função principal é manter o estado abstracto - duma outra classe, para transmissão, armazenamento, codificação ou armazenamento persistente + duma outra classe, para transmissão, armazenamento, codificação ou armazenamento persistente interfaceClass - Conjunto nomeado de operações que caracterizam o comportamento dum elemento + Conjunto nomeado de operações que caracterizam o comportamento dum elemento unionClass - - Classe que descreve uma selecção de um dos tipos específicados + + Classe que descreve uma selecção de um dos tipos específicados metaClass - Classe cujas casos são classes + Classe cujas casos são classes typeClass - Classe usada para a específicação dum âmbito de casos (objectos), é aplicável aos - objectos em conjunto com as operações. Um tipo possa ter atributos e associacões + Classe usada para a específicação dum âmbito de casos (objectos), é aplicável aos + objectos em conjunto com as operações. Um tipo possa ter atributos e associacões @@ -809,14 +809,14 @@ integer - - Campo númerico + + Campo númerico association - - Relação semântica entre duas classes, aquela envolve ligações entre seus casos + + Relação semântica entre duas classes, aquela envolve ligações entre seus casos @@ -843,14 +843,14 @@ track - - Ao longo da direcção de movimento do ponto de scan + + Ao longo da direcção de movimento do ponto de scan crossTrack - - Perpendicular ao direcção de movimento do ponto de scan + + Perpendicular ao direcção de movimento do ponto de scan @@ -861,14 +861,14 @@ sample - + Elemento ao longo duma linha de scan time - Duração + Duração @@ -877,47 +877,47 @@ complex - Conjunto de 'primitivos geométricos' (geometric primitives) tal que os limites + Conjunto de 'primitivos geométricos' (geometric primitives) tal que os limites deles podem ser - representado como uma união de outros 'primitivos' + representado como uma união de outros 'primitivos' composite - - Conjunto conectado de curvas, sólidos ou superfícies + + Conjunto conectado de curvas, sólidos ou superfícies curve - 'Primitivo geométrico' limitado e monodimensional, a representar a imagem - contínua duma linha + 'Primitivo geométrico' limitado e monodimensional, a representar a imagem + contínua duma linha point - 'Primitivo geométrico' sem dimensão, a representar uma posição, sem ter uma - expansão + 'Primitivo geométrico' sem dimensão, a representar uma posição, sem ter uma + expansão solid - - 'Primitivo geométrico' limitado, conectado e tridimensional, - a representar a imagem contínua duma região de espaço + + 'Primitivo geométrico' limitado, conectado e tridimensional, + a representar a imagem contínua duma região de espaço surface - - 'Primitivo geométrico' limitado, conectado e bidimensional, - a representar a imagem contínua duma região de plano + + 'Primitivo geométrico' limitado, conectado e bidimensional, + a representar a imagem contínua duma região de plano @@ -927,19 +927,19 @@ blurredImage - Porcão da foto é desfocada + Porcão da foto é desfocada cloud - A foto é parcialmente coberta por nuvens + A foto é parcialmente coberta por nuvens degradingObliquity - Ângulo agudo entre o plano da eclíptica (plano da órbita da terra) e o plano do + Ângulo agudo entre o plano da eclíptica (plano da órbita da terra) e o plano do equador celeste @@ -947,13 +947,13 @@ fog - A foto é parcialmente coberta por nevoeiro + A foto é parcialmente coberta por nevoeiro heavySmokeOrDust - - A foto é parcialmente coberta por fumaça ou pó + + A foto é parcialmente coberta por fumaça ou pó @@ -977,20 +977,20 @@ shadow - A foto é parcialmente coberta por sombra + A foto é parcialmente coberta por sombra snow - A foto é parcialmente coberta por neve + A foto é parcialmente coberta por neve terrainMasking - A ausência de dados coligidos dum ponto ou duma área dado, causado pela - localização relativa de características topográficas, quais obstruem + A ausência de dados coligidos dum ponto ou duma área dado, causado pela + localização relativa de características topográficas, quais obstruem o atalho de recolha entre o(s) colector(es) e o(s) sujeito(s) de interesse @@ -1001,27 +1001,27 @@ discipline - Palavra-chave identifica um ramo de instrução ou aprendizagem especilizada + Palavra-chave identifica um ramo de instrução ou aprendizagem especilizada place - Palavra-chave identifica uma localização + Palavra-chave identifica uma localização stratum - Palavra-chave identifica a(s) camada(s) de qualquer substância depositada + Palavra-chave identifica a(s) camada(s) de qualquer substância depositada temporal - Palavra-chave identifica um período relacionado ao conjunto de dados + Palavra-chave identifica um período relacionado ao conjunto de dados @@ -1036,74 +1036,74 @@ continual - - Os dados são actualizados repetidamente e frequentemente + + Os dados são actualizados repetidamente e frequentemente daily - - Os dados são actualizados todos os dias + + Os dados são actualizados todos os dias weekly - Os dados são actualizados uma vez por semana + Os dados são actualizados uma vez por semana fortnightly - Os dados são actualizados de duas em duas semanas + Os dados são actualizados de duas em duas semanas monthly - Os dados são actualizados uma vez por mês + Os dados são actualizados uma vez por mês quarterly - Os dados são actualizados de três em três meses + Os dados são actualizados de três em três meses biannually - Os dados são actualizados duas vezes por ano + Os dados são actualizados duas vezes por ano annually - Os dados são actualizados uma vez por ano + Os dados são actualizados uma vez por ano asNeeded - - Os dados são actualizados quando é considerado como necessário + + Os dados são actualizados quando é considerado como necessário irregular - Os dados são actualizados em intervalos alternantes + Os dados são actualizados em intervalos alternantes notPlanned - - Não há planos para actualizar os dados + + Não há planos para actualizar os dados unknown - A frequência da manutenção de dados é desconhecida + A frequência da manutenção de dados é desconhecida @@ -1235,25 +1235,25 @@ onLine - Ligação directa de computador + Ligação directa de computador satellite - - Ligação por uma sistema de comunicação de satélite + + Ligação por uma sistema de comunicação de satélite telephoneLink - Comunicação por uma rede de telefone + Comunicação por uma rede de telefone hardcopy - - Brochura ou folheto contém informação descritiva + + Brochura ou folheto contém informação descritiva @@ -1261,20 +1261,20 @@ mandatory - - O elemento é sempre necessário + + O elemento é sempre necessário optional - O elemento não é obrigatório + O elemento não é obrigatório conditional - O elemento necessário quando uma condição específica ocorrer + O elemento necessário quando uma condição específica ocorrer @@ -1291,32 +1291,32 @@ lowerLeft - O canto do pixel mais perto da origem de SRS; se há dois na - mesma distância da origem, seria aquele com o valor menor de x + O canto do pixel mais perto da origem de SRS; se há dois na + mesma distância da origem, seria aquele com o valor menor de x lowerRight - O canto a seguir àquele inferior esquerdo no sentido contrário dos ponteiros do - relógio + O canto a seguir àquele inferior esquerdo no sentido contrário dos ponteiros do + relógio upperRight - O canto a seguir àquele inferior direito no sentido contrário dos ponteiros do - relógio + O canto a seguir àquele inferior direito no sentido contrário dos ponteiros do + relógio upperLeft - O canto a seguir àquele superior direito no sentido contrário dos ponteiros do - relógio + O canto a seguir àquele superior direito no sentido contrário dos ponteiros do + relógio @@ -1325,46 +1325,46 @@ completed - - Produção dos dados concluída + + Produção dos dados concluída historicalArchive - Os dados foram gravados numa instalação de armazenamento offline + Os dados foram gravados numa instalação de armazenamento offline obsolete - Os dados não são mais relevantes + Os dados não são mais relevantes onGoing - - Os dados são actualizados continuamente + + Os dados são actualizados continuamente planned - Data fixa estabelecida, a partir da qual, os dados foram ou serão criados ou + Data fixa estabelecida, a partir da qual, os dados foram ou serão criados ou actualizados required - + Os dados precisam de ser gerado ou actualizado underDevelopment - Os dados estão no processo de criação + Os dados estão no processo de criação @@ -1372,11 +1372,11 @@ copyright - - Direitos em exlusivo à publicação, produção ou venda dos direitos para uma - obra literária, dramática, musical ou artística, ou para o uso numa impressão comercial ou - num rótulo, - são concedido pela lei a um autor, compositor, artista ou distribuidor durante um período + + Direitos em exlusivo à publicação, produção ou venda dos direitos para uma + obra literária, dramática, musical ou artística, ou para o uso numa impressão comercial ou + num rótulo, + são concedido pela lei a um autor, compositor, artista ou distribuidor durante um período especificado @@ -1385,49 +1385,49 @@ patent O governo concedeu o direito exclusivo para fazer, vender, usar - ou dar licenças (d)uma invenção ou descoberta + ou dar licenças (d)uma invenção ou descoberta patentPending - Informação produzida ou vendida a espera duma patente + Informação produzida ou vendida a espera duma patente trademark - Um nome, símbolo ou uma outra marca que identifica um produto; registado + Um nome, símbolo ou uma outra marca que identifica um produto; registado oficialmente - e restrito legalmente ao uso pelo proprietário ou fabricante + e restrito legalmente ao uso pelo proprietário ou fabricante license - - Permissão formal para fazer alguma coisa + + Permissão formal para fazer alguma coisa intellectualPropertyRights - Direitos de controlar a distribuição e lucrar financeiramente de - propriedade não-palpável que é um resultado de criatividade + Direitos de controlar a distribuição e lucrar financeiramente de + propriedade não-palpável que é um resultado de criatividade restricted - Retido da circulação ou divulgação geral + Retido da circulação ou divulgação geral otherRestrictions - - Limitação não listado + + Limitação não listado @@ -1436,85 +1436,85 @@ attribute - Informação aplica-se à classe de atributos + Informação aplica-se à classe de atributos attributeType - Informação aplica-se à particularidade duma característica + Informação aplica-se à particularidade duma característica collectionHardware - - Informação aplica-se à classe de hardware de colecção + + Informação aplica-se à classe de hardware de colecção collectionSession - - Informação aplica-se à sessão de colecçõ + + Informação aplica-se à sessão de colecçõ dataset - Informação aplica-se ao conjunto de dados + Informação aplica-se ao conjunto de dados series - - Informação aplica-se à série + + Informação aplica-se à série nonGeographicDataset - - Informação aplica-se a dados não-geográficos + + Informação aplica-se a dados não-geográficos dimensionGroup - - Informação aplica-se a um grupo de dimensão + + Informação aplica-se a um grupo de dimensão feature - - Informação aplica-se a uma característica + + Informação aplica-se a uma característica featureType - - Informação aplica-se a feature type + + Informação aplica-se a feature type propertyType - Informação aplica-se a um tipo de propriedade + Informação aplica-se a um tipo de propriedade fieldSession - - Informação aplica-se a uma sessão de campo + + Informação aplica-se a uma sessão de campo software - Informação aplica-se a uma programa ou rotina de computador + Informação aplica-se a uma programa ou rotina de computador service - - Informação aplica-se a uma capacidade, qual um provedor de acesso à Internet + + Informação aplica-se a uma capacidade, qual um provedor de acesso à Internet disponibiliza a um utilizador por um conjunto de interfaces, que define um comportamento, tal como um caso de uso @@ -1524,14 +1524,14 @@ model - Informação aplica-se a uma cópia ou imitaçõ dum objecto existente ou hipotético + Informação aplica-se a uma cópia ou imitaçõ dum objecto existente ou hipotético tile - Informação aplica-se a um 'tile', um subconjunto espacial de dados geográficos + Informação aplica-se a um 'tile', um subconjunto espacial de dados geográficos @@ -1558,19 +1558,19 @@ vector - Informação vectorial é usada para representar informação geográfica + Informação vectorial é usada para representar informação geográfica grid - Informação matricial é usada para representar informação geográfica + Informação matricial é usada para representar informação geográfica textTable - Dados textuais ou tabelares são usados para representar informação geográfica + Dados textuais ou tabelares são usados para representar informação geográfica @@ -1582,16 +1582,16 @@ stereoModel - - Vista tridimensional, que é formada pelos reios homólogos intersectandos + + Vista tridimensional, que é formada pelos reios homólogos intersectandos dum par de imagens sobrepostos video - - Cena duma gravação de vídeo + + Cena duma gravação de vídeo @@ -1599,9 +1599,9 @@ farming - - Criação de animais e/ou cultivo de espécies vegetais. Exemplos: agricultura, - irrigação, aquacultura, plantações, pecuária, pestes e doenças que afectam as colheitas e o + + Criação de animais e/ou cultivo de espécies vegetais. Exemplos: agricultura, + irrigação, aquacultura, plantações, pecuária, pestes e doenças que afectam as colheitas e o gado @@ -1609,39 +1609,39 @@ biota - Fauna e flora em habitat natural. Exemplos: vida selvagem, vegetação, ciências - biológicas, ecologia, desertos, vida marinha, zonas húmidas, habitat + Fauna e flora em habitat natural. Exemplos: vida selvagem, vegetação, ciências + biológicas, ecologia, desertos, vida marinha, zonas húmidas, habitat boundaries - Limites legais do território. Exemplos: fronteiras administrativas e políticas + Limites legais do território. Exemplos: fronteiras administrativas e políticas climatologyMeteorologyAtmosphere - Processos e fenômenos atmosféricos. Exemplos: nebulosidade, estado do tempo, - clima, condições atmosféricas, alterações climáticas, precipitação + Processos e fenômenos atmosféricos. Exemplos: nebulosidade, estado do tempo, + clima, condições atmosféricas, alterações climáticas, precipitação economy - Actividades econômicas e emprego. Exemplos: produção, emprego, rendimentos, - comércio, indústria, turismo e eco-turismo, florestas, pescas, caça para fins comerciais ou - de subsistência, exploração e extracção de recursos minerais, petróleo e gás + Actividades econômicas e emprego. Exemplos: produção, emprego, rendimentos, + comércio, indústria, turismo e eco-turismo, florestas, pescas, caça para fins comerciais ou + de subsistência, exploração e extracção de recursos minerais, petróleo e gás elevation - Elevação abaixo ou acima do nível do mar. Exemplos: altitude, batimetria, modelos + Elevação abaixo ou acima do nível do mar. Exemplos: altitude, batimetria, modelos digitais do terreno, declives e produtos derivados @@ -1649,114 +1649,114 @@ environment - Recursos ambientais, protecção e conservação da natureza. Exemplos: poluição, - armazenamento e tratamento de resíduos, avaliação de impactos ambientais, monitoramento do + Recursos ambientais, protecção e conservação da natureza. Exemplos: poluição, + armazenamento e tratamento de resíduos, avaliação de impactos ambientais, monitoramento do risco ambiental, reservas naturais, paisagem geoscientificInformation - - Informação relativa às ciências da terra. Exemplos: aspectos e processos - geofísicos, geologia, minerais, sismicidade, actividade vulcânica, derrocadas, informação - gravimétrica, solos, permafrost, hidrogeologia e erosão + + Informação relativa às ciências da terra. Exemplos: aspectos e processos + geofísicos, geologia, minerais, sismicidade, actividade vulcânica, derrocadas, informação + gravimétrica, solos, permafrost, hidrogeologia e erosão health - - Saúde, serviços de saúde, ecologia humana e segurança. Exemplos: doenças, - factores condicionantes da saúde, higiene, abuso de substâncias, saúde física e mental, - serviços de saúde + + Saúde, serviços de saúde, ecologia humana e segurança. Exemplos: doenças, + factores condicionantes da saúde, higiene, abuso de substâncias, saúde física e mental, + serviços de saúde imageryBaseMapsEarthCover - - cartografia de base. Exemplos: mapas topográficos, imagens de satélite, - coberturas aero-fotográficas + + cartografia de base. Exemplos: mapas topográficos, imagens de satélite, + coberturas aero-fotográficas intelligenceMilitary - + Bases, estruturas e actividades militares. Exemplos: campos de treino, - transportes militares, quartéis, casernas + transportes militares, quartéis, casernas inlandWaters - - Entidades relativas a águas interiores, sistemas de drenagem e suas - características. Exemplos: rios, glaciares, lagos salgados, planos de gestão da água, - diques, correntes, cheias, qualidade da água, aspectos hidrográficos + + Entidades relativas a águas interiores, sistemas de drenagem e suas + características. Exemplos: rios, glaciares, lagos salgados, planos de gestão da água, + diques, correntes, cheias, qualidade da água, aspectos hidrográficos location - - Informação e serviços de localização. Exemplos: moradas, redes geodésicas, pontos - de controlo, zonas postais e serviços, designações de lugares + + Informação e serviços de localização. Exemplos: moradas, redes geodésicas, pontos + de controlo, zonas postais e serviços, designações de lugares oceans - Entidades e características dos corpos de água salgada (excluindo águas - interiores). Exemplos: marés, ondulação e vagas, informação costeira, recifes e baixios + Entidades e características dos corpos de água salgada (excluindo águas + interiores). Exemplos: marés, ondulação e vagas, informação costeira, recifes e baixios planningCadastre - informação destinada ao planejamento do uso do território. Exemplos: mapas de uso - do solo, mapas de zonamento, levantamentos cadastrais, registo predial e rústico + informação destinada ao planejamento do uso do território. Exemplos: mapas de uso + do solo, mapas de zonamento, levantamentos cadastrais, registo predial e rústico society - Características sociais e culturais. Exemplos: residências e estabelecimentos, - antropologia, arqueologia, educação, crenças tradicionais, hábitos e costumes, dados - demográficos, áreas e actividades recreacionais, avaliação de impactos sociais, crime e - justiça, informação dos censos + Características sociais e culturais. Exemplos: residências e estabelecimentos, + antropologia, arqueologia, educação, crenças tradicionais, hábitos e costumes, dados + demográficos, áreas e actividades recreacionais, avaliação de impactos sociais, crime e + justiça, informação dos censos structure - - Construção desenvolvida pelo homem. Exemplos: edifícios, museus, igrejas, - fábricas, habitação, monumentos, lojas + + Construção desenvolvida pelo homem. Exemplos: edifícios, museus, igrejas, + fábricas, habitação, monumentos, lojas transportation - Meios e formas de deslocação de pessoas e/ou mercadorias. Exemplos: estradas, - aeroportos, rotas de navegação, túneis, cartas náuticas e aeronáuticas, localização de + Meios e formas de deslocação de pessoas e/ou mercadorias. Exemplos: estradas, + aeroportos, rotas de navegação, túneis, cartas náuticas e aeronáuticas, localização de frotas de transporte, caminhos de ferro utilitiesCommunication - - Energia, sistemas de água e esgoto e infra-estrutura e serviços de comunicação. - Exemplos: hidroeléctricidade, fontes da energia geotérmica, solar e nuclear, purificação - e distribuição da água, colecção e disposição final de esgotos, distribuição de + + Energia, sistemas de água e esgoto e infra-estrutura e serviços de comunicação. + Exemplos: hidroeléctricidade, fontes da energia geotérmica, solar e nuclear, purificação + e distribuição da água, colecção e disposição final de esgotos, distribuição de electricidade - e gás, tráfego e transmissão de dados, telecomunicação, rádio, redes de comunicação + e gás, tráfego e transmissão de dados, telecomunicação, rádio, redes de comunicação @@ -1774,66 +1774,66 @@ topology1D - Complexo topológico monodimensional - comummente chamado topologia de + Complexo topológico monodimensional - comummente chamado topologia de 'chain-node' planarGraph - - Complexo topológico monodimensional que é plano. (Um gráfico plano pode ser + + Complexo topológico monodimensional que é plano. (Um gráfico plano pode ser desenhado - numa planície numa maneira que duas arestas não se cruzam, excepto num vértice.) + numa planície numa maneira que duas arestas não se cruzam, excepto num vértice.) fullPlanarGraph - - Complexo topológico bidimensional que é plano. (Um complexo topológico + + Complexo topológico bidimensional que é plano. (Um complexo topológico bidimensional - é comummente chamado 'topologia completa' num ambiente cartográfico bidimensional.) + é comummente chamado 'topologia completa' num ambiente cartográfico bidimensional.) surfaceGraph - - Complexo topológico monodimensional que é isomórfico a um subconjunto duma - superfície. - (Um complexo geométrico é isomórfico a um complexo topológico se os elementos deles seriam + + Complexo topológico monodimensional que é isomórfico a um subconjunto duma + superfície. + (Um complexo geométrico é isomórfico a um complexo topológico se os elementos deles seriam uns aos - outros numa correspondência um-para-um e conversando dimensões e fronteiras.) + outros numa correspondência um-para-um e conversando dimensões e fronteiras.) fullSurfaceGraph - - Complexo topológico bidimensional que é isomórfico a um subconjunto duma - superfície. + + Complexo topológico bidimensional que é isomórfico a um subconjunto duma + superfície. topology3D - Complexo topológico tridimensional. (Um complexo topológico é uma colecção de - 'primitivos topológicos' que são fechados em baixa de operacões de fronteira.) + Complexo topológico tridimensional. (Um complexo topológico é uma colecção de + 'primitivos topológicos' que são fechados em baixa de operacões de fronteira.) fullTopology3D - Cobertura completa dum espaço coordenado euclidiano tridimensional + Cobertura completa dum espaço coordenado euclidiano tridimensional abstract - Complexo topológico sem qualquer realização geométrica específicada + Complexo topológico sem qualquer realização geométrica específicada @@ -1842,85 +1842,85 @@ attribute - Informação aplica-se à classe de atributos + Informação aplica-se à classe de atributos attributeType - Informação aplica-se à particularidade duma característica + Informação aplica-se à particularidade duma característica collectionHardware - - Informação aplica-se à classe de hardware de colecção + + Informação aplica-se à classe de hardware de colecção collectionSession - - Informação aplica-se à sessão de colecçõ + + Informação aplica-se à sessão de colecçõ dataset - Informação aplica-se ao conjunto de dados + Informação aplica-se ao conjunto de dados series - - Informação aplica-se à série + + Informação aplica-se à série nonGeographicDataset - - Informação aplica-se a dados não-geográficos + + Informação aplica-se a dados não-geográficos dimensionGroup - - Informação aplica-se a um grupo de dimensão + + Informação aplica-se a um grupo de dimensão feature - - Informação aplica-se a uma característica + + Informação aplica-se a uma característica featureType - - Informação aplica-se a feature type + + Informação aplica-se a feature type propertyType - Informação aplica-se a um tipo de propriedade + Informação aplica-se a um tipo de propriedade fieldSession - - Informação aplica-se a uma sessão de campo + + Informação aplica-se a uma sessão de campo software - Informação aplica-se a uma programa ou rotina de computador + Informação aplica-se a uma programa ou rotina de computador service - - Informação aplica-se a uma capacidade, que um provedor de acesso à Internet + + Informação aplica-se a uma capacidade, que um provedor de acesso à Internet disponibiliza a um utilizador por um conjunto de interfaces, qual define um comportamento, tal como um caso de uso @@ -1930,21 +1930,21 @@ model - Informação aplica-se a uma cópia ou imitaçõ dum objecto existente ou hipotético + Informação aplica-se a uma cópia ou imitaçõ dum objecto existente ou hipotético tile - Informação aplica-se a um 'tile', um subconjunto espacial de dados geográficos + Informação aplica-se a um 'tile', um subconjunto espacial de dados geográficos initiative - A entidade referenciando aplica-se a um agregado de transferência que foi + A entidade referenciando aplica-se a um agregado de transferência que foi identificado originalmente como uma iniciativa (DS_Initiative) @@ -1952,7 +1952,7 @@ stereomate - A entidade referenciando aplica-se a um agregado de transferência que foi + A entidade referenciando aplica-se a um agregado de transferência que foi identificado originalmente como um 'stereo mate' (DS_StereoMate) @@ -1960,48 +1960,48 @@ sensor - A entidade referenciando aplica-se a um agregado de transferência que foi + A entidade referenciando aplica-se a um agregado de transferência que foi identificado originalmente como um sensor (DS_Sensor) platformSeries - - A entidade referenciando aplica-se a um agregado de transferência que foi - identificado originalmente como uma série de plataforma (DS_PlatformSeries) + + A entidade referenciando aplica-se a um agregado de transferência que foi + identificado originalmente como uma série de plataforma (DS_PlatformSeries) sensorSeries - - A entidade referenciando aplica-se a um agregado de transferência que foi - identificado originalmente como uma série de sensor (DS_SensorSeries) + + A entidade referenciando aplica-se a um agregado de transferência que foi + identificado originalmente como uma série de sensor (DS_SensorSeries) productionSeries - - A entidade referenciando aplica-se a um agregado de transferência que foi - identificado originalmente como uma série de produção (DS_ProductionSeries) + + A entidade referenciando aplica-se a um agregado de transferência que foi + identificado originalmente como uma série de produção (DS_ProductionSeries) transferAggregate - - A entidade referenciando aplica-se a um agregado de transferência - que não existe fora de contexto da transferência + + A entidade referenciando aplica-se a um agregado de transferência + que não existe fora de contexto da transferência otherAggregate - A entidade referenciando aplica-se a um agregado de transferência que existe - fora de contexto da transferência, mas não pertence a um tipo de agregado específico. + A entidade referenciando aplica-se a um agregado de transferência que existe + fora de contexto da transferência, mas não pertence a um tipo de agregado específico. @@ -2044,19 +2044,19 @@ tight - Enlaçado apertadamente: dados associados + Enlaçado apertadamente: dados associados mixed - Enlaçado misto: dados associados; dados externos podem ser processados + Enlaçado misto: dados associados; dados externos podem ser processados adicionalmente loose - Enlaçado frouxamente: não hão dados associados + Enlaçado frouxamente: não hão dados associados diff --git a/services/src/main/java/org/fao/geonet/api/es/EsHTTPProxy.java b/services/src/main/java/org/fao/geonet/api/es/EsHTTPProxy.java index caf3c9ea8ad..dca972556b7 100644 --- a/services/src/main/java/org/fao/geonet/api/es/EsHTTPProxy.java +++ b/services/src/main/java/org/fao/geonet/api/es/EsHTTPProxy.java @@ -301,7 +301,7 @@ private static boolean hasOperation(ObjectNode doc, ReservedGroup group, Reserve @ResponseStatus(value = HttpStatus.OK) @ResponseBody public void search( - @RequestParam(defaultValue = SelectionManager.SELECTION_METADATA) + @RequestParam(defaultValue = SelectionManager.SELECTION_BUCKET) String bucket, @Parameter(description = "Type of related resource. If none, no associated resource returned.", required = false @@ -387,7 +387,7 @@ public void msearch( @PreAuthorize("hasAuthority('Administrator')") @ResponseBody public void call( - @RequestParam(defaultValue = SelectionManager.SELECTION_METADATA) + @RequestParam(defaultValue = SelectionManager.SELECTION_BUCKET) String bucket, @Parameter(description = "'_search' for search service.") @PathVariable String endPoint, diff --git a/services/src/main/java/org/fao/geonet/api/users/PasswordApi.java b/services/src/main/java/org/fao/geonet/api/users/PasswordApi.java index 13dcce6d877..00e4010dad8 100644 --- a/services/src/main/java/org/fao/geonet/api/users/PasswordApi.java +++ b/services/src/main/java/org/fao/geonet/api/users/PasswordApi.java @@ -1,5 +1,5 @@ //============================================================================= -//=== Copyright (C) 2001-2007 Food and Agriculture Organization of the +//=== Copyright (C) 2001-2024 Food and Agriculture Organization of the //=== United Nations (FAO-UN), United Nations World Food Programme (WFP) //=== and United Nations Environment Programme (UNEP) //=== @@ -27,7 +27,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jeeves.server.context.ServiceContext; import org.fao.geonet.ApplicationContextHolder; -import org.fao.geonet.api.API; import org.fao.geonet.api.ApiUtils; import org.fao.geonet.api.tools.i18n.LanguageUtils; import org.fao.geonet.constants.Geonet; @@ -57,6 +56,7 @@ import javax.servlet.http.HttpServletRequest; import java.text.SimpleDateFormat; import java.util.Calendar; +import java.util.List; import java.util.Locale; import java.util.ResourceBundle; @@ -76,6 +76,7 @@ public class PasswordApi { public static final String LOGGER = Geonet.GEONETWORK + ".api.user"; public static final String DATE_FORMAT = "yyyy-MM-dd"; + public static final String USER_PASSWORD_SENT = "user_password_sent"; @Autowired LanguageUtils languageUtils; @Autowired @@ -85,14 +86,13 @@ public class PasswordApi { @Autowired FeedbackLanguages feedbackLanguages; - @Autowired(required=false) + @Autowired(required = false) SecurityProviderConfiguration securityProviderConfiguration; @io.swagger.v3.oas.annotations.Operation(summary = "Update user password", description = "Get a valid changekey by email first and then update your password.") - @RequestMapping( + @PatchMapping( value = "/{username}", - method = RequestMethod.PATCH, produces = MediaType.TEXT_PLAIN_VALUE) @ResponseStatus(value = HttpStatus.CREATED) @ResponseBody @@ -100,13 +100,12 @@ public ResponseEntity updatePassword( @Parameter(description = "The user name", required = true) @PathVariable - String username, + String username, @Parameter(description = "The new password and a valid change key", required = true) @RequestBody - PasswordUpdateParameter passwordAndChangeKey, - HttpServletRequest request) - throws Exception { + PasswordUpdateParameter passwordAndChangeKey, + HttpServletRequest request) { Locale locale = languageUtils.parseAcceptLanguage(request.getLocales()); ResourceBundle messages = ResourceBundle.getBundle("org.fao.geonet.api.Messages", locale); Locale[] feedbackLocales = feedbackLanguages.getLocales(locale); @@ -117,8 +116,9 @@ public ResponseEntity updatePassword( ServiceContext context = ApiUtils.createServiceContext(request); - User user = userRepository.findOneByUsername(username); - if (user == null) { + List existingUsers = userRepository.findByUsernameIgnoreCase(username); + + if (existingUsers.isEmpty()) { Log.warning(LOGGER, String.format("User update password. Can't find user '%s'", username)); @@ -128,6 +128,9 @@ public ResponseEntity updatePassword( XslUtil.encodeForJavaScript(username) ), HttpStatus.PRECONDITION_FAILED); } + + User user = existingUsers.get(0); + if (LDAPConstants.LDAP_FLAG.equals(user.getSecurity().getAuthType())) { Log.warning(LOGGER, String.format("User '%s' is authenticated using LDAP. Password can't be sent by email.", username)); @@ -183,14 +186,16 @@ public ResponseEntity updatePassword( String content = localizedEmail.getParsedMessage(feedbackLocales); // send change link via email with admin in CC - if (!MailUtil.sendMail(user.getEmail(), + Boolean mailSent = MailUtil.sendMail(user.getEmail(), subject, content, null, sm, - adminEmail, "")) { + adminEmail, ""); + if (Boolean.FALSE.equals(mailSent)) { return new ResponseEntity<>(String.format( messages.getString("mail_error")), HttpStatus.PRECONDITION_FAILED); } + return new ResponseEntity<>(String.format( messages.getString("user_password_changed"), XslUtil.encodeForJavaScript(username) @@ -202,9 +207,8 @@ public ResponseEntity updatePassword( "reset his password. User MUST have an email to get the link. " + "LDAP users will not be able to retrieve their password " + "using this service.") - @RequestMapping( + @PutMapping( value = "/actions/forgot-password", - method = RequestMethod.PUT, produces = MediaType.TEXT_PLAIN_VALUE) @ResponseStatus(value = HttpStatus.CREATED) @ResponseBody @@ -212,9 +216,8 @@ public ResponseEntity sendPasswordByEmail( @Parameter(description = "The user name", required = true) @RequestParam - String username, - HttpServletRequest request) - throws Exception { + String username, + HttpServletRequest request) { Locale locale = languageUtils.parseAcceptLanguage(request.getLocales()); ResourceBundle messages = ResourceBundle.getBundle("org.fao.geonet.api.Messages", locale); Locale[] feedbackLocales = feedbackLanguages.getLocales(locale); @@ -225,17 +228,19 @@ public ResponseEntity sendPasswordByEmail( ServiceContext serviceContext = ApiUtils.createServiceContext(request); - final User user = userRepository.findOneByUsername(username); - if (user == null) { + List existingUsers = userRepository.findByUsernameIgnoreCase(username); + + if (existingUsers.isEmpty()) { Log.warning(LOGGER, String.format("User reset password. Can't find user '%s'", username)); // Return response not providing details about the issue, that should be logged. return new ResponseEntity<>(String.format( - messages.getString("user_password_sent"), + messages.getString(USER_PASSWORD_SENT), XslUtil.encodeForJavaScript(username) ), HttpStatus.CREATED); } + User user = existingUsers.get(0); if (LDAPConstants.LDAP_FLAG.equals(user.getSecurity().getAuthType())) { Log.warning(LOGGER, String.format("User '%s' is authenticated using LDAP. Password can't be sent by email.", @@ -243,19 +248,19 @@ public ResponseEntity sendPasswordByEmail( // Return response not providing details about the issue, that should be logged. return new ResponseEntity<>(String.format( - messages.getString("user_password_sent"), + messages.getString(USER_PASSWORD_SENT), XslUtil.encodeForJavaScript(username) ), HttpStatus.CREATED); } String email = user.getEmail(); - if (StringUtils.isEmpty(email)) { + if (!StringUtils.hasLength(email)) { Log.warning(LOGGER, String.format("User reset password. User '%s' has no email", username)); // Return response not providing details about the issue, that should be logged. return new ResponseEntity<>(String.format( - messages.getString("user_password_sent"), + messages.getString(USER_PASSWORD_SENT), XslUtil.encodeForJavaScript(username) ), HttpStatus.CREATED); } @@ -298,16 +303,18 @@ public ResponseEntity sendPasswordByEmail( String content = localizedEmail.getParsedMessage(feedbackLocales); // send change link via email with admin in CC - if (!MailUtil.sendMail(email, + Boolean mailSent = MailUtil.sendMail(email, subject, content, null, sm, - adminEmail, "")) { + adminEmail, ""); + if (Boolean.FALSE.equals(mailSent)) { return new ResponseEntity<>(String.format( messages.getString("mail_error")), HttpStatus.PRECONDITION_FAILED); } + return new ResponseEntity<>(String.format( - messages.getString("user_password_sent"), + messages.getString(USER_PASSWORD_SENT), XslUtil.encodeForJavaScript(username) ), HttpStatus.CREATED); } diff --git a/web-ui/src/main/resources/catalog/components/viewer/layermanager/partials/layermanageritem.html b/web-ui/src/main/resources/catalog/components/viewer/layermanager/partials/layermanageritem.html index b40b9faa08b..5a1e1fee2ff 100644 --- a/web-ui/src/main/resources/catalog/components/viewer/layermanager/partials/layermanageritem.html +++ b/web-ui/src/main/resources/catalog/components/viewer/layermanager/partials/layermanageritem.html @@ -29,7 +29,6 @@
- -
diff --git a/web-ui/src/main/resources/catalog/js/LoginController.js b/web-ui/src/main/resources/catalog/js/LoginController.js index 15a0a862c6e..d4c71283d6f 100644 --- a/web-ui/src/main/resources/catalog/js/LoginController.js +++ b/web-ui/src/main/resources/catalog/js/LoginController.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2016 Food and Agriculture Organization of the + * Copyright (C) 2001-2024 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -89,7 +89,13 @@ gnConfig["system.security.passwordEnforcement.maxLength"], 6 ); - $scope.passwordPattern = gnConfig["system.security.passwordEnforcement.pattern"]; + + $scope.usePattern = gnConfig["system.security.passwordEnforcement.usePattern"]; + + if ($scope.usePattern) { + $scope.passwordPattern = + gnConfig["system.security.passwordEnforcement.pattern"]; + } }); $scope.resolveRecaptcha = false; diff --git a/web-ui/src/main/resources/catalog/style/gn_viewer.less b/web-ui/src/main/resources/catalog/style/gn_viewer.less index 2ca082d6b22..e2e1deddcc9 100644 --- a/web-ui/src/main/resources/catalog/style/gn_viewer.less +++ b/web-ui/src/main/resources/catalog/style/gn_viewer.less @@ -308,7 +308,6 @@ } } .dropdown-left { - @toggleWidth: 32px; @toggleHeight: 32px; @@ -339,10 +338,7 @@ } } } - } - - } .gn-searchlayer-list { diff --git a/web/src/main/webapp/WEB-INF/classes/ESAPI.properties b/web/src/main/webapp/WEB-INF/classes/ESAPI.properties index 68f7366c711..d943542e9b4 100644 --- a/web/src/main/webapp/WEB-INF/classes/ESAPI.properties +++ b/web/src/main/webapp/WEB-INF/classes/ESAPI.properties @@ -68,11 +68,12 @@ ESAPI.HTTPUtilities=org.owasp.esapi.reference.DefaultHTTPUtilities ESAPI.IntrusionDetector=org.owasp.esapi.reference.DefaultIntrusionDetector # Log4JFactory Requires log4j.xml or log4j.properties in classpath - http://www.laliluna.de/log4j-tutorial.html # Note that this is now considered deprecated! -ESAPI.Logger=org.owasp.esapi.logging.log4j.Log4JLogFactory +#ESAPI.Logger=org.owasp.esapi.logging.log4j.Log4JLogFactory #ESAPI.Logger=org.owasp.esapi.logging.java.JavaLogFactory # To use the new SLF4J logger in ESAPI (see GitHub issue #129), set # ESAPI.Logger=org.owasp.esapi.logging.slf4j.Slf4JLogFactory # and do whatever other normal SLF4J configuration that you normally would do for your application. +ESAPI.Logger=org.owasp.esapi.logging.slf4j.Slf4JLogFactory ESAPI.Randomizer=org.owasp.esapi.reference.DefaultRandomizer ESAPI.Validator=org.owasp.esapi.reference.DefaultValidator