diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ClarinUserMetadataRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ClarinUserMetadataRestController.java index 31101e97f3c7..3f8c6e380d0e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ClarinUserMetadataRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ClarinUserMetadataRestController.java @@ -20,14 +20,19 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Objects; +import java.util.Optional; +import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import javax.mail.MessagingException; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.NotFoundException; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -57,7 +62,6 @@ import org.dspace.core.I18nUtil; import org.dspace.eperson.EPerson; import org.dspace.services.ConfigurationService; -import org.json.simple.parser.ParseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -74,6 +78,8 @@ public class ClarinUserMetadataRestController { public static final String CHECK_EMAIL_RESPONSE_CONTENT = "checkEmail"; + private static final List OPTIONAL_METADATA_KEYS = List.of("ORGANIZATION"); + @Autowired ClarinUserMetadataService clarinUserMetadataService; @Autowired @@ -93,14 +99,17 @@ public class ClarinUserMetadataRestController { @Autowired ConfigurationService configurationService; + @Autowired + ObjectMapper objectMapper; + // Enum to distinguish between the two types of the email enum MailType { ALLZIP, BITSTREAM } @RequestMapping(value = "/zip", method = POST, consumes = APPLICATION_JSON) @PreAuthorize("permitAll()") public ResponseEntity manageUserMetadataForZIP(@RequestParam("itemUUID") UUID itemUUID, - HttpServletRequest request) - throws SQLException, ParseException, IOException, AuthorizeException, MessagingException { + HttpServletRequest request) + throws SQLException, IOException, AuthorizeException { // Get context from the request Context context = obtainContext(request); @@ -127,17 +136,26 @@ public ResponseEntity manageUserMetadataForZIP(@RequestParam("itemUUID") UUID it boolean shouldEmailToken = false; ClarinLicense clarinLicense = null; - // Get ClarinUserMetadataRest Array from the request body - ClarinUserMetadataRest[] clarinUserMetadataRestArray = - new ObjectMapper().readValue(request.getInputStream(), ClarinUserMetadataRest[].class); - if (Objects.isNull(clarinUserMetadataRestArray)) { - throw new RuntimeException("The clarinUserMetadataRestArray cannot be null. It could be empty, but" + - " not null"); - } + Set requiredInfoKeys = new HashSet<>(); - // Convert Array to the List - List clarinUserMetadataRestList = Arrays.asList(clarinUserMetadataRestArray); List bundles = item.getBundles("ORIGINAL"); + if (!bundles.isEmpty()) { + List bitstreams = bundles.get(0).getBitstreams(); + if (!bitstreams.isEmpty()) { + ClarinLicenseResourceMapping clarinLicenseResourceMapping = + this.getLicenseResourceMapping(context, bitstreams.get(0).getID()); + clarinLicense = getClarinLicense(clarinLicenseResourceMapping); + shouldEmailToken = this.shouldEmailToken(clarinLicenseResourceMapping); + requiredInfoKeys.addAll(getRequiredInfoKeys(clarinLicense)); + } + } + + ClarinUserMetadataRest[] clarinUserMetadataRestArray = getClarinUserMetadata(request); + checkForRequiredInfoKeys(clarinUserMetadataRestArray, requiredInfoKeys); + List clarinUserMetadataRestList = (clarinLicense == null) + ? Arrays.asList(clarinUserMetadataRestArray) + : getFilteredUserMetadataForClarinLicense(clarinUserMetadataRestArray, requiredInfoKeys); + for (Bundle original : bundles) { List bss = original.getBitstreams(); for (Bitstream bitstream : bss) { @@ -159,18 +177,11 @@ public ResponseEntity manageUserMetadataForZIP(@RequestParam("itemUUID") UUID it this.processSignedInUser(context, currentUser, clarinUserMetadataRestList, clarinLicenseResourceMapping, downloadToken); } - - // Check (only once) if the Clarin License contains the required information to SEND_TOKEN, which means - // the item must be downloaded after the user confirms the download with the token sent by email. - if (Objects.isNull(clarinLicense)) { - clarinLicense = this.getClarinLicense(clarinLicenseResourceMapping); - shouldEmailToken = this.shouldEmailToken(clarinLicenseResourceMapping); - } } } context.commit(); - if (shouldEmailToken) { + if (clarinLicense != null && shouldEmailToken) { // If yes - send token to e-mail try { String email = getEmailFromUserMetadata(clarinUserMetadataRestList); @@ -191,8 +202,8 @@ public ResponseEntity manageUserMetadataForZIP(@RequestParam("itemUUID") UUID it @RequestMapping(method = POST, consumes = APPLICATION_JSON) @PreAuthorize("permitAll()") public ResponseEntity manageUserMetadata(@RequestParam("bitstreamUUID") UUID bitstreamUUID, - HttpServletRequest request) - throws SQLException, ParseException, IOException, AuthorizeException, MessagingException { + HttpServletRequest request) + throws SQLException, IOException, AuthorizeException { // Get context from the request Context context = obtainContext(request); @@ -201,7 +212,6 @@ public ResponseEntity manageUserMetadata(@RequestParam("bitstreamUUID") UUID bit return null; } - Bitstream bitstream = bitstreamService.find(context, bitstreamUUID); if (Objects.isNull(bitstream)) { log.error("Cannot find the bitstream with ID: " + bitstreamUUID); @@ -228,16 +238,13 @@ public ResponseEntity manageUserMetadata(@RequestParam("bitstreamUUID") UUID bit throw new AuthorizeException("Anonymous user is not allowed to get access token"); } - // Get ClarinUserMetadataRest Array from the request body - ClarinUserMetadataRest[] clarinUserMetadataRestArray = - new ObjectMapper().readValue(request.getInputStream(), ClarinUserMetadataRest[].class); - if (Objects.isNull(clarinUserMetadataRestArray)) { - throw new RuntimeException("The clarinUserMetadataRestArray cannot be null. It could be empty, but" + - " not null"); - } + ClarinUserMetadataRest[] clarinUserMetadataRestArray = getClarinUserMetadata(request); + + Set requiredInfoKeys = getRequiredInfoKeys(clarinLicense); + checkForRequiredInfoKeys(clarinUserMetadataRestArray, requiredInfoKeys); + List clarinUserMetadataRestList = getFilteredUserMetadataForClarinLicense( + clarinUserMetadataRestArray, requiredInfoKeys); - // Convert Array to the List - List clarinUserMetadataRestList = Arrays.asList(clarinUserMetadataRestArray); if (Objects.isNull(currentUser)) { // The user is not signed in this.processNonSignedInUser(context, clarinUserMetadataRestList, clarinLicenseResourceMapping, @@ -284,7 +291,7 @@ private void sendEmailWithDownloadLink(Context context, DSpaceObject dso, MailType mailType, List clarinUserMetadataRestList, String itemHandle) - throws IOException, SQLException, MessagingException { + throws IOException, MessagingException { if (StringUtils.isBlank(email)) { log.error("Cannot send email with download link because the email is empty."); throw new DSpaceBadRequestException("Cannot send email with download link because the email is empty."); @@ -611,4 +618,79 @@ private List createUserMetadataFromRequest(Context context, } return clarinUserMetadataList; } + + /** + * Get ClarinUserMetadataRest array from the request body. Throws exception if the request body is empty or invalid. + * + * @param request request + * @return ClarinUserMetadataRest array + */ + private ClarinUserMetadataRest[] getClarinUserMetadata(HttpServletRequest request) throws IOException { + ClarinUserMetadataRest[] clarinUserMetadataRestArray; + try { + clarinUserMetadataRestArray = objectMapper.readValue( + request.getInputStream(), ClarinUserMetadataRest[].class); + } catch (JsonProcessingException ex) { + throw new DSpaceBadRequestException("Missing or Invalid User Data", ex); + } + if (Objects.isNull(clarinUserMetadataRestArray)) { + throw new DSpaceBadRequestException("Missing or Invalid User Data"); + } + return clarinUserMetadataRestArray; + } + + /** + * Gets the required info keys for the given ClarinLicense. + * Note that "SEND_TOKEN" is not considered required. + * + * @param clarinLicense the ClarinLicense object to get required info keys from + * @return Set of required info keys for the given ClarinLicense + */ + private static Set getRequiredInfoKeys(ClarinLicense clarinLicense) { + return Optional.ofNullable(clarinLicense.getRequiredInfo()) + .map(requiredInfo -> Arrays.stream(requiredInfo.split(",")) + .map(String::trim) + .filter(s -> StringUtils.isNotBlank(s) && !"SEND_TOKEN".equals(s)) + .collect(Collectors.toSet())) + .orElse(Set.of()); + } + + /** + * Checks if the given array of ClarinUserMetadataRest objects contains all required info keys. + * Note that the OPTIONAL_METADATA_KEYS are not required. + * + * @param clarinUserMetadataRestArray the array of ClarinUserMetadataRest objects to check + * @param requiredInfoKeys the set of required info keys to check against + */ + private static void checkForRequiredInfoKeys(ClarinUserMetadataRest[] clarinUserMetadataRestArray, + Set requiredInfoKeys) { + if (!requiredInfoKeys.stream().allMatch(requiredInfoKey -> + OPTIONAL_METADATA_KEYS.contains(requiredInfoKey) || + Arrays.stream(clarinUserMetadataRestArray) + .anyMatch(clarinUserMetadataRest -> + requiredInfoKey.equals(clarinUserMetadataRest.getMetadataKey()) + && StringUtils.isNotBlank(clarinUserMetadataRest.getMetadataValue())))) { + throw new DSpaceBadRequestException("Missing required user metadata keys for the license."); + } + } + + /** + * Filters the given array of ClarinUserMetadataRest objects to include only those + * required by the given ClarinLicense. + * Note that the "IP" should be included if present. + * + * @param clarinUserMetadataRestArray the array of ClarinUserMetadataRest objects to filter + * @param requiredInfoKeys the set of required keys for the given ClarinLicense + * @return a list of filtered ClarinUserMetadataRest objects + */ + private static List getFilteredUserMetadataForClarinLicense( + ClarinUserMetadataRest[] clarinUserMetadataRestArray, + Set requiredInfoKeys) { + return Arrays.stream(clarinUserMetadataRestArray) + .filter(clarinUserMetadataRest -> + StringUtils.isNotBlank(clarinUserMetadataRest.getMetadataValue()) + && ("IP".equals(clarinUserMetadataRest.getMetadataKey()) + || requiredInfoKeys.contains(clarinUserMetadataRest.getMetadataKey()))) + .collect(Collectors.toList()); + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ClarinUserMetadataRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ClarinUserMetadataRestControllerIT.java index 53fc5e867c9e..599398eb9b57 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ClarinUserMetadataRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ClarinUserMetadataRestControllerIT.java @@ -186,7 +186,7 @@ public void notAuthorizedUser_withLicenseConfirmation_ASK_ALWAYS() throws Except @Test public void notAuthorizedUser_withAllowingAnonymousLicense_shouldSendEmail() throws Exception { - this.prepareEnvironment("SEND_TOKEN", Confirmation.ALLOW_ANONYMOUS); + this.prepareEnvironment("SEND_TOKEN,EXTRA_EMAIL", Confirmation.ALLOW_ANONYMOUS); ObjectMapper mapper = new ObjectMapper(); ClarinUserMetadataRest clarinUserMetadata1 = new ClarinUserMetadataRest(); clarinUserMetadata1.setMetadataKey("NAME"); @@ -225,6 +225,165 @@ public void notAuthorizedUser_withAllowingAnonymousLicense_shouldSendEmail() thr .andExpect(jsonPath("$.page.totalElements", is(1))); } + @Test + public void notAuthorizedUser_manageUserMetadata_withEmptyMetadata_shouldReturn_400() throws Exception { + this.prepareEnvironment(null, Confirmation.ALLOW_ANONYMOUS); + getClient().perform(post("/api/core/clarinusermetadata/manage?bitstreamUUID=" + bitstream.getID()) + .content(new byte[0]) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + public void authorizedUser_manageUserMetadata_shouldStoreOnlyRequiredMetadata() throws Exception { + this.prepareEnvironment("SEND_TOKEN,EXTRA_EMAIL,NAME", Confirmation.ALLOW_ANONYMOUS); + ObjectMapper mapper = new ObjectMapper(); + ClarinUserMetadataRest clarinUserMetadata1 = new ClarinUserMetadataRest(); + clarinUserMetadata1.setMetadataKey("NAME"); + clarinUserMetadata1.setMetadataValue("Test"); + + ClarinUserMetadataRest clarinUserMetadata2 = new ClarinUserMetadataRest(); + clarinUserMetadata2.setMetadataKey("ADDRESS"); + clarinUserMetadata2.setMetadataValue("Test2"); + + ClarinUserMetadataRest clarinUserMetadata3 = new ClarinUserMetadataRest(); + clarinUserMetadata3.setMetadataKey("SEND_TOKEN"); + + ClarinUserMetadataRest clarinUserMetadata4 = new ClarinUserMetadataRest(); + clarinUserMetadata4.setMetadataKey("EXTRA_EMAIL"); + clarinUserMetadata4.setMetadataValue("test@test.edu"); + + ClarinUserMetadataRest clarinUserMetadata5 = new ClarinUserMetadataRest(); + clarinUserMetadata5.setMetadataKey("ORGANIZATION"); + clarinUserMetadata5.setMetadataValue("organization"); + + List clarinUserMetadataRestList = new ArrayList<>(); + clarinUserMetadataRestList.add(clarinUserMetadata1); + clarinUserMetadataRestList.add(clarinUserMetadata2); + clarinUserMetadataRestList.add(clarinUserMetadata3); + clarinUserMetadataRestList.add(clarinUserMetadata4); + clarinUserMetadataRestList.add(clarinUserMetadata5); + + String adminToken = getAuthToken(admin.getEmail(), password); + // Load bitstream from the item. + getClient().perform(post("/api/core/clarinusermetadata/manage?bitstreamUUID=" + bitstream.getID()) + .content(mapper.writeValueAsBytes(clarinUserMetadataRestList.toArray())) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", notNullValue())) + .andExpect(jsonPath("$", is(CHECK_EMAIL_RESPONSE_CONTENT))); + + // Get created CLRUA + getClient(adminToken).perform(get("/api/core/clarinlruallowances") + .contentType(contentType)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Get created User Metadata - there should be 2 records, + // because only 2 metadata fields are required by the license (EXTRA_EMAIL and NAME) + getClient(adminToken).perform(get("/api/core/clarinusermetadata") + .contentType(contentType)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(2))); + } + + @Test + public void authorizedUser_manageUserMetadata_organizationIsOptional() throws Exception { + this.prepareEnvironment("NAME,ORGANIZATION", Confirmation.ALLOW_ANONYMOUS); + ObjectMapper mapper = new ObjectMapper(); + ClarinUserMetadataRest clarinUserMetadata1 = new ClarinUserMetadataRest(); + clarinUserMetadata1.setMetadataKey("NAME"); + clarinUserMetadata1.setMetadataValue("Test"); + + ClarinUserMetadataRest clarinUserMetadata2 = new ClarinUserMetadataRest(); + clarinUserMetadata2.setMetadataKey("ADDRESS"); + clarinUserMetadata2.setMetadataValue("Test2"); + + List clarinUserMetadataRestList = new ArrayList<>(); + clarinUserMetadataRestList.add(clarinUserMetadata1); + clarinUserMetadataRestList.add(clarinUserMetadata2); + + String adminToken = getAuthToken(admin.getEmail(), password); + // Load bitstream from the item. + getClient().perform(post("/api/core/clarinusermetadata/manage?bitstreamUUID=" + bitstream.getID()) + .content(mapper.writeValueAsBytes(clarinUserMetadataRestList.toArray())) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", notNullValue())); + + // Get created CLRUA + getClient(adminToken).perform(get("/api/core/clarinlruallowances") + .contentType(contentType)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Get created User Metadata - only NAME metadata is stored, + // because ORGANIZATION metadata is empty and should not be stored + getClient(adminToken).perform(get("/api/core/clarinusermetadata") + .contentType(contentType)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void authorizedUser_manageUserMetadata_shouldStoreOrganizationAndIP() throws Exception { + this.prepareEnvironment("ORGANIZATION", Confirmation.ALLOW_ANONYMOUS); + ObjectMapper mapper = new ObjectMapper(); + ClarinUserMetadataRest clarinUserMetadata1 = new ClarinUserMetadataRest(); + clarinUserMetadata1.setMetadataKey("ORGANIZATION"); + clarinUserMetadata1.setMetadataValue("Test"); + + ClarinUserMetadataRest clarinUserMetadata2 = new ClarinUserMetadataRest(); + clarinUserMetadata2.setMetadataKey("IP"); + clarinUserMetadata2.setMetadataValue("127.0.0.1"); + + List clarinUserMetadataRestList = new ArrayList<>(); + clarinUserMetadataRestList.add(clarinUserMetadata1); + clarinUserMetadataRestList.add(clarinUserMetadata2); + + String adminToken = getAuthToken(admin.getEmail(), password); + // Load bitstream from the item. + getClient().perform(post("/api/core/clarinusermetadata/manage?bitstreamUUID=" + bitstream.getID()) + .content(mapper.writeValueAsBytes(clarinUserMetadataRestList.toArray())) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", notNullValue())); + + // Get created CLRUA + getClient(adminToken).perform(get("/api/core/clarinlruallowances") + .contentType(contentType)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Get created User Metadata - there should be 2 records (ORGANIZATION and IP) + getClient(adminToken).perform(get("/api/core/clarinusermetadata") + .contentType(contentType)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(2))); + } + + @Test + public void authorizedUser_manageUserMetadata_withMissingRequiredKeys_shouldFail() throws Exception { + this.prepareEnvironment("SEND_TOKEN,EXTRA_EMAIL,REQUIRED_ORGANIZATION", Confirmation.ALLOW_ANONYMOUS); + ObjectMapper mapper = new ObjectMapper(); + + ClarinUserMetadataRest clarinUserMetadata1 = new ClarinUserMetadataRest(); + clarinUserMetadata1.setMetadataKey("SEND_TOKEN"); + + ClarinUserMetadataRest clarinUserMetadata2 = new ClarinUserMetadataRest(); + clarinUserMetadata2.setMetadataKey("EXTRA_EMAIL"); + clarinUserMetadata2.setMetadataValue("test@test.edu"); + + List clarinUserMetadataRestList = new ArrayList<>(); + clarinUserMetadataRestList.add(clarinUserMetadata1); + clarinUserMetadataRestList.add(clarinUserMetadata2); + + getClient().perform(post("/api/core/clarinusermetadata/manage?bitstreamUUID=" + bitstream.getID()) + .content(mapper.writeValueAsBytes(clarinUserMetadataRestList.toArray())) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + @Test public void authorizedUserWithoutMetadata_shouldReturnToken() throws Exception { this.prepareEnvironment("NAME", Confirmation.NOT_REQUIRED); @@ -272,7 +431,7 @@ public void authorizedUserWithoutMetadata_shouldReturnToken() throws Exception { @Test public void authorizedUserWithoutMetadata_shouldSendEmail() throws Exception { - this.prepareEnvironment("SEND_TOKEN", Confirmation.NOT_REQUIRED); + this.prepareEnvironment("SEND_TOKEN,EXTRA_EMAIL", Confirmation.NOT_REQUIRED); context.turnOffAuthorisationSystem(); ClarinUserRegistration clarinUserRegistration = ClarinUserRegistrationBuilder .createClarinUserRegistration(context).withEPersonID(admin.getID()).build(); @@ -375,7 +534,7 @@ public void authorizedUserWithMetadata_shouldSendToken() throws Exception { @Test public void authorizedUserWithMetadata_shouldSendEmail() throws Exception { - this.prepareEnvironment("SEND_TOKEN,NAME,ADDRESS", Confirmation.NOT_REQUIRED); + this.prepareEnvironment("SEND_TOKEN,NAME,ADDRESS,EXTRA_EMAIL", Confirmation.NOT_REQUIRED); context.turnOffAuthorisationSystem(); ClarinUserRegistration clarinUserRegistration = ClarinUserRegistrationBuilder .createClarinUserRegistration(context).withEPersonID(admin.getID()).build();