diff --git a/dspace-api/src/main/java/org/dspace/validation/MetadataValidator.java b/dspace-api/src/main/java/org/dspace/validation/MetadataValidator.java index 3be35da792c6..0f02e9da05ab 100644 --- a/dspace-api/src/main/java/org/dspace/validation/MetadataValidator.java +++ b/dspace-api/src/main/java/org/dspace/validation/MetadataValidator.java @@ -27,6 +27,7 @@ import org.dspace.content.InProgressSubmission; import org.dspace.content.Item; import org.dspace.content.MetadataValue; +import org.dspace.content.WorkspaceItem; import org.dspace.content.authority.service.MetadataAuthorityService; import org.dspace.content.service.ItemService; import org.dspace.core.Constants; @@ -34,7 +35,6 @@ import org.dspace.core.exception.SQLRuntimeException; import org.dspace.services.ConfigurationService; import org.dspace.validation.model.ValidationError; -import org.dspace.workflow.WorkflowItem; /** * Execute three validation check on fields validation: - mandatory metadata @@ -49,7 +49,7 @@ public class MetadataValidator implements SubmissionStepValidator { private static final String ERROR_VALIDATION_AUTHORITY_REQUIRED = "error.validation.authority.required"; - private static final String ERROR_VALIDATION_REGEX = "error.validation.regex"; + private static final String ERROR_VALIDATION_PREFIX = "error.validation.regex"; private static final String ERROR_VALIDATION_NOT_REPEATABLE = "error.validation.notRepeatable"; @@ -67,6 +67,10 @@ public class MetadataValidator implements SubmissionStepValidator { @Override public List validate(Context context, InProgressSubmission obj, SubmissionStepConfig config) { + // Determine current scope + String currentScope = (obj instanceof WorkspaceItem) ? + DCInput.SUBMISSION_SCOPE : DCInput.WORKFLOW_SCOPE; + List errors = new ArrayList<>(); @@ -108,10 +112,18 @@ public List validate(Context context, InProgressSubmission o } } if (input.isRequired() && !foundResult) { - // for this required qualdrop no value was found, add to the list of error fields - addError(errors, ERROR_VALIDATION_REQUIRED, - "/" + OPERATION_PATH_SECTIONS + "/" + config.getId() + "/" + - input.getFieldName()); + + // Check if field is visible and not readonly in current scope + boolean isVisibleInCurrentScope = input.isVisible(currentScope); + boolean isReadonlyInCurrentScope = input.isReadOnly(currentScope); + + // Only add error if field is visible, not readonly, and allowed for document type + if (isVisibleInCurrentScope && !isReadonlyInCurrentScope && input.isAllowedFor(documentType)) { + // for this required qualdrop no value was found, add to the list of error fields + addError(errors, ERROR_VALIDATION_REQUIRED, + "/" + OPERATION_PATH_SECTIONS + "/" + config.getId() + "/" + + input.getFieldName()); + } } } else { @@ -140,16 +152,21 @@ public List validate(Context context, InProgressSubmission o } } validateMetadataValues(obj.getCollection(), mdv, input, config, - isAuthorityControlled, fieldKey, errors); - if ((input.isRequired() && mdv.size() == 0) - && (input.isVisible(DCInput.SUBMISSION_SCOPE) - || (obj instanceof WorkflowItem && input.isVisible(DCInput.WORKFLOW_SCOPE))) - && !valuesRemoved) { - // Is the input required for *this* type? In other words, are we looking at a required - // input that is also allowed for this document type - if (input.isAllowedFor(documentType)) { - // since this field is missing add to list of error - // fields + isAuthorityControlled, fieldKey, errors); + if ((input.isRequired() && mdv.size() == 0) && !valuesRemoved) { + + // Check if field is visible in current scope + boolean isVisibleInCurrentScope = input.isVisible(currentScope); + + // Check if field is readonly or hidden in current scope + boolean isReadonlyInCurrentScope = input.isReadOnly(currentScope); + + // Only validate as required if: + // 1. Field is visible in current scope AND + // 2. Field is NOT readonly in current scope AND + // 3. Field is allowed for this document type + if (isVisibleInCurrentScope && !isReadonlyInCurrentScope && input.isAllowedFor(documentType)) { + // since this field is missing add to list of error fields addError(errors, ERROR_VALIDATION_REQUIRED, "/" + OPERATION_PATH_SECTIONS + "/" + config.getId() + "/" + input.getFieldName()); @@ -181,7 +198,7 @@ private void validateMetadataValues(Collection collection, List m for (MetadataValue md : metadataValues) { if (! (input.validate(md.getValue()))) { - addError(errors, ERROR_VALIDATION_REGEX, + addError(errors, ERROR_VALIDATION_PREFIX, "/" + OPERATION_PATH_SECTIONS + "/" + config.getId() + "/" + input.getFieldName() + "/" + md.getPlace()); } diff --git a/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml b/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml index 3aebabb74bb2..5bed5c06358c 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml @@ -33,6 +33,7 @@ + @@ -319,6 +320,13 @@ org.dspace.app.rest.submit.step.NotifyStep coarnotify + + + + org.dspace.app.rest.submit.step.DescribeStep + collection + + submit.progressbar.upload-no-required-metadata org.dspace.app.rest.submit.step.UploadStep @@ -543,6 +551,10 @@ + + + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/submission-forms.xml b/dspace-api/src/test/data/dspaceFolder/config/submission-forms.xml index a960ec5eba1e..cd19daa205e7 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/submission-forms.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/submission-forms.xml @@ -2450,6 +2450,78 @@ it, please enter the types and the actual numbers or codes. +
+ + + dc + title + false + + onebox + Title is required + Enter the title of the item + + + dc + contributor + author + true + + onebox + Author is required + Enter the author(s) of the item + workflow + + + dc + date + issued + false + + date + Date issued is required + Enter the date the item was issued + submission + submission + + + + + dc + description + abstract + false + + textarea + Abstract is required + Enter an abstract for the item + workflow + submission + + + dc + subject + true + + onebox + Subject is required + Enter subject keywords for the item + workflow + workflow + + + dc + type + false + + onebox + Type is required + Enter the type of the item + submission + all + + +
diff --git a/dspace-api/src/test/java/org/dspace/validation/MetadataValidatorIT.java b/dspace-api/src/test/java/org/dspace/validation/MetadataValidatorIT.java new file mode 100644 index 000000000000..15d372aa77ec --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/validation/MetadataValidatorIT.java @@ -0,0 +1,176 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.validation; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; + +import java.util.List; +import java.util.stream.Collectors; + +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.app.util.DCInputsReader; +import org.dspace.app.util.SubmissionConfig; +import org.dspace.app.util.SubmissionConfigReader; +import org.dspace.app.util.SubmissionStepConfig; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.WorkflowItemBuilder; +import org.dspace.builder.WorkspaceItemBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.WorkspaceItem; +import org.dspace.content.authority.factory.ContentAuthorityServiceFactory; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.ItemService; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.dspace.validation.model.ValidationError; +import org.dspace.workflow.WorkflowItem; +import org.junit.Before; +import org.junit.Test; + +/** + * Integration test for {@link MetadataValidator} to ensure it respects + * readonly and hidden scopes for required fields. + * + * This test class relies on a custom submission definition ("test-metadata-validator") + * which must be configured in dspace-api/src/test/data/dspaceFolder/config/submission-forms.xml and + * dspace-api/src/test/data/dspaceFolder/config/item-submission.xml. + * + */ +public class MetadataValidatorIT extends AbstractIntegrationTestWithDatabase { + + private Collection collection; + private MetadataValidator validator; + private SubmissionStepConfig submissionStepConfig; + private ItemService itemService = ContentServiceFactory.getInstance().getItemService(); + private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + + context.turnOffAuthorisationSystem(); + + Community community = CommunityBuilder.createCommunity(context).build(); + collection = CollectionBuilder.createCollection(context, community) + .withSubmissionDefinition("test-metadata-validator") + .build(); + + context.restoreAuthSystemState(); + + SubmissionConfigReader submissionConfigReader = new SubmissionConfigReader(); + SubmissionConfig submissionConfig = submissionConfigReader.getSubmissionConfigByCollection(collection); + // Assumes the test submission process has one step defined + submissionStepConfig = submissionConfig.getStep(0); + + validator = new MetadataValidator(); + validator.setName("test-validator"); + validator.setItemService(itemService); + validator.setMetadataAuthorityService(ContentAuthorityServiceFactory.getInstance() + .getMetadataAuthorityService()); + validator.setConfigurationService(configurationService); + validator.setInputReader(new DCInputsReader()); + } + + /** + * In SUBMISSION scope, ensures validation reports only required fields + * not marked readonly/hidden for submission. Missing dc.title and + * dc.contributor.author should be flagged; dc.date.issued is ignored. + */ + @Test + public void testValidationInSubmissionScopeWithErrors() throws Exception { + context.turnOffAuthorisationSystem(); + WorkspaceItem wsi = WorkspaceItemBuilder.createWorkspaceItem(context, collection).build(); + context.restoreAuthSystemState(); + + List errors = validator.validate(context, wsi, submissionStepConfig); + + assertThat(errors, hasSize(1)); + + List errorPaths = getErrorPaths(errors); + assertThat(errorPaths, containsInAnyOrder( + "/sections/test-metadata-validator-step/dc.title", + "/sections/test-metadata-validator-step/dc.contributor.author" + )); + } + + /** + * In SUBMISSION scope, ensures no errors when all required fields + * for this scope are present. Fields readonly/hidden in submission + * can remain empty without errors. + */ + @Test + public void testValidationInSubmissionScopeWithoutErrors() throws Exception { + context.turnOffAuthorisationSystem(); + WorkspaceItem wsi = WorkspaceItemBuilder.createWorkspaceItem(context, collection) + .withTitle("Test Title") + .withAuthor("Test, Author") + .withIssueDate("2025-08-14") + .build(); + context.restoreAuthSystemState(); + + List errors = validator.validate(context, wsi, submissionStepConfig); + + assertThat(errors, empty()); + } + + /** + * In WORKFLOW scope, ensures validation reports only required fields + * not marked readonly/hidden for workflow. Missing dc.title and + * dc.description.abstract should be flagged; others are ignored. + */ + @Test + public void testValidationInWorkflowScopeWithErrors() throws Exception { + context.turnOffAuthorisationSystem(); + WorkflowItem wfi = WorkflowItemBuilder.createWorkflowItem(context, collection).build(); + context.restoreAuthSystemState(); + + List errors = validator.validate(context, wfi, submissionStepConfig); + + assertThat(errors, hasSize(1)); + + List errorPaths = getErrorPaths(errors); + assertThat(errorPaths, containsInAnyOrder( + "/sections/test-metadata-validator-step/dc.title", + "/sections/test-metadata-validator-step/dc.description.abstract" + )); + } + + /** + * In WORKFLOW scope, ensures no errors when all required fields + * for this scope are present. Fields readonly/hidden in workflow + * can remain empty without errors. + */ + @Test + public void testValidationInWorkflowScopeWithoutErrors() throws Exception { + context.turnOffAuthorisationSystem(); + WorkflowItem wfi = WorkflowItemBuilder.createWorkflowItem(context, collection).build(); + + // Add metadata for fields required in the workflow scope + itemService.addMetadata(context, wfi.getItem(), "dc", "title", null, null, "Test Title"); + itemService.addMetadata(context, wfi.getItem(), "dc", "description", "abstract", null, "Test Abstract"); + itemService.addMetadata(context, wfi.getItem(), "dc", "subject", null, null, "Test Subject"); + + context.restoreAuthSystemState(); + + List errors = validator.validate(context, wfi, submissionStepConfig); + + assertThat(errors, empty()); + } + + private List getErrorPaths(List errors) { + return errors.stream() + .flatMap(error -> error.getPaths().stream()) + .collect(Collectors.toList()); + } +}