From d796959c8cd1ce0ef05bc8474a5da109c6219d02 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 16 Dec 2024 17:51:57 +0100 Subject: [PATCH 01/57] Stash: update featureItems endpoint PoC WIP --- .../harvard/iq/dataverse/api/Dataverses.java | 56 ++++++++++++++++++- src/main/java/propertyFiles/Bundle.properties | 1 + .../iq/dataverse/api/DataversesIT.java | 13 +++++ .../edu/harvard/iq/dataverse/api/UtilIT.java | 10 ++++ 4 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index f864a5a9d1c..e7ca7033210 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -24,6 +24,7 @@ import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.ConstraintViolationUtil; +import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.StringUtil; import static edu.harvard.iq.dataverse.util.StringUtil.nonEmpty; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.*; @@ -33,7 +34,9 @@ import edu.harvard.iq.dataverse.util.json.JsonPrinter; import edu.harvard.iq.dataverse.util.json.JsonUtil; -import java.io.StringReader; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; @@ -59,8 +62,6 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; -import java.io.IOException; -import java.io.OutputStream; import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.stream.Collectors; @@ -68,6 +69,9 @@ import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.StreamingOutput; +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; +import org.glassfish.jersey.media.multipart.FormDataParam; + import javax.xml.stream.XMLStreamException; /** @@ -1729,4 +1733,50 @@ public Response getUserPermissionsOnDataverse(@Context ContainerRequestContext c jsonObjectBuilder.add("canDeleteDataverse", permissionService.userOn(requestUser, dataverse).has(Permission.DeleteDataverse)); return ok(jsonObjectBuilder); } + + @PUT + @AuthRequired + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Path("{identifier}/featuredItems") + public Response updateFeaturedItems(@Context ContainerRequestContext crc, + @PathParam("identifier") String dvIdtf, + @FormDataParam("title") String title, + @FormDataParam("content") String content, + @FormDataParam("file") InputStream fileInputStream, + @FormDataParam("file") FormDataContentDisposition contentDispositionHeader) { + Dataverse dataverse; + try { + dataverse = findDataverseOrDie(dvIdtf); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + try { + String fileName = contentDispositionHeader.getFileName(); + File uploadedFile = new File(createTempDir(dataverse), fileName); + if (!uploadedFile.exists()) { + uploadedFile.createNewFile(); + } + File file = FileUtil.inputStreamToFile(fileInputStream); + if (file.length() > systemConfig.getUploadLogoSizeLimit()) { + return error(Response.Status.BAD_REQUEST, "File is larger than maximum size: " + systemConfig.getUploadLogoSizeLimit() + "."); + } + Files.copy(fileInputStream, uploadedFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new RuntimeException(e); + } + return ok(""); + } + + private File createTempDir(Dataverse editDv) { + try { + // Create the temporary space if not yet existing (will silently ignore preexisting) + // Note that the docroot directory is checked within ConfigCheckService for presence and write access. + java.nio.file.Path tempRoot = java.nio.file.Path.of(JvmSettings.DOCROOT_DIRECTORY.lookup(), "featuredItems"); + Files.createDirectories(tempRoot); + + return Files.createTempDirectory(tempRoot, editDv.getId().toString()).toFile(); + } catch (IOException e) { + throw new RuntimeException("Error creating temp directory", e); // improve error handling + } + } } diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index c47356008ff..193e4d2263f 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -2790,6 +2790,7 @@ dataverses.api.create.dataset.error.mustIncludeAuthorName=Please provide author dataverses.api.validate.json.succeeded=The Dataset JSON provided is valid for this Dataverse Collection. dataverses.api.validate.json.failed=The Dataset JSON provided failed validation with the following error: dataverses.api.validate.json.exception=Validation failed with following exception: +dataverses.api.update.featured.items.error.onlyImageFilesAllowed=Invalid file type. Only image files are allowed. #Access.java access.api.allowRequests.failure.noDataset=Could not find Dataset with id: {0} diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java index 76bb515beb2..53f9b1bf06d 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java @@ -1559,4 +1559,17 @@ public void testGetUserPermissionsOnDataverse() { Response getUserPermissionsOnDataverseInvalidIdResponse = UtilIT.getUserPermissionsOnDataverse("testInvalidAlias", apiToken); getUserPermissionsOnDataverseInvalidIdResponse.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); } + + @Test + public void testUpdateFeaturedItems() { + Response createUserResponse = UtilIT.createRandomUser(); + String apiToken = UtilIT.getApiTokenFromResponse(createUserResponse); + Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); + createDataverseResponse.then().assertThat().statusCode(CREATED.getStatusCode()); + String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + + String pathToTestFile = "src/test/resources/images/coffeeshop.png"; + Response updateFeatureItemsResponse = UtilIT.updateFeaturedItems(dataverseAlias, apiToken, "test", "test", pathToTestFile); + updateFeatureItemsResponse.prettyPrint(); + } } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index 1930610532a..c7886124191 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -4305,4 +4305,14 @@ static Response deleteDatasetTypes(long doomed, String apiToken) { .delete("/api/datasets/datasetTypes/" + doomed); } + static Response updateFeaturedItems(String dataverseAlias, String apiToken, String title, String content, String pathToFile) { + return given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .contentType(ContentType.MULTIPART) + .multiPart("title", title) + .multiPart("content", content) + .multiPart("file", new File(pathToFile)) + .when() + .put("/api/dataverses/" + dataverseAlias + "/featuredItems"); + } } From 755fd4dcd283f960a52c7ee69a222a729e9cd72b Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 23 Dec 2024 13:50:03 +0100 Subject: [PATCH 02/57] Stash: CreateDataverseFeaturedItemCommand WIP --- .../iq/dataverse/DataverseFeaturedItem.java | 22 ++++++++++++++ .../harvard/iq/dataverse/api/Dataverses.java | 4 ++- .../CreateDataverseFeaturedItemCommand.java | 29 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java new file mode 100644 index 00000000000..014f323c1bd --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java @@ -0,0 +1,22 @@ +package edu.harvard.iq.dataverse; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Entity +public class DataverseFeaturedItem { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + public void setId(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index e7ca7033210..2c934f92180 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -1734,6 +1734,7 @@ public Response getUserPermissionsOnDataverse(@Context ContainerRequestContext c return ok(jsonObjectBuilder); } + // TODO @PUT @AuthRequired @Consumes(MediaType.MULTIPART_FORM_DATA) @@ -1742,6 +1743,7 @@ public Response updateFeaturedItems(@Context ContainerRequestContext crc, @PathParam("identifier") String dvIdtf, @FormDataParam("title") String title, @FormDataParam("content") String content, + @FormDataParam("order") int order, @FormDataParam("file") InputStream fileInputStream, @FormDataParam("file") FormDataContentDisposition contentDispositionHeader) { Dataverse dataverse; @@ -1757,7 +1759,7 @@ public Response updateFeaturedItems(@Context ContainerRequestContext crc, uploadedFile.createNewFile(); } File file = FileUtil.inputStreamToFile(fileInputStream); - if (file.length() > systemConfig.getUploadLogoSizeLimit()) { + if (file.length() > 1000000) { return error(Response.Status.BAD_REQUEST, "File is larger than maximum size: " + systemConfig.getUploadLogoSizeLimit() + "."); } Files.copy(fileInputStream, uploadedFile.toPath(), StandardCopyOption.REPLACE_EXISTING); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java new file mode 100644 index 00000000000..7c3c36731dc --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java @@ -0,0 +1,29 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.DataverseFeaturedItem; +import edu.harvard.iq.dataverse.engine.command.AbstractCommand; +import edu.harvard.iq.dataverse.engine.command.CommandContext; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; + +/** + * A command that creates a featured item in a {@link Dataverse}. + */ +//TODO permissions +@RequiredPermissions({}) +public class CreateDataverseFeaturedItemCommand extends AbstractCommand { + + private final Dataverse dataverse; + + public CreateDataverseFeaturedItemCommand(DataverseRequest request, Dataverse dataverse) { + super(request, dataverse); + this.dataverse = dataverse; + } + + @Override + public DataverseFeaturedItem execute(CommandContext ctxt) throws CommandException { + return null; + } +} From 67e3fe0c03702dc82a80c8541c224aa03d3e0034 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 24 Dec 2024 19:04:10 +0100 Subject: [PATCH 03/57] Changed: multiple featured item creation endpoint converted to single --- .../iq/dataverse/DataverseFeaturedItem.java | 57 +++++++++++- .../harvard/iq/dataverse/api/Dataverses.java | 29 ++++++ .../CreateDataverseFeaturedItemCommand.java | 89 +++++++++++++++++-- .../iq/dataverse/settings/JvmSettings.java | 3 + .../iq/dataverse/util/json/JsonPrinter.java | 8 ++ src/main/java/propertyFiles/Bundle.properties | 3 + .../META-INF/microprofile-config.properties | 2 + .../iq/dataverse/api/DataversesIT.java | 16 +++- .../edu/harvard/iq/dataverse/api/UtilIT.java | 4 +- 9 files changed, 198 insertions(+), 13 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java index 014f323c1bd..700fe3f2320 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java @@ -1,17 +1,34 @@ package edu.harvard.iq.dataverse; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Size; @Entity +@Table(indexes = {@Index(columnList = "dataverse_id"), @Index(columnList = "displayOrder")}) public class DataverseFeaturedItem { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @ManyToOne + @JoinColumn(name = "dataverse_id", nullable = false) + private Dataverse dataverse; + + @NotBlank + @Size(max = 15000) + @Lob + @Column(columnDefinition = "TEXT", nullable = false) + private String content; + + @Min(0) + @Column(nullable = false) + private int displayOrder; + + private String imageFileName; + public void setId(Long id) { this.id = id; } @@ -19,4 +36,36 @@ public void setId(Long id) { public Long getId() { return id; } + + public Dataverse getDataverse() { + return dataverse; + } + + public void setDataverse(Dataverse dataverse) { + this.dataverse = dataverse; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public int getDisplayOrder() { + return displayOrder; + } + + public void setDisplayOrder(int displayOrder) { + this.displayOrder = displayOrder; + } + + public String getImageFileName() { + return imageFileName; + } + + public void setImageFileName(String imageFileName) { + this.imageFileName = imageFileName; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index 2c934f92180..315d3279ba6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -1781,4 +1781,33 @@ private File createTempDir(Dataverse editDv) { throw new RuntimeException("Error creating temp directory", e); // improve error handling } } + + @POST + @AuthRequired + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Path("{identifier}/featuredItem") + public Response createFeaturedItem(@Context ContainerRequestContext crc, + @PathParam("identifier") String dvIdtf, + @FormDataParam("content") String content, + @FormDataParam("order") int order, + @FormDataParam("file") InputStream fileInputStream, + @FormDataParam("file") FormDataContentDisposition contentDispositionHeader) { + Dataverse dataverse; + try { + dataverse = findDataverseOrDie(dvIdtf); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + NewDataverseFeaturedItemDTO newDataverseFeaturedItemDTO = NewDataverseFeaturedItemDTO.fromFormData(content, order, fileInputStream, contentDispositionHeader); + try { + DataverseFeaturedItem dataverseFeaturedItem = execCommand(new CreateDataverseFeaturedItemCommand( + createDataverseRequest(getRequestUser(crc)), + dataverse, + newDataverseFeaturedItemDTO + )); + return ok(json(dataverseFeaturedItem)); + } catch (WrappedResponse e) { + return e.getResponse(); + } + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java index 7c3c36731dc..fd29a5f9eab 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java @@ -2,28 +2,105 @@ import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.DataverseFeaturedItem; +import edu.harvard.iq.dataverse.api.dto.NewDataverseFeaturedItemDTO; import edu.harvard.iq.dataverse.engine.command.AbstractCommand; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; +import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.util.BundleUtil; +import edu.harvard.iq.dataverse.util.FileUtil; +import org.apache.tika.Tika; -/** - * A command that creates a featured item in a {@link Dataverse}. - */ -//TODO permissions +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.List; + +// TODO: Permissions @RequiredPermissions({}) public class CreateDataverseFeaturedItemCommand extends AbstractCommand { private final Dataverse dataverse; + private final NewDataverseFeaturedItemDTO newDataverseFeaturedItemDTO; - public CreateDataverseFeaturedItemCommand(DataverseRequest request, Dataverse dataverse) { + public CreateDataverseFeaturedItemCommand(DataverseRequest request, Dataverse dataverse, NewDataverseFeaturedItemDTO newDataverseFeaturedItemDTO) { super(request, dataverse); this.dataverse = dataverse; + this.newDataverseFeaturedItemDTO = newDataverseFeaturedItemDTO; } @Override public DataverseFeaturedItem execute(CommandContext ctxt) throws CommandException { - return null; + DataverseFeaturedItem featuredItem = new DataverseFeaturedItem(); + setImageIfAvailable(featuredItem); + featuredItem.setContent(newDataverseFeaturedItemDTO.getContent()); + featuredItem.setDisplayOrder(newDataverseFeaturedItemDTO.getDisplayOrder()); + featuredItem.setDataverse(dataverse); + return featuredItem; + } + + private void setImageIfAvailable(DataverseFeaturedItem featuredItem) throws IllegalCommandException { + if (newDataverseFeaturedItemDTO.getImageFileName() != null) { + try { + prepareUploadedImageFile(); + } catch (IOException e) { + throw new RuntimeException(BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.imageFileProcessing", List.of(e.getMessage())), e); + } + featuredItem.setImageFileName(newDataverseFeaturedItemDTO.getImageFileName()); + } + } + + private void prepareUploadedImageFile() throws IOException, IllegalCommandException { + // Step 1: Create a temporary directory to store the uploaded image + Path tempDir = createTempDir(); + File uploadedFile = new File(tempDir.toFile(), newDataverseFeaturedItemDTO.getImageFileName()); + + if (!uploadedFile.exists()) { + uploadedFile.createNewFile(); + } + + // Step 2: Convert the InputStream into a temporary file for validation + File tempFile = FileUtil.inputStreamToFile(newDataverseFeaturedItemDTO.getFileInputStream()); + + // Step 3: Validate the uploaded file (type and size) + validateFile(tempFile); + + // Step 4: Copy the validated file to the final destination + Files.copy(tempFile.toPath(), uploadedFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + + private Path createTempDir() throws IOException { + Path tempRoot = Path.of(JvmSettings.DOCROOT_DIRECTORY.lookup(), JvmSettings.FEATURED_ITEMS_IMAGE_UPLOADS_DIRECTORY.lookup(), dataverse.getId().toString(), String.valueOf(newDataverseFeaturedItemDTO.getDisplayOrder())); + Files.createDirectories(tempRoot); + return tempRoot; + } + + private void validateFile(File file) throws IOException, IllegalCommandException { + validateFileType(file); + validateFileSize(file); + } + + private void validateFileType(File file) throws IOException, IllegalCommandException { + Tika tika = new Tika(); + String mimeType = tika.detect(file); + boolean isImageFile = mimeType != null && mimeType.startsWith("image/"); + if (!isImageFile) { + throw new IllegalCommandException( + BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.invalidFileType"), + this + ); + } + } + + private void validateFileSize(File file) throws IllegalCommandException { + Integer featuredItemsImageMaxSize = JvmSettings.FEATURED_ITEMS_IMAGE_MAXSIZE.lookup(Integer.class); + if (file.length() > featuredItemsImageMaxSize) { + throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.fileSizeExceedsLimit", List.of(featuredItemsImageMaxSize.toString())), this); + } } } diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java index d7eea970b8a..482c5ecbbb5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java @@ -52,6 +52,9 @@ public enum JvmSettings { GUESTBOOK_AT_REQUEST(SCOPE_FILES, "guestbook-at-request"), GLOBUS_CACHE_MAXAGE(SCOPE_FILES, "globus-cache-maxage"), GLOBUS_TASK_MONITORING_SERVER(SCOPE_FILES, "globus-monitoring-server"), + SCOPE_FEATURED_ITEMS(SCOPE_FILES, "featured-items"), + FEATURED_ITEMS_IMAGE_MAXSIZE(SCOPE_FEATURED_ITEMS, "image-maxsize"), + FEATURED_ITEMS_IMAGE_UPLOADS_DIRECTORY(SCOPE_FEATURED_ITEMS, "image-uploads"), //STORAGE DRIVER SETTINGS SCOPE_DRIVER(SCOPE_FILES), diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index 91af13c79a3..8d6bf3a9bd2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -1417,4 +1417,12 @@ private static JsonObjectBuilder jsonDataverseInputLevel(DataverseFieldTypeInput jsonObjectBuilder.add("include", inputLevel.isInclude()); return jsonObjectBuilder; } + + public static JsonObjectBuilder json(DataverseFeaturedItem dataverseFeaturedItem) { + return jsonObjectBuilder() + .add("id", dataverseFeaturedItem.getId()) + .add("content", dataverseFeaturedItem.getContent()) + .add("imageFileName", dataverseFeaturedItem.getImageFileName()) + .add("displayOrder", dataverseFeaturedItem.getDisplayOrder()); + } } diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 193e4d2263f..750b1b4f429 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -979,6 +979,9 @@ dataverse.facets.error.fieldtypenotfacetable=Dataset field type '{0}' is not fac dataverse.metadatablocks.error.invalidmetadatablockname=Invalid metadata block name: {0} dataverse.create.error.jsonparse=Error parsing Json: {0} dataverse.create.error.jsonparsetodataverse=Error parsing the POSTed json into a dataverse: {0} +dataverse.create.featuredItem.error.imageFileProcessing=Error processing featured item file: {0} +dataverse.create.featuredItem.error.fileSizeExceedsLimit=File exceeds the maximum size of {0} +dataverse.create.featuredItem.error.invalidFileType=Invalid image file type # rolesAndPermissionsFragment.xhtml # advanced.xhtml diff --git a/src/main/resources/META-INF/microprofile-config.properties b/src/main/resources/META-INF/microprofile-config.properties index b0bc92cf975..95f30b6ba1d 100644 --- a/src/main/resources/META-INF/microprofile-config.properties +++ b/src/main/resources/META-INF/microprofile-config.properties @@ -19,6 +19,8 @@ dataverse.files.directory=${STORAGE_DIR:/tmp/dataverse} dataverse.files.uploads=${STORAGE_DIR:${com.sun.aas.instanceRoot}}/uploads dataverse.files.docroot=${STORAGE_DIR:${com.sun.aas.instanceRoot}}/docroot dataverse.files.globus-cache-maxage=5 +dataverse.files.featured-items.image-maxsize=1000000 +dataverse.files.featured-items.image-uploads=featuredItems # SEARCH INDEX dataverse.solr.host=localhost diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java index 53f9b1bf06d..40567ec2580 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java @@ -1568,8 +1568,22 @@ public void testUpdateFeaturedItems() { createDataverseResponse.then().assertThat().statusCode(CREATED.getStatusCode()); String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + // Should not return any error when passing correct file and data String pathToTestFile = "src/test/resources/images/coffeeshop.png"; - Response updateFeatureItemsResponse = UtilIT.updateFeaturedItems(dataverseAlias, apiToken, "test", "test", pathToTestFile); + Response updateFeatureItemsResponse = UtilIT.createFeaturedItem(dataverseAlias, apiToken, "test", "test", pathToTestFile); + updateFeatureItemsResponse.then().assertThat() + .body("data.content", equalTo("test")) + .body("data.imageFileName", equalTo("coffeeshop.png")) + .body("data.displayOrder", equalTo(0)) + .statusCode(OK.getStatusCode()); + + // Should return error when passing incorrect file type + pathToTestFile = "src/test/resources/tab/test.tab"; + updateFeatureItemsResponse = UtilIT.createFeaturedItem(dataverseAlias, apiToken, "test", "test", pathToTestFile); updateFeatureItemsResponse.prettyPrint(); + updateFeatureItemsResponse.then().assertThat() + .body("message", equalTo(BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.invalidFileType"))) + // FIXME + .statusCode(BAD_REQUEST.getStatusCode()); } } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index c7886124191..4101504db10 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -4305,7 +4305,7 @@ static Response deleteDatasetTypes(long doomed, String apiToken) { .delete("/api/datasets/datasetTypes/" + doomed); } - static Response updateFeaturedItems(String dataverseAlias, String apiToken, String title, String content, String pathToFile) { + static Response createFeaturedItem(String dataverseAlias, String apiToken, String title, String content, String pathToFile) { return given() .header(API_TOKEN_HTTP_HEADER, apiToken) .contentType(ContentType.MULTIPART) @@ -4313,6 +4313,6 @@ static Response updateFeaturedItems(String dataverseAlias, String apiToken, Stri .multiPart("content", content) .multiPart("file", new File(pathToFile)) .when() - .put("/api/dataverses/" + dataverseAlias + "/featuredItems"); + .post("/api/dataverses/" + dataverseAlias + "/featuredItem"); } } From b7352aa4df68a2de076e4bc274f20962da608f4a Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 24 Dec 2024 19:05:27 +0100 Subject: [PATCH 04/57] Removed: unused/incorrect code --- .../harvard/iq/dataverse/api/Dataverses.java | 48 ------------------- 1 file changed, 48 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index 315d3279ba6..ad074703e9c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -1734,54 +1734,6 @@ public Response getUserPermissionsOnDataverse(@Context ContainerRequestContext c return ok(jsonObjectBuilder); } - // TODO - @PUT - @AuthRequired - @Consumes(MediaType.MULTIPART_FORM_DATA) - @Path("{identifier}/featuredItems") - public Response updateFeaturedItems(@Context ContainerRequestContext crc, - @PathParam("identifier") String dvIdtf, - @FormDataParam("title") String title, - @FormDataParam("content") String content, - @FormDataParam("order") int order, - @FormDataParam("file") InputStream fileInputStream, - @FormDataParam("file") FormDataContentDisposition contentDispositionHeader) { - Dataverse dataverse; - try { - dataverse = findDataverseOrDie(dvIdtf); - } catch (WrappedResponse wr) { - return wr.getResponse(); - } - try { - String fileName = contentDispositionHeader.getFileName(); - File uploadedFile = new File(createTempDir(dataverse), fileName); - if (!uploadedFile.exists()) { - uploadedFile.createNewFile(); - } - File file = FileUtil.inputStreamToFile(fileInputStream); - if (file.length() > 1000000) { - return error(Response.Status.BAD_REQUEST, "File is larger than maximum size: " + systemConfig.getUploadLogoSizeLimit() + "."); - } - Files.copy(fileInputStream, uploadedFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - } catch (IOException e) { - throw new RuntimeException(e); - } - return ok(""); - } - - private File createTempDir(Dataverse editDv) { - try { - // Create the temporary space if not yet existing (will silently ignore preexisting) - // Note that the docroot directory is checked within ConfigCheckService for presence and write access. - java.nio.file.Path tempRoot = java.nio.file.Path.of(JvmSettings.DOCROOT_DIRECTORY.lookup(), "featuredItems"); - Files.createDirectories(tempRoot); - - return Files.createTempDirectory(tempRoot, editDv.getId().toString()).toFile(); - } catch (IOException e) { - throw new RuntimeException("Error creating temp directory", e); // improve error handling - } - } - @POST @AuthRequired @Consumes(MediaType.MULTIPART_FORM_DATA) From 0744c306040792ac212b6df804ba8f72a4b03845 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 24 Dec 2024 19:06:12 +0100 Subject: [PATCH 05/57] Removed: unused imports --- src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index ad074703e9c..61dff7173fe 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -24,7 +24,6 @@ import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.ConstraintViolationUtil; -import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.StringUtil; import static edu.harvard.iq.dataverse.util.StringUtil.nonEmpty; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.*; @@ -35,8 +34,6 @@ import edu.harvard.iq.dataverse.util.json.JsonUtil; import java.io.*; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; From 2c96f21f55f4bc787611b80074fb9574db54bc7b Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 26 Dec 2024 11:54:39 +0100 Subject: [PATCH 06/57] Added: persisting featured items in the database --- .../DataverseFeaturedItemServiceBean.java | 25 ++++++++++++ .../iq/dataverse/EjbDataverseEngine.java | 10 ++++- .../api/dto/NewDataverseFeaturedItemDTO.java | 40 +++++++++++++++++++ .../engine/command/CommandContext.java | 23 ++--------- .../CreateDataverseFeaturedItemCommand.java | 2 +- .../dataverse/engine/TestCommandContext.java | 5 +++ 6 files changed, 83 insertions(+), 22 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/api/dto/NewDataverseFeaturedItemDTO.java diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java new file mode 100644 index 00000000000..eb31923a360 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java @@ -0,0 +1,25 @@ +package edu.harvard.iq.dataverse; + +import jakarta.ejb.Stateless; +import jakarta.inject.Named; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import java.io.Serializable; + +@Stateless +@Named +public class DataverseFeaturedItemServiceBean implements Serializable { + + @PersistenceContext(unitName = "VDCNet-ejbPU") + private EntityManager em; + + public DataverseFeaturedItem save(DataverseFeaturedItem dataverseFeaturedItem) { + if (dataverseFeaturedItem.getId() == null) { + em.persist(dataverseFeaturedItem); + return dataverseFeaturedItem; + } else { + return em.merge(dataverseFeaturedItem); + } + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java b/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java index 0561fed8a97..f8ba218d485 100644 --- a/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java +++ b/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java @@ -184,7 +184,10 @@ public class EjbDataverseEngine { ConfirmEmailServiceBean confirmEmailService; @EJB - StorageUseServiceBean storageUseService; + StorageUseServiceBean storageUseService; + + @EJB + DataverseFeaturedItemServiceBean dataverseFeaturedItemServiceBean; @EJB EjbDataverseEngineInner innerEngine; @@ -522,6 +525,11 @@ public DatasetFieldServiceBean dsField() { return dsField; } + @Override + public DataverseFeaturedItemServiceBean dataverseFeaturedItems() { + return dataverseFeaturedItemServiceBean; + } + @Override public StorageUseServiceBean storageUse() { return storageUseService; diff --git a/src/main/java/edu/harvard/iq/dataverse/api/dto/NewDataverseFeaturedItemDTO.java b/src/main/java/edu/harvard/iq/dataverse/api/dto/NewDataverseFeaturedItemDTO.java new file mode 100644 index 00000000000..b81dd67aa97 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/api/dto/NewDataverseFeaturedItemDTO.java @@ -0,0 +1,40 @@ +package edu.harvard.iq.dataverse.api.dto; + +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; + +import java.io.InputStream; + +public class NewDataverseFeaturedItemDTO { + private String content; + private int displayOrder; + private InputStream fileInputStream; + private String imageFileName; + + public static NewDataverseFeaturedItemDTO fromFormData(String content, + int order, + InputStream fileInputStream, + FormDataContentDisposition contentDispositionHeader) { + NewDataverseFeaturedItemDTO newDataverseFeaturedItemDTO = new NewDataverseFeaturedItemDTO(); + newDataverseFeaturedItemDTO.content = content; + newDataverseFeaturedItemDTO.displayOrder = order; + newDataverseFeaturedItemDTO.fileInputStream = fileInputStream; + newDataverseFeaturedItemDTO.imageFileName = contentDispositionHeader.getFileName(); + return newDataverseFeaturedItemDTO; + } + + public String getContent() { + return content; + } + + public int getDisplayOrder() { + return displayOrder; + } + + public InputStream getFileInputStream() { + return fileInputStream; + } + + public String getImageFileName() { + return imageFileName; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/CommandContext.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/CommandContext.java index 282cbb88988..b58f5f07ebc 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/CommandContext.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/CommandContext.java @@ -1,28 +1,9 @@ package edu.harvard.iq.dataverse.engine.command; -import edu.harvard.iq.dataverse.DataFileServiceBean; -import edu.harvard.iq.dataverse.DatasetFieldServiceBean; -import edu.harvard.iq.dataverse.DatasetLinkingServiceBean; -import edu.harvard.iq.dataverse.DatasetServiceBean; -import edu.harvard.iq.dataverse.DatasetVersionServiceBean; -import edu.harvard.iq.dataverse.DataverseFacetServiceBean; -import edu.harvard.iq.dataverse.DataverseFieldTypeInputLevelServiceBean; -import edu.harvard.iq.dataverse.DataverseLinkingServiceBean; -import edu.harvard.iq.dataverse.DataverseRoleServiceBean; -import edu.harvard.iq.dataverse.DataverseServiceBean; +import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUserServiceBean; -import edu.harvard.iq.dataverse.DvObjectServiceBean; -import edu.harvard.iq.dataverse.FeaturedDataverseServiceBean; -import edu.harvard.iq.dataverse.FileDownloadServiceBean; -import edu.harvard.iq.dataverse.GuestbookResponseServiceBean; -import edu.harvard.iq.dataverse.GuestbookServiceBean; -import edu.harvard.iq.dataverse.MetadataBlockServiceBean; import edu.harvard.iq.dataverse.search.IndexServiceBean; -import edu.harvard.iq.dataverse.PermissionServiceBean; -import edu.harvard.iq.dataverse.RoleAssigneeServiceBean; import edu.harvard.iq.dataverse.search.SearchServiceBean; -import edu.harvard.iq.dataverse.TemplateServiceBean; -import edu.harvard.iq.dataverse.UserNotificationServiceBean; import edu.harvard.iq.dataverse.actionlogging.ActionLogServiceBean; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean; @@ -152,4 +133,6 @@ public interface CommandContext { public void addCommand(Command command); public DatasetFieldServiceBean dsField(); + + public DataverseFeaturedItemServiceBean dataverseFeaturedItems(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java index fd29a5f9eab..ee496148ab5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java @@ -41,7 +41,7 @@ public DataverseFeaturedItem execute(CommandContext ctxt) throws CommandExceptio featuredItem.setContent(newDataverseFeaturedItemDTO.getContent()); featuredItem.setDisplayOrder(newDataverseFeaturedItemDTO.getDisplayOrder()); featuredItem.setDataverse(dataverse); - return featuredItem; + return ctxt.dataverseFeaturedItems().save(featuredItem); } private void setImageIfAvailable(DataverseFeaturedItem featuredItem) throws IllegalCommandException { diff --git a/src/test/java/edu/harvard/iq/dataverse/engine/TestCommandContext.java b/src/test/java/edu/harvard/iq/dataverse/engine/TestCommandContext.java index b4b9c0d33f2..fd7e3f69bd2 100644 --- a/src/test/java/edu/harvard/iq/dataverse/engine/TestCommandContext.java +++ b/src/test/java/edu/harvard/iq/dataverse/engine/TestCommandContext.java @@ -246,6 +246,11 @@ public StorageUseServiceBean storageUse() { return null; } + @Override + public DataverseFeaturedItemServiceBean dataverseFeaturedItems() { + return null; + } + @Override public void beginCommandSequence() { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. From 9cc611799a0fcab9b07cbf3cdcfc462d53093460 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 26 Dec 2024 12:06:30 +0100 Subject: [PATCH 07/57] Added: new InvalidCommandArgumentsException and using it in CreateDataverseFeaturedItemCommand --- .../iq/dataverse/api/AbstractApiBean.java | 7 +++--- .../InvalidCommandArgumentsException.java | 25 +++++++++++++++++++ .../CreateDataverseFeaturedItemCommand.java | 16 ++++++------ 3 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/engine/command/exception/InvalidCommandArgumentsException.java diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index 3257a3cc7ac..4efd161db53 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -14,14 +14,11 @@ import edu.harvard.iq.dataverse.dataset.DatasetTypeServiceBean; import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; -import edu.harvard.iq.dataverse.engine.command.exception.CommandException; -import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; -import edu.harvard.iq.dataverse.engine.command.exception.PermissionException; +import edu.harvard.iq.dataverse.engine.command.exception.*; import edu.harvard.iq.dataverse.engine.command.impl.GetDraftDatasetVersionCommand; import edu.harvard.iq.dataverse.engine.command.impl.GetLatestAccessibleDatasetVersionCommand; import edu.harvard.iq.dataverse.engine.command.impl.GetLatestPublishedDatasetVersionCommand; import edu.harvard.iq.dataverse.engine.command.impl.GetSpecificPublishedDatasetVersionCommand; -import edu.harvard.iq.dataverse.engine.command.exception.RateLimitCommandException; import edu.harvard.iq.dataverse.externaltools.ExternalToolServiceBean; import edu.harvard.iq.dataverse.license.LicenseServiceBean; import edu.harvard.iq.dataverse.pidproviders.PidUtil; @@ -635,6 +632,8 @@ protected T execCommand( Command cmd ) throws WrappedResponse { throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, "User " + cmd.getRequest().getUser().getIdentifier() + " is not permitted to perform requested action.") ); + } catch (InvalidCommandArgumentsException ex) { + throw new WrappedResponse(ex, error(Status.BAD_REQUEST, ex.getMessage())); } catch (CommandException ex) { Logger.getLogger(AbstractApiBean.class.getName()).log(Level.SEVERE, "Error while executing command " + cmd, ex); throw new WrappedResponse(ex, error(Status.INTERNAL_SERVER_ERROR, ex.getMessage())); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/exception/InvalidCommandArgumentsException.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/exception/InvalidCommandArgumentsException.java new file mode 100644 index 00000000000..95c6f52b880 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/exception/InvalidCommandArgumentsException.java @@ -0,0 +1,25 @@ +package edu.harvard.iq.dataverse.engine.command.exception; + +import edu.harvard.iq.dataverse.engine.command.Command; + +/** + * Exception thrown when a {@link Command} is executed with invalid or malformed arguments. + *

+ * This exception typically indicates that the input parameters provided to the command + * do not meet the required criteria (e.g., missing fields, invalid formats, or other + * constraints). + *

+ *

+ * Example scenarios: + *

    + *
  • A required argument is null or missing.
  • + *
  • An argument is in an invalid format (e.g., a malformed email address).
  • + *
  • Arguments violate business rules or constraints.
  • + *
+ */ +public class InvalidCommandArgumentsException extends CommandException { + + public InvalidCommandArgumentsException(String message, Command aCommand) { + super(message, aCommand); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java index ee496148ab5..36506c67e43 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java @@ -8,7 +8,7 @@ import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; -import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException; +import edu.harvard.iq.dataverse.engine.command.exception.InvalidCommandArgumentsException; import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.FileUtil; @@ -44,7 +44,7 @@ public DataverseFeaturedItem execute(CommandContext ctxt) throws CommandExceptio return ctxt.dataverseFeaturedItems().save(featuredItem); } - private void setImageIfAvailable(DataverseFeaturedItem featuredItem) throws IllegalCommandException { + private void setImageIfAvailable(DataverseFeaturedItem featuredItem) throws InvalidCommandArgumentsException { if (newDataverseFeaturedItemDTO.getImageFileName() != null) { try { prepareUploadedImageFile(); @@ -55,7 +55,7 @@ private void setImageIfAvailable(DataverseFeaturedItem featuredItem) throws Ille } } - private void prepareUploadedImageFile() throws IOException, IllegalCommandException { + private void prepareUploadedImageFile() throws IOException, InvalidCommandArgumentsException { // Step 1: Create a temporary directory to store the uploaded image Path tempDir = createTempDir(); File uploadedFile = new File(tempDir.toFile(), newDataverseFeaturedItemDTO.getImageFileName()); @@ -80,27 +80,27 @@ private Path createTempDir() throws IOException { return tempRoot; } - private void validateFile(File file) throws IOException, IllegalCommandException { + private void validateFile(File file) throws IOException, InvalidCommandArgumentsException { validateFileType(file); validateFileSize(file); } - private void validateFileType(File file) throws IOException, IllegalCommandException { + private void validateFileType(File file) throws IOException, InvalidCommandArgumentsException { Tika tika = new Tika(); String mimeType = tika.detect(file); boolean isImageFile = mimeType != null && mimeType.startsWith("image/"); if (!isImageFile) { - throw new IllegalCommandException( + throw new InvalidCommandArgumentsException( BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.invalidFileType"), this ); } } - private void validateFileSize(File file) throws IllegalCommandException { + private void validateFileSize(File file) throws InvalidCommandArgumentsException { Integer featuredItemsImageMaxSize = JvmSettings.FEATURED_ITEMS_IMAGE_MAXSIZE.lookup(Integer.class); if (file.length() > featuredItemsImageMaxSize) { - throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.fileSizeExceedsLimit", List.of(featuredItemsImageMaxSize.toString())), this); + throw new InvalidCommandArgumentsException(BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.fileSizeExceedsLimit", List.of(featuredItemsImageMaxSize.toString())), this); } } } From 11a05925233dab975d20168e4a39ede1e6924563 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 26 Dec 2024 16:14:37 +0100 Subject: [PATCH 08/57] Added: endpoint for listing featured items --- .../edu/harvard/iq/dataverse/Dataverse.java | 7 ++++ .../harvard/iq/dataverse/api/Dataverses.java | 13 +++++++ .../CreateDataverseFeaturedItemCommand.java | 4 +- .../ListDataverseFeaturedItemsCommand.java | 39 +++++++++++++++++++ .../iq/dataverse/util/json/JsonPrinter.java | 8 ++++ .../iq/dataverse/api/DataversesIT.java | 8 ++-- .../edu/harvard/iq/dataverse/api/UtilIT.java | 7 ++++ 7 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseFeaturedItemsCommand.java diff --git a/src/main/java/edu/harvard/iq/dataverse/Dataverse.java b/src/main/java/edu/harvard/iq/dataverse/Dataverse.java index 1f11725e581..cf6c763d8fc 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Dataverse.java +++ b/src/main/java/edu/harvard/iq/dataverse/Dataverse.java @@ -351,6 +351,13 @@ public void setMetadataBlockFacets(List metadataBlo this.metadataBlockFacets = metadataBlockFacets; } + @OneToMany(mappedBy = "dataverse") + private List dataverseFeaturedItems = new ArrayList<>(); + + public List getDataverseFeaturedItems() { + return this.dataverseFeaturedItems; + } + public List getParentGuestbooks() { List retList = new ArrayList<>(); Dataverse testDV = this; diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index 61dff7173fe..b2f174d9449 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -1759,4 +1759,17 @@ public Response createFeaturedItem(@Context ContainerRequestContext crc, return e.getResponse(); } } + + @GET + @AuthRequired + @Path("{identifier}/featuredItems") + public Response listFeaturedItems(@Context ContainerRequestContext crc, @PathParam("identifier") String dvIdtf) { + try { + Dataverse dataverse = findDataverseOrDie(dvIdtf); + List featuredItems = execCommand(new ListDataverseFeaturedItemsCommand(createDataverseRequest(getRequestUser(crc)), dataverse)); + return ok(jsonDataverseFeaturedItems(featuredItems)); + } catch (WrappedResponse e) { + return e.getResponse(); + } + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java index 36506c67e43..3057824b937 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java @@ -3,6 +3,7 @@ import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.DataverseFeaturedItem; import edu.harvard.iq.dataverse.api.dto.NewDataverseFeaturedItemDTO; +import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.engine.command.AbstractCommand; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; @@ -21,8 +22,7 @@ import java.nio.file.StandardCopyOption; import java.util.List; -// TODO: Permissions -@RequiredPermissions({}) +@RequiredPermissions({Permission.EditDataverse}) public class CreateDataverseFeaturedItemCommand extends AbstractCommand { private final Dataverse dataverse; diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseFeaturedItemsCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseFeaturedItemsCommand.java new file mode 100644 index 00000000000..d8d36352e79 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseFeaturedItemsCommand.java @@ -0,0 +1,39 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.DataverseFeaturedItem; +import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.engine.command.AbstractCommand; +import edu.harvard.iq.dataverse.engine.command.CommandContext; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * List the featured items {@link DataverseFeaturedItem} of a {@link Dataverse}. + */ +public class ListDataverseFeaturedItemsCommand extends AbstractCommand> { + + private final Dataverse dataverse; + + public ListDataverseFeaturedItemsCommand(DataverseRequest request, Dataverse dataverse) { + super(request, dataverse); + this.dataverse = dataverse; + } + + @Override + public List execute(CommandContext ctxt) throws CommandException { + return dataverse.getDataverseFeaturedItems(); + } + + @Override + public Map> getRequiredPermissions() { + return Collections.singletonMap("", + dataverse.isReleased() ? Collections.emptySet() + : Collections.singleton(Permission.ViewUnpublishedDataverse)); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index 8d6bf3a9bd2..a4b5faa1e12 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -1418,6 +1418,14 @@ private static JsonObjectBuilder jsonDataverseInputLevel(DataverseFieldTypeInput return jsonObjectBuilder; } + public static JsonArrayBuilder jsonDataverseFeaturedItems(List dataverseFeaturedItems) { + JsonArrayBuilder featuredItemsArrayBuilder = Json.createArrayBuilder(); + for (DataverseFeaturedItem dataverseFeaturedItem : dataverseFeaturedItems) { + featuredItemsArrayBuilder.add(json(dataverseFeaturedItem)); + } + return featuredItemsArrayBuilder; + } + public static JsonObjectBuilder json(DataverseFeaturedItem dataverseFeaturedItem) { return jsonObjectBuilder() .add("id", dataverseFeaturedItem.getId()) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java index 40567ec2580..72205022b8c 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java @@ -1561,7 +1561,7 @@ public void testGetUserPermissionsOnDataverse() { } @Test - public void testUpdateFeaturedItems() { + public void testCreateFeaturedItem() { Response createUserResponse = UtilIT.createRandomUser(); String apiToken = UtilIT.getApiTokenFromResponse(createUserResponse); Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); @@ -1577,13 +1577,15 @@ public void testUpdateFeaturedItems() { .body("data.displayOrder", equalTo(0)) .statusCode(OK.getStatusCode()); + // TODO + Response listFeaturedItemsResponse = UtilIT.listDataverseFeaturedItems(dataverseAlias, apiToken); + listFeaturedItemsResponse.prettyPrint(); + // Should return error when passing incorrect file type pathToTestFile = "src/test/resources/tab/test.tab"; updateFeatureItemsResponse = UtilIT.createFeaturedItem(dataverseAlias, apiToken, "test", "test", pathToTestFile); - updateFeatureItemsResponse.prettyPrint(); updateFeatureItemsResponse.then().assertThat() .body("message", equalTo(BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.invalidFileType"))) - // FIXME .statusCode(BAD_REQUEST.getStatusCode()); } } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index 4101504db10..b9947e2e870 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -4315,4 +4315,11 @@ static Response createFeaturedItem(String dataverseAlias, String apiToken, Strin .when() .post("/api/dataverses/" + dataverseAlias + "/featuredItem"); } + + static Response listDataverseFeaturedItems(String dataverseAlias, String apiToken) { + return given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .contentType("application/json") + .get("/api/dataverses/" + dataverseAlias + "/featuredItems"); + } } From ec3b6f2d741008e191d062d8305c332af6dfb72c Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 27 Dec 2024 18:56:31 +0000 Subject: [PATCH 09/57] Added: returning imageUrl with new associated endpoint for retrieving the featured item image from the client --- .../iq/dataverse/DataverseFeaturedItem.java | 5 ++ .../DataverseFeaturedItemServiceBean.java | 17 ++++++ .../edu/harvard/iq/dataverse/api/Access.java | 60 +++++++------------ .../CreateDataverseFeaturedItemCommand.java | 14 ++--- .../impl/GetDataverseFeaturedItemCommand.java | 37 ++++++++++++ .../ListDataverseFeaturedItemsCommand.java | 2 +- .../iq/dataverse/util/json/JsonPrinter.java | 1 + 7 files changed, 91 insertions(+), 45 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDataverseFeaturedItemCommand.java diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java index 700fe3f2320..48a2fc335fa 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java @@ -1,5 +1,6 @@ package edu.harvard.iq.dataverse; +import edu.harvard.iq.dataverse.util.SystemConfig; import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Min; @@ -68,4 +69,8 @@ public String getImageFileName() { public void setImageFileName(String imageFileName) { this.imageFileName = imageFileName; } + + public String getImageFileUrl() { + return SystemConfig.getDataverseSiteUrlStatic() + "/api/access/dataverseFeatureItemImage/" + id; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java index eb31923a360..21bf9b82b0f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java @@ -1,11 +1,16 @@ package edu.harvard.iq.dataverse; +import edu.harvard.iq.dataverse.settings.JvmSettings; import jakarta.ejb.Stateless; import jakarta.inject.Named; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; +import java.io.IOException; +import java.io.InputStream; import java.io.Serializable; +import java.nio.file.Files; +import java.nio.file.Path; @Stateless @Named @@ -14,6 +19,10 @@ public class DataverseFeaturedItemServiceBean implements Serializable { @PersistenceContext(unitName = "VDCNet-ejbPU") private EntityManager em; + public DataverseFeaturedItem findById(Long id) { + return em.find(DataverseFeaturedItem.class, id); + } + public DataverseFeaturedItem save(DataverseFeaturedItem dataverseFeaturedItem) { if (dataverseFeaturedItem.getId() == null) { em.persist(dataverseFeaturedItem); @@ -22,4 +31,12 @@ public DataverseFeaturedItem save(DataverseFeaturedItem dataverseFeaturedItem) { return em.merge(dataverseFeaturedItem); } } + + public InputStream getImageFileAsInputStream(DataverseFeaturedItem dataverseFeaturedItem) throws IOException { + Path imagePath = Path.of(JvmSettings.DOCROOT_DIRECTORY.lookup(), + JvmSettings.FEATURED_ITEMS_IMAGE_UPLOADS_DIRECTORY.lookup(), + dataverseFeaturedItem.getDataverse().getId().toString(), + dataverseFeaturedItem.getImageFileName()); + return Files.newInputStream(imagePath); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Access.java b/src/main/java/edu/harvard/iq/dataverse/api/Access.java index 16ac884180b..d970c5eed44 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Access.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Access.java @@ -6,32 +6,7 @@ package edu.harvard.iq.dataverse.api; -import edu.harvard.iq.dataverse.AuxiliaryFile; -import edu.harvard.iq.dataverse.AuxiliaryFileServiceBean; -import edu.harvard.iq.dataverse.DataCitation; -import edu.harvard.iq.dataverse.DataFile; -import edu.harvard.iq.dataverse.FileAccessRequest; -import edu.harvard.iq.dataverse.FileMetadata; -import edu.harvard.iq.dataverse.DataFileServiceBean; -import edu.harvard.iq.dataverse.Dataset; -import edu.harvard.iq.dataverse.DatasetVersion; -import edu.harvard.iq.dataverse.DatasetVersionServiceBean; -import edu.harvard.iq.dataverse.DatasetServiceBean; -import edu.harvard.iq.dataverse.Dataverse; -import edu.harvard.iq.dataverse.DataverseRequestServiceBean; -import edu.harvard.iq.dataverse.DataverseRoleServiceBean; -import edu.harvard.iq.dataverse.DataverseServiceBean; -import edu.harvard.iq.dataverse.DataverseSession; -import edu.harvard.iq.dataverse.DataverseTheme; -import edu.harvard.iq.dataverse.FileDownloadServiceBean; -import edu.harvard.iq.dataverse.GuestbookResponse; -import edu.harvard.iq.dataverse.GuestbookResponseServiceBean; -import edu.harvard.iq.dataverse.PermissionServiceBean; -import edu.harvard.iq.dataverse.PermissionsWrapper; -import edu.harvard.iq.dataverse.RoleAssignment; -import edu.harvard.iq.dataverse.UserNotification; -import edu.harvard.iq.dataverse.UserNotificationServiceBean; -import edu.harvard.iq.dataverse.ThemeWidgetFragment; +import edu.harvard.iq.dataverse.*; import static edu.harvard.iq.dataverse.api.Datasets.handleVersion; @@ -55,15 +30,7 @@ import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; -import edu.harvard.iq.dataverse.engine.command.impl.AssignRoleCommand; -import edu.harvard.iq.dataverse.engine.command.impl.GetDatasetCommand; -import edu.harvard.iq.dataverse.engine.command.impl.GetDraftDatasetVersionCommand; -import edu.harvard.iq.dataverse.engine.command.impl.GetLatestAccessibleDatasetVersionCommand; -import edu.harvard.iq.dataverse.engine.command.impl.GetLatestPublishedDatasetVersionCommand; -import edu.harvard.iq.dataverse.engine.command.impl.GetSpecificPublishedDatasetVersionCommand; -import edu.harvard.iq.dataverse.engine.command.impl.RequestAccessCommand; -import edu.harvard.iq.dataverse.engine.command.impl.RevokeRoleCommand; -import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand; +import edu.harvard.iq.dataverse.engine.command.impl.*; import edu.harvard.iq.dataverse.export.DDIExportServiceBean; import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean; import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean.MakeDataCountEntry; @@ -88,7 +55,6 @@ import java.util.Arrays; import java.util.Date; import java.util.List; -import java.util.Properties; import java.util.logging.Level; import jakarta.inject.Inject; import jakarta.json.Json; @@ -133,7 +99,6 @@ import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.media.Content; -import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; @@ -199,6 +164,8 @@ public class Access extends AbstractApiBean { PermissionsWrapper permissionsWrapper; @Inject MakeDataCountLoggingServiceBean mdcLogService; + @Inject + DataverseFeaturedItemServiceBean dataverseFeaturedItemServiceBean; //@EJB @@ -2015,4 +1982,23 @@ private URI handleCustomZipDownload(User user, String customZipServiceUrl, Strin } return redirectUri; } + + @Path("dataverseFeatureItemImage/{itemId}") + @GET + @Produces({"image/png"}) + public InputStream getDataverseFeatureItemImage(@Context ContainerRequestContext crc, @PathParam("itemId") Long itemId) { + DataverseFeaturedItem dataverseFeaturedItem; + try { + dataverseFeaturedItem = execCommand(new GetDataverseFeaturedItemCommand(createDataverseRequest(getRequestUser(crc)), dataverseFeaturedItemServiceBean.findById(itemId))); + } catch (WrappedResponse wr) { + logger.warning("Cannot locate a dataverse featured item with id " + itemId); + return null; + } + try { + return dataverseFeaturedItemServiceBean.getImageFileAsInputStream(dataverseFeaturedItem); + } catch (IOException e) { + logger.warning("Error while obtaining the input stream for the image file associated with the dataverse featured item with id " + itemId); + return null; + } + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java index 3057824b937..074e6f34743 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java @@ -56,9 +56,9 @@ private void setImageIfAvailable(DataverseFeaturedItem featuredItem) throws Inva } private void prepareUploadedImageFile() throws IOException, InvalidCommandArgumentsException { - // Step 1: Create a temporary directory to store the uploaded image - Path tempDir = createTempDir(); - File uploadedFile = new File(tempDir.toFile(), newDataverseFeaturedItemDTO.getImageFileName()); + // Step 1: Create a directory to store the uploaded image + Path imageDir = createImageDir(); + File uploadedFile = new File(imageDir.toFile(), newDataverseFeaturedItemDTO.getImageFileName()); if (!uploadedFile.exists()) { uploadedFile.createNewFile(); @@ -74,10 +74,10 @@ private void prepareUploadedImageFile() throws IOException, InvalidCommandArgume Files.copy(tempFile.toPath(), uploadedFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } - private Path createTempDir() throws IOException { - Path tempRoot = Path.of(JvmSettings.DOCROOT_DIRECTORY.lookup(), JvmSettings.FEATURED_ITEMS_IMAGE_UPLOADS_DIRECTORY.lookup(), dataverse.getId().toString(), String.valueOf(newDataverseFeaturedItemDTO.getDisplayOrder())); - Files.createDirectories(tempRoot); - return tempRoot; + private Path createImageDir() throws IOException { + Path imagePath = Path.of(JvmSettings.DOCROOT_DIRECTORY.lookup(), JvmSettings.FEATURED_ITEMS_IMAGE_UPLOADS_DIRECTORY.lookup(), dataverse.getId().toString()); + Files.createDirectories(imagePath); + return imagePath; } private void validateFile(File file) throws IOException, InvalidCommandArgumentsException { diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDataverseFeaturedItemCommand.java new file mode 100644 index 00000000000..722c8050dbd --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDataverseFeaturedItemCommand.java @@ -0,0 +1,37 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.DataverseFeaturedItem; +import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.engine.command.AbstractCommand; +import edu.harvard.iq.dataverse.engine.command.CommandContext; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +/** + * Retrieves a particular featured item {@link DataverseFeaturedItem}. + */ +public class GetDataverseFeaturedItemCommand extends AbstractCommand { + + private final DataverseFeaturedItem dataverseFeaturedItem; + + public GetDataverseFeaturedItemCommand(DataverseRequest request, DataverseFeaturedItem dataverseFeaturedItem) { + super(request, dataverseFeaturedItem.getDataverse()); + this.dataverseFeaturedItem = dataverseFeaturedItem; + } + + @Override + public DataverseFeaturedItem execute(CommandContext ctxt) throws CommandException { + return dataverseFeaturedItem; + } + + @Override + public Map> getRequiredPermissions() { + return Collections.singletonMap("", + dataverseFeaturedItem.getDataverse().isReleased() ? Collections.emptySet() + : Collections.singleton(Permission.ViewUnpublishedDataverse)); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseFeaturedItemsCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseFeaturedItemsCommand.java index d8d36352e79..9ff2d2e1e71 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseFeaturedItemsCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseFeaturedItemsCommand.java @@ -14,7 +14,7 @@ import java.util.Set; /** - * List the featured items {@link DataverseFeaturedItem} of a {@link Dataverse}. + * Lists the featured items {@link DataverseFeaturedItem} of a {@link Dataverse}. */ public class ListDataverseFeaturedItemsCommand extends AbstractCommand> { diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index a4b5faa1e12..4f5b2898eff 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -1431,6 +1431,7 @@ public static JsonObjectBuilder json(DataverseFeaturedItem dataverseFeaturedItem .add("id", dataverseFeaturedItem.getId()) .add("content", dataverseFeaturedItem.getContent()) .add("imageFileName", dataverseFeaturedItem.getImageFileName()) + .add("imageFileUrl", dataverseFeaturedItem.getImageFileUrl()) .add("displayOrder", dataverseFeaturedItem.getDisplayOrder()); } } From 160ffa592e09d4fc17c4a22caf0e17012628bf83 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 30 Dec 2024 08:36:02 +0000 Subject: [PATCH 10/57] Removed: wrongly committed files --- ...arvard.iq.dataverse.pidproviders.PidProviderFactory | 6 ------ .../META-INF/services/io.gdcc.spi.export.Exporter | 10 ---------- 2 files changed, 16 deletions(-) delete mode 100644 src/main/resources/META-INF/services/edu.harvard.iq.dataverse.pidproviders.PidProviderFactory delete mode 100644 src/main/resources/META-INF/services/io.gdcc.spi.export.Exporter diff --git a/src/main/resources/META-INF/services/edu.harvard.iq.dataverse.pidproviders.PidProviderFactory b/src/main/resources/META-INF/services/edu.harvard.iq.dataverse.pidproviders.PidProviderFactory deleted file mode 100644 index acdfd927e45..00000000000 --- a/src/main/resources/META-INF/services/edu.harvard.iq.dataverse.pidproviders.PidProviderFactory +++ /dev/null @@ -1,6 +0,0 @@ -edu.harvard.iq.dataverse.pidproviders.doi.crossref.CrossRefDOIProviderFactory -edu.harvard.iq.dataverse.pidproviders.doi.datacite.DataCiteProviderFactory -edu.harvard.iq.dataverse.pidproviders.doi.ezid.EZIdProviderFactory -edu.harvard.iq.dataverse.pidproviders.doi.fake.FakeProviderFactory -edu.harvard.iq.dataverse.pidproviders.handle.HandleProviderFactory -edu.harvard.iq.dataverse.pidproviders.perma.PermaLinkProviderFactory diff --git a/src/main/resources/META-INF/services/io.gdcc.spi.export.Exporter b/src/main/resources/META-INF/services/io.gdcc.spi.export.Exporter deleted file mode 100644 index 873cea58911..00000000000 --- a/src/main/resources/META-INF/services/io.gdcc.spi.export.Exporter +++ /dev/null @@ -1,10 +0,0 @@ -edu.harvard.iq.dataverse.export.DCTermsExporter -edu.harvard.iq.dataverse.export.DDIExporter -edu.harvard.iq.dataverse.export.DataCiteExporter -edu.harvard.iq.dataverse.export.DublinCoreExporter -edu.harvard.iq.dataverse.export.HtmlCodeBookExporter -edu.harvard.iq.dataverse.export.JSONExporter -edu.harvard.iq.dataverse.export.OAI_DDIExporter -edu.harvard.iq.dataverse.export.OAI_OREExporter -edu.harvard.iq.dataverse.export.OpenAireExporter -edu.harvard.iq.dataverse.export.SchemaDotOrgExporter From f124f4a7c912d3bc2d965e0eeba85f9a1456669a Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 30 Dec 2024 11:43:14 +0000 Subject: [PATCH 11/57] Stash: relocating file-specific logic and unit tests in progress --- .../iq/dataverse/DataverseFeaturedItem.java | 8 +- .../DataverseFeaturedItemServiceBean.java | 56 ++++++++ .../api/dto/NewDataverseFeaturedItemDTO.java | 16 +++ .../CreateDataverseFeaturedItemCommand.java | 84 ++++-------- ...reateDataverseFeaturedItemCommandTest.java | 120 ++++++++++++++++++ 5 files changed, 218 insertions(+), 66 deletions(-) create mode 100644 src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java index 48a2fc335fa..ef2421623f3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java @@ -30,14 +30,14 @@ public class DataverseFeaturedItem { private String imageFileName; - public void setId(Long id) { - this.id = id; - } - public Long getId() { return id; } + public void setId(Long id) { + this.id = id; + } + public Dataverse getDataverse() { return dataverse; } diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java index 21bf9b82b0f..1c71f667b5c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java @@ -1,16 +1,22 @@ package edu.harvard.iq.dataverse; import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.util.BundleUtil; +import edu.harvard.iq.dataverse.util.FileUtil; import jakarta.ejb.Stateless; import jakarta.inject.Named; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; +import org.apache.tika.Tika; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.List; @Stateless @Named @@ -39,4 +45,54 @@ public InputStream getImageFileAsInputStream(DataverseFeaturedItem dataverseFeat dataverseFeaturedItem.getImageFileName()); return Files.newInputStream(imagePath); } + + public void saveDataverseFeaturedItemImageFile(InputStream inputStream, String imageFileName, Long dataverseId) throws IOException { + File tempFile = FileUtil.inputStreamToFile(inputStream); + validateImageFile(tempFile); + + Path imageDir = createDataverseFeaturedItemImageDir(dataverseId); + File uploadedFile = new File(imageDir.toFile(), imageFileName); + + if (!uploadedFile.exists()) { + uploadedFile.createNewFile(); + } + + Files.copy(tempFile.toPath(), uploadedFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + + // TODO: Move file-specific logic out of here + + private Path createDataverseFeaturedItemImageDir(Long dataverseId) throws IOException { + Path imagePath = Path.of(JvmSettings.DOCROOT_DIRECTORY.lookup(), JvmSettings.FEATURED_ITEMS_IMAGE_UPLOADS_DIRECTORY.lookup(), dataverseId.toString()); + Files.createDirectories(imagePath); + return imagePath; + } + + private void validateImageFile(File file) throws IOException, IllegalArgumentException { + if (!isValidDataverseFeaturedItemFileType(file)) { + throw new IllegalArgumentException( + BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.invalidFileType") + ); + } + if (!isValidDataverseFeaturedItemFileSize(file)) { + String maxAllowedSize = getMaxAllowedDataverseFeaturedItemFileSize().toString(); + throw new IllegalArgumentException( + BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.fileSizeExceedsLimit", List.of(maxAllowedSize)) + ); + } + } + + private boolean isValidDataverseFeaturedItemFileType(File file) throws IOException { + Tika tika = new Tika(); + String mimeType = tika.detect(file); + return mimeType != null && mimeType.startsWith("image/"); + } + + private boolean isValidDataverseFeaturedItemFileSize(File file) { + return file.length() <= getMaxAllowedDataverseFeaturedItemFileSize(); + } + + private Integer getMaxAllowedDataverseFeaturedItemFileSize() { + return JvmSettings.FEATURED_ITEMS_IMAGE_MAXSIZE.lookup(Integer.class); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/dto/NewDataverseFeaturedItemDTO.java b/src/main/java/edu/harvard/iq/dataverse/api/dto/NewDataverseFeaturedItemDTO.java index b81dd67aa97..e1f5190cb41 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/dto/NewDataverseFeaturedItemDTO.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/dto/NewDataverseFeaturedItemDTO.java @@ -22,18 +22,34 @@ public static NewDataverseFeaturedItemDTO fromFormData(String content, return newDataverseFeaturedItemDTO; } + public void setContent(String content) { + this.content = content; + } + public String getContent() { return content; } + public void setDisplayOrder(int displayOrder) { + this.displayOrder = displayOrder; + } + public int getDisplayOrder() { return displayOrder; } + public void setFileInputStream(InputStream fileInputStream) { + this.fileInputStream = fileInputStream; + } + public InputStream getFileInputStream() { return fileInputStream; } + public void setImageFileName(String imageFileName) { + this.imageFileName = imageFileName; + } + public String getImageFileName() { return imageFileName; } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java index 074e6f34743..c79f36c10fa 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java @@ -10,16 +10,9 @@ import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.InvalidCommandArgumentsException; -import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.util.BundleUtil; -import edu.harvard.iq.dataverse.util.FileUtil; -import org.apache.tika.Tika; -import java.io.File; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.util.List; @RequiredPermissions({Permission.EditDataverse}) @@ -37,70 +30,37 @@ public CreateDataverseFeaturedItemCommand(DataverseRequest request, Dataverse da @Override public DataverseFeaturedItem execute(CommandContext ctxt) throws CommandException { DataverseFeaturedItem featuredItem = new DataverseFeaturedItem(); - setImageIfAvailable(featuredItem); + + setImageIfAvailable(featuredItem, ctxt); + featuredItem.setContent(newDataverseFeaturedItemDTO.getContent()); featuredItem.setDisplayOrder(newDataverseFeaturedItemDTO.getDisplayOrder()); featuredItem.setDataverse(dataverse); + return ctxt.dataverseFeaturedItems().save(featuredItem); } - private void setImageIfAvailable(DataverseFeaturedItem featuredItem) throws InvalidCommandArgumentsException { - if (newDataverseFeaturedItemDTO.getImageFileName() != null) { + private void setImageIfAvailable(DataverseFeaturedItem featuredItem, CommandContext ctxt) throws CommandException { + String imageFileName = newDataverseFeaturedItemDTO.getImageFileName(); + if (imageFileName != null) { try { - prepareUploadedImageFile(); + ctxt.dataverseFeaturedItems().saveDataverseFeaturedItemImageFile(newDataverseFeaturedItemDTO.getFileInputStream(), imageFileName, dataverse.getId()); + } catch (IllegalArgumentException e) { + // TODO check the message that is thrown + throw new InvalidCommandArgumentsException( + e.getMessage(), + this + ); } catch (IOException e) { - throw new RuntimeException(BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.imageFileProcessing", List.of(e.getMessage())), e); + throw new CommandException( + BundleUtil.getStringFromBundle( + "dataverse.create.featuredItem.error.imageFileProcessing", + List.of(e.getMessage()) + ), + this + ); } - featuredItem.setImageFileName(newDataverseFeaturedItemDTO.getImageFileName()); - } - } - - private void prepareUploadedImageFile() throws IOException, InvalidCommandArgumentsException { - // Step 1: Create a directory to store the uploaded image - Path imageDir = createImageDir(); - File uploadedFile = new File(imageDir.toFile(), newDataverseFeaturedItemDTO.getImageFileName()); - - if (!uploadedFile.exists()) { - uploadedFile.createNewFile(); - } - - // Step 2: Convert the InputStream into a temporary file for validation - File tempFile = FileUtil.inputStreamToFile(newDataverseFeaturedItemDTO.getFileInputStream()); - - // Step 3: Validate the uploaded file (type and size) - validateFile(tempFile); - - // Step 4: Copy the validated file to the final destination - Files.copy(tempFile.toPath(), uploadedFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - } - - private Path createImageDir() throws IOException { - Path imagePath = Path.of(JvmSettings.DOCROOT_DIRECTORY.lookup(), JvmSettings.FEATURED_ITEMS_IMAGE_UPLOADS_DIRECTORY.lookup(), dataverse.getId().toString()); - Files.createDirectories(imagePath); - return imagePath; - } - - private void validateFile(File file) throws IOException, InvalidCommandArgumentsException { - validateFileType(file); - validateFileSize(file); - } - - private void validateFileType(File file) throws IOException, InvalidCommandArgumentsException { - Tika tika = new Tika(); - String mimeType = tika.detect(file); - boolean isImageFile = mimeType != null && mimeType.startsWith("image/"); - if (!isImageFile) { - throw new InvalidCommandArgumentsException( - BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.invalidFileType"), - this - ); - } - } - - private void validateFileSize(File file) throws InvalidCommandArgumentsException { - Integer featuredItemsImageMaxSize = JvmSettings.FEATURED_ITEMS_IMAGE_MAXSIZE.lookup(Integer.class); - if (file.length() > featuredItemsImageMaxSize) { - throw new InvalidCommandArgumentsException(BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.fileSizeExceedsLimit", List.of(featuredItemsImageMaxSize.toString())), this); + featuredItem.setImageFileName(imageFileName); } } } diff --git a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java new file mode 100644 index 00000000000..8506af85343 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java @@ -0,0 +1,120 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.DataverseFeaturedItem; +import edu.harvard.iq.dataverse.DataverseFeaturedItemServiceBean; +import edu.harvard.iq.dataverse.api.dto.NewDataverseFeaturedItemDTO; +import edu.harvard.iq.dataverse.engine.command.CommandContext; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import edu.harvard.iq.dataverse.engine.command.exception.InvalidCommandArgumentsException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.IOException; +import java.io.InputStream; + +import static edu.harvard.iq.dataverse.mocks.MocksFactory.makeRequest; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class CreateDataverseFeaturedItemCommandTest { + @Mock + private CommandContext contextStub; + + @Mock + private DataverseFeaturedItemServiceBean dataverseFeaturedItemServiceStub; + + @InjectMocks + private CreateDataverseFeaturedItemCommand sut; + + private Dataverse testDataverse; + private NewDataverseFeaturedItemDTO testNewDataverseFeaturedItemDTO; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + + testDataverse = new Dataverse(); + testDataverse.setId(123L); + + testNewDataverseFeaturedItemDTO = new NewDataverseFeaturedItemDTO(); + testNewDataverseFeaturedItemDTO.setImageFileName("test.png"); + testNewDataverseFeaturedItemDTO.setContent("test content"); + testNewDataverseFeaturedItemDTO.setDisplayOrder(0); + testNewDataverseFeaturedItemDTO.setFileInputStream(mock(InputStream.class)); + + when(contextStub.dataverseFeaturedItems()).thenReturn(dataverseFeaturedItemServiceStub); + sut = new CreateDataverseFeaturedItemCommand(makeRequest(), testDataverse, testNewDataverseFeaturedItemDTO); + } + + @Test + void execute_imageFileProvidedAndValid_savesFeaturedItem() throws Exception { + DataverseFeaturedItem expectedFeaturedItem = new DataverseFeaturedItem(); + expectedFeaturedItem.setDataverse(testDataverse); + expectedFeaturedItem.setImageFileName(testNewDataverseFeaturedItemDTO.getImageFileName()); + expectedFeaturedItem.setDisplayOrder(testNewDataverseFeaturedItemDTO.getDisplayOrder()); + expectedFeaturedItem.setContent(testNewDataverseFeaturedItemDTO.getContent()); + + when(dataverseFeaturedItemServiceStub.save(any(DataverseFeaturedItem.class))).thenReturn(expectedFeaturedItem); + + DataverseFeaturedItem result = sut.execute(contextStub); + + assertNotNull(result); + + assertEquals(testNewDataverseFeaturedItemDTO.getImageFileName(), result.getImageFileName()); + assertEquals(testNewDataverseFeaturedItemDTO.getDisplayOrder(), result.getDisplayOrder()); + assertEquals(testNewDataverseFeaturedItemDTO.getContent(), result.getContent()); + assertEquals(testDataverse, result.getDataverse()); + + verify(dataverseFeaturedItemServiceStub).save(any(DataverseFeaturedItem.class)); + verify(dataverseFeaturedItemServiceStub).saveDataverseFeaturedItemImageFile( + testNewDataverseFeaturedItemDTO.getFileInputStream(), + testNewDataverseFeaturedItemDTO.getImageFileName(), + testDataverse.getId() + ); + } + + @Test + void execute_noImageFileProvided_featuredItemSavedWithoutImage() throws Exception { + testNewDataverseFeaturedItemDTO.setImageFileName(null); + + DataverseFeaturedItem expectedFeaturedItem = new DataverseFeaturedItem(); + when(dataverseFeaturedItemServiceStub.save(any(DataverseFeaturedItem.class))).thenReturn(expectedFeaturedItem); + + DataverseFeaturedItem result = sut.execute(contextStub); + + assertNotNull(result); + verify(dataverseFeaturedItemServiceStub).save(any(DataverseFeaturedItem.class)); + verify(dataverseFeaturedItemServiceStub, never()).saveDataverseFeaturedItemImageFile(any(), any(), any()); + } + + @Test + void execute_imageFileProcessingFails_throwsCommandException() throws IOException { + testNewDataverseFeaturedItemDTO.setImageFileName("invalid.png"); + InputStream inputStreamMock = mock(InputStream.class); + testNewDataverseFeaturedItemDTO.setFileInputStream(inputStreamMock); + + doThrow(new IOException("File processing failed")) + .when(dataverseFeaturedItemServiceStub) + .saveDataverseFeaturedItemImageFile(any(InputStream.class), any(String.class), any(Long.class)); + + CommandException exception = assertThrows(CommandException.class, () -> sut.execute(contextStub)); + assertTrue(exception.getMessage().contains("File processing failed")); + } + + @Test + void execute_invalidArgumentsProvided_throwsInvalidCommandArgumentsException() throws IOException { + testNewDataverseFeaturedItemDTO.setImageFileName("invalid.png"); + InputStream inputStreamMock = mock(InputStream.class); + testNewDataverseFeaturedItemDTO.setFileInputStream(inputStreamMock); + + doThrow(new IllegalArgumentException("Invalid file type")) + .when(dataverseFeaturedItemServiceStub).saveDataverseFeaturedItemImageFile(any(InputStream.class), any(String.class), any(Long.class)); + + InvalidCommandArgumentsException exception = assertThrows(InvalidCommandArgumentsException.class, () -> sut.execute(contextStub)); + assertTrue(exception.getMessage().contains("Invalid file type")); + } +} From 64ec37be6387a93ce31e26a455d817b0b5e737ee Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 31 Dec 2024 09:41:24 +0000 Subject: [PATCH 12/57] Fixed: exception handling when image file is invalid in a featured item --- .../DataverseFeaturedItemServiceBean.java | 14 ++++++++++---- .../impl/CreateDataverseFeaturedItemCommand.java | 4 ++-- .../CreateDataverseFeaturedItemCommandTest.java | 6 +++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java index 1c71f667b5c..acefdeb01a8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java @@ -22,6 +22,12 @@ @Named public class DataverseFeaturedItemServiceBean implements Serializable { + public static class InvalidImageFileException extends Exception { + public InvalidImageFileException(String message) { + super(message); + } + } + @PersistenceContext(unitName = "VDCNet-ejbPU") private EntityManager em; @@ -46,7 +52,7 @@ public InputStream getImageFileAsInputStream(DataverseFeaturedItem dataverseFeat return Files.newInputStream(imagePath); } - public void saveDataverseFeaturedItemImageFile(InputStream inputStream, String imageFileName, Long dataverseId) throws IOException { + public void saveDataverseFeaturedItemImageFile(InputStream inputStream, String imageFileName, Long dataverseId) throws IOException, InvalidImageFileException { File tempFile = FileUtil.inputStreamToFile(inputStream); validateImageFile(tempFile); @@ -68,15 +74,15 @@ private Path createDataverseFeaturedItemImageDir(Long dataverseId) throws IOExce return imagePath; } - private void validateImageFile(File file) throws IOException, IllegalArgumentException { + private void validateImageFile(File file) throws IOException, InvalidImageFileException { if (!isValidDataverseFeaturedItemFileType(file)) { - throw new IllegalArgumentException( + throw new InvalidImageFileException( BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.invalidFileType") ); } if (!isValidDataverseFeaturedItemFileSize(file)) { String maxAllowedSize = getMaxAllowedDataverseFeaturedItemFileSize().toString(); - throw new IllegalArgumentException( + throw new InvalidImageFileException( BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.fileSizeExceedsLimit", List.of(maxAllowedSize)) ); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java index c79f36c10fa..8873a3efea4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java @@ -2,6 +2,7 @@ import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.DataverseFeaturedItem; +import edu.harvard.iq.dataverse.DataverseFeaturedItemServiceBean; import edu.harvard.iq.dataverse.api.dto.NewDataverseFeaturedItemDTO; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.engine.command.AbstractCommand; @@ -45,8 +46,7 @@ private void setImageIfAvailable(DataverseFeaturedItem featuredItem, CommandCont if (imageFileName != null) { try { ctxt.dataverseFeaturedItems().saveDataverseFeaturedItemImageFile(newDataverseFeaturedItemDTO.getFileInputStream(), imageFileName, dataverse.getId()); - } catch (IllegalArgumentException e) { - // TODO check the message that is thrown + } catch (DataverseFeaturedItemServiceBean.InvalidImageFileException e) { throw new InvalidCommandArgumentsException( e.getMessage(), this diff --git a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java index 8506af85343..350fc66c230 100644 --- a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java @@ -92,7 +92,7 @@ void execute_noImageFileProvided_featuredItemSavedWithoutImage() throws Exceptio } @Test - void execute_imageFileProcessingFails_throwsCommandException() throws IOException { + void execute_imageFileProcessingFails_throwsCommandException() throws IOException, DataverseFeaturedItemServiceBean.InvalidImageFileException { testNewDataverseFeaturedItemDTO.setImageFileName("invalid.png"); InputStream inputStreamMock = mock(InputStream.class); testNewDataverseFeaturedItemDTO.setFileInputStream(inputStreamMock); @@ -106,12 +106,12 @@ void execute_imageFileProcessingFails_throwsCommandException() throws IOExceptio } @Test - void execute_invalidArgumentsProvided_throwsInvalidCommandArgumentsException() throws IOException { + void execute_invalidArgumentsProvided_throwsInvalidCommandArgumentsException() throws IOException, DataverseFeaturedItemServiceBean.InvalidImageFileException { testNewDataverseFeaturedItemDTO.setImageFileName("invalid.png"); InputStream inputStreamMock = mock(InputStream.class); testNewDataverseFeaturedItemDTO.setFileInputStream(inputStreamMock); - doThrow(new IllegalArgumentException("Invalid file type")) + doThrow(new DataverseFeaturedItemServiceBean.InvalidImageFileException("Invalid file type")) .when(dataverseFeaturedItemServiceStub).saveDataverseFeaturedItemImageFile(any(InputStream.class), any(String.class), any(Long.class)); InvalidCommandArgumentsException exception = assertThrows(InvalidCommandArgumentsException.class, () -> sut.execute(contextStub)); From 98f3645725a54a17dfef2629b4008aab1f2318d9 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 31 Dec 2024 10:47:14 +0000 Subject: [PATCH 13/57] Added: tweaks and fixes for featured items --- .../iq/dataverse/DataverseFeaturedItem.java | 5 ++- .../DataverseFeaturedItemServiceBean.java | 37 +++++-------------- .../edu/harvard/iq/dataverse/api/Access.java | 3 +- .../harvard/iq/dataverse/util/FileUtil.java | 13 +++++++ 4 files changed, 28 insertions(+), 30 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java index ef2421623f3..92bbc5223dd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java @@ -71,6 +71,9 @@ public void setImageFileName(String imageFileName) { } public String getImageFileUrl() { - return SystemConfig.getDataverseSiteUrlStatic() + "/api/access/dataverseFeatureItemImage/" + id; + if (id != null) { + return SystemConfig.getDataverseSiteUrlStatic() + "/api/access/dataverseFeatureItemImage/" + id; + } + return null; } } diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java index acefdeb01a8..1ac5eb76651 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java @@ -7,7 +7,6 @@ import jakarta.inject.Named; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; -import org.apache.tika.Tika; import java.io.File; import java.io.IOException; @@ -56,7 +55,11 @@ public void saveDataverseFeaturedItemImageFile(InputStream inputStream, String i File tempFile = FileUtil.inputStreamToFile(inputStream); validateImageFile(tempFile); - Path imageDir = createDataverseFeaturedItemImageDir(dataverseId); + Path imageDir = FileUtil.createDirStructure( + JvmSettings.DOCROOT_DIRECTORY.lookup(), + JvmSettings.FEATURED_ITEMS_IMAGE_UPLOADS_DIRECTORY.lookup(), + dataverseId.toString() + ); File uploadedFile = new File(imageDir.toFile(), imageFileName); if (!uploadedFile.exists()) { @@ -66,39 +69,17 @@ public void saveDataverseFeaturedItemImageFile(InputStream inputStream, String i Files.copy(tempFile.toPath(), uploadedFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } - // TODO: Move file-specific logic out of here - - private Path createDataverseFeaturedItemImageDir(Long dataverseId) throws IOException { - Path imagePath = Path.of(JvmSettings.DOCROOT_DIRECTORY.lookup(), JvmSettings.FEATURED_ITEMS_IMAGE_UPLOADS_DIRECTORY.lookup(), dataverseId.toString()); - Files.createDirectories(imagePath); - return imagePath; - } - private void validateImageFile(File file) throws IOException, InvalidImageFileException { - if (!isValidDataverseFeaturedItemFileType(file)) { + if (!FileUtil.isFileOfImageType(file)) { throw new InvalidImageFileException( BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.invalidFileType") ); } - if (!isValidDataverseFeaturedItemFileSize(file)) { - String maxAllowedSize = getMaxAllowedDataverseFeaturedItemFileSize().toString(); + Integer maxAllowedSize = JvmSettings.FEATURED_ITEMS_IMAGE_MAXSIZE.lookup(Integer.class); + if (file.length() > maxAllowedSize) { throw new InvalidImageFileException( - BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.fileSizeExceedsLimit", List.of(maxAllowedSize)) + BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.fileSizeExceedsLimit", List.of(maxAllowedSize.toString())) ); } } - - private boolean isValidDataverseFeaturedItemFileType(File file) throws IOException { - Tika tika = new Tika(); - String mimeType = tika.detect(file); - return mimeType != null && mimeType.startsWith("image/"); - } - - private boolean isValidDataverseFeaturedItemFileSize(File file) { - return file.length() <= getMaxAllowedDataverseFeaturedItemFileSize(); - } - - private Integer getMaxAllowedDataverseFeaturedItemFileSize() { - return JvmSettings.FEATURED_ITEMS_IMAGE_MAXSIZE.lookup(Integer.class); - } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Access.java b/src/main/java/edu/harvard/iq/dataverse/api/Access.java index d970c5eed44..0bf02411d3a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Access.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Access.java @@ -1983,9 +1983,10 @@ private URI handleCustomZipDownload(User user, String customZipServiceUrl, Strin return redirectUri; } - @Path("dataverseFeatureItemImage/{itemId}") @GET + @AuthRequired @Produces({"image/png"}) + @Path("dataverseFeatureItemImage/{itemId}") public InputStream getDataverseFeatureItemImage(@Context ContainerRequestContext crc, @PathParam("itemId") Long itemId) { DataverseFeaturedItem dataverseFeaturedItem; try { diff --git a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java index 991682ec8e8..e134c64277c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java @@ -103,6 +103,7 @@ import java.util.Arrays; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.tika.Tika; import ucar.nc2.NetcdfFile; import ucar.nc2.NetcdfFiles; @@ -1828,4 +1829,16 @@ public static String getStorageDriver(DataFile dataFile) { public static String sanitizeFileName(String fileNameIn) { return fileNameIn == null ? null : fileNameIn.replace(' ', '_').replaceAll("[\\\\/:*?\"<>|,;]", ""); } + + public static Path createDirStructure(String rootDirectory, String... subdirectories) throws IOException { + Path path = Path.of(rootDirectory, subdirectories); + Files.createDirectories(path); + return path; + } + + public static boolean isFileOfImageType(File file) throws IOException { + Tika tika = new Tika(); + String mimeType = tika.detect(file); + return mimeType != null && mimeType.startsWith("image/"); + } } From ea03a07df5139153fee6b3ea75d20192dedf01ea Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 1 Jan 2025 11:33:30 +0000 Subject: [PATCH 14/57] Added: flush call when saving DataverseFeaturedItem --- .../harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java index 1ac5eb76651..e41d2d59551 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java @@ -37,6 +37,7 @@ public DataverseFeaturedItem findById(Long id) { public DataverseFeaturedItem save(DataverseFeaturedItem dataverseFeaturedItem) { if (dataverseFeaturedItem.getId() == null) { em.persist(dataverseFeaturedItem); + em.flush(); return dataverseFeaturedItem; } else { return em.merge(dataverseFeaturedItem); From ea388ba6a1d665fa4aab11c6994d0abc088b4f23 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 1 Jan 2025 11:33:57 +0000 Subject: [PATCH 15/57] Added: DeleteDataverseFeaturedItemCommand --- .../DeleteDataverseFeaturedItemCommand.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseFeaturedItemCommand.java diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseFeaturedItemCommand.java new file mode 100644 index 00000000000..7ed0e60cccb --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseFeaturedItemCommand.java @@ -0,0 +1,28 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.DataverseFeaturedItem; +import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.engine.command.*; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; + +/** + * Deletes a particular featured item {@link DataverseFeaturedItem} of a {@link Dataverse}. + */ +@RequiredPermissions({Permission.EditDataverse}) +public class DeleteDataverseFeaturedItemCommand extends AbstractVoidCommand { + + private final DataverseFeaturedItem doomed; + + public DeleteDataverseFeaturedItemCommand(DataverseRequest request, DataverseFeaturedItem doomed) { + super(request, doomed.getDataverse()); + this.doomed = doomed; + } + + @Override + protected void executeImpl(CommandContext ctxt) throws CommandException { + ctxt.em().merge(doomed.getDataverse()); + DataverseFeaturedItem doomedAndMerged = ctxt.em().merge(doomed); + ctxt.em().remove(doomedAndMerged); + } +} From 7ede831144f238cd9e3292e4a9b3a96d6e43e01e Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 1 Jan 2025 11:34:47 +0000 Subject: [PATCH 16/57] Added: DataverseFeaturedItems API with delete method --- .../dataverse/api/DataverseFeaturedItems.java | 38 +++++++++++++++++++ src/main/java/propertyFiles/Bundle.properties | 4 ++ .../api/DataverseFeaturedItemsIT.java | 27 +++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 src/main/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItems.java create mode 100644 src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java diff --git a/src/main/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItems.java b/src/main/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItems.java new file mode 100644 index 00000000000..de372d55bb1 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItems.java @@ -0,0 +1,38 @@ +package edu.harvard.iq.dataverse.api; + +import edu.harvard.iq.dataverse.*; +import edu.harvard.iq.dataverse.api.auth.AuthRequired; +import edu.harvard.iq.dataverse.engine.command.impl.*; +import edu.harvard.iq.dataverse.util.BundleUtil; +import jakarta.ejb.Stateless; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; + +import java.text.MessageFormat; + +@Stateless +@Path("dataverseFeaturedItems") +public class DataverseFeaturedItems extends AbstractApiBean { + + @Inject + DataverseFeaturedItemServiceBean dataverseFeaturedItemServiceBean; + + @DELETE + @AuthRequired + @Path("{itemId}") + public Response deleteItem(@Context ContainerRequestContext crc, @PathParam("itemId") Long itemId) { + try { + DataverseFeaturedItem dataverseFeaturedItem = dataverseFeaturedItemServiceBean.findById(itemId); + if (dataverseFeaturedItem == null) { + throw new WrappedResponse(error(Response.Status.NOT_FOUND, MessageFormat.format(BundleUtil.getStringFromBundle("dataverseFeaturedItems.errors.notFound"), itemId))); + } + execCommand(new DeleteDataverseFeaturedItemCommand(createDataverseRequest(getRequestUser(crc)), dataverseFeaturedItem)); + return ok(MessageFormat.format(BundleUtil.getStringFromBundle("dataverseFeaturedItems.delete.successful"), itemId)); + } catch (WrappedResponse e) { + return e.getResponse(); + } + } +} diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index f01e17dceea..947f914c396 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -3111,3 +3111,7 @@ bearerTokenAuthMechanism.errors.tokenValidatedButNoRegisteredUser=Bearer token i authenticationServiceBean.errors.unauthorizedBearerToken=Unauthorized bearer token. authenticationServiceBean.errors.invalidBearerToken=Could not parse bearer token. authenticationServiceBean.errors.bearerTokenDetectedNoOIDCProviderConfigured=Bearer token detected, no OIDC provider configured. + +#DataverseFeaturedItems.java +dataverseFeaturedItems.errors.notFound=Can't find dataverse featured item with identifier='{0}' +dataverseFeaturedItems.delete.successful=Successfully deleted dataverse featured item with identifier='{0}' diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java new file mode 100644 index 00000000000..d3e6a3f9364 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java @@ -0,0 +1,27 @@ +package edu.harvard.iq.dataverse.api; + +import io.restassured.RestAssured; +import io.restassured.response.Response; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static jakarta.ws.rs.core.Response.Status.*; + +public class DataverseFeaturedItemsIT { + + @BeforeAll + public static void setUpClass() { + RestAssured.baseURI = UtilIT.getRestAssuredBaseUri(); + } + + @Test + public void testDeleteFeaturedItem() { + Response createUserResponse = UtilIT.createRandomUser(); + String apiToken = UtilIT.getApiTokenFromResponse(createUserResponse); + Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); + createDataverseResponse.then().assertThat().statusCode(CREATED.getStatusCode()); + String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + + // TODO + } +} From d50a943c7e65172470d310743570e849bba31396 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 1 Jan 2025 14:00:26 +0000 Subject: [PATCH 17/57] Added: DataverseFeaturedItems API with delete method IT --- .../iq/dataverse/DataverseFeaturedItem.java | 3 +-- .../dataverse/api/DataverseFeaturedItems.java | 10 ++++----- .../api/DataverseFeaturedItemsIT.java | 22 ++++++++++++++++++- .../edu/harvard/iq/dataverse/api/UtilIT.java | 8 ++++++- tests/integration-tests.txt | 2 +- 5 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java index 92bbc5223dd..d0040745bdf 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java @@ -7,7 +7,6 @@ import jakarta.validation.constraints.Size; @Entity -@Table(indexes = {@Index(columnList = "dataverse_id"), @Index(columnList = "displayOrder")}) public class DataverseFeaturedItem { @Id @@ -15,7 +14,7 @@ public class DataverseFeaturedItem { private Long id; @ManyToOne - @JoinColumn(name = "dataverse_id", nullable = false) + @JoinColumn(nullable = false) private Dataverse dataverse; @NotBlank diff --git a/src/main/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItems.java b/src/main/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItems.java index de372d55bb1..4d4c246a6b1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItems.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItems.java @@ -22,15 +22,15 @@ public class DataverseFeaturedItems extends AbstractApiBean { @DELETE @AuthRequired - @Path("{itemId}") - public Response deleteItem(@Context ContainerRequestContext crc, @PathParam("itemId") Long itemId) { + @Path("{id}") + public Response deleteFeaturedItem(@Context ContainerRequestContext crc, @PathParam("id") Long id) { try { - DataverseFeaturedItem dataverseFeaturedItem = dataverseFeaturedItemServiceBean.findById(itemId); + DataverseFeaturedItem dataverseFeaturedItem = dataverseFeaturedItemServiceBean.findById(id); if (dataverseFeaturedItem == null) { - throw new WrappedResponse(error(Response.Status.NOT_FOUND, MessageFormat.format(BundleUtil.getStringFromBundle("dataverseFeaturedItems.errors.notFound"), itemId))); + throw new WrappedResponse(error(Response.Status.NOT_FOUND, MessageFormat.format(BundleUtil.getStringFromBundle("dataverseFeaturedItems.errors.notFound"), id))); } execCommand(new DeleteDataverseFeaturedItemCommand(createDataverseRequest(getRequestUser(crc)), dataverseFeaturedItem)); - return ok(MessageFormat.format(BundleUtil.getStringFromBundle("dataverseFeaturedItems.delete.successful"), itemId)); + return ok(MessageFormat.format(BundleUtil.getStringFromBundle("dataverseFeaturedItems.delete.successful"), id)); } catch (WrappedResponse e) { return e.getResponse(); } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java index d3e6a3f9364..57d21ceb4f9 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java @@ -1,6 +1,7 @@ package edu.harvard.iq.dataverse.api; import io.restassured.RestAssured; +import io.restassured.path.json.JsonPath; import io.restassured.response.Response; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -22,6 +23,25 @@ public void testDeleteFeaturedItem() { createDataverseResponse.then().assertThat().statusCode(CREATED.getStatusCode()); String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); - // TODO + String pathToTestFile = "src/test/resources/images/coffeeshop.png"; + Response createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, apiToken, "test", "test", pathToTestFile); + createFeatureItemResponse.then().assertThat().statusCode(OK.getStatusCode()); + + JsonPath createdFeaturedItem = JsonPath.from(createFeatureItemResponse.body().asString()); + Long featuredItemId = createdFeaturedItem.getLong("data.id"); + + // Should return not found when passing incorrect item id + Response deleteFeatureItemResponse = UtilIT.deleteDataverseFeaturedItem(100000L, apiToken); + deleteFeatureItemResponse.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); + + // Should return unauthorized when passing correct id and user does not have permissions + Response createRandomUser = UtilIT.createRandomUser(); + String randomUserApiToken = UtilIT.getApiTokenFromResponse(createRandomUser); + deleteFeatureItemResponse = UtilIT.deleteDataverseFeaturedItem(featuredItemId, randomUserApiToken); + deleteFeatureItemResponse.then().assertThat().statusCode(UNAUTHORIZED.getStatusCode()); + + // Should delete featured item when passing correct id and user have permissions + deleteFeatureItemResponse = UtilIT.deleteDataverseFeaturedItem(featuredItemId, apiToken); + deleteFeatureItemResponse.then().assertThat().statusCode(OK.getStatusCode()); } } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index 6c4eae0d06f..11d303156ae 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -4360,7 +4360,7 @@ static Response performKeycloakROPCLogin(String username, String password) { .post("http://keycloak.mydomain.com:8090/realms/test/protocol/openid-connect/token"); } - static Response createFeaturedItem(String dataverseAlias, String apiToken, String title, String content, String pathToFile) { + static Response createDataverseFeaturedItem(String dataverseAlias, String apiToken, String title, String content, String pathToFile) { return given() .header(API_TOKEN_HTTP_HEADER, apiToken) .contentType(ContentType.MULTIPART) @@ -4371,6 +4371,12 @@ static Response createFeaturedItem(String dataverseAlias, String apiToken, Strin .post("/api/dataverses/" + dataverseAlias + "/featuredItem"); } + static Response deleteDataverseFeaturedItem(long id, String apiToken) { + return given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .delete("/api/dataverseFeaturedItems/" + id); + } + static Response listDataverseFeaturedItems(String dataverseAlias, String apiToken) { return given() .header(API_TOKEN_HTTP_HEADER, apiToken) diff --git a/tests/integration-tests.txt b/tests/integration-tests.txt index e1dad7a75b1..a20d65e26e3 100644 --- a/tests/integration-tests.txt +++ b/tests/integration-tests.txt @@ -1 +1 @@ -DataversesIT,DatasetsIT,SwordIT,AdminIT,BuiltinUsersIT,UsersIT,UtilIT,ConfirmEmailIT,FileMetadataIT,FilesIT,SearchIT,InReviewWorkflowIT,HarvestingServerIT,HarvestingClientsIT,MoveIT,MakeDataCountApiIT,FileTypeDetectionIT,EditDDIIT,ExternalToolsIT,AccessIT,DuplicateFilesIT,DownloadFilesIT,LinkIT,DeleteUsersIT,DeactivateUsersIT,AuxiliaryFilesIT,InvalidCharactersIT,LicensesIT,NotificationsIT,BagIT,MetadataBlocksIT,NetcdfIT,SignpostingIT,FitsIT,LogoutIT,DataRetrieverApiIT,ProvIT,S3AccessIT,OpenApiIT,InfoIT,DatasetFieldsIT,SavedSearchIT,DatasetTypesIT \ No newline at end of file +DataversesIT,DatasetsIT,SwordIT,AdminIT,BuiltinUsersIT,UsersIT,UtilIT,ConfirmEmailIT,FileMetadataIT,FilesIT,SearchIT,InReviewWorkflowIT,HarvestingServerIT,HarvestingClientsIT,MoveIT,MakeDataCountApiIT,FileTypeDetectionIT,EditDDIIT,ExternalToolsIT,AccessIT,DuplicateFilesIT,DownloadFilesIT,LinkIT,DeleteUsersIT,DeactivateUsersIT,AuxiliaryFilesIT,InvalidCharactersIT,LicensesIT,NotificationsIT,BagIT,MetadataBlocksIT,NetcdfIT,SignpostingIT,FitsIT,LogoutIT,DataRetrieverApiIT,ProvIT,S3AccessIT,OpenApiIT,InfoIT,DatasetFieldsIT,SavedSearchIT,DatasetTypesIT,DataverseFeaturedItemsIT \ No newline at end of file From 220e02a1749bc9d7af617cc7a993768529fa1b4c Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 1 Jan 2025 14:27:07 +0000 Subject: [PATCH 18/57] Added: DataverseFeaturedItemsIT assertion --- .../harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java index 57d21ceb4f9..58b33183e54 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test; import static jakarta.ws.rs.core.Response.Status.*; +import static org.hamcrest.CoreMatchers.equalTo; public class DataverseFeaturedItemsIT { @@ -43,5 +44,10 @@ public void testDeleteFeaturedItem() { // Should delete featured item when passing correct id and user have permissions deleteFeatureItemResponse = UtilIT.deleteDataverseFeaturedItem(featuredItemId, apiToken); deleteFeatureItemResponse.then().assertThat().statusCode(OK.getStatusCode()); + + Response listFeaturedItemsResponse = UtilIT.listDataverseFeaturedItems(dataverseAlias, apiToken); + listFeaturedItemsResponse.then() + .body("data.size()", equalTo(0)) + .assertThat().statusCode(OK.getStatusCode()); } } From 1bc2ba74f413e617972ef4e894d2fa934941791e Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 1 Jan 2025 14:37:03 +0000 Subject: [PATCH 19/57] Added: testCreateFeaturedItem IT cases --- .../iq/dataverse/api/DataversesIT.java | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java index 488e2d93120..a083716d2e2 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java @@ -1587,22 +1587,30 @@ public void testCreateFeaturedItem() { // Should not return any error when passing correct file and data String pathToTestFile = "src/test/resources/images/coffeeshop.png"; - Response updateFeatureItemsResponse = UtilIT.createFeaturedItem(dataverseAlias, apiToken, "test", "test", pathToTestFile); - updateFeatureItemsResponse.then().assertThat() + Response createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, apiToken, "test", "test", pathToTestFile); + createFeatureItemResponse.then().assertThat() .body("data.content", equalTo("test")) .body("data.imageFileName", equalTo("coffeeshop.png")) .body("data.displayOrder", equalTo(0)) .statusCode(OK.getStatusCode()); - // TODO - Response listFeaturedItemsResponse = UtilIT.listDataverseFeaturedItems(dataverseAlias, apiToken); - listFeaturedItemsResponse.prettyPrint(); - - // Should return error when passing incorrect file type - pathToTestFile = "src/test/resources/tab/test.tab"; - updateFeatureItemsResponse = UtilIT.createFeaturedItem(dataverseAlias, apiToken, "test", "test", pathToTestFile); - updateFeatureItemsResponse.then().assertThat() + // Should return bad request error when passing incorrect file type + pathToTestFile = "src/test/resources/tab/test.tab"; + createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, apiToken, "test", "test", pathToTestFile); + createFeatureItemResponse.then().assertThat() .body("message", equalTo(BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.invalidFileType"))) .statusCode(BAD_REQUEST.getStatusCode()); + + // Should return unauthorized error when user has no permissions + Response createRandomUser = UtilIT.createRandomUser(); + String randomUserApiToken = UtilIT.getApiTokenFromResponse(createRandomUser); + createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, randomUserApiToken, "test", "test", pathToTestFile); + createFeatureItemResponse.then().assertThat().statusCode(UNAUTHORIZED.getStatusCode()); + + // Should return not found error when dataverse does not exist + createFeatureItemResponse = UtilIT.createDataverseFeaturedItem("thisDataverseDoesNotExist", apiToken, "test", "test", pathToTestFile); + createFeatureItemResponse.then().assertThat() + .body("message", equalTo("Can't find dataverse with identifier='thisDataverseDoesNotExist'")) + .statusCode(NOT_FOUND.getStatusCode()); } } From f7d60c7269eb4391d8944be8656a84fe3e0aac37 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 2 Jan 2025 14:06:37 +0000 Subject: [PATCH 20/57] Added: handling image file optionality in dataverse featured items --- .../iq/dataverse/DataverseFeaturedItem.java | 2 +- .../harvard/iq/dataverse/api/Dataverses.java | 6 ++--- .../api/dto/NewDataverseFeaturedItemDTO.java | 25 +++++++++++-------- .../CreateDataverseFeaturedItemCommand.java | 6 +++-- .../api/DataverseFeaturedItemsIT.java | 2 +- .../iq/dataverse/api/DataversesIT.java | 18 +++++++++---- .../edu/harvard/iq/dataverse/api/UtilIT.java | 12 ++++++--- ...reateDataverseFeaturedItemCommandTest.java | 8 +++--- 8 files changed, 50 insertions(+), 29 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java index d0040745bdf..52da2bb9be5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java @@ -70,7 +70,7 @@ public void setImageFileName(String imageFileName) { } public String getImageFileUrl() { - if (id != null) { + if (id != null && imageFileName != null) { return SystemConfig.getDataverseSiteUrlStatic() + "/api/access/dataverseFeatureItemImage/" + id; } return null; diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index b2f174d9449..015485c9a9e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -1738,8 +1738,8 @@ public Response getUserPermissionsOnDataverse(@Context ContainerRequestContext c public Response createFeaturedItem(@Context ContainerRequestContext crc, @PathParam("identifier") String dvIdtf, @FormDataParam("content") String content, - @FormDataParam("order") int order, - @FormDataParam("file") InputStream fileInputStream, + @FormDataParam("displayOrder") int displayOrder, + @FormDataParam("file") InputStream imageFileInputStream, @FormDataParam("file") FormDataContentDisposition contentDispositionHeader) { Dataverse dataverse; try { @@ -1747,7 +1747,7 @@ public Response createFeaturedItem(@Context ContainerRequestContext crc, } catch (WrappedResponse wr) { return wr.getResponse(); } - NewDataverseFeaturedItemDTO newDataverseFeaturedItemDTO = NewDataverseFeaturedItemDTO.fromFormData(content, order, fileInputStream, contentDispositionHeader); + NewDataverseFeaturedItemDTO newDataverseFeaturedItemDTO = NewDataverseFeaturedItemDTO.fromFormData(content, displayOrder, imageFileInputStream, contentDispositionHeader); try { DataverseFeaturedItem dataverseFeaturedItem = execCommand(new CreateDataverseFeaturedItemCommand( createDataverseRequest(getRequestUser(crc)), diff --git a/src/main/java/edu/harvard/iq/dataverse/api/dto/NewDataverseFeaturedItemDTO.java b/src/main/java/edu/harvard/iq/dataverse/api/dto/NewDataverseFeaturedItemDTO.java index e1f5190cb41..47003761abc 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/dto/NewDataverseFeaturedItemDTO.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/dto/NewDataverseFeaturedItemDTO.java @@ -7,18 +7,23 @@ public class NewDataverseFeaturedItemDTO { private String content; private int displayOrder; - private InputStream fileInputStream; + private InputStream imageFileInputStream; private String imageFileName; public static NewDataverseFeaturedItemDTO fromFormData(String content, - int order, - InputStream fileInputStream, + int displayOrder, + InputStream imageFileInputStream, FormDataContentDisposition contentDispositionHeader) { NewDataverseFeaturedItemDTO newDataverseFeaturedItemDTO = new NewDataverseFeaturedItemDTO(); + newDataverseFeaturedItemDTO.content = content; - newDataverseFeaturedItemDTO.displayOrder = order; - newDataverseFeaturedItemDTO.fileInputStream = fileInputStream; - newDataverseFeaturedItemDTO.imageFileName = contentDispositionHeader.getFileName(); + newDataverseFeaturedItemDTO.displayOrder = displayOrder; + + if (imageFileInputStream != null) { + newDataverseFeaturedItemDTO.imageFileInputStream = imageFileInputStream; + newDataverseFeaturedItemDTO.imageFileName = contentDispositionHeader.getFileName(); + } + return newDataverseFeaturedItemDTO; } @@ -38,12 +43,12 @@ public int getDisplayOrder() { return displayOrder; } - public void setFileInputStream(InputStream fileInputStream) { - this.fileInputStream = fileInputStream; + public void setImageFileInputStream(InputStream imageFileInputStream) { + this.imageFileInputStream = imageFileInputStream; } - public InputStream getFileInputStream() { - return fileInputStream; + public InputStream getImageFileInputStream() { + return imageFileInputStream; } public void setImageFileName(String imageFileName) { diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java index 8873a3efea4..1bd296fb384 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java @@ -14,6 +14,7 @@ import edu.harvard.iq.dataverse.util.BundleUtil; import java.io.IOException; +import java.io.InputStream; import java.util.List; @RequiredPermissions({Permission.EditDataverse}) @@ -43,9 +44,10 @@ public DataverseFeaturedItem execute(CommandContext ctxt) throws CommandExceptio private void setImageIfAvailable(DataverseFeaturedItem featuredItem, CommandContext ctxt) throws CommandException { String imageFileName = newDataverseFeaturedItemDTO.getImageFileName(); - if (imageFileName != null) { + InputStream imageFileInputStream = newDataverseFeaturedItemDTO.getImageFileInputStream(); + if (imageFileName != null && imageFileInputStream != null) { try { - ctxt.dataverseFeaturedItems().saveDataverseFeaturedItemImageFile(newDataverseFeaturedItemDTO.getFileInputStream(), imageFileName, dataverse.getId()); + ctxt.dataverseFeaturedItems().saveDataverseFeaturedItemImageFile(imageFileInputStream, imageFileName, dataverse.getId()); } catch (DataverseFeaturedItemServiceBean.InvalidImageFileException e) { throw new InvalidCommandArgumentsException( e.getMessage(), diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java index 58b33183e54..765b1cb25ae 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java @@ -25,7 +25,7 @@ public void testDeleteFeaturedItem() { String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); String pathToTestFile = "src/test/resources/images/coffeeshop.png"; - Response createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, apiToken, "test", "test", pathToTestFile); + Response createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, apiToken, "test", "test", 0, pathToTestFile); createFeatureItemResponse.then().assertThat().statusCode(OK.getStatusCode()); JsonPath createdFeaturedItem = JsonPath.from(createFeatureItemResponse.body().asString()); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java index a083716d2e2..640e0d42245 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java @@ -1585,18 +1585,26 @@ public void testCreateFeaturedItem() { createDataverseResponse.then().assertThat().statusCode(CREATED.getStatusCode()); String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + // Should not return any error when not passing a file + Response createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, apiToken, "test", "test", 0, null); + createFeatureItemResponse.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.content", equalTo("test")) + .body("data.imageFileName", equalTo(null)) + .body("data.displayOrder", equalTo(0)); + // Should not return any error when passing correct file and data String pathToTestFile = "src/test/resources/images/coffeeshop.png"; - Response createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, apiToken, "test", "test", pathToTestFile); + createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, apiToken, "test", "test", 1, pathToTestFile); createFeatureItemResponse.then().assertThat() .body("data.content", equalTo("test")) .body("data.imageFileName", equalTo("coffeeshop.png")) - .body("data.displayOrder", equalTo(0)) + .body("data.displayOrder", equalTo(1)) .statusCode(OK.getStatusCode()); // Should return bad request error when passing incorrect file type pathToTestFile = "src/test/resources/tab/test.tab"; - createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, apiToken, "test", "test", pathToTestFile); + createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, apiToken, "test", "test", 0, pathToTestFile); createFeatureItemResponse.then().assertThat() .body("message", equalTo(BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.invalidFileType"))) .statusCode(BAD_REQUEST.getStatusCode()); @@ -1604,11 +1612,11 @@ public void testCreateFeaturedItem() { // Should return unauthorized error when user has no permissions Response createRandomUser = UtilIT.createRandomUser(); String randomUserApiToken = UtilIT.getApiTokenFromResponse(createRandomUser); - createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, randomUserApiToken, "test", "test", pathToTestFile); + createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, randomUserApiToken, "test", "test", 0, pathToTestFile); createFeatureItemResponse.then().assertThat().statusCode(UNAUTHORIZED.getStatusCode()); // Should return not found error when dataverse does not exist - createFeatureItemResponse = UtilIT.createDataverseFeaturedItem("thisDataverseDoesNotExist", apiToken, "test", "test", pathToTestFile); + createFeatureItemResponse = UtilIT.createDataverseFeaturedItem("thisDataverseDoesNotExist", apiToken, "test", "test", 0, pathToTestFile); createFeatureItemResponse.then().assertThat() .body("message", equalTo("Can't find dataverse with identifier='thisDataverseDoesNotExist'")) .statusCode(NOT_FOUND.getStatusCode()); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index 11d303156ae..9591f84bdf6 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -4360,13 +4360,19 @@ static Response performKeycloakROPCLogin(String username, String password) { .post("http://keycloak.mydomain.com:8090/realms/test/protocol/openid-connect/token"); } - static Response createDataverseFeaturedItem(String dataverseAlias, String apiToken, String title, String content, String pathToFile) { - return given() + static Response createDataverseFeaturedItem(String dataverseAlias, String apiToken, String title, String content, int displayOrder, String pathToFile) { + RequestSpecification requestSpecification = given() .header(API_TOKEN_HTTP_HEADER, apiToken) .contentType(ContentType.MULTIPART) .multiPart("title", title) .multiPart("content", content) - .multiPart("file", new File(pathToFile)) + .multiPart("displayOrder", displayOrder); + + if (pathToFile != null) { + requestSpecification.multiPart("file", new File(pathToFile)); + } + + return requestSpecification .when() .post("/api/dataverses/" + dataverseAlias + "/featuredItem"); } diff --git a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java index 350fc66c230..d8bb6549c60 100644 --- a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java @@ -44,7 +44,7 @@ void setUp() { testNewDataverseFeaturedItemDTO.setImageFileName("test.png"); testNewDataverseFeaturedItemDTO.setContent("test content"); testNewDataverseFeaturedItemDTO.setDisplayOrder(0); - testNewDataverseFeaturedItemDTO.setFileInputStream(mock(InputStream.class)); + testNewDataverseFeaturedItemDTO.setImageFileInputStream(mock(InputStream.class)); when(contextStub.dataverseFeaturedItems()).thenReturn(dataverseFeaturedItemServiceStub); sut = new CreateDataverseFeaturedItemCommand(makeRequest(), testDataverse, testNewDataverseFeaturedItemDTO); @@ -71,7 +71,7 @@ void execute_imageFileProvidedAndValid_savesFeaturedItem() throws Exception { verify(dataverseFeaturedItemServiceStub).save(any(DataverseFeaturedItem.class)); verify(dataverseFeaturedItemServiceStub).saveDataverseFeaturedItemImageFile( - testNewDataverseFeaturedItemDTO.getFileInputStream(), + testNewDataverseFeaturedItemDTO.getImageFileInputStream(), testNewDataverseFeaturedItemDTO.getImageFileName(), testDataverse.getId() ); @@ -95,7 +95,7 @@ void execute_noImageFileProvided_featuredItemSavedWithoutImage() throws Exceptio void execute_imageFileProcessingFails_throwsCommandException() throws IOException, DataverseFeaturedItemServiceBean.InvalidImageFileException { testNewDataverseFeaturedItemDTO.setImageFileName("invalid.png"); InputStream inputStreamMock = mock(InputStream.class); - testNewDataverseFeaturedItemDTO.setFileInputStream(inputStreamMock); + testNewDataverseFeaturedItemDTO.setImageFileInputStream(inputStreamMock); doThrow(new IOException("File processing failed")) .when(dataverseFeaturedItemServiceStub) @@ -109,7 +109,7 @@ void execute_imageFileProcessingFails_throwsCommandException() throws IOExceptio void execute_invalidArgumentsProvided_throwsInvalidCommandArgumentsException() throws IOException, DataverseFeaturedItemServiceBean.InvalidImageFileException { testNewDataverseFeaturedItemDTO.setImageFileName("invalid.png"); InputStream inputStreamMock = mock(InputStream.class); - testNewDataverseFeaturedItemDTO.setFileInputStream(inputStreamMock); + testNewDataverseFeaturedItemDTO.setImageFileInputStream(inputStreamMock); doThrow(new DataverseFeaturedItemServiceBean.InvalidImageFileException("Invalid file type")) .when(dataverseFeaturedItemServiceStub).saveDataverseFeaturedItemImageFile(any(InputStream.class), any(String.class), any(Long.class)); From 034cea463a520c1f875f05c920d0c38aa3da55a4 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 2 Jan 2025 15:34:43 +0000 Subject: [PATCH 21/57] Added: API endpoint for updating a dataverse featured item --- .../DataverseFeaturedItemServiceBean.java | 6 +- .../dataverse/api/DataverseFeaturedItems.java | 30 ++++++++ .../dto/UpdatedDataverseFeaturedItemDTO.java | 72 ++++++++++++++++++ ...ractWriteDataverseFeaturedItemCommand.java | 53 +++++++++++++ .../CreateDataverseFeaturedItemCommand.java | 60 ++++----------- .../UpdateDataverseFeaturedItemCommand.java | 41 ++++++++++ .../api/DataverseFeaturedItemsIT.java | 74 +++++++++++++++---- .../iq/dataverse/api/DataversesIT.java | 10 +-- .../edu/harvard/iq/dataverse/api/UtilIT.java | 29 +++++++- 9 files changed, 305 insertions(+), 70 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/api/dto/UpdatedDataverseFeaturedItemDTO.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemCommand.java diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java index e41d2d59551..90fb3e04d1f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java @@ -37,11 +37,11 @@ public DataverseFeaturedItem findById(Long id) { public DataverseFeaturedItem save(DataverseFeaturedItem dataverseFeaturedItem) { if (dataverseFeaturedItem.getId() == null) { em.persist(dataverseFeaturedItem); - em.flush(); - return dataverseFeaturedItem; } else { - return em.merge(dataverseFeaturedItem); + dataverseFeaturedItem = em.merge(dataverseFeaturedItem); } + em.flush(); + return dataverseFeaturedItem; } public InputStream getImageFileAsInputStream(DataverseFeaturedItem dataverseFeaturedItem) throws IOException { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItems.java b/src/main/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItems.java index 4d4c246a6b1..fdaccaa08d2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItems.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItems.java @@ -2,6 +2,7 @@ import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.api.auth.AuthRequired; +import edu.harvard.iq.dataverse.api.dto.UpdatedDataverseFeaturedItemDTO; import edu.harvard.iq.dataverse.engine.command.impl.*; import edu.harvard.iq.dataverse.util.BundleUtil; import jakarta.ejb.Stateless; @@ -9,10 +10,16 @@ import jakarta.ws.rs.*; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; +import org.glassfish.jersey.media.multipart.FormDataParam; +import java.io.InputStream; import java.text.MessageFormat; +import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; + @Stateless @Path("dataverseFeaturedItems") public class DataverseFeaturedItems extends AbstractApiBean { @@ -35,4 +42,27 @@ public Response deleteFeaturedItem(@Context ContainerRequestContext crc, @PathPa return e.getResponse(); } } + + @PUT + @AuthRequired + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Path("{id}") + public Response updateFeaturedItem(@Context ContainerRequestContext crc, + @PathParam("id") Long id, + @FormDataParam("content") String content, + @FormDataParam("displayOrder") int displayOrder, + @FormDataParam("keepFile") boolean keepFile, + @FormDataParam("file") InputStream imageFileInputStream, + @FormDataParam("file") FormDataContentDisposition contentDispositionHeader) { + try { + DataverseFeaturedItem dataverseFeaturedItem = dataverseFeaturedItemServiceBean.findById(id); + if (dataverseFeaturedItem == null) { + throw new WrappedResponse(error(Response.Status.NOT_FOUND, MessageFormat.format(BundleUtil.getStringFromBundle("dataverseFeaturedItems.errors.notFound"), id))); + } + UpdatedDataverseFeaturedItemDTO updatedDataverseFeaturedItemDTO = UpdatedDataverseFeaturedItemDTO.fromFormData(content, displayOrder, keepFile, imageFileInputStream, contentDispositionHeader); + return ok(json(execCommand(new UpdateDataverseFeaturedItemCommand(createDataverseRequest(getRequestUser(crc)), dataverseFeaturedItem, updatedDataverseFeaturedItemDTO)))); + } catch (WrappedResponse e) { + return e.getResponse(); + } + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/dto/UpdatedDataverseFeaturedItemDTO.java b/src/main/java/edu/harvard/iq/dataverse/api/dto/UpdatedDataverseFeaturedItemDTO.java new file mode 100644 index 00000000000..43d1afc31e2 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/api/dto/UpdatedDataverseFeaturedItemDTO.java @@ -0,0 +1,72 @@ +package edu.harvard.iq.dataverse.api.dto; + +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; + +import java.io.InputStream; + +public class UpdatedDataverseFeaturedItemDTO { + private String content; + private int displayOrder; + private boolean keepFile; + private InputStream imageFileInputStream; + private String imageFileName; + + public static UpdatedDataverseFeaturedItemDTO fromFormData(String content, + int displayOrder, + boolean keepFile, + InputStream imageFileInputStream, + FormDataContentDisposition contentDispositionHeader) { + UpdatedDataverseFeaturedItemDTO updatedDataverseFeaturedItemDTO = new UpdatedDataverseFeaturedItemDTO(); + + updatedDataverseFeaturedItemDTO.content = content; + updatedDataverseFeaturedItemDTO.displayOrder = displayOrder; + updatedDataverseFeaturedItemDTO.keepFile = keepFile; + + if (imageFileInputStream != null) { + updatedDataverseFeaturedItemDTO.imageFileInputStream = imageFileInputStream; + updatedDataverseFeaturedItemDTO.imageFileName = contentDispositionHeader.getFileName(); + } + + return updatedDataverseFeaturedItemDTO; + } + + public void setContent(String content) { + this.content = content; + } + + public String getContent() { + return content; + } + + public void setDisplayOrder(int displayOrder) { + this.displayOrder = displayOrder; + } + + public int getDisplayOrder() { + return displayOrder; + } + + public void setKeepFile(boolean keepFile) { + this.keepFile = keepFile; + } + + public boolean isKeepFile() { + return keepFile; + } + + public void setImageFileInputStream(InputStream imageFileInputStream) { + this.imageFileInputStream = imageFileInputStream; + } + + public InputStream getImageFileInputStream() { + return imageFileInputStream; + } + + public void setImageFileName(String imageFileName) { + this.imageFileName = imageFileName; + } + + public String getImageFileName() { + return imageFileName; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java new file mode 100644 index 00000000000..0b3395d6f78 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java @@ -0,0 +1,53 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.*; +import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.engine.command.AbstractCommand; +import edu.harvard.iq.dataverse.engine.command.CommandContext; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import edu.harvard.iq.dataverse.engine.command.exception.InvalidCommandArgumentsException; +import edu.harvard.iq.dataverse.util.BundleUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +/** + * An abstract base class for commands that perform write operations on {@link DataverseFeaturedItem}s. + */ +@RequiredPermissions({Permission.EditDataverse}) +abstract class AbstractWriteDataverseFeaturedItemCommand extends AbstractCommand { + + protected final Dataverse dataverse; + + public AbstractWriteDataverseFeaturedItemCommand(DataverseRequest request, Dataverse affectedDataverse) { + super(request, affectedDataverse); + this.dataverse = affectedDataverse; + } + + protected void setFileImageIfAvailableOrNull(DataverseFeaturedItem featuredItem, String imageFileName, InputStream imageFileInputStream, CommandContext ctxt) throws CommandException { + if (imageFileName != null && imageFileInputStream != null) { + try { + ctxt.dataverseFeaturedItems().saveDataverseFeaturedItemImageFile(imageFileInputStream, imageFileName, dataverse.getId()); + } catch (DataverseFeaturedItemServiceBean.InvalidImageFileException e) { + throw new InvalidCommandArgumentsException( + e.getMessage(), + this + ); + } catch (IOException e) { + throw new CommandException( + BundleUtil.getStringFromBundle( + "dataverse.create.featuredItem.error.imageFileProcessing", + List.of(e.getMessage()) + ), + this + ); + } + featuredItem.setImageFileName(imageFileName); + } else { + featuredItem.setImageFileName(null); + } + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java index 1bd296fb384..e1ede59817c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java @@ -2,67 +2,37 @@ import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.DataverseFeaturedItem; -import edu.harvard.iq.dataverse.DataverseFeaturedItemServiceBean; import edu.harvard.iq.dataverse.api.dto.NewDataverseFeaturedItemDTO; -import edu.harvard.iq.dataverse.authorization.Permission; -import edu.harvard.iq.dataverse.engine.command.AbstractCommand; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; -import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; -import edu.harvard.iq.dataverse.engine.command.exception.InvalidCommandArgumentsException; -import edu.harvard.iq.dataverse.util.BundleUtil; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; +public class CreateDataverseFeaturedItemCommand extends AbstractWriteDataverseFeaturedItemCommand { -@RequiredPermissions({Permission.EditDataverse}) -public class CreateDataverseFeaturedItemCommand extends AbstractCommand { - - private final Dataverse dataverse; private final NewDataverseFeaturedItemDTO newDataverseFeaturedItemDTO; - public CreateDataverseFeaturedItemCommand(DataverseRequest request, Dataverse dataverse, NewDataverseFeaturedItemDTO newDataverseFeaturedItemDTO) { + public CreateDataverseFeaturedItemCommand(DataverseRequest request, + Dataverse dataverse, + NewDataverseFeaturedItemDTO newDataverseFeaturedItemDTO) { super(request, dataverse); - this.dataverse = dataverse; this.newDataverseFeaturedItemDTO = newDataverseFeaturedItemDTO; } @Override public DataverseFeaturedItem execute(CommandContext ctxt) throws CommandException { - DataverseFeaturedItem featuredItem = new DataverseFeaturedItem(); - - setImageIfAvailable(featuredItem, ctxt); + DataverseFeaturedItem dataverseFeaturedItem = new DataverseFeaturedItem(); - featuredItem.setContent(newDataverseFeaturedItemDTO.getContent()); - featuredItem.setDisplayOrder(newDataverseFeaturedItemDTO.getDisplayOrder()); - featuredItem.setDataverse(dataverse); + setFileImageIfAvailableOrNull( + dataverseFeaturedItem, + newDataverseFeaturedItemDTO.getImageFileName(), + newDataverseFeaturedItemDTO.getImageFileInputStream(), + ctxt + ); - return ctxt.dataverseFeaturedItems().save(featuredItem); - } + dataverseFeaturedItem.setContent(newDataverseFeaturedItemDTO.getContent()); + dataverseFeaturedItem.setDisplayOrder(newDataverseFeaturedItemDTO.getDisplayOrder()); + dataverseFeaturedItem.setDataverse(dataverse); - private void setImageIfAvailable(DataverseFeaturedItem featuredItem, CommandContext ctxt) throws CommandException { - String imageFileName = newDataverseFeaturedItemDTO.getImageFileName(); - InputStream imageFileInputStream = newDataverseFeaturedItemDTO.getImageFileInputStream(); - if (imageFileName != null && imageFileInputStream != null) { - try { - ctxt.dataverseFeaturedItems().saveDataverseFeaturedItemImageFile(imageFileInputStream, imageFileName, dataverse.getId()); - } catch (DataverseFeaturedItemServiceBean.InvalidImageFileException e) { - throw new InvalidCommandArgumentsException( - e.getMessage(), - this - ); - } catch (IOException e) { - throw new CommandException( - BundleUtil.getStringFromBundle( - "dataverse.create.featuredItem.error.imageFileProcessing", - List.of(e.getMessage()) - ), - this - ); - } - featuredItem.setImageFileName(imageFileName); - } + return ctxt.dataverseFeaturedItems().save(dataverseFeaturedItem); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemCommand.java new file mode 100644 index 00000000000..51e5d43a7aa --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemCommand.java @@ -0,0 +1,41 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.DataverseFeaturedItem; +import edu.harvard.iq.dataverse.api.dto.UpdatedDataverseFeaturedItemDTO; +import edu.harvard.iq.dataverse.engine.command.*; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; + +/** + * Updates a particular featured item {@link DataverseFeaturedItem} of a {@link Dataverse}. + */ +public class UpdateDataverseFeaturedItemCommand extends AbstractWriteDataverseFeaturedItemCommand { + + private final DataverseFeaturedItem dataverseFeaturedItem; + private final UpdatedDataverseFeaturedItemDTO updatedDataverseFeaturedItemDTO; + + public UpdateDataverseFeaturedItemCommand(DataverseRequest request, + DataverseFeaturedItem dataverseFeaturedItem, + UpdatedDataverseFeaturedItemDTO updatedDataverseFeaturedItemDTO) { + super(request, dataverseFeaturedItem.getDataverse()); + this.dataverseFeaturedItem = dataverseFeaturedItem; + this.updatedDataverseFeaturedItemDTO = updatedDataverseFeaturedItemDTO; + } + + @Override + public DataverseFeaturedItem execute(CommandContext ctxt) throws CommandException { + dataverseFeaturedItem.setContent(updatedDataverseFeaturedItemDTO.getContent()); + dataverseFeaturedItem.setDisplayOrder(updatedDataverseFeaturedItemDTO.getDisplayOrder()); + + if (!updatedDataverseFeaturedItemDTO.isKeepFile()) { + setFileImageIfAvailableOrNull( + dataverseFeaturedItem, + updatedDataverseFeaturedItemDTO.getImageFileName(), + updatedDataverseFeaturedItemDTO.getImageFileInputStream(), + ctxt + ); + } + + return ctxt.dataverseFeaturedItems().save(dataverseFeaturedItem); + } +} diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java index 765b1cb25ae..d53414d4207 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java @@ -18,30 +18,20 @@ public static void setUpClass() { @Test public void testDeleteFeaturedItem() { - Response createUserResponse = UtilIT.createRandomUser(); - String apiToken = UtilIT.getApiTokenFromResponse(createUserResponse); - Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); - createDataverseResponse.then().assertThat().statusCode(CREATED.getStatusCode()); - String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); - - String pathToTestFile = "src/test/resources/images/coffeeshop.png"; - Response createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, apiToken, "test", "test", 0, pathToTestFile); - createFeatureItemResponse.then().assertThat().statusCode(OK.getStatusCode()); - - JsonPath createdFeaturedItem = JsonPath.from(createFeatureItemResponse.body().asString()); - Long featuredItemId = createdFeaturedItem.getLong("data.id"); + String apiToken = createUserAndGetApiToken(); + String dataverseAlias = createDataverseAndGetAlias(apiToken); + Long featuredItemId = createFeaturedItemAndGetId(dataverseAlias, apiToken, "src/test/resources/images/coffeeshop.png"); // Should return not found when passing incorrect item id Response deleteFeatureItemResponse = UtilIT.deleteDataverseFeaturedItem(100000L, apiToken); deleteFeatureItemResponse.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); // Should return unauthorized when passing correct id and user does not have permissions - Response createRandomUser = UtilIT.createRandomUser(); - String randomUserApiToken = UtilIT.getApiTokenFromResponse(createRandomUser); + String randomUserApiToken = createUserAndGetApiToken(); deleteFeatureItemResponse = UtilIT.deleteDataverseFeaturedItem(featuredItemId, randomUserApiToken); deleteFeatureItemResponse.then().assertThat().statusCode(UNAUTHORIZED.getStatusCode()); - // Should delete featured item when passing correct id and user have permissions + // Should delete featured item when passing correct id and user has permissions deleteFeatureItemResponse = UtilIT.deleteDataverseFeaturedItem(featuredItemId, apiToken); deleteFeatureItemResponse.then().assertThat().statusCode(OK.getStatusCode()); @@ -50,4 +40,58 @@ public void testDeleteFeaturedItem() { .body("data.size()", equalTo(0)) .assertThat().statusCode(OK.getStatusCode()); } + + @Test + public void testUpdateFeaturedItem() { + String apiToken = createUserAndGetApiToken(); + String dataverseAlias = createDataverseAndGetAlias(apiToken); + Long featuredItemId = createFeaturedItemAndGetId(dataverseAlias, apiToken, "src/test/resources/images/coffeeshop.png"); + + // Should return not found when passing incorrect item id + Response updateFeatureItemResponse = UtilIT.updateDataverseFeaturedItem(100000L, "updatedTitle", 1, false, null, apiToken); + updateFeatureItemResponse.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); + + // Should return unauthorized when passing correct id and user does not have permissions + String randomUserApiToken = createUserAndGetApiToken(); + updateFeatureItemResponse = UtilIT.updateDataverseFeaturedItem(featuredItemId, "updatedTitle", 1, false, null, randomUserApiToken); + updateFeatureItemResponse.then().assertThat().statusCode(UNAUTHORIZED.getStatusCode()); + + // Update featured item: keep image file + updateFeatureItemResponse = UtilIT.updateDataverseFeaturedItem(featuredItemId, "updatedTitle1", 1, true, null, apiToken); + verifyUpdatedFeaturedItem(updateFeatureItemResponse, "updatedTitle1", "coffeeshop.png", 1); + + // Update featured item: remove image file + updateFeatureItemResponse = UtilIT.updateDataverseFeaturedItem(featuredItemId, "updatedTitle1", 2, false, null, apiToken); + verifyUpdatedFeaturedItem(updateFeatureItemResponse, "updatedTitle1", null, 2); + + // Update featured item: set new image file + updateFeatureItemResponse = UtilIT.updateDataverseFeaturedItem(featuredItemId, "updatedTitle1", 2, false, "src/test/resources/images/coffeeshop.png", apiToken); + verifyUpdatedFeaturedItem(updateFeatureItemResponse, "updatedTitle1", "coffeeshop.png", 2); + } + + private String createUserAndGetApiToken() { + Response createUserResponse = UtilIT.createRandomUser(); + return UtilIT.getApiTokenFromResponse(createUserResponse); + } + + private String createDataverseAndGetAlias(String apiToken) { + Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); + createDataverseResponse.then().assertThat().statusCode(CREATED.getStatusCode()); + return UtilIT.getAliasFromResponse(createDataverseResponse); + } + + private Long createFeaturedItemAndGetId(String dataverseAlias, String apiToken, String pathToTestFile) { + Response createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, apiToken, "test", 0, pathToTestFile); + createFeatureItemResponse.then().assertThat().statusCode(OK.getStatusCode()); + JsonPath createdFeaturedItem = JsonPath.from(createFeatureItemResponse.body().asString()); + return createdFeaturedItem.getLong("data.id"); + } + + private void verifyUpdatedFeaturedItem(Response response, String expectedTitle, String expectedImageFileName, int expectedDisplayOrder) { + response.then().assertThat() + .body("data.content", equalTo(expectedTitle)) + .body("data.imageFileName", equalTo(expectedImageFileName)) + .body("data.displayOrder", equalTo(expectedDisplayOrder)) + .statusCode(OK.getStatusCode()); + } } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java index 640e0d42245..8153f7ffe91 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java @@ -1586,7 +1586,7 @@ public void testCreateFeaturedItem() { String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); // Should not return any error when not passing a file - Response createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, apiToken, "test", "test", 0, null); + Response createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, apiToken, "test", 0, null); createFeatureItemResponse.then().assertThat() .statusCode(OK.getStatusCode()) .body("data.content", equalTo("test")) @@ -1595,7 +1595,7 @@ public void testCreateFeaturedItem() { // Should not return any error when passing correct file and data String pathToTestFile = "src/test/resources/images/coffeeshop.png"; - createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, apiToken, "test", "test", 1, pathToTestFile); + createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, apiToken, "test", 1, pathToTestFile); createFeatureItemResponse.then().assertThat() .body("data.content", equalTo("test")) .body("data.imageFileName", equalTo("coffeeshop.png")) @@ -1604,7 +1604,7 @@ public void testCreateFeaturedItem() { // Should return bad request error when passing incorrect file type pathToTestFile = "src/test/resources/tab/test.tab"; - createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, apiToken, "test", "test", 0, pathToTestFile); + createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, apiToken, "test", 0, pathToTestFile); createFeatureItemResponse.then().assertThat() .body("message", equalTo(BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.invalidFileType"))) .statusCode(BAD_REQUEST.getStatusCode()); @@ -1612,11 +1612,11 @@ public void testCreateFeaturedItem() { // Should return unauthorized error when user has no permissions Response createRandomUser = UtilIT.createRandomUser(); String randomUserApiToken = UtilIT.getApiTokenFromResponse(createRandomUser); - createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, randomUserApiToken, "test", "test", 0, pathToTestFile); + createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, randomUserApiToken, "test", 0, pathToTestFile); createFeatureItemResponse.then().assertThat().statusCode(UNAUTHORIZED.getStatusCode()); // Should return not found error when dataverse does not exist - createFeatureItemResponse = UtilIT.createDataverseFeaturedItem("thisDataverseDoesNotExist", apiToken, "test", "test", 0, pathToTestFile); + createFeatureItemResponse = UtilIT.createDataverseFeaturedItem("thisDataverseDoesNotExist", apiToken, "test", 0, pathToTestFile); createFeatureItemResponse.then().assertThat() .body("message", equalTo("Can't find dataverse with identifier='thisDataverseDoesNotExist'")) .statusCode(NOT_FOUND.getStatusCode()); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index 9591f84bdf6..40df922b4fc 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -4360,11 +4360,14 @@ static Response performKeycloakROPCLogin(String username, String password) { .post("http://keycloak.mydomain.com:8090/realms/test/protocol/openid-connect/token"); } - static Response createDataverseFeaturedItem(String dataverseAlias, String apiToken, String title, String content, int displayOrder, String pathToFile) { + static Response createDataverseFeaturedItem(String dataverseAlias, + String apiToken, + String content, + int displayOrder, + String pathToFile) { RequestSpecification requestSpecification = given() .header(API_TOKEN_HTTP_HEADER, apiToken) .contentType(ContentType.MULTIPART) - .multiPart("title", title) .multiPart("content", content) .multiPart("displayOrder", displayOrder); @@ -4383,6 +4386,28 @@ static Response deleteDataverseFeaturedItem(long id, String apiToken) { .delete("/api/dataverseFeaturedItems/" + id); } + static Response updateDataverseFeaturedItem(long featuredItemId, + String content, + int displayOrder, + boolean keepFile, + String pathToFile, + String apiToken) { + RequestSpecification requestSpecification = given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .contentType(ContentType.MULTIPART) + .multiPart("content", content) + .multiPart("displayOrder", displayOrder) + .multiPart("keepFile", keepFile); + + if (pathToFile != null) { + requestSpecification.multiPart("file", new File(pathToFile)); + } + + return requestSpecification + .when() + .put("/api/dataverseFeaturedItems/" + featuredItemId); + } + static Response listDataverseFeaturedItems(String dataverseAlias, String apiToken) { return given() .header(API_TOKEN_HTTP_HEADER, apiToken) From 812ff0f3a2d34356b2af171d7027fccd3c114dbe Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 7 Jan 2025 14:21:06 +0000 Subject: [PATCH 22/57] Stash: endpoint for create/update/delete all dv featured items at once WIP --- .../harvard/iq/dataverse/api/Dataverses.java | 68 +++++++++++++++++++ .../UpdateDataverseFeaturedItemsCommand.java | 64 +++++++++++++++++ .../iq/dataverse/api/DataversesIT.java | 23 +++++++ .../edu/harvard/iq/dataverse/api/UtilIT.java | 50 ++++++++++++-- 4 files changed, 199 insertions(+), 6 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemsCommand.java diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index 015485c9a9e..bf5aff431af 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -66,6 +66,7 @@ import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.StreamingOutput; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataContentDisposition; import org.glassfish.jersey.media.multipart.FormDataParam; @@ -112,6 +113,9 @@ public class Dataverses extends AbstractApiBean { @EJB PermissionServiceBean permissionService; + + @EJB + DataverseFeaturedItemServiceBean dataverseFeaturedItemServiceBean; @POST @AuthRequired @@ -1772,4 +1776,68 @@ public Response listFeaturedItems(@Context ContainerRequestContext crc, @PathPar return e.getResponse(); } } + + // TODO: Refine + @PUT + @AuthRequired + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Path("{dataverseId}/featuredItems") + public Response updateFeaturedItems( + @Context ContainerRequestContext crc, + @PathParam("dataverseId") String dvIdtf, + @FormDataParam("id") List ids, + @FormDataParam("content") List contents, + @FormDataParam("displayOrder") List displayOrders, + @FormDataParam("keepFile") List keepFiles, + @FormDataParam("file") List files) { + + try { + if (contents.size() != displayOrders.size() || displayOrders.size() != files.size()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("Mismatch between contents, displayOrders, and files.") + .build(); + } + + Dataverse dataverse = findDataverseOrDie(dvIdtf); + List newDataverseFeaturedItemDTOs = new ArrayList<>(); + Map dataverseFeaturedItemsToUpdate = new HashMap<>(); + + for (int i = 0; i < contents.size(); i++) { + Long id = ids.get(i); + String content = contents.get(i); + Integer displayOrder = displayOrders.get(i); + boolean keepFile = keepFiles.get(i); + FormDataBodyPart fileBodyPart = files.get(i); + + InputStream fileInputStream = fileBodyPart.getValueAs(InputStream.class); + FormDataContentDisposition contentDispositionHeader = fileBodyPart.getFormDataContentDisposition(); + + if (id == 0) { + NewDataverseFeaturedItemDTO newDTO = NewDataverseFeaturedItemDTO.fromFormData(content, displayOrder, fileInputStream, contentDispositionHeader); + newDataverseFeaturedItemDTOs.add(newDTO); + } else { + DataverseFeaturedItem existingItem = dataverseFeaturedItemServiceBean.findById(id); + if (existingItem == null) { + return Response.status(Response.Status.NOT_FOUND) + // TODO + .entity("Featured item not found with ID: " + id) + .build(); + } + UpdatedDataverseFeaturedItemDTO updatedDTO = UpdatedDataverseFeaturedItemDTO.fromFormData(content, displayOrder, keepFile, fileInputStream, contentDispositionHeader); + dataverseFeaturedItemsToUpdate.put(existingItem, updatedDTO); + } + } + + execCommand(new UpdateDataverseFeaturedItemsCommand( + createDataverseRequest(getRequestUser(crc)), + dataverse, + newDataverseFeaturedItemDTOs, + dataverseFeaturedItemsToUpdate + )); + + return Response.ok().entity("Featured items updated successfully").build(); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemsCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemsCommand.java new file mode 100644 index 00000000000..a1132188e4b --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemsCommand.java @@ -0,0 +1,64 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.DataverseFeaturedItem; +import edu.harvard.iq.dataverse.api.dto.NewDataverseFeaturedItemDTO; +import edu.harvard.iq.dataverse.api.dto.UpdatedDataverseFeaturedItemDTO; +import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.engine.command.AbstractVoidCommand; +import edu.harvard.iq.dataverse.engine.command.CommandContext; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; + +import java.util.List; +import java.util.Map; + +@RequiredPermissions({Permission.EditDataverse}) +public class UpdateDataverseFeaturedItemsCommand extends AbstractVoidCommand { + + private final Dataverse dataverse; + private final List newDataverseFeaturedItemDTOs; + private final Map dataverseFeaturedItemsToUpdate; + + public UpdateDataverseFeaturedItemsCommand(DataverseRequest request, + Dataverse dataverse, + List newDataverseFeaturedItemDTOs, + Map dataverseFeaturedItemsToUpdate) { + super(request, dataverse); + this.dataverse = dataverse; + this.newDataverseFeaturedItemDTOs = newDataverseFeaturedItemDTOs; + this.dataverseFeaturedItemsToUpdate = dataverseFeaturedItemsToUpdate; + } + + @Override + protected void executeImpl(CommandContext ctxt) throws CommandException { + updateOrDeleteExistingFeaturedItems(ctxt); + createNewFeaturedItems(ctxt); + } + + private void updateOrDeleteExistingFeaturedItems(CommandContext ctxt) throws CommandException { + List featuredItemsToDelete = dataverse.getDataverseFeaturedItems(); + + for (Map.Entry entry : dataverseFeaturedItemsToUpdate.entrySet()) { + DataverseFeaturedItem featuredItem = entry.getKey(); + UpdatedDataverseFeaturedItemDTO updatedDTO = entry.getValue(); + + if (featuredItemsToDelete.contains(featuredItem)) { + featuredItemsToDelete.remove(featuredItem); + } + + ctxt.engine().submit(new UpdateDataverseFeaturedItemCommand(getRequest(), featuredItem, updatedDTO)); + } + + for (DataverseFeaturedItem featuredItem : featuredItemsToDelete) { + ctxt.engine().submit(new DeleteDataverseFeaturedItemCommand(getRequest(), featuredItem)); + } + } + + private void createNewFeaturedItems(CommandContext ctxt) throws CommandException { + for (NewDataverseFeaturedItemDTO dto : newDataverseFeaturedItemDTOs) { + ctxt.engine().submit(new CreateDataverseFeaturedItemCommand(getRequest(), dataverse, dto)); + } + } +} diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java index 8153f7ffe91..51876e13a72 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java @@ -1621,4 +1621,27 @@ public void testCreateFeaturedItem() { .body("message", equalTo("Can't find dataverse with identifier='thisDataverseDoesNotExist'")) .statusCode(NOT_FOUND.getStatusCode()); } + + // TODO: Complete + @Test + public void testUpdateFeaturedItems() { + Response createUserResponse = UtilIT.createRandomUser(); + String apiToken = UtilIT.getApiTokenFromResponse(createUserResponse); + Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); + createDataverseResponse.then().assertThat().statusCode(CREATED.getStatusCode()); + String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + + List contents = Arrays.asList("Content 1", "Content 2"); + List orders = Arrays.asList(1, 2); + List pathsToFiles = Arrays.asList("src/test/resources/images/coffeeshop.png", "src/test/resources/images/coffeeshop.png"); + + Response updateDataverseFeaturedItemsResponse = UtilIT.updateDataverseFeaturedItems(dataverseAlias, contents, orders, pathsToFiles, apiToken); + updateDataverseFeaturedItemsResponse.prettyPrint(); + updateDataverseFeaturedItemsResponse.then().assertThat() + .statusCode(OK.getStatusCode()); + + Response listFeaturedItemsResponse = UtilIT.listDataverseFeaturedItems(dataverseAlias, apiToken); + listFeaturedItemsResponse.prettyPrint(); + + } } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index 40df922b4fc..2f0417e767d 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -1,12 +1,14 @@ package edu.harvard.iq.dataverse.api; +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.harvard.iq.dataverse.api.dto.NewDataverseFeaturedItemDTO; import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; import io.restassured.http.ContentType; import io.restassured.path.json.JsonPath; import io.restassured.response.Response; import java.io.*; -import java.util.UUID; +import java.util.*; import java.util.logging.Logger; import jakarta.json.Json; import jakarta.json.JsonObjectBuilder; @@ -29,15 +31,13 @@ import org.junit.jupiter.api.Test; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import io.restassured.specification.RequestSpecification; -import java.util.List; import com.mashape.unirest.http.Unirest; import com.mashape.unirest.http.exceptions.UnirestException; import com.mashape.unirest.request.GetRequest; import edu.harvard.iq.dataverse.util.FileUtil; -import java.util.Base64; import org.apache.commons.io.IOUtils; import java.nio.file.Path; -import java.util.ArrayList; + import org.apache.commons.lang3.math.NumberUtils; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; @@ -52,8 +52,7 @@ import edu.harvard.iq.dataverse.settings.FeatureFlags; import edu.harvard.iq.dataverse.util.StringUtil; -import java.util.Collections; - +import static org.apache.http.entity.ContentType.APPLICATION_JSON; import static org.junit.jupiter.api.Assertions.*; public class UtilIT { @@ -4414,4 +4413,43 @@ static Response listDataverseFeaturedItems(String dataverseAlias, String apiToke .contentType("application/json") .get("/api/dataverses/" + dataverseAlias + "/featuredItems"); } + + + // TODO: Refine + static Response updateDataverseFeaturedItems( + String dataverseAlias, + List contents, + List orders, + List pathsToFiles, + String apiToken) { + + RequestSpecification requestSpecification = given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .contentType(ContentType.MULTIPART); + + int size = contents.size(); + if (orders.size() != size || pathsToFiles.size() != size) { + throw new IllegalArgumentException("Contents, orders, and pathsToFiles must have the same size."); + } + + for (int i = 0; i < size; i++) { + String content = contents.get(i); + Integer order = orders.get(i); + + requestSpecification.multiPart("content", content); + requestSpecification.multiPart("displayOrder", order); + requestSpecification.multiPart("keepFile", false); + requestSpecification.multiPart("id", 0); + + String pathToFile = pathsToFiles.get(i); + if (pathToFile != null && !pathToFile.isEmpty()) { + requestSpecification.multiPart("file", new File(pathToFile)); + } + } + + return requestSpecification + .when() + .put("/api/dataverses/" + dataverseAlias + "/featuredItems"); + } + } From 791336cd73dea40e4016974c19fdb2b7ffc7505e Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 7 Jan 2025 15:40:49 +0000 Subject: [PATCH 23/57] Stash: updateFeaturedItems endpoint WIP. Updated the endpoint to return the latest dataverse featured items after successful update --- .../harvard/iq/dataverse/api/Dataverses.java | 23 ++++++++----------- .../CreateDataverseFeaturedItemCommand.java | 3 +++ .../UpdateDataverseFeaturedItemsCommand.java | 23 +++++++++++-------- src/main/java/propertyFiles/Bundle.properties | 2 ++ .../iq/dataverse/api/DataversesIT.java | 9 ++++---- .../edu/harvard/iq/dataverse/api/UtilIT.java | 15 ++++++------ 6 files changed, 40 insertions(+), 35 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index 4550bad41d0..ea7b41b9ca9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -1777,7 +1777,6 @@ public Response listFeaturedItems(@Context ContainerRequestContext crc, @PathPar } } - // TODO: Refine @PUT @AuthRequired @Consumes(MediaType.MULTIPART_FORM_DATA) @@ -1790,12 +1789,14 @@ public Response updateFeaturedItems( @FormDataParam("displayOrder") List displayOrders, @FormDataParam("keepFile") List keepFiles, @FormDataParam("file") List files) { - try { - if (contents.size() != displayOrders.size() || displayOrders.size() != files.size()) { - return Response.status(Response.Status.BAD_REQUEST) - .entity("Mismatch between contents, displayOrders, and files.") - .build(); + if (ids == null || contents == null || displayOrders == null || keepFiles == null || files == null) { + throw new WrappedResponse(error(Response.Status.BAD_REQUEST, BundleUtil.getStringFromBundle("dataverse.update.featuredItems.error.missingInputParams"))); + } + + int size = ids.size(); + if (contents.size() != size || displayOrders.size() != size || keepFiles.size() != size || files.size() != size) { + throw new WrappedResponse(error(Response.Status.BAD_REQUEST, BundleUtil.getStringFromBundle("dataverse.update.featuredItems.error.inputListsSizeMismatch"))); } Dataverse dataverse = findDataverseOrDie(dvIdtf); @@ -1818,24 +1819,20 @@ public Response updateFeaturedItems( } else { DataverseFeaturedItem existingItem = dataverseFeaturedItemServiceBean.findById(id); if (existingItem == null) { - return Response.status(Response.Status.NOT_FOUND) - // TODO - .entity("Featured item not found with ID: " + id) - .build(); + throw new WrappedResponse(error(Response.Status.NOT_FOUND, MessageFormat.format(BundleUtil.getStringFromBundle("dataverseFeaturedItems.errors.notFound"), id))); } UpdatedDataverseFeaturedItemDTO updatedDTO = UpdatedDataverseFeaturedItemDTO.fromFormData(content, displayOrder, keepFile, fileInputStream, contentDispositionHeader); dataverseFeaturedItemsToUpdate.put(existingItem, updatedDTO); } } - execCommand(new UpdateDataverseFeaturedItemsCommand( + List featuredItems = execCommand(new UpdateDataverseFeaturedItemsCommand( createDataverseRequest(getRequestUser(crc)), dataverse, newDataverseFeaturedItemDTOs, dataverseFeaturedItemsToUpdate )); - - return Response.ok().entity("Featured items updated successfully").build(); + return ok(jsonDataverseFeaturedItems(featuredItems)); } catch (WrappedResponse wr) { return wr.getResponse(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java index e1ede59817c..e1f21587bb0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java @@ -7,6 +7,9 @@ import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +/** + * Creates a featured item {@link DataverseFeaturedItem} for a {@link Dataverse}. + */ public class CreateDataverseFeaturedItemCommand extends AbstractWriteDataverseFeaturedItemCommand { private final NewDataverseFeaturedItemDTO newDataverseFeaturedItemDTO; diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemsCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemsCommand.java index a1132188e4b..afb09bdf455 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemsCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemsCommand.java @@ -5,26 +5,27 @@ import edu.harvard.iq.dataverse.api.dto.NewDataverseFeaturedItemDTO; import edu.harvard.iq.dataverse.api.dto.UpdatedDataverseFeaturedItemDTO; import edu.harvard.iq.dataverse.authorization.Permission; -import edu.harvard.iq.dataverse.engine.command.AbstractVoidCommand; -import edu.harvard.iq.dataverse.engine.command.CommandContext; -import edu.harvard.iq.dataverse.engine.command.DataverseRequest; -import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; +import edu.harvard.iq.dataverse.engine.command.*; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import java.util.List; import java.util.Map; +/** + * Updates all featured items ({@link DataverseFeaturedItem}) for a specified {@link Dataverse}. + *

+ * This command allows for the creation of multiple new featured items, updates to existing items with new parameters, + * or the deletion of existing items, all in a single command. + *

+ **/ @RequiredPermissions({Permission.EditDataverse}) -public class UpdateDataverseFeaturedItemsCommand extends AbstractVoidCommand { +public class UpdateDataverseFeaturedItemsCommand extends AbstractCommand> { private final Dataverse dataverse; private final List newDataverseFeaturedItemDTOs; private final Map dataverseFeaturedItemsToUpdate; - public UpdateDataverseFeaturedItemsCommand(DataverseRequest request, - Dataverse dataverse, - List newDataverseFeaturedItemDTOs, - Map dataverseFeaturedItemsToUpdate) { + public UpdateDataverseFeaturedItemsCommand(DataverseRequest request, Dataverse dataverse, List newDataverseFeaturedItemDTOs, Map dataverseFeaturedItemsToUpdate) { super(request, dataverse); this.dataverse = dataverse; this.newDataverseFeaturedItemDTOs = newDataverseFeaturedItemDTOs; @@ -32,9 +33,11 @@ public UpdateDataverseFeaturedItemsCommand(DataverseRequest request, } @Override - protected void executeImpl(CommandContext ctxt) throws CommandException { + public List execute(CommandContext ctxt) throws CommandException { updateOrDeleteExistingFeaturedItems(ctxt); createNewFeaturedItems(ctxt); + + return ctxt.engine().submit(new ListDataverseFeaturedItemsCommand(getRequest(), dataverse)); } private void updateOrDeleteExistingFeaturedItems(CommandContext ctxt) throws CommandException { diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index f973cda3770..78adae7062c 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -982,6 +982,8 @@ dataverse.create.error.jsonparsetodataverse=Error parsing the POSTed json into a dataverse.create.featuredItem.error.imageFileProcessing=Error processing featured item file: {0} dataverse.create.featuredItem.error.fileSizeExceedsLimit=File exceeds the maximum size of {0} dataverse.create.featuredItem.error.invalidFileType=Invalid image file type +dataverse.update.featuredItems.error.missingInputParams=All input parameters (id, content, displayOrder, keepFile, file) are required. +dataverse.update.featuredItems.error.inputListsSizeMismatch=All input lists (id, content, displayOrder, keepFile, file) must have the same size. # rolesAndPermissionsFragment.xhtml # advanced.xhtml diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java index 51876e13a72..6089a44054c 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java @@ -1631,17 +1631,16 @@ public void testUpdateFeaturedItems() { createDataverseResponse.then().assertThat().statusCode(CREATED.getStatusCode()); String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + // Create new items + List ids = Arrays.asList(0L, 0L); List contents = Arrays.asList("Content 1", "Content 2"); List orders = Arrays.asList(1, 2); + List keepFiles = Arrays.asList(false, false); List pathsToFiles = Arrays.asList("src/test/resources/images/coffeeshop.png", "src/test/resources/images/coffeeshop.png"); - Response updateDataverseFeaturedItemsResponse = UtilIT.updateDataverseFeaturedItems(dataverseAlias, contents, orders, pathsToFiles, apiToken); + Response updateDataverseFeaturedItemsResponse = UtilIT.updateDataverseFeaturedItems(dataverseAlias, ids, contents, orders, keepFiles, pathsToFiles, apiToken); updateDataverseFeaturedItemsResponse.prettyPrint(); updateDataverseFeaturedItemsResponse.then().assertThat() .statusCode(OK.getStatusCode()); - - Response listFeaturedItemsResponse = UtilIT.listDataverseFeaturedItems(dataverseAlias, apiToken); - listFeaturedItemsResponse.prettyPrint(); - } } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index 54dc622bfdd..d0da601eb9a 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -4450,12 +4450,12 @@ static Response listDataverseFeaturedItems(String dataverseAlias, String apiToke .get("/api/dataverses/" + dataverseAlias + "/featuredItems"); } - - // TODO: Refine static Response updateDataverseFeaturedItems( String dataverseAlias, + List ids, List contents, List orders, + List keepFiles, List pathsToFiles, String apiToken) { @@ -4464,18 +4464,20 @@ static Response updateDataverseFeaturedItems( .contentType(ContentType.MULTIPART); int size = contents.size(); - if (orders.size() != size || pathsToFiles.size() != size) { - throw new IllegalArgumentException("Contents, orders, and pathsToFiles must have the same size."); + if (ids.size() != size || orders.size() != size || keepFiles.size() != size || pathsToFiles.size() != size) { + throw new IllegalArgumentException("'ids', 'contents', 'orders', 'keepFiles' and 'pathsToFiles' lists must have the same size."); } for (int i = 0; i < size; i++) { + Long id = ids.get(i); String content = contents.get(i); Integer order = orders.get(i); + boolean keepFile = keepFiles.get(i); requestSpecification.multiPart("content", content); requestSpecification.multiPart("displayOrder", order); - requestSpecification.multiPart("keepFile", false); - requestSpecification.multiPart("id", 0); + requestSpecification.multiPart("keepFile", keepFile); + requestSpecification.multiPart("id", id); String pathToFile = pathsToFiles.get(i); if (pathToFile != null && !pathToFile.isEmpty()) { @@ -4487,5 +4489,4 @@ static Response updateDataverseFeaturedItems( .when() .put("/api/dataverses/" + dataverseAlias + "/featuredItems"); } - } From d1c7a040e15daba09385ff16d9540de805a5fcf6 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 9 Jan 2025 14:59:49 +0000 Subject: [PATCH 24/57] Changed: relocated flush call to avoid unnecessary call in DataverseFeaturedItemServiceBean --- .../harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java index 90fb3e04d1f..b5ee7f60166 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java @@ -37,10 +37,10 @@ public DataverseFeaturedItem findById(Long id) { public DataverseFeaturedItem save(DataverseFeaturedItem dataverseFeaturedItem) { if (dataverseFeaturedItem.getId() == null) { em.persist(dataverseFeaturedItem); + em.flush(); } else { dataverseFeaturedItem = em.merge(dataverseFeaturedItem); } - em.flush(); return dataverseFeaturedItem; } From b8853b7a62b0ecdcc7f67c59a2305d66472f9218 Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 10 Jan 2025 09:50:25 +0000 Subject: [PATCH 25/57] Fixed: delete featured item inconsistencies --- .../edu/harvard/iq/dataverse/DataverseFeaturedItem.java | 4 ++++ .../iq/dataverse/DataverseFeaturedItemServiceBean.java | 6 ++++++ .../command/impl/DeleteDataverseFeaturedItemCommand.java | 4 +--- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java index 52da2bb9be5..fb2077c5f27 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java @@ -6,6 +6,10 @@ import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Size; +@NamedQueries({ + @NamedQuery(name = "DataverseFeaturedItem.deleteById", + query = "DELETE FROM DataverseFeaturedItem item WHERE item.id=:id") +}) @Entity public class DataverseFeaturedItem { diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java index b5ee7f60166..73f7b5c5057 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java @@ -44,6 +44,12 @@ public DataverseFeaturedItem save(DataverseFeaturedItem dataverseFeaturedItem) { return dataverseFeaturedItem; } + public void delete(Long id) { + em.createNamedQuery("DataverseFeaturedItem.deleteById", DataverseFeaturedItem.class) + .setParameter("id", id) + .executeUpdate(); + } + public InputStream getImageFileAsInputStream(DataverseFeaturedItem dataverseFeaturedItem) throws IOException { Path imagePath = Path.of(JvmSettings.DOCROOT_DIRECTORY.lookup(), JvmSettings.FEATURED_ITEMS_IMAGE_UPLOADS_DIRECTORY.lookup(), diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseFeaturedItemCommand.java index 7ed0e60cccb..3a7504b8c77 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseFeaturedItemCommand.java @@ -21,8 +21,6 @@ public DeleteDataverseFeaturedItemCommand(DataverseRequest request, DataverseFea @Override protected void executeImpl(CommandContext ctxt) throws CommandException { - ctxt.em().merge(doomed.getDataverse()); - DataverseFeaturedItem doomedAndMerged = ctxt.em().merge(doomed); - ctxt.em().remove(doomedAndMerged); + ctxt.dataverseFeaturedItems().delete(doomed.getId()); } } From 3df4723d5d308ed3593e3d5cdae1ba1092d5c5b2 Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 10 Jan 2025 09:51:22 +0000 Subject: [PATCH 26/57] Added: updateFeaturedItems working endpoint --- .../harvard/iq/dataverse/api/Dataverses.java | 59 +++++---- .../UpdateDataverseFeaturedItemsCommand.java | 33 +++-- src/main/java/propertyFiles/Bundle.properties | 4 +- .../iq/dataverse/api/DataversesIT.java | 114 ++++++++++++++++-- .../edu/harvard/iq/dataverse/api/UtilIT.java | 33 ++--- 5 files changed, 178 insertions(+), 65 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index ea7b41b9ca9..d4bbb50b971 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -1788,50 +1788,61 @@ public Response updateFeaturedItems( @FormDataParam("content") List contents, @FormDataParam("displayOrder") List displayOrders, @FormDataParam("keepFile") List keepFiles, + @FormDataParam("fileName") List fileNames, @FormDataParam("file") List files) { try { - if (ids == null || contents == null || displayOrders == null || keepFiles == null || files == null) { - throw new WrappedResponse(error(Response.Status.BAD_REQUEST, BundleUtil.getStringFromBundle("dataverse.update.featuredItems.error.missingInputParams"))); + if (ids == null || contents == null || displayOrders == null || keepFiles == null || fileNames == null) { + throw new WrappedResponse(error(Response.Status.BAD_REQUEST, + BundleUtil.getStringFromBundle("dataverse.update.featuredItems.error.missingInputParams"))); } int size = ids.size(); - if (contents.size() != size || displayOrders.size() != size || keepFiles.size() != size || files.size() != size) { - throw new WrappedResponse(error(Response.Status.BAD_REQUEST, BundleUtil.getStringFromBundle("dataverse.update.featuredItems.error.inputListsSizeMismatch"))); + if (contents.size() != size || displayOrders.size() != size || keepFiles.size() != size || fileNames.size() != size) { + throw new WrappedResponse(error(Response.Status.BAD_REQUEST, + BundleUtil.getStringFromBundle("dataverse.update.featuredItems.error.inputListsSizeMismatch"))); } Dataverse dataverse = findDataverseOrDie(dvIdtf); - List newDataverseFeaturedItemDTOs = new ArrayList<>(); - Map dataverseFeaturedItemsToUpdate = new HashMap<>(); + List newItems = new ArrayList<>(); + Map itemsToUpdate = new HashMap<>(); for (int i = 0; i < contents.size(); i++) { - Long id = ids.get(i); - String content = contents.get(i); - Integer displayOrder = displayOrders.get(i); - boolean keepFile = keepFiles.get(i); - FormDataBodyPart fileBodyPart = files.get(i); - - InputStream fileInputStream = fileBodyPart.getValueAs(InputStream.class); - FormDataContentDisposition contentDispositionHeader = fileBodyPart.getFormDataContentDisposition(); - - if (id == 0) { - NewDataverseFeaturedItemDTO newDTO = NewDataverseFeaturedItemDTO.fromFormData(content, displayOrder, fileInputStream, contentDispositionHeader); - newDataverseFeaturedItemDTOs.add(newDTO); + String fileName = fileNames.get(i); + InputStream fileInputStream = null; + FormDataContentDisposition contentDisposition = null; + + if (files != null) { + Optional matchingFile = files.stream() + .filter(file -> file.getFormDataContentDisposition().getFileName().equals(fileName)) + .findFirst(); + + if (matchingFile.isPresent()) { + fileInputStream = matchingFile.get().getValueAs(InputStream.class); + contentDisposition = matchingFile.get().getFormDataContentDisposition(); + } + } + + if (ids.get(i) == 0) { + newItems.add(NewDataverseFeaturedItemDTO.fromFormData( + contents.get(i), displayOrders.get(i), fileInputStream, contentDisposition)); } else { - DataverseFeaturedItem existingItem = dataverseFeaturedItemServiceBean.findById(id); + DataverseFeaturedItem existingItem = dataverseFeaturedItemServiceBean.findById(ids.get(i)); if (existingItem == null) { - throw new WrappedResponse(error(Response.Status.NOT_FOUND, MessageFormat.format(BundleUtil.getStringFromBundle("dataverseFeaturedItems.errors.notFound"), id))); + throw new WrappedResponse(error(Response.Status.NOT_FOUND, + MessageFormat.format(BundleUtil.getStringFromBundle("dataverseFeaturedItems.errors.notFound"), ids.get(i)))); } - UpdatedDataverseFeaturedItemDTO updatedDTO = UpdatedDataverseFeaturedItemDTO.fromFormData(content, displayOrder, keepFile, fileInputStream, contentDispositionHeader); - dataverseFeaturedItemsToUpdate.put(existingItem, updatedDTO); + itemsToUpdate.put(existingItem, UpdatedDataverseFeaturedItemDTO.fromFormData( + contents.get(i), displayOrders.get(i), keepFiles.get(i), fileInputStream, contentDisposition)); } } List featuredItems = execCommand(new UpdateDataverseFeaturedItemsCommand( createDataverseRequest(getRequestUser(crc)), dataverse, - newDataverseFeaturedItemDTOs, - dataverseFeaturedItemsToUpdate + newItems, + itemsToUpdate )); + return ok(jsonDataverseFeaturedItems(featuredItems)); } catch (WrappedResponse wr) { return wr.getResponse(); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemsCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemsCommand.java index afb09bdf455..8fb5f4f09c8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemsCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemsCommand.java @@ -8,6 +8,8 @@ import edu.harvard.iq.dataverse.engine.command.*; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Map; @@ -34,34 +36,43 @@ public UpdateDataverseFeaturedItemsCommand(DataverseRequest request, Dataverse d @Override public List execute(CommandContext ctxt) throws CommandException { - updateOrDeleteExistingFeaturedItems(ctxt); - createNewFeaturedItems(ctxt); - - return ctxt.engine().submit(new ListDataverseFeaturedItemsCommand(getRequest(), dataverse)); + List dataverseFeaturedItems = updateOrDeleteExistingFeaturedItems(ctxt); + dataverseFeaturedItems.addAll(createNewFeaturedItems(ctxt)); + dataverseFeaturedItems.sort(Comparator.comparingLong(DataverseFeaturedItem::getId)); + return dataverseFeaturedItems; } - private void updateOrDeleteExistingFeaturedItems(CommandContext ctxt) throws CommandException { + private List updateOrDeleteExistingFeaturedItems(CommandContext ctxt) throws CommandException { + List updatedFeaturedItems = new ArrayList<>(); List featuredItemsToDelete = dataverse.getDataverseFeaturedItems(); for (Map.Entry entry : dataverseFeaturedItemsToUpdate.entrySet()) { DataverseFeaturedItem featuredItem = entry.getKey(); UpdatedDataverseFeaturedItemDTO updatedDTO = entry.getValue(); - if (featuredItemsToDelete.contains(featuredItem)) { - featuredItemsToDelete.remove(featuredItem); - } + featuredItemsToDelete.stream() + .filter(item -> item.getId().equals(featuredItem.getId())) + .findFirst().ifPresent(featuredItemsToDelete::remove); - ctxt.engine().submit(new UpdateDataverseFeaturedItemCommand(getRequest(), featuredItem, updatedDTO)); + DataverseFeaturedItem updatedFeatureItem = ctxt.engine().submit(new UpdateDataverseFeaturedItemCommand(getRequest(), featuredItem, updatedDTO)); + updatedFeaturedItems.add(updatedFeatureItem); } for (DataverseFeaturedItem featuredItem : featuredItemsToDelete) { ctxt.engine().submit(new DeleteDataverseFeaturedItemCommand(getRequest(), featuredItem)); } + + return updatedFeaturedItems; } - private void createNewFeaturedItems(CommandContext ctxt) throws CommandException { + private List createNewFeaturedItems(CommandContext ctxt) throws CommandException { + List createdFeaturedItems = new ArrayList<>(); + for (NewDataverseFeaturedItemDTO dto : newDataverseFeaturedItemDTOs) { - ctxt.engine().submit(new CreateDataverseFeaturedItemCommand(getRequest(), dataverse, dto)); + DataverseFeaturedItem createdFeatureItem = ctxt.engine().submit(new CreateDataverseFeaturedItemCommand(getRequest(), dataverse, dto)); + createdFeaturedItems.add(createdFeatureItem); } + + return createdFeaturedItems; } } diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 78adae7062c..ab674a9dc15 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -982,8 +982,8 @@ dataverse.create.error.jsonparsetodataverse=Error parsing the POSTed json into a dataverse.create.featuredItem.error.imageFileProcessing=Error processing featured item file: {0} dataverse.create.featuredItem.error.fileSizeExceedsLimit=File exceeds the maximum size of {0} dataverse.create.featuredItem.error.invalidFileType=Invalid image file type -dataverse.update.featuredItems.error.missingInputParams=All input parameters (id, content, displayOrder, keepFile, file) are required. -dataverse.update.featuredItems.error.inputListsSizeMismatch=All input lists (id, content, displayOrder, keepFile, file) must have the same size. +dataverse.update.featuredItems.error.missingInputParams=All input parameters (id, content, displayOrder, keepFile, fileNames) are required. +dataverse.update.featuredItems.error.inputListsSizeMismatch=All input lists (id, content, displayOrder, keepFile, fileNames) must have the same size. # rolesAndPermissionsFragment.xhtml # advanced.xhtml diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java index 6089a44054c..ea29fb487fc 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java @@ -1622,7 +1622,6 @@ public void testCreateFeaturedItem() { .statusCode(NOT_FOUND.getStatusCode()); } - // TODO: Complete @Test public void testUpdateFeaturedItems() { Response createUserResponse = UtilIT.createRandomUser(); @@ -1630,17 +1629,118 @@ public void testUpdateFeaturedItems() { Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); createDataverseResponse.then().assertThat().statusCode(CREATED.getStatusCode()); String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + String baseUri = UtilIT.getRestAssuredBaseUri(); // Create new items - List ids = Arrays.asList(0L, 0L); - List contents = Arrays.asList("Content 1", "Content 2"); - List orders = Arrays.asList(1, 2); - List keepFiles = Arrays.asList(false, false); - List pathsToFiles = Arrays.asList("src/test/resources/images/coffeeshop.png", "src/test/resources/images/coffeeshop.png"); + + List ids = Arrays.asList(0L, 0L, 0L); + List contents = Arrays.asList("Content 1", "Content 2", "Content 3"); + List orders = Arrays.asList(0, 1, 2); + List keepFiles = Arrays.asList(false, false, false); + List pathsToFiles = Arrays.asList("src/test/resources/images/coffeeshop.png", null, null); Response updateDataverseFeaturedItemsResponse = UtilIT.updateDataverseFeaturedItems(dataverseAlias, ids, contents, orders, keepFiles, pathsToFiles, apiToken); - updateDataverseFeaturedItemsResponse.prettyPrint(); updateDataverseFeaturedItemsResponse.then().assertThat() + .body("data.size()", equalTo(3)) + .body("data[0].content", equalTo("Content 1")) + .body("data[0].imageFileName", equalTo("coffeeshop.png")) + .body("data[0].imageFileUrl", containsString(baseUri + "/api/access/dataverseFeatureItemImage/")) + .body("data[0].displayOrder", equalTo(0)) + .body("data[1].content", equalTo("Content 2")) + .body("data[1].imageFileName", equalTo(null)) + .body("data[1].imageFileUrl", equalTo(null)) + .body("data[1].displayOrder", equalTo(1)) + .body("data[2].content", equalTo("Content 3")) + .body("data[2].imageFileName", equalTo(null)) + .body("data[2].imageFileUrl", equalTo(null)) + .body("data[2].displayOrder", equalTo(2)) + .statusCode(OK.getStatusCode()); + + Long firstItemId = JsonPath.from(updateDataverseFeaturedItemsResponse.body().asString()).getLong("data[0].id"); + Long secondItemId = JsonPath.from(updateDataverseFeaturedItemsResponse.body().asString()).getLong("data[1].id"); + Long thirdItemId = JsonPath.from(updateDataverseFeaturedItemsResponse.body().asString()).getLong("data[2].id"); + + // Update first item (content, order, and keeping image), delete the rest and create new items + + ids = Arrays.asList(firstItemId, 0L, 0L); + contents = Arrays.asList("Content 1 updated", "Content 2", "Content 3"); + orders = Arrays.asList(1, 0, 2); + keepFiles = Arrays.asList(true, false, false); + pathsToFiles = Arrays.asList(null, null, null); + + updateDataverseFeaturedItemsResponse = UtilIT.updateDataverseFeaturedItems(dataverseAlias, ids, contents, orders, keepFiles, pathsToFiles, apiToken); + updateDataverseFeaturedItemsResponse.then().assertThat() + .body("data.size()", equalTo(3)) + .body("data[0].content", equalTo("Content 1 updated")) + .body("data[0].imageFileName", equalTo("coffeeshop.png")) + .body("data[0].imageFileUrl", containsString(baseUri + "/api/access/dataverseFeatureItemImage/")) + .body("data[0].displayOrder", equalTo(1)) + .body("data[1].content", equalTo("Content 2")) + .body("data[1].imageFileName", equalTo(null)) + .body("data[1].imageFileUrl", equalTo(null)) + .body("data[1].displayOrder", equalTo(0)) + .body("data[2].content", equalTo("Content 3")) + .body("data[2].imageFileName", equalTo(null)) + .body("data[2].imageFileUrl", equalTo(null)) + .body("data[2].displayOrder", equalTo(2)) + .statusCode(OK.getStatusCode()); + + Long firstItemIdAfterUpdate = JsonPath.from(updateDataverseFeaturedItemsResponse.body().asString()).getLong("data[0].id"); + Long secondItemIdAfterUpdate = JsonPath.from(updateDataverseFeaturedItemsResponse.body().asString()).getLong("data[1].id"); + Long thirdItemIdAfterUpdate = JsonPath.from(updateDataverseFeaturedItemsResponse.body().asString()).getLong("data[2].id"); + + assertEquals(firstItemId, firstItemIdAfterUpdate); + assertNotEquals(secondItemId, secondItemIdAfterUpdate); + assertNotEquals(thirdItemId, thirdItemIdAfterUpdate); + + // Update first item (removing image), update second item (adding image), delete the third item and create a new item + + ids = Arrays.asList(firstItemId, secondItemIdAfterUpdate, 0L); + contents = Arrays.asList("Content 1 updated", "Content 2", "Content 3"); + orders = Arrays.asList(1, 0, 2); + keepFiles = Arrays.asList(false, false, false); + pathsToFiles = Arrays.asList(null, "src/test/resources/images/coffeeshop.png", null); + + updateDataverseFeaturedItemsResponse = UtilIT.updateDataverseFeaturedItems(dataverseAlias, ids, contents, orders, keepFiles, pathsToFiles, apiToken); + updateDataverseFeaturedItemsResponse.then().assertThat() + .body("data.size()", equalTo(3)) + .body("data[0].content", equalTo("Content 1 updated")) + .body("data[0].imageFileName", equalTo(null)) + .body("data[0].imageFileUrl", equalTo(null)) + .body("data[0].displayOrder", equalTo(1)) + .body("data[1].content", equalTo("Content 2")) + .body("data[1].imageFileName", equalTo("coffeeshop.png")) + .body("data[1].imageFileUrl", containsString(baseUri + "/api/access/dataverseFeatureItemImage/")) + .body("data[1].displayOrder", equalTo(0)) + .body("data[2].content", equalTo("Content 3")) + .body("data[2].imageFileName", equalTo(null)) + .body("data[2].imageFileUrl", equalTo(null)) + .body("data[2].displayOrder", equalTo(2)) + .statusCode(OK.getStatusCode()); + + Long firstItemIdAftersSecondUpdate = JsonPath.from(updateDataverseFeaturedItemsResponse.body().asString()).getLong("data[0].id"); + Long secondItemIdAfterSecondUpdate = JsonPath.from(updateDataverseFeaturedItemsResponse.body().asString()).getLong("data[1].id"); + Long thirdItemIdAfterSecondUpdate = JsonPath.from(updateDataverseFeaturedItemsResponse.body().asString()).getLong("data[2].id"); + + assertEquals(firstItemId, firstItemIdAftersSecondUpdate); + assertEquals(secondItemIdAfterUpdate, secondItemIdAfterSecondUpdate); + assertNotEquals(thirdItemIdAfterUpdate, thirdItemIdAfterSecondUpdate); + + // Only keep first featured item + + ids = List.of(firstItemId); + contents = List.of("Content 1 updated"); + orders = List.of(0); + keepFiles = List.of(false); + pathsToFiles = null; + + updateDataverseFeaturedItemsResponse = UtilIT.updateDataverseFeaturedItems(dataverseAlias, ids, contents, orders, keepFiles, pathsToFiles, apiToken); + updateDataverseFeaturedItemsResponse.then().assertThat() + .body("data.size()", equalTo(1)) + .body("data[0].content", equalTo("Content 1 updated")) + .body("data[0].imageFileName", equalTo(null)) + .body("data[0].imageFileUrl", equalTo(null)) + .body("data[0].displayOrder", equalTo(0)) .statusCode(OK.getStatusCode()); } } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index d0da601eb9a..6e95dace0e3 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -4459,34 +4459,25 @@ static Response updateDataverseFeaturedItems( List pathsToFiles, String apiToken) { - RequestSpecification requestSpecification = given() + RequestSpecification requestSpec = given() .header(API_TOKEN_HTTP_HEADER, apiToken) .contentType(ContentType.MULTIPART); - int size = contents.size(); - if (ids.size() != size || orders.size() != size || keepFiles.size() != size || pathsToFiles.size() != size) { - throw new IllegalArgumentException("'ids', 'contents', 'orders', 'keepFiles' and 'pathsToFiles' lists must have the same size."); - } - - for (int i = 0; i < size; i++) { - Long id = ids.get(i); - String content = contents.get(i); - Integer order = orders.get(i); - boolean keepFile = keepFiles.get(i); + for (int i = 0; i < contents.size(); i++) { + requestSpec.multiPart("content", contents.get(i)) + .multiPart("displayOrder", orders.get(i)) + .multiPart("keepFile", keepFiles.get(i)) + .multiPart("id", ids.get(i)); - requestSpecification.multiPart("content", content); - requestSpecification.multiPart("displayOrder", order); - requestSpecification.multiPart("keepFile", keepFile); - requestSpecification.multiPart("id", id); - - String pathToFile = pathsToFiles.get(i); + String pathToFile = pathsToFiles != null ? pathsToFiles.get(i) : null; if (pathToFile != null && !pathToFile.isEmpty()) { - requestSpecification.multiPart("file", new File(pathToFile)); + requestSpec.multiPart("fileName", Paths.get(pathToFile).getFileName().toString()) + .multiPart("file", new File(pathToFile)); + } else { + requestSpec.multiPart("fileName", ""); } } - return requestSpecification - .when() - .put("/api/dataverses/" + dataverseAlias + "/featuredItems"); + return requestSpec.when().put("/api/dataverses/" + dataverseAlias + "/featuredItems"); } } From aabc4d3f18c6f47a8f7fdc05189b7c9e5e4b3af2 Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 10 Jan 2025 10:08:00 +0000 Subject: [PATCH 27/57] Added: deleteDataverseFeaturedItems endpoint --- .../harvard/iq/dataverse/api/Dataverses.java | 14 ++++++++ src/main/java/propertyFiles/Bundle.properties | 1 + .../iq/dataverse/api/DataversesIT.java | 33 +++++++++++++++++++ .../edu/harvard/iq/dataverse/api/UtilIT.java | 6 ++++ 4 files changed, 54 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index d4bbb50b971..22eea188150 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -1,5 +1,6 @@ package edu.harvard.iq.dataverse.api; +import com.google.api.client.util.ArrayMap; import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.api.auth.AuthRequired; import edu.harvard.iq.dataverse.api.datadeposit.SwordServiceBean; @@ -1848,4 +1849,17 @@ public Response updateFeaturedItems( return wr.getResponse(); } } + + @DELETE + @AuthRequired + @Path("{identifier}/featuredItems") + public Response deleteFeaturedItems(@Context ContainerRequestContext crc, @PathParam("identifier") String dvIdtf) { + try { + Dataverse dataverse = findDataverseOrDie(dvIdtf); + execCommand(new UpdateDataverseFeaturedItemsCommand(createDataverseRequest(getRequestUser(crc)), dataverse, new ArrayList<>(), new ArrayMap<>())); + return ok(BundleUtil.getStringFromBundle("dataverse.delete.featuredItems.success")); + } catch (WrappedResponse e) { + return e.getResponse(); + } + } } diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 07962712008..88358af5468 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -991,6 +991,7 @@ dataverse.create.featuredItem.error.fileSizeExceedsLimit=File exceeds the maximu dataverse.create.featuredItem.error.invalidFileType=Invalid image file type dataverse.update.featuredItems.error.missingInputParams=All input parameters (id, content, displayOrder, keepFile, fileNames) are required. dataverse.update.featuredItems.error.inputListsSizeMismatch=All input lists (id, content, displayOrder, keepFile, fileNames) must have the same size. +dataverse.delete.featuredItems.success=All featured items of this Dataverse have been successfully deleted. # rolesAndPermissionsFragment.xhtml # advanced.xhtml diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java index ea29fb487fc..c6e9a8a5975 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java @@ -1743,4 +1743,37 @@ public void testUpdateFeaturedItems() { .body("data[0].displayOrder", equalTo(0)) .statusCode(OK.getStatusCode()); } + + @Test + public void testDeleteFeaturedItems() { + Response createUserResponse = UtilIT.createRandomUser(); + String apiToken = UtilIT.getApiTokenFromResponse(createUserResponse); + Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); + createDataverseResponse.then().assertThat().statusCode(CREATED.getStatusCode()); + String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + + // Create test featured items + + List ids = Arrays.asList(0L, 0L, 0L); + List contents = Arrays.asList("Content 1", "Content 2", "Content 3"); + List orders = Arrays.asList(0, 1, 2); + List keepFiles = Arrays.asList(false, false, false); + List pathsToFiles = Arrays.asList("src/test/resources/images/coffeeshop.png", null, null); + + Response updateDataverseFeaturedItemsResponse = UtilIT.updateDataverseFeaturedItems(dataverseAlias, ids, contents, orders, keepFiles, pathsToFiles, apiToken); + updateDataverseFeaturedItemsResponse.then().assertThat() + .body("data.size()", equalTo(3)) + .statusCode(OK.getStatusCode()); + + // Check that the featured items are successfully deleted when calling the delete endpoint + + Response deleteDataverseFeaturedItemsResponse = UtilIT.deleteDataverseFeaturedItems(dataverseAlias, apiToken); + deleteDataverseFeaturedItemsResponse.then().assertThat() + .statusCode(OK.getStatusCode()); + + Response listFeaturedItemsResponse = UtilIT.listDataverseFeaturedItems(dataverseAlias, apiToken); + listFeaturedItemsResponse.then() + .body("data.size()", equalTo(0)) + .assertThat().statusCode(OK.getStatusCode()); + } } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index 666f765e4a6..7e8da8a3310 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -4484,4 +4484,10 @@ static Response updateDataverseFeaturedItems( return requestSpec.when().put("/api/dataverses/" + dataverseAlias + "/featuredItems"); } + + static Response deleteDataverseFeaturedItems(String dataverseAlias, String apiToken) { + return given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .delete("/api/dataverses/" + dataverseAlias + "/featuredItems"); + } } From b2eca4bebd56c91480c1d653afaf69d539849ab5 Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 10 Jan 2025 12:30:39 +0000 Subject: [PATCH 28/57] Added: featured item content validation --- .../iq/dataverse/DataverseFeaturedItem.java | 4 +- ...ractWriteDataverseFeaturedItemCommand.java | 20 ++++++++++ .../CreateDataverseFeaturedItemCommand.java | 5 ++- .../UpdateDataverseFeaturedItemCommand.java | 2 +- src/main/java/propertyFiles/Bundle.properties | 2 + ...reateDataverseFeaturedItemCommandTest.java | 40 ++++++++++++++++++- 6 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java index fb2077c5f27..3aa15825ac6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java @@ -13,6 +13,8 @@ @Entity public class DataverseFeaturedItem { + public static final int MAX_FEATURED_ITEM_CONTENT_SIZE = 15000; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -22,7 +24,7 @@ public class DataverseFeaturedItem { private Dataverse dataverse; @NotBlank - @Size(max = 15000) + @Size(max = MAX_FEATURED_ITEM_CONTENT_SIZE) @Lob @Column(columnDefinition = "TEXT", nullable = false) private String content; diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java index 0b3395d6f78..8d70ff5a6ab 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.io.InputStream; +import java.text.MessageFormat; import java.util.List; /** @@ -27,6 +28,25 @@ public AbstractWriteDataverseFeaturedItemCommand(DataverseRequest request, Datav this.dataverse = affectedDataverse; } + protected void validateAndSetContent(DataverseFeaturedItem featuredItem, String content) throws InvalidCommandArgumentsException { + if (content == null) { + throw new InvalidCommandArgumentsException( + BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.contentShouldBeProvided"), + this + ); + } + if (content.length() > DataverseFeaturedItem.MAX_FEATURED_ITEM_CONTENT_SIZE) { + throw new InvalidCommandArgumentsException( + MessageFormat.format( + BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.contentExceedsLengthLimit"), + List.of(DataverseFeaturedItem.MAX_FEATURED_ITEM_CONTENT_SIZE) + ), + this + ); + } + featuredItem.setContent(content); + } + protected void setFileImageIfAvailableOrNull(DataverseFeaturedItem featuredItem, String imageFileName, InputStream imageFileInputStream, CommandContext ctxt) throws CommandException { if (imageFileName != null && imageFileInputStream != null) { try { diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java index e1f21587bb0..b21ed1403cd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java @@ -25,6 +25,9 @@ public CreateDataverseFeaturedItemCommand(DataverseRequest request, public DataverseFeaturedItem execute(CommandContext ctxt) throws CommandException { DataverseFeaturedItem dataverseFeaturedItem = new DataverseFeaturedItem(); + validateAndSetContent(dataverseFeaturedItem, newDataverseFeaturedItemDTO.getContent()); + dataverseFeaturedItem.setDisplayOrder(newDataverseFeaturedItemDTO.getDisplayOrder()); + setFileImageIfAvailableOrNull( dataverseFeaturedItem, newDataverseFeaturedItemDTO.getImageFileName(), @@ -32,8 +35,6 @@ public DataverseFeaturedItem execute(CommandContext ctxt) throws CommandExceptio ctxt ); - dataverseFeaturedItem.setContent(newDataverseFeaturedItemDTO.getContent()); - dataverseFeaturedItem.setDisplayOrder(newDataverseFeaturedItemDTO.getDisplayOrder()); dataverseFeaturedItem.setDataverse(dataverse); return ctxt.dataverseFeaturedItems().save(dataverseFeaturedItem); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemCommand.java index 51e5d43a7aa..a54f0324a3e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemCommand.java @@ -24,7 +24,7 @@ public UpdateDataverseFeaturedItemCommand(DataverseRequest request, @Override public DataverseFeaturedItem execute(CommandContext ctxt) throws CommandException { - dataverseFeaturedItem.setContent(updatedDataverseFeaturedItemDTO.getContent()); + validateAndSetContent(dataverseFeaturedItem, updatedDataverseFeaturedItemDTO.getContent()); dataverseFeaturedItem.setDisplayOrder(updatedDataverseFeaturedItemDTO.getDisplayOrder()); if (!updatedDataverseFeaturedItemDTO.isKeepFile()) { diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 88358af5468..f3264dabd2b 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -989,6 +989,8 @@ dataverse.create.error.jsonparsetodataverse=Error parsing the POSTed json into a dataverse.create.featuredItem.error.imageFileProcessing=Error processing featured item file: {0} dataverse.create.featuredItem.error.fileSizeExceedsLimit=File exceeds the maximum size of {0} dataverse.create.featuredItem.error.invalidFileType=Invalid image file type +dataverse.create.featuredItem.error.contentShouldBeProvided=Featured item 'content' property should be provided. +dataverse.create.featuredItem.error.contentExceedsLengthLimit=Featured item content exceeds the maximum allowed length of {0} characters. dataverse.update.featuredItems.error.missingInputParams=All input parameters (id, content, displayOrder, keepFile, fileNames) are required. dataverse.update.featuredItems.error.inputListsSizeMismatch=All input lists (id, content, displayOrder, keepFile, fileNames) must have the same size. dataverse.delete.featuredItems.success=All featured items of this Dataverse have been successfully deleted. diff --git a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java index d8bb6549c60..d584b1795a9 100644 --- a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java @@ -7,6 +7,7 @@ import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.InvalidCommandArgumentsException; +import edu.harvard.iq.dataverse.util.BundleUtil; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; @@ -15,6 +16,8 @@ import java.io.IOException; import java.io.InputStream; +import java.text.MessageFormat; +import java.util.List; import static edu.harvard.iq.dataverse.mocks.MocksFactory.makeRequest; import static org.junit.jupiter.api.Assertions.*; @@ -106,8 +109,8 @@ void execute_imageFileProcessingFails_throwsCommandException() throws IOExceptio } @Test - void execute_invalidArgumentsProvided_throwsInvalidCommandArgumentsException() throws IOException, DataverseFeaturedItemServiceBean.InvalidImageFileException { - testNewDataverseFeaturedItemDTO.setImageFileName("invalid.png"); + void execute_invalidFileTypeProvided_throwsInvalidCommandArgumentsException() throws IOException, DataverseFeaturedItemServiceBean.InvalidImageFileException { + testNewDataverseFeaturedItemDTO.setImageFileName("invalid.type"); InputStream inputStreamMock = mock(InputStream.class); testNewDataverseFeaturedItemDTO.setImageFileInputStream(inputStreamMock); @@ -117,4 +120,37 @@ void execute_invalidArgumentsProvided_throwsInvalidCommandArgumentsException() t InvalidCommandArgumentsException exception = assertThrows(InvalidCommandArgumentsException.class, () -> sut.execute(contextStub)); assertTrue(exception.getMessage().contains("Invalid file type")); } + + @Test + void execute_contentNotProvided_throwsInvalidCommandArgumentsException() { + testNewDataverseFeaturedItemDTO.setContent(null); + InputStream inputStreamMock = mock(InputStream.class); + testNewDataverseFeaturedItemDTO.setImageFileInputStream(inputStreamMock); + + InvalidCommandArgumentsException exception = assertThrows(InvalidCommandArgumentsException.class, () -> sut.execute(contextStub)); + assertEquals( + BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.contentShouldBeProvided"), + exception.getMessage() + ); + } + + @Test + void execute_contentExceedsLimit_throwsInvalidCommandArgumentsException() { + testNewDataverseFeaturedItemDTO.setContent(createContentExceedingMaxLength()); + InputStream inputStreamMock = mock(InputStream.class); + testNewDataverseFeaturedItemDTO.setImageFileInputStream(inputStreamMock); + + InvalidCommandArgumentsException exception = assertThrows(InvalidCommandArgumentsException.class, () -> sut.execute(contextStub)); + assertEquals( + MessageFormat.format( + BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.contentExceedsLengthLimit"), + List.of(DataverseFeaturedItem.MAX_FEATURED_ITEM_CONTENT_SIZE) + ), + exception.getMessage() + ); + } + + private String createContentExceedingMaxLength() { + return "a".repeat(Math.max(0, DataverseFeaturedItem.MAX_FEATURED_ITEM_CONTENT_SIZE + 1)); + } } From b4f5ce983b47df4a137d07ca52a6e2f05ccbb183 Mon Sep 17 00:00:00 2001 From: GPortas Date: Sat, 11 Jan 2025 12:51:59 +0000 Subject: [PATCH 29/57] Added: order featured items results by display order and missing tests added --- .../iq/dataverse/DataverseFeaturedItem.java | 5 +- .../DataverseFeaturedItemServiceBean.java | 7 + .../ListDataverseFeaturedItemsCommand.java | 7 +- .../UpdateDataverseFeaturedItemsCommand.java | 2 +- .../iq/dataverse/api/DataversesIT.java | 123 +++++++++++++++--- 5 files changed, 116 insertions(+), 28 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java index 3aa15825ac6..ee91c258426 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java @@ -8,9 +8,12 @@ @NamedQueries({ @NamedQuery(name = "DataverseFeaturedItem.deleteById", - query = "DELETE FROM DataverseFeaturedItem item WHERE item.id=:id") + query = "DELETE FROM DataverseFeaturedItem item WHERE item.id=:id"), + @NamedQuery(name = "DataverseFeaturedItem.findByDataverseOrderedByDisplayOrder", + query = "SELECT item FROM DataverseFeaturedItem item WHERE item.dataverse = :dataverse ORDER BY item.displayOrder ASC") }) @Entity +@Table(indexes = @Index(columnList = "displayOrder")) public class DataverseFeaturedItem { public static final int MAX_FEATURED_ITEM_CONTENT_SIZE = 15000; diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java index 73f7b5c5057..89a970fae64 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java @@ -50,6 +50,13 @@ public void delete(Long id) { .executeUpdate(); } + public List findAllByDataverseOrdered(Dataverse dataverse) { + return em + .createNamedQuery("DataverseFeaturedItem.findByDataverseOrderedByDisplayOrder", DataverseFeaturedItem.class) + .setParameter("dataverse", dataverse) + .getResultList(); + } + public InputStream getImageFileAsInputStream(DataverseFeaturedItem dataverseFeaturedItem) throws IOException { Path imagePath = Path.of(JvmSettings.DOCROOT_DIRECTORY.lookup(), JvmSettings.FEATURED_ITEMS_IMAGE_UPLOADS_DIRECTORY.lookup(), diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseFeaturedItemsCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseFeaturedItemsCommand.java index 9ff2d2e1e71..474747ccb0d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseFeaturedItemsCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseFeaturedItemsCommand.java @@ -8,10 +8,7 @@ import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; /** * Lists the featured items {@link DataverseFeaturedItem} of a {@link Dataverse}. @@ -27,7 +24,7 @@ public ListDataverseFeaturedItemsCommand(DataverseRequest request, Dataverse dat @Override public List execute(CommandContext ctxt) throws CommandException { - return dataverse.getDataverseFeaturedItems(); + return ctxt.dataverseFeaturedItems().findAllByDataverseOrdered(dataverse); } @Override diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemsCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemsCommand.java index 8fb5f4f09c8..12a972f2e47 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemsCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemsCommand.java @@ -38,7 +38,7 @@ public UpdateDataverseFeaturedItemsCommand(DataverseRequest request, Dataverse d public List execute(CommandContext ctxt) throws CommandException { List dataverseFeaturedItems = updateOrDeleteExistingFeaturedItems(ctxt); dataverseFeaturedItems.addAll(createNewFeaturedItems(ctxt)); - dataverseFeaturedItems.sort(Comparator.comparingLong(DataverseFeaturedItem::getId)); + dataverseFeaturedItems.sort(Comparator.comparingInt(DataverseFeaturedItem::getDisplayOrder)); return dataverseFeaturedItems; } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java index c6e9a8a5975..ddd85f65562 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java @@ -1586,6 +1586,7 @@ public void testCreateFeaturedItem() { String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); // Should not return any error when not passing a file + Response createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, apiToken, "test", 0, null); createFeatureItemResponse.then().assertThat() .statusCode(OK.getStatusCode()) @@ -1594,6 +1595,7 @@ public void testCreateFeaturedItem() { .body("data.displayOrder", equalTo(0)); // Should not return any error when passing correct file and data + String pathToTestFile = "src/test/resources/images/coffeeshop.png"; createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, apiToken, "test", 1, pathToTestFile); createFeatureItemResponse.then().assertThat() @@ -1603,6 +1605,7 @@ public void testCreateFeaturedItem() { .statusCode(OK.getStatusCode()); // Should return bad request error when passing incorrect file type + pathToTestFile = "src/test/resources/tab/test.tab"; createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, apiToken, "test", 0, pathToTestFile); createFeatureItemResponse.then().assertThat() @@ -1610,18 +1613,68 @@ public void testCreateFeaturedItem() { .statusCode(BAD_REQUEST.getStatusCode()); // Should return unauthorized error when user has no permissions + Response createRandomUser = UtilIT.createRandomUser(); String randomUserApiToken = UtilIT.getApiTokenFromResponse(createRandomUser); createFeatureItemResponse = UtilIT.createDataverseFeaturedItem(dataverseAlias, randomUserApiToken, "test", 0, pathToTestFile); createFeatureItemResponse.then().assertThat().statusCode(UNAUTHORIZED.getStatusCode()); // Should return not found error when dataverse does not exist + createFeatureItemResponse = UtilIT.createDataverseFeaturedItem("thisDataverseDoesNotExist", apiToken, "test", 0, pathToTestFile); createFeatureItemResponse.then().assertThat() .body("message", equalTo("Can't find dataverse with identifier='thisDataverseDoesNotExist'")) .statusCode(NOT_FOUND.getStatusCode()); } + @Test + public void testListFeaturedItems() { + Response createUserResponse = UtilIT.createRandomUser(); + String apiToken = UtilIT.getApiTokenFromResponse(createUserResponse); + Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); + createDataverseResponse.then().assertThat().statusCode(CREATED.getStatusCode()); + String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + + // Create test items + + List ids = Arrays.asList(0L, 0L, 0L); + List contents = Arrays.asList("Content 1", "Content 2", "Content 3"); + List orders = Arrays.asList(2, 1, 0); + List keepFiles = Arrays.asList(false, false, false); + List pathsToFiles = Arrays.asList("src/test/resources/images/coffeeshop.png", null, null); + + Response updateDataverseFeaturedItemsResponse = UtilIT.updateDataverseFeaturedItems(dataverseAlias, ids, contents, orders, keepFiles, pathsToFiles, apiToken); + updateDataverseFeaturedItemsResponse.then().assertThat() + .statusCode(OK.getStatusCode()); + + // Items should be retrieved with all their properties and sorted by displayOrder + + Response listDataverseFeaturedItemsResponse = UtilIT.listDataverseFeaturedItems(dataverseAlias, apiToken); + listDataverseFeaturedItemsResponse.then().assertThat() + .body("data.size()", equalTo(3)) + .body("data[0].content", equalTo("Content 3")) + .body("data[0].imageFileName", equalTo(null)) + .body("data[0].imageFileUrl", equalTo(null)) + .body("data[0].displayOrder", equalTo(0)) + .body("data[1].content", equalTo("Content 2")) + .body("data[1].imageFileName", equalTo(null)) + .body("data[1].imageFileUrl", equalTo(null)) + .body("data[1].displayOrder", equalTo(1)) + .body("data[2].content", equalTo("Content 1")) + .body("data[2].imageFileName", equalTo("coffeeshop.png")) + .body("data[2].imageFileUrl", containsString(UtilIT.getRestAssuredBaseUri() + "/api/access/dataverseFeatureItemImage/")) + .body("data[2].displayOrder", equalTo(2)) + .statusCode(OK.getStatusCode()); + + // Should return not found error when dataverse does not exist + + listDataverseFeaturedItemsResponse = UtilIT.listDataverseFeaturedItems("thisDataverseDoesNotExist", apiToken); + listDataverseFeaturedItemsResponse.then().assertThat() + .body("message", equalTo("Can't find dataverse with identifier='thisDataverseDoesNotExist'")) + .statusCode(NOT_FOUND.getStatusCode()); + + } + @Test public void testUpdateFeaturedItems() { Response createUserResponse = UtilIT.createRandomUser(); @@ -1671,22 +1724,22 @@ public void testUpdateFeaturedItems() { updateDataverseFeaturedItemsResponse = UtilIT.updateDataverseFeaturedItems(dataverseAlias, ids, contents, orders, keepFiles, pathsToFiles, apiToken); updateDataverseFeaturedItemsResponse.then().assertThat() .body("data.size()", equalTo(3)) - .body("data[0].content", equalTo("Content 1 updated")) - .body("data[0].imageFileName", equalTo("coffeeshop.png")) - .body("data[0].imageFileUrl", containsString(baseUri + "/api/access/dataverseFeatureItemImage/")) - .body("data[0].displayOrder", equalTo(1)) - .body("data[1].content", equalTo("Content 2")) - .body("data[1].imageFileName", equalTo(null)) - .body("data[1].imageFileUrl", equalTo(null)) - .body("data[1].displayOrder", equalTo(0)) + .body("data[0].content", equalTo("Content 2")) + .body("data[0].imageFileName", equalTo(null)) + .body("data[0].imageFileUrl", equalTo(null)) + .body("data[0].displayOrder", equalTo(0)) + .body("data[1].content", equalTo("Content 1 updated")) + .body("data[1].imageFileName", equalTo("coffeeshop.png")) + .body("data[1].imageFileUrl", containsString(baseUri + "/api/access/dataverseFeatureItemImage/")) + .body("data[1].displayOrder", equalTo(1)) .body("data[2].content", equalTo("Content 3")) .body("data[2].imageFileName", equalTo(null)) .body("data[2].imageFileUrl", equalTo(null)) .body("data[2].displayOrder", equalTo(2)) .statusCode(OK.getStatusCode()); - Long firstItemIdAfterUpdate = JsonPath.from(updateDataverseFeaturedItemsResponse.body().asString()).getLong("data[0].id"); - Long secondItemIdAfterUpdate = JsonPath.from(updateDataverseFeaturedItemsResponse.body().asString()).getLong("data[1].id"); + Long firstItemIdAfterUpdate = JsonPath.from(updateDataverseFeaturedItemsResponse.body().asString()).getLong("data[1].id"); + Long secondItemIdAfterUpdate = JsonPath.from(updateDataverseFeaturedItemsResponse.body().asString()).getLong("data[0].id"); Long thirdItemIdAfterUpdate = JsonPath.from(updateDataverseFeaturedItemsResponse.body().asString()).getLong("data[2].id"); assertEquals(firstItemId, firstItemIdAfterUpdate); @@ -1704,25 +1757,25 @@ public void testUpdateFeaturedItems() { updateDataverseFeaturedItemsResponse = UtilIT.updateDataverseFeaturedItems(dataverseAlias, ids, contents, orders, keepFiles, pathsToFiles, apiToken); updateDataverseFeaturedItemsResponse.then().assertThat() .body("data.size()", equalTo(3)) - .body("data[0].content", equalTo("Content 1 updated")) - .body("data[0].imageFileName", equalTo(null)) - .body("data[0].imageFileUrl", equalTo(null)) - .body("data[0].displayOrder", equalTo(1)) - .body("data[1].content", equalTo("Content 2")) - .body("data[1].imageFileName", equalTo("coffeeshop.png")) - .body("data[1].imageFileUrl", containsString(baseUri + "/api/access/dataverseFeatureItemImage/")) - .body("data[1].displayOrder", equalTo(0)) + .body("data[0].content", equalTo("Content 2")) + .body("data[0].imageFileName", equalTo("coffeeshop.png")) + .body("data[0].imageFileUrl", containsString(baseUri + "/api/access/dataverseFeatureItemImage/")) + .body("data[0].displayOrder", equalTo(0)) + .body("data[1].content", equalTo("Content 1 updated")) + .body("data[1].imageFileName", equalTo(null)) + .body("data[1].imageFileUrl", equalTo(null)) + .body("data[1].displayOrder", equalTo(1)) .body("data[2].content", equalTo("Content 3")) .body("data[2].imageFileName", equalTo(null)) .body("data[2].imageFileUrl", equalTo(null)) .body("data[2].displayOrder", equalTo(2)) .statusCode(OK.getStatusCode()); - Long firstItemIdAftersSecondUpdate = JsonPath.from(updateDataverseFeaturedItemsResponse.body().asString()).getLong("data[0].id"); - Long secondItemIdAfterSecondUpdate = JsonPath.from(updateDataverseFeaturedItemsResponse.body().asString()).getLong("data[1].id"); + Long firstItemIdAfterSecondUpdate = JsonPath.from(updateDataverseFeaturedItemsResponse.body().asString()).getLong("data[1].id"); + Long secondItemIdAfterSecondUpdate = JsonPath.from(updateDataverseFeaturedItemsResponse.body().asString()).getLong("data[0].id"); Long thirdItemIdAfterSecondUpdate = JsonPath.from(updateDataverseFeaturedItemsResponse.body().asString()).getLong("data[2].id"); - assertEquals(firstItemId, firstItemIdAftersSecondUpdate); + assertEquals(firstItemId, firstItemIdAfterSecondUpdate); assertEquals(secondItemIdAfterUpdate, secondItemIdAfterSecondUpdate); assertNotEquals(thirdItemIdAfterUpdate, thirdItemIdAfterSecondUpdate); @@ -1742,6 +1795,20 @@ public void testUpdateFeaturedItems() { .body("data[0].imageFileUrl", equalTo(null)) .body("data[0].displayOrder", equalTo(0)) .statusCode(OK.getStatusCode()); + + // Should return unauthorized error when user has no permissions + + Response createRandomUser = UtilIT.createRandomUser(); + String randomUserApiToken = UtilIT.getApiTokenFromResponse(createRandomUser); + updateDataverseFeaturedItemsResponse = UtilIT.updateDataverseFeaturedItems(dataverseAlias, ids, contents, orders, keepFiles, pathsToFiles, randomUserApiToken); + updateDataverseFeaturedItemsResponse.then().assertThat().statusCode(UNAUTHORIZED.getStatusCode()); + + // Should return not found error when dataverse does not exist + + updateDataverseFeaturedItemsResponse = UtilIT.updateDataverseFeaturedItems("thisDataverseDoesNotExist", ids, contents, orders, keepFiles, pathsToFiles, apiToken); + updateDataverseFeaturedItemsResponse.then().assertThat() + .body("message", equalTo("Can't find dataverse with identifier='thisDataverseDoesNotExist'")) + .statusCode(NOT_FOUND.getStatusCode()); } @Test @@ -1775,5 +1842,19 @@ public void testDeleteFeaturedItems() { listFeaturedItemsResponse.then() .body("data.size()", equalTo(0)) .assertThat().statusCode(OK.getStatusCode()); + + // Should return unauthorized error when user has no permissions + + Response createRandomUser = UtilIT.createRandomUser(); + String randomUserApiToken = UtilIT.getApiTokenFromResponse(createRandomUser); + deleteDataverseFeaturedItemsResponse = UtilIT.deleteDataverseFeaturedItems(dataverseAlias, randomUserApiToken); + deleteDataverseFeaturedItemsResponse.then().assertThat().statusCode(UNAUTHORIZED.getStatusCode()); + + // Should return not found error when dataverse does not exist + + deleteDataverseFeaturedItemsResponse = UtilIT.deleteDataverseFeaturedItems("thisDataverseDoesNotExist", apiToken); + deleteDataverseFeaturedItemsResponse.then().assertThat() + .body("message", equalTo("Can't find dataverse with identifier='thisDataverseDoesNotExist'")) + .statusCode(NOT_FOUND.getStatusCode()); } } From e265ae873b0c053e4b6b0b3542d8f4a8323bcda9 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 13 Jan 2025 14:12:36 +0000 Subject: [PATCH 30/57] Fixed: response message string formatting --- src/main/java/propertyFiles/Bundle.properties | 4 ++-- .../dataverse/api/DataverseFeaturedItemsIT.java | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index f3264dabd2b..9a89fc0eee5 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -3126,5 +3126,5 @@ authenticationServiceBean.errors.invalidBearerToken=Could not parse bearer token authenticationServiceBean.errors.bearerTokenDetectedNoOIDCProviderConfigured=Bearer token detected, no OIDC provider configured. #DataverseFeaturedItems.java -dataverseFeaturedItems.errors.notFound=Can't find dataverse featured item with identifier='{0}' -dataverseFeaturedItems.delete.successful=Successfully deleted dataverse featured item with identifier='{0}' +dataverseFeaturedItems.errors.notFound=Can't find dataverse featured item with identifier {0} +dataverseFeaturedItems.delete.successful=Successfully deleted dataverse featured item with identifier {0} diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java index d53414d4207..edb1e2bebba 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java @@ -1,11 +1,14 @@ package edu.harvard.iq.dataverse.api; +import edu.harvard.iq.dataverse.util.BundleUtil; import io.restassured.RestAssured; import io.restassured.path.json.JsonPath; import io.restassured.response.Response; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import java.text.MessageFormat; + import static jakarta.ws.rs.core.Response.Status.*; import static org.hamcrest.CoreMatchers.equalTo; @@ -24,7 +27,9 @@ public void testDeleteFeaturedItem() { // Should return not found when passing incorrect item id Response deleteFeatureItemResponse = UtilIT.deleteDataverseFeaturedItem(100000L, apiToken); - deleteFeatureItemResponse.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); + deleteFeatureItemResponse.then() + .body("message", equalTo(MessageFormat.format(BundleUtil.getStringFromBundle("dataverseFeaturedItems.errors.notFound"), featuredItemId))) + .assertThat().statusCode(NOT_FOUND.getStatusCode()); // Should return unauthorized when passing correct id and user does not have permissions String randomUserApiToken = createUserAndGetApiToken(); @@ -33,7 +38,9 @@ public void testDeleteFeaturedItem() { // Should delete featured item when passing correct id and user has permissions deleteFeatureItemResponse = UtilIT.deleteDataverseFeaturedItem(featuredItemId, apiToken); - deleteFeatureItemResponse.then().assertThat().statusCode(OK.getStatusCode()); + deleteFeatureItemResponse.then() + .body("data.message", equalTo(MessageFormat.format(BundleUtil.getStringFromBundle("dataverseFeaturedItems.delete.successful"), featuredItemId))) + .assertThat().statusCode(OK.getStatusCode()); Response listFeaturedItemsResponse = UtilIT.listDataverseFeaturedItems(dataverseAlias, apiToken); listFeaturedItemsResponse.then() @@ -49,7 +56,9 @@ public void testUpdateFeaturedItem() { // Should return not found when passing incorrect item id Response updateFeatureItemResponse = UtilIT.updateDataverseFeaturedItem(100000L, "updatedTitle", 1, false, null, apiToken); - updateFeatureItemResponse.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); + updateFeatureItemResponse.then() + .body("message", equalTo(MessageFormat.format(BundleUtil.getStringFromBundle("dataverseFeaturedItems.errors.notFound"), featuredItemId))) + .assertThat().statusCode(NOT_FOUND.getStatusCode()); // Should return unauthorized when passing correct id and user does not have permissions String randomUserApiToken = createUserAndGetApiToken(); From bff8e94dd57d62be9e61139d7877b942b1b4633a Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 13 Jan 2025 14:40:59 +0000 Subject: [PATCH 31/57] Added: featured items deletion in DeleteDataverseCommand --- src/main/java/edu/harvard/iq/dataverse/Dataverse.java | 4 ++++ .../engine/command/impl/DeleteDataverseCommand.java | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/Dataverse.java b/src/main/java/edu/harvard/iq/dataverse/Dataverse.java index 024d37a730d..eaebf654dca 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Dataverse.java +++ b/src/main/java/edu/harvard/iq/dataverse/Dataverse.java @@ -358,6 +358,10 @@ public List getDataverseFeaturedItems() { return this.dataverseFeaturedItems; } + public void setDataverseFeaturedItems(List dataverseFeaturedItems) { + this.dataverseFeaturedItems = dataverseFeaturedItems; + } + public List getParentGuestbooks() { List retList = new ArrayList<>(); Dataverse testDV = this; diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseCommand.java index c7c592f9458..7fbb659ab96 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseCommand.java @@ -1,6 +1,7 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.DataverseFeaturedItem; import edu.harvard.iq.dataverse.DataverseFieldTypeInputLevel; import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.RoleAssignment; @@ -78,6 +79,14 @@ protected void executeImpl(CommandContext ctxt) throws CommandException { ctxt.em().remove(merged); } doomed.setDataverseFieldTypeInputLevels(new ArrayList<>()); + + // Featured Items + for (DataverseFeaturedItem featuredItem : doomed.getDataverseFeaturedItems()) { + DataverseFeaturedItem merged = ctxt.em().merge(featuredItem); + ctxt.em().remove(merged); + } + doomed.setDataverseFeaturedItems(new ArrayList<>()); + // DATAVERSE Dataverse doomedAndMerged = ctxt.em().merge(doomed); ctxt.em().remove(doomedAndMerged); From 5c61511d465baec72395e52cd6e35b347f6fb506 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 13 Jan 2025 15:27:22 +0000 Subject: [PATCH 32/57] Added: sanitizing featured item content property --- .../impl/AbstractWriteDataverseFeaturedItemCommand.java | 2 ++ .../iq/dataverse/api/DataverseFeaturedItemsIT.java | 8 ++++++-- .../edu/harvard/iq/dataverse/util/MarkupCheckerTest.java | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java index 8d70ff5a6ab..ad7a8ca7807 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java @@ -9,6 +9,7 @@ import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.exception.InvalidCommandArgumentsException; import edu.harvard.iq.dataverse.util.BundleUtil; +import edu.harvard.iq.dataverse.util.MarkupChecker; import java.io.IOException; import java.io.InputStream; @@ -35,6 +36,7 @@ protected void validateAndSetContent(DataverseFeaturedItem featuredItem, String this ); } + content = MarkupChecker.sanitizeBasicHTML(content); if (content.length() > DataverseFeaturedItem.MAX_FEATURED_ITEM_CONTENT_SIZE) { throw new InvalidCommandArgumentsException( MessageFormat.format( diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java index edb1e2bebba..d815fc77919 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java @@ -76,6 +76,10 @@ public void testUpdateFeaturedItem() { // Update featured item: set new image file updateFeatureItemResponse = UtilIT.updateDataverseFeaturedItem(featuredItemId, "updatedTitle1", 2, false, "src/test/resources/images/coffeeshop.png", apiToken); verifyUpdatedFeaturedItem(updateFeatureItemResponse, "updatedTitle1", "coffeeshop.png", 2); + + // Update featured item: set malicious content which should be sanitized + updateFeatureItemResponse = UtilIT.updateDataverseFeaturedItem(featuredItemId, "

hello

", 2, false, "src/test/resources/images/coffeeshop.png", apiToken); + verifyUpdatedFeaturedItem(updateFeatureItemResponse, "

hello

", "coffeeshop.png", 2); } private String createUserAndGetApiToken() { @@ -96,9 +100,9 @@ private Long createFeaturedItemAndGetId(String dataverseAlias, String apiToken, return createdFeaturedItem.getLong("data.id"); } - private void verifyUpdatedFeaturedItem(Response response, String expectedTitle, String expectedImageFileName, int expectedDisplayOrder) { + private void verifyUpdatedFeaturedItem(Response response, String expectedContent, String expectedImageFileName, int expectedDisplayOrder) { response.then().assertThat() - .body("data.content", equalTo(expectedTitle)) + .body("data.content", equalTo(expectedContent)) .body("data.imageFileName", equalTo(expectedImageFileName)) .body("data.displayOrder", equalTo(expectedDisplayOrder)) .statusCode(OK.getStatusCode()); diff --git a/src/test/java/edu/harvard/iq/dataverse/util/MarkupCheckerTest.java b/src/test/java/edu/harvard/iq/dataverse/util/MarkupCheckerTest.java index 07876e56eb8..19d656b2736 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/MarkupCheckerTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/MarkupCheckerTest.java @@ -23,6 +23,8 @@ public class MarkupCheckerTest { "'

hello

', '

hello

'", "'the Dataverse project in a new window', 'the Dataverse project in a new window'", "'the Dataverse project in a new window', 'the Dataverse project in a new window'", + // make sure we keep text as it is when it is not html + "'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'", "NULL, NULL" }, nullValues = {"NULL"}) public void testSanitizeBasicHTML(String unsafe, String safe) { From f4b6e0caba1a3626c2883f5df6a9f597a78fd426 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 14 Jan 2025 09:36:31 +0000 Subject: [PATCH 33/57] Added: empty content validation for featured items --- ...ractWriteDataverseFeaturedItemCommand.java | 2 +- src/main/java/propertyFiles/Bundle.properties | 2 +- ...reateDataverseFeaturedItemCommandTest.java | 30 +++++++++++++------ 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java index ad7a8ca7807..e76553dd9d4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java @@ -30,7 +30,7 @@ public AbstractWriteDataverseFeaturedItemCommand(DataverseRequest request, Datav } protected void validateAndSetContent(DataverseFeaturedItem featuredItem, String content) throws InvalidCommandArgumentsException { - if (content == null) { + if (content == null || content.trim().isEmpty()) { throw new InvalidCommandArgumentsException( BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.contentShouldBeProvided"), this diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 9a89fc0eee5..ea4bcbbd30f 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -989,7 +989,7 @@ dataverse.create.error.jsonparsetodataverse=Error parsing the POSTed json into a dataverse.create.featuredItem.error.imageFileProcessing=Error processing featured item file: {0} dataverse.create.featuredItem.error.fileSizeExceedsLimit=File exceeds the maximum size of {0} dataverse.create.featuredItem.error.invalidFileType=Invalid image file type -dataverse.create.featuredItem.error.contentShouldBeProvided=Featured item 'content' property should be provided. +dataverse.create.featuredItem.error.contentShouldBeProvided=Featured item 'content' property should be provided and not empty. dataverse.create.featuredItem.error.contentExceedsLengthLimit=Featured item content exceeds the maximum allowed length of {0} characters. dataverse.update.featuredItems.error.missingInputParams=All input parameters (id, content, displayOrder, keepFile, fileNames) are required. dataverse.update.featuredItems.error.inputListsSizeMismatch=All input lists (id, content, displayOrder, keepFile, fileNames) must have the same size. diff --git a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java index d584b1795a9..01c5f499b0f 100644 --- a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java @@ -10,6 +10,8 @@ import edu.harvard.iq.dataverse.util.BundleUtil; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -122,16 +124,14 @@ void execute_invalidFileTypeProvided_throwsInvalidCommandArgumentsException() th } @Test - void execute_contentNotProvided_throwsInvalidCommandArgumentsException() { - testNewDataverseFeaturedItemDTO.setContent(null); - InputStream inputStreamMock = mock(InputStream.class); - testNewDataverseFeaturedItemDTO.setImageFileInputStream(inputStreamMock); + void execute_contentIsNull_throwsInvalidCommandArgumentsException() { + assertContentShouldBeProvidedInvalidCommandArgumentsException(null); + } - InvalidCommandArgumentsException exception = assertThrows(InvalidCommandArgumentsException.class, () -> sut.execute(contextStub)); - assertEquals( - BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.contentShouldBeProvided"), - exception.getMessage() - ); + @ParameterizedTest + @ValueSource(strings = {" ", ""}) + void execute_contentIsEmpty_throwsInvalidCommandArgumentsException(String content) { + assertContentShouldBeProvidedInvalidCommandArgumentsException(content); } @Test @@ -150,6 +150,18 @@ void execute_contentExceedsLimit_throwsInvalidCommandArgumentsException() { ); } + private void assertContentShouldBeProvidedInvalidCommandArgumentsException(String content) { + testNewDataverseFeaturedItemDTO.setContent(content); + InputStream inputStreamMock = mock(InputStream.class); + testNewDataverseFeaturedItemDTO.setImageFileInputStream(inputStreamMock); + + InvalidCommandArgumentsException exception = assertThrows(InvalidCommandArgumentsException.class, () -> sut.execute(contextStub)); + assertEquals( + BundleUtil.getStringFromBundle("dataverse.create.featuredItem.error.contentShouldBeProvided"), + exception.getMessage() + ); + } + private String createContentExceedingMaxLength() { return "a".repeat(Math.max(0, DataverseFeaturedItem.MAX_FEATURED_ITEM_CONTENT_SIZE + 1)); } From 107a16e1efdfcfc130be4607dec56c5aa081fb59 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 14 Jan 2025 11:09:09 +0000 Subject: [PATCH 34/57] Changed: using advanced HTML sanitization for featured item content --- ...ractWriteDataverseFeaturedItemCommand.java | 2 +- .../iq/dataverse/util/MarkupChecker.java | 108 ++++++++++++------ .../api/DataverseFeaturedItemsIT.java | 6 +- .../iq/dataverse/util/MarkupCheckerTest.java | 38 ++++++ 4 files changed, 119 insertions(+), 35 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java index e76553dd9d4..ae2db1c752a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java @@ -36,7 +36,7 @@ protected void validateAndSetContent(DataverseFeaturedItem featuredItem, String this ); } - content = MarkupChecker.sanitizeBasicHTML(content); + content = MarkupChecker.sanitizeAdvancedHTML(content); if (content.length() > DataverseFeaturedItem.MAX_FEATURED_ITEM_CONTENT_SIZE) { throw new InvalidCommandArgumentsException( MessageFormat.format( diff --git a/src/main/java/edu/harvard/iq/dataverse/util/MarkupChecker.java b/src/main/java/edu/harvard/iq/dataverse/util/MarkupChecker.java index ef74819f073..02055ad60e9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/MarkupChecker.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/MarkupChecker.java @@ -1,8 +1,3 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ package edu.harvard.iq.dataverse.util; import org.apache.commons.text.StringEscapeUtils; @@ -11,56 +6,105 @@ import org.jsoup.parser.Parser; /** - * Wrapper for Jsoup clean - * + * Provides utility methods for sanitizing and processing HTML content. + *

+ * This class serves as a wrapper for the {@code Jsoup.clean} method and offers + * multiple configurations for cleaning HTML input. It also provides a method + * for escaping HTML entities and stripping all HTML tags. + *

+ * * @author rmp553 */ public class MarkupChecker { - - - + /** - * Wrapper around Jsoup clean method with the basic Safe list - * http://jsoup.org/cookbook/cleaning-html/safelist-sanitizer - * @param unsafe - * @return + * Sanitizes the provided HTML content using a customizable configuration. + *

+ * This method uses the {@code Jsoup.clean} method with a configurable {@code Safelist}. + * For more details, see the + * Jsoup SafeList Sanitizer. + *

+ *

+ * It supports preserving class attributes and optionally adding "noopener noreferrer nofollow" + * attributes to anchor tags to enhance security and usability. + *

+ * + * @param unsafe the HTML content to be sanitized; may contain unsafe or untrusted elements. + * @param keepClasses whether to preserve class attributes in the sanitized HTML. + * @param includeNoopenerNoreferrer whether to add "noopener noreferrer nofollow" to tags. + * @return a sanitized HTML string, free from potentially harmful content. */ - public static String sanitizeBasicHTML(String unsafe) { - + private static String sanitizeHTML(String unsafe, boolean keepClasses, boolean includeNoopenerNoreferrer) { if (unsafe == null) { return null; } - // basic includes: a, b, blockquote, br, cite, code, dd, dl, dt, em, i, li, ol, p, pre, q, small, span, strike, strong, sub, sup, u, ul - //Whitelist wl = Whitelist.basic().addTags("img", "h1", "h2", "h3", "kbd", "hr", "s", "del"); - Safelist sl = Safelist.basicWithImages().addTags("h1", "h2", "h3", "kbd", "hr", "s", "del", "map", "area").addAttributes("img", "usemap") - .addAttributes("map", "name").addAttributes("area", "shape", "coords", "href", "title", "alt") + // Create a base Safelist configuration + Safelist sl = Safelist.basicWithImages() + .addTags("h1", "h2", "h3", "kbd", "hr", "s", "del", "map", "area") + .addAttributes("img", "usemap") + .addAttributes("map", "name") + .addAttributes("area", "shape", "coords", "href", "title", "alt") .addEnforcedAttribute("a", "target", "_blank"); + // Add class attributes if requested + if (keepClasses) { + sl.addAttributes(":all", "class"); + } + + // Add "noopener noreferrer nofollow" to tags if requested + if (includeNoopenerNoreferrer) { + sl.addEnforcedAttribute("a", "rel", "noopener noreferrer nofollow"); + } + return Jsoup.clean(unsafe, sl); + } + /** + * Sanitizes the provided HTML content using a basic configuration. + * + * @param unsafe the HTML content to be sanitized; may contain unsafe or untrusted elements. + * @return a sanitized HTML string, free from potentially harmful content. + */ + public static String sanitizeBasicHTML(String unsafe) { + return sanitizeHTML(unsafe, false, false); } - + /** - * Strip all HTMl tags - * - * http://jsoup.org/apidocs/org/jsoup/safety/Safelist.html#none - * - * @param unsafe - * @return + * Sanitizes the provided HTML content using an advanced configuration. + *

+ * This configuration preserves class attributes and adds "noopener noreferrer nofollow" + * attributes to tags to enhance security and usability. + *

+ * + * @param unsafe the HTML content to be sanitized; may contain unsafe or untrusted elements. + * @return a sanitized HTML string, free from potentially harmful content. */ - public static String stripAllTags(String unsafe) { + public static String sanitizeAdvancedHTML(String unsafe) { + return sanitizeHTML(unsafe, true, true); + } + /** + * Removes all HTML tags from the provided content, leaving only plain text. + * + * @param unsafe the HTML content to process; may contain HTML tags. + * @return the plain text content with all HTML tags removed, or {@code null} if the input is {@code null}. + */ + public static String stripAllTags(String unsafe) { if (unsafe == null) { return null; } return Parser.unescapeEntities(Jsoup.clean(unsafe, Safelist.none()), true); - } - + + /** + * Escapes special characters in the provided string into their corresponding HTML entities. + * + * @param unsafe the string to escape; may contain special characters. + * @return a string with HTML entities escaped. + */ public static String escapeHtml(String unsafe) { - return StringEscapeUtils.escapeHtml4(unsafe); + return StringEscapeUtils.escapeHtml4(unsafe); } - } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java index d815fc77919..2fa45200977 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java @@ -78,8 +78,10 @@ public void testUpdateFeaturedItem() { verifyUpdatedFeaturedItem(updateFeatureItemResponse, "updatedTitle1", "coffeeshop.png", 2); // Update featured item: set malicious content which should be sanitized - updateFeatureItemResponse = UtilIT.updateDataverseFeaturedItem(featuredItemId, "

hello

", 2, false, "src/test/resources/images/coffeeshop.png", apiToken); - verifyUpdatedFeaturedItem(updateFeatureItemResponse, "

hello

", "coffeeshop.png", 2); + String unsafeContent = "

A title

link"; + String sanitizedContent = "

A title

link"; + updateFeatureItemResponse = UtilIT.updateDataverseFeaturedItem(featuredItemId, unsafeContent, 2, false, "src/test/resources/images/coffeeshop.png", apiToken); + verifyUpdatedFeaturedItem(updateFeatureItemResponse, sanitizedContent, "coffeeshop.png", 2); } private String createUserAndGetApiToken() { diff --git a/src/test/java/edu/harvard/iq/dataverse/util/MarkupCheckerTest.java b/src/test/java/edu/harvard/iq/dataverse/util/MarkupCheckerTest.java index 19d656b2736..02219059db3 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/MarkupCheckerTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/MarkupCheckerTest.java @@ -31,6 +31,43 @@ public void testSanitizeBasicHTML(String unsafe, String safe) { assertEquals(safe, MarkupChecker.sanitizeBasicHTML(unsafe)); } + /** + * Test of sanitizeAdvancedHTML method, of class MarkupChecker. + */ + @ParameterizedTest + @CsvSource(value = { + ", ''", + "'', ''", + // make sure we do not destroy the tags + "'\"Galactic', '\"Galactic'", + // make sure we do not destroy the tags + "'\"Galactic', '\"Galactic'", + "'

hellohello</

'", + "'

hello

', '

hello

'", + // make sure we keep text as it is when it is not html + "'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'", + // Should add noopener noreferrer attributes to tags and keep classes + "'

A title

Lorem ipsumtestlink

', '

A title

Lorem ipsumtestlink

'", + "NULL, NULL" + }, nullValues = {"NULL"}) + public void testSanitizeAdvancedHTML(String unsafe, String safe) { + String sanitizedOutput = MarkupChecker.sanitizeAdvancedHTML(unsafe); + + // Normalize both the expected and actual content by removing whitespaces + + String normalizedSafe = null; + if (safe != null) { + normalizedSafe = safe.replaceAll("\\s+", "").trim(); + } + + String normalizedOutput = null; + if (sanitizedOutput != null) { + normalizedOutput = sanitizedOutput.replaceAll("\\s+", "").trim(); + } + + assertEquals(normalizedSafe, normalizedOutput); + } + /** * Test of stripAllTags method, of class MarkupChecker. */ @@ -39,6 +76,7 @@ public void testSanitizeBasicHTML(String unsafe, String safe) { "'', ''", "NULL, NULL", "Johnson & Johnson <>, Johnson & Johnson <>", + "

Johnson & Johnson

, Johnson & Johnson", "Johnson && Johnson <&>&, Johnson && Johnson <&>&" }, nullValues = {"NULL"}) public void testStripAllTags(String unsafe, String safe) { From b83f79ca10f374eb096ff72b4603391a9c56d9b9 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 14 Jan 2025 12:13:10 +0000 Subject: [PATCH 35/57] Added: docs for listing and deleting all featured items --- doc/sphinx-guides/source/api/native-api.rst | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index a363466ce57..d3e1f824c83 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1145,6 +1145,43 @@ Use the ``/settings`` API to enable or disable the enforcement of storage quotas curl -X PUT -d 'true' http://localhost:8080/api/admin/settings/:UseStorageQuotas +List Collection Featured Items +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +List the featured items configured for a given Dataverse collection ``id``: + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export SERVER_URL=https://demo.dataverse.org + export ID=root + + curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/dataverses/$ID/featuredItems" + +The fully expanded example above (without environment variables) looks like this: + +.. code-block:: bash + + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" "https://demo.dataverse.org/api/dataverses/root/featuredItems" + +Delete All Collection Featured Items +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Deletes the featured items configured for a given Dataverse collection ``id``: + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export SERVER_URL=https://demo.dataverse.org + export ID=root + + curl -H "X-Dataverse-key: $API_TOKEN" -X DELETE "$SERVER_URL/api/dataverses/$ID/featuredItems" + +The fully expanded example above (without environment variables) looks like this: + +.. code-block:: bash + + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE "https://demo.dataverse.org/api/dataverses/root/featuredItems" Datasets -------- From e0067807a363d21f24b1dcd9eb74d8c7017ed45e Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 15 Jan 2025 13:25:01 +0000 Subject: [PATCH 36/57] Added: API docs for creating a collection featured item --- doc/sphinx-guides/source/api/native-api.rst | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index d3e1f824c83..c4d75377769 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1183,6 +1183,30 @@ The fully expanded example above (without environment variables) looks like this curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE "https://demo.dataverse.org/api/dataverses/root/featuredItems" +Create A Collection Featured Item +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Creates a featured item in the given Dataverse collection ``id``: + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export IMAGE_FILENAME='image.png' + export CONTENT='Content for featured item.' + export DISPLAY_ORDER=1 + export SERVER_URL=https://demo.dataverse.org + export ID=root + + curl -H "X-Dataverse-key:$API_TOKEN" -X POST -F "file=@$IMAGE_FILENAME" -F "content=$CONTENT" -F "displayOrder=$DISPLAY_ORDER" "$SERVER_URL/api/dataverses/$ID/featuredItems" + +The fully expanded example above (without environment variables) looks like this: + +.. code-block:: bash + + curl -H "X-Dataverse-key:0da2519d-eea5-49ac-bb5d-c349e2e36503" -X POST -F "file=@image.png" -F "content=Content for featured item." -F "displayOrder=1" "https://demo.dataverse.org/api/dataverses/root/featuredItems" + +A featured item may or may not contain an image. If you wish to create it without an image, simply omit the file parameter in the request. + Datasets -------- From a5b268706a7fc66576e9774f7a9cf3c469dc6891 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 15 Jan 2025 16:32:27 +0000 Subject: [PATCH 37/57] Added: docs for updating a single featured item through API --- doc/sphinx-guides/source/api/native-api.rst | 43 ++++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 12d51166cf4..a2c36c4a995 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1214,9 +1214,48 @@ The fully expanded example above (without environment variables) looks like this .. code-block:: bash - curl -H "X-Dataverse-key:0da2519d-eea5-49ac-bb5d-c349e2e36503" -X POST -F "file=@image.png" -F "content=Content for featured item." -F "displayOrder=1" "https://demo.dataverse.org/api/dataverses/root/featuredItems" + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X POST -F "file=@image.png" -F "content=Content for featured item." -F "displayOrder=1" "https://demo.dataverse.org/api/dataverses/root/featuredItems" -A featured item may or may not contain an image. If you wish to create it without an image, simply omit the file parameter in the request. +A featured item may or may not contain an image. If you wish to create it without an image, omit the file parameter in the request. + +Dataverse Collection Featured Items +----------------------------------- + +Update A Collection Featured Item +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Updates a featured item given its ``id``: + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export IMAGE_FILENAME='image.png' + export CONTENT='Content for featured item.' + export DISPLAY_ORDER=1 + export SERVER_URL=https://demo.dataverse.org + export ID=1 + + curl -H "X-Dataverse-key:$API_TOKEN" -X PUT -F "file=@$IMAGE_FILENAME" -F "content=$CONTENT" -F "displayOrder=$DISPLAY_ORDER" "$SERVER_URL/api/api/dataverseFeaturedItems/2" + +The fully expanded example above (without environment variables) looks like this: + +.. code-block:: bash + + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X PUT -F "file=@image.png" -F "content=Content for featured item." -F "displayOrder=1" "https://demo.dataverse.org/api/dataverseFeaturedItems/1" + +``content`` and ``displayOrder`` must always be provided; otherwise, an error will occur. Use the ``file`` parameter to set a new image for the featured item. To keep the existing image, omit ``file`` and send ``keepFile=true``. To remove the image, omit the file parameter. + +Updating the featured item keeping the existing image: + +.. code-block:: bash + + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X PUT -F "keepFile=true" -F "content=Content for featured item." -F "displayOrder=1" "https://demo.dataverse.org/api/dataverseFeaturedItems/1" + +Updating the featured item removing the existing image: + +.. code-block:: bash + + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X PUT -F "content=Content for featured item." -F "displayOrder=1" "https://demo.dataverse.org/api/dataverseFeaturedItems/1" Datasets -------- From 651347e050d836fd2fc59c4b2e998eadcc1a27fd Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 15 Jan 2025 16:39:41 +0000 Subject: [PATCH 38/57] Added: docs for deleting a single featured item through API --- doc/sphinx-guides/source/api/native-api.rst | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index a2c36c4a995..98321f15de7 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1235,7 +1235,7 @@ Updates a featured item given its ``id``: export SERVER_URL=https://demo.dataverse.org export ID=1 - curl -H "X-Dataverse-key:$API_TOKEN" -X PUT -F "file=@$IMAGE_FILENAME" -F "content=$CONTENT" -F "displayOrder=$DISPLAY_ORDER" "$SERVER_URL/api/api/dataverseFeaturedItems/2" + curl -H "X-Dataverse-key:$API_TOKEN" -X PUT -F "file=@$IMAGE_FILENAME" -F "content=$CONTENT" -F "displayOrder=$DISPLAY_ORDER" "$SERVER_URL/api/dataverseFeaturedItems/$ID" The fully expanded example above (without environment variables) looks like this: @@ -1257,6 +1257,25 @@ Updating the featured item removing the existing image: curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X PUT -F "content=Content for featured item." -F "displayOrder=1" "https://demo.dataverse.org/api/dataverseFeaturedItems/1" +Delete A Collection Featured Item +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Deletes a featured item given its ``id``: + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export SERVER_URL=https://demo.dataverse.org + export ID=1 + + curl -H "X-Dataverse-key:$API_TOKEN" -X DELETE "$SERVER_URL/api/dataverseFeaturedItems/$ID" + +The fully expanded example above (without environment variables) looks like this: + +.. code-block:: bash + + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE "https://demo.dataverse.org/api/dataverseFeaturedItems/1" + Datasets -------- From 4503aa896b808b55d7c9e1e84bf53d2e5861ce8a Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 15 Jan 2025 16:55:15 +0000 Subject: [PATCH 39/57] Fixed: Bundle.properties string format --- src/main/java/propertyFiles/Bundle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 441a489ed3b..085761bff56 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -3127,5 +3127,5 @@ authenticationServiceBean.errors.invalidBearerToken=Could not parse bearer token authenticationServiceBean.errors.bearerTokenDetectedNoOIDCProviderConfigured=Bearer token detected, no OIDC provider configured. #DataverseFeaturedItems.java -dataverseFeaturedItems.errors.notFound=Can't find dataverse featured item with identifier {0} +dataverseFeaturedItems.errors.notFound=Could not find dataverse featured item with identifier {0} dataverseFeaturedItems.delete.successful=Successfully deleted dataverse featured item with identifier {0} From 15568a87f83c97a831a887515b870869d10fe0ee Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 16 Jan 2025 13:21:57 +0000 Subject: [PATCH 40/57] Fixed: typos in Bundle.properties --- src/main/java/propertyFiles/Bundle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 085761bff56..a0b5cff2c49 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -992,8 +992,8 @@ dataverse.create.featuredItem.error.fileSizeExceedsLimit=File exceeds the maximu dataverse.create.featuredItem.error.invalidFileType=Invalid image file type dataverse.create.featuredItem.error.contentShouldBeProvided=Featured item 'content' property should be provided and not empty. dataverse.create.featuredItem.error.contentExceedsLengthLimit=Featured item content exceeds the maximum allowed length of {0} characters. -dataverse.update.featuredItems.error.missingInputParams=All input parameters (id, content, displayOrder, keepFile, fileNames) are required. -dataverse.update.featuredItems.error.inputListsSizeMismatch=All input lists (id, content, displayOrder, keepFile, fileNames) must have the same size. +dataverse.update.featuredItems.error.missingInputParams=All input parameters (id, content, displayOrder, keepFile, fileName) are required. +dataverse.update.featuredItems.error.inputListsSizeMismatch=All input lists (id, content, displayOrder, keepFile, fileName) must have the same size. dataverse.delete.featuredItems.success=All featured items of this Dataverse have been successfully deleted. # rolesAndPermissionsFragment.xhtml From 93c1eca0b59dec5511e51bee31d181c6136bb7dd Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 16 Jan 2025 15:36:12 +0000 Subject: [PATCH 41/57] Added: API docs for updating all featured items --- doc/sphinx-guides/source/api/native-api.rst | 68 +++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index b19d7cff668..a30a17054d2 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1218,6 +1218,74 @@ The fully expanded example above (without environment variables) looks like this A featured item may or may not contain an image. If you wish to create it without an image, omit the file parameter in the request. +Update All Collection Featured Items +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Updates all featured items in the given Dataverse collection ``id``. + +The data sent to the endpoint represents the desired final state of the featured items in the Dataverse collection and overwrites any existing featured items configuration. + +The parameters ``id``, ``content``, ``displayOrder``, and ``fileName`` must be specified as many times as the number of items we want to add or update. The order in which these parameters are repeated must match to ensure they correspond to the same featured item. + +The ``file`` parameter must be specified for each image we want to attach to featured items. Note that images can be shared between featured items, so ``fileName`` can have the same value in different featured items. + +The ``id`` parameter must be ``0`` for new items or set to the item's identifier for updates. The ``fileName`` parameter should be empty to exclude an image or match the name of a file sent in a ``file`` parameter to set a new image. ``keepFile`` must always be set to ``false``, unless it's an update to a featured item where we want to preserve the existing image, if one exists. + +Note that any existing featured item not included in the call with its associated identifier and corresponding properties will be removed from the collection. + +The following example creates two featured items, with an image assigned to the second one: + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export SERVER_URL=https://demo.dataverse.org + export ID=root + + export FIRST_ITEM_CONTENT='Content 1' + export FIRST_ITEM_DISPLAY_ORDER=1 + + export SECOND_ITEM_IMAGE_FILENAME='image.png' + export SECOND_ITEM_CONTENT='Content 2' + export SECOND_ITEM_DISPLAY_ORDER=2 + + curl -H "X-Dataverse-key:$API_TOKEN" \ + -X PUT \ + -F "id=0" -F "id=0" \ + -F "content=$FIRST_ITEM_CONTENT" -F "content=$SECOND_ITEM_CONTENT" \ + -F "displayOrder=$FIRST_ITEM_DISPLAY_ORDER" -F "displayOrder=$SECOND_ITEM_DISPLAY_ORDER" \ + -F "fileName=" -F "fileName=$SECOND_ITEM_IMAGE_FILENAME" \ + -F "keepFile=false" -F "keepFile=false" \ + -F "file=@$SECOND_ITEM_IMAGE_FILENAME" \ + "$SERVER_URL/api/dataverses/$ID/featuredItems" + + +The fully expanded example above (without environment variables) looks like this: + +.. code-block:: bash + + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \ + -X PUT \ + -F "id=0" -F "id=0" \ + -F "content=Content 1" -F "content=Content 2" \ + -F "displayOrder=1" -F "displayOrder=2" \ + -F "fileName=" -F "fileName=image.png" \ + -F "keepFile=false" -F "keepFile=false" \ + -F "file=@image.png" \ + "https://demo.dataverse.org/api/dataverses/root/featuredItems" + +The following example creates one featured item and updates a second one, keeping the existing image it may have had: + +.. code-block:: bash + + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \ + -X PUT \ + -F "id=0" -F "id=1" \ + -F "content=Content 1" -F "content=Updated content 2" \ + -F "displayOrder=1" -F "displayOrder=2" \ + -F "fileName=" -F "fileName=" \ + -F "keepFile=false" -F "keepFile=true" \ + "https://demo.dataverse.org/api/dataverses/root/featuredItems" + Dataverse Collection Featured Items ----------------------------------- From 13301be341da84b9512c636d7de4f24d4ea959ad Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 16 Jan 2025 15:46:19 +0000 Subject: [PATCH 42/57] Added: release notes for #10943 --- doc/release-notes/10943-featured-items.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 doc/release-notes/10943-featured-items.md diff --git a/doc/release-notes/10943-featured-items.md b/doc/release-notes/10943-featured-items.md new file mode 100644 index 00000000000..0b485fdb30a --- /dev/null +++ b/doc/release-notes/10943-featured-items.md @@ -0,0 +1,8 @@ +CRUD endpoints for Collection Featured Items have been implemented. In particular, the following endpoints have been implemented: + +- Create a feature item (POST /api/dataverses//featuredItems) +- Update a feature item (PUT /api/dataverseFeaturedItems/) +- Delete a feature item (DELETE /api/dataverseFeaturedItems/) +- List all featured items in a collection (GET /api/dataverses//featuredItems) +- Delete all featured items in a collection (DELETE /api/dataverses//featuredItems) +- Update all featured items in a collection (PUT /api/dataverses//featuredItems) From 38e7d88f81a1b08022cee40b91c68ee06e39d56d Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 16 Jan 2025 15:53:32 +0000 Subject: [PATCH 43/57] Changed: API endpoint route --- src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java | 2 +- src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index f5abaa82004..8cfe9b9bcf6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -1778,7 +1778,7 @@ public Response getUserPermissionsOnDataverse(@Context ContainerRequestContext c @POST @AuthRequired @Consumes(MediaType.MULTIPART_FORM_DATA) - @Path("{identifier}/featuredItem") + @Path("{identifier}/featuredItems") public Response createFeaturedItem(@Context ContainerRequestContext crc, @PathParam("identifier") String dvIdtf, @FormDataParam("content") String content, diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index 3f3e08f1a6b..cd089db3863 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -4441,7 +4441,7 @@ static Response createDataverseFeaturedItem(String dataverseAlias, return requestSpecification .when() - .post("/api/dataverses/" + dataverseAlias + "/featuredItem"); + .post("/api/dataverses/" + dataverseAlias + "/featuredItems"); } static Response deleteDataverseFeaturedItem(long id, String apiToken) { From c4387d59f095de7891a8491da9c9136b3fd6f061 Mon Sep 17 00:00:00 2001 From: Guillermo Portas Date: Tue, 21 Jan 2025 18:59:28 +0000 Subject: [PATCH 44/57] Changed: updated release notes Co-authored-by: Philip Durbin --- doc/release-notes/10943-featured-items.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/release-notes/10943-featured-items.md b/doc/release-notes/10943-featured-items.md index 0b485fdb30a..e7778a2f9c9 100644 --- a/doc/release-notes/10943-featured-items.md +++ b/doc/release-notes/10943-featured-items.md @@ -6,3 +6,5 @@ CRUD endpoints for Collection Featured Items have been implemented. In particula - List all featured items in a collection (GET /api/dataverses//featuredItems) - Delete all featured items in a collection (DELETE /api/dataverses//featuredItems) - Update all featured items in a collection (PUT /api/dataverses//featuredItems) + +See also #10943 and #11124. From 17f523af6209188ed92066022be839626d09d277 Mon Sep 17 00:00:00 2001 From: Guillermo Portas Date: Tue, 21 Jan 2025 19:00:14 +0000 Subject: [PATCH 45/57] Changed: docs title format tweak Co-authored-by: Philip Durbin --- doc/sphinx-guides/source/api/native-api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index a30a17054d2..e779dac3779 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1194,7 +1194,7 @@ The fully expanded example above (without environment variables) looks like this curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE "https://demo.dataverse.org/api/dataverses/root/featuredItems" -Create A Collection Featured Item +Create a Collection Featured Item ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Creates a featured item in the given Dataverse collection ``id``: From 69de6dd3fd972d98cc557e32f46e27d091378232 Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 24 Jan 2025 12:45:03 +0000 Subject: [PATCH 46/57] Fixed: DataverseFeaturedItemsIT --- .../harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java index 2fa45200977..032c1739d53 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItemsIT.java @@ -28,7 +28,7 @@ public void testDeleteFeaturedItem() { // Should return not found when passing incorrect item id Response deleteFeatureItemResponse = UtilIT.deleteDataverseFeaturedItem(100000L, apiToken); deleteFeatureItemResponse.then() - .body("message", equalTo(MessageFormat.format(BundleUtil.getStringFromBundle("dataverseFeaturedItems.errors.notFound"), featuredItemId))) + .body("message", equalTo(MessageFormat.format(BundleUtil.getStringFromBundle("dataverseFeaturedItems.errors.notFound"), 100000L))) .assertThat().statusCode(NOT_FOUND.getStatusCode()); // Should return unauthorized when passing correct id and user does not have permissions @@ -57,7 +57,7 @@ public void testUpdateFeaturedItem() { // Should return not found when passing incorrect item id Response updateFeatureItemResponse = UtilIT.updateDataverseFeaturedItem(100000L, "updatedTitle", 1, false, null, apiToken); updateFeatureItemResponse.then() - .body("message", equalTo(MessageFormat.format(BundleUtil.getStringFromBundle("dataverseFeaturedItems.errors.notFound"), featuredItemId))) + .body("message", equalTo(MessageFormat.format(BundleUtil.getStringFromBundle("dataverseFeaturedItems.errors.notFound"), 100000L))) .assertThat().statusCode(NOT_FOUND.getStatusCode()); // Should return unauthorized when passing correct id and user does not have permissions From e14476706840283d74ee5f6a85fd693e0dcb4d02 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 27 Jan 2025 14:40:40 +0000 Subject: [PATCH 47/57] Fixed: wrong endpoint path name --- .../java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java | 2 +- src/main/java/edu/harvard/iq/dataverse/api/Access.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java index ee91c258426..852baf5b0db 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java @@ -80,7 +80,7 @@ public void setImageFileName(String imageFileName) { public String getImageFileUrl() { if (id != null && imageFileName != null) { - return SystemConfig.getDataverseSiteUrlStatic() + "/api/access/dataverseFeatureItemImage/" + id; + return SystemConfig.getDataverseSiteUrlStatic() + "/api/access/dataverseFeaturedItemImage/" + id; } return null; } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Access.java b/src/main/java/edu/harvard/iq/dataverse/api/Access.java index 0bf02411d3a..c819da6ffe8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Access.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Access.java @@ -1986,7 +1986,7 @@ private URI handleCustomZipDownload(User user, String customZipServiceUrl, Strin @GET @AuthRequired @Produces({"image/png"}) - @Path("dataverseFeatureItemImage/{itemId}") + @Path("dataverseFeaturedItemImage/{itemId}") public InputStream getDataverseFeatureItemImage(@Context ContainerRequestContext crc, @PathParam("itemId") Long itemId) { DataverseFeaturedItem dataverseFeaturedItem; try { From fb61d89a88d7d2d7d8eeb2054509b34ad26301a3 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 27 Jan 2025 14:50:49 +0000 Subject: [PATCH 48/57] Added: docs for api/access/dataverseFeaturedItemImage/{ID} --- doc/sphinx-guides/source/api/native-api.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 9fb7341044d..249da446c8c 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1344,6 +1344,25 @@ The fully expanded example above (without environment variables) looks like this curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE "https://demo.dataverse.org/api/dataverseFeaturedItems/1" +Get A Collection Featured Item Image +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Returns the image of a featured item if one is assigned, given the featured item ``id``: + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export SERVER_URL=https://demo.dataverse.org + export ID=1 + + curl -H "X-Dataverse-key:$API_TOKEN" -X GET "$SERVER_URL/api/access/dataverseFeaturedItemImage/{ID}" + +The fully expanded example above (without environment variables) looks like this: + +.. code-block:: bash + + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X GET "https://demo.dataverse.org/api/access/dataverseFeaturedItemImage/1" + Datasets -------- From e91524bc8362a9b314f48ffb5dd16f13a924616f Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 27 Jan 2025 15:01:10 +0000 Subject: [PATCH 49/57] Refactor: package structure for dataverse featured items --- src/main/java/edu/harvard/iq/dataverse/Dataverse.java | 1 + .../java/edu/harvard/iq/dataverse/EjbDataverseEngine.java | 1 + src/main/java/edu/harvard/iq/dataverse/api/Access.java | 2 ++ .../edu/harvard/iq/dataverse/api/DataverseFeaturedItems.java | 3 ++- src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java | 2 ++ .../{ => dataverse/featured}/DataverseFeaturedItem.java | 3 ++- .../featured}/DataverseFeaturedItemServiceBean.java | 3 ++- .../harvard/iq/dataverse/engine/command/CommandContext.java | 1 + .../impl/AbstractWriteDataverseFeaturedItemCommand.java | 2 ++ .../command/impl/CreateDataverseFeaturedItemCommand.java | 2 +- .../dataverse/engine/command/impl/DeleteDataverseCommand.java | 2 +- .../command/impl/DeleteDataverseFeaturedItemCommand.java | 2 +- .../engine/command/impl/GetDataverseFeaturedItemCommand.java | 2 +- .../command/impl/ListDataverseFeaturedItemsCommand.java | 2 +- .../command/impl/UpdateDataverseFeaturedItemCommand.java | 2 +- .../command/impl/UpdateDataverseFeaturedItemsCommand.java | 2 +- .../java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java | 1 + .../edu/harvard/iq/dataverse/engine/TestCommandContext.java | 1 + .../command/impl/CreateDataverseFeaturedItemCommandTest.java | 4 ++-- 19 files changed, 26 insertions(+), 12 deletions(-) rename src/main/java/edu/harvard/iq/dataverse/{ => dataverse/featured}/DataverseFeaturedItem.java (95%) rename src/main/java/edu/harvard/iq/dataverse/{ => dataverse/featured}/DataverseFeaturedItemServiceBean.java (97%) diff --git a/src/main/java/edu/harvard/iq/dataverse/Dataverse.java b/src/main/java/edu/harvard/iq/dataverse/Dataverse.java index f73405222b8..9bb8992e789 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Dataverse.java +++ b/src/main/java/edu/harvard/iq/dataverse/Dataverse.java @@ -1,5 +1,6 @@ package edu.harvard.iq.dataverse; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItem; import edu.harvard.iq.dataverse.harvest.client.HarvestingClient; import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.search.savedsearch.SavedSearch; diff --git a/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java b/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java index f8ba218d485..0f211dc6713 100644 --- a/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java +++ b/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java @@ -4,6 +4,7 @@ import edu.harvard.iq.dataverse.actionlogging.ActionLogServiceBean; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUserServiceBean; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItemServiceBean; import edu.harvard.iq.dataverse.util.cache.CacheFactoryBean; import edu.harvard.iq.dataverse.engine.DataverseEngine; import edu.harvard.iq.dataverse.authorization.Permission; diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Access.java b/src/main/java/edu/harvard/iq/dataverse/api/Access.java index c819da6ffe8..2a27c89eaaa 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Access.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Access.java @@ -27,6 +27,8 @@ import edu.harvard.iq.dataverse.dataaccess.ImageThumbConverter; import edu.harvard.iq.dataverse.datavariable.DataVariable; import edu.harvard.iq.dataverse.datavariable.VariableServiceBean; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItem; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItemServiceBean; import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; diff --git a/src/main/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItems.java b/src/main/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItems.java index fdaccaa08d2..a77ea000415 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItems.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/DataverseFeaturedItems.java @@ -1,8 +1,9 @@ package edu.harvard.iq.dataverse.api; -import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.api.auth.AuthRequired; import edu.harvard.iq.dataverse.api.dto.UpdatedDataverseFeaturedItemDTO; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItem; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItemServiceBean; import edu.harvard.iq.dataverse.engine.command.impl.*; import edu.harvard.iq.dataverse.util.BundleUtil; import jakarta.ejb.Stateless; diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index 8cfe9b9bcf6..bbd9476b9e8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -18,6 +18,8 @@ import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.dataverse.DataverseUtil; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItem; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItemServiceBean; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.impl.*; import edu.harvard.iq.dataverse.pidproviders.PidProvider; diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java b/src/main/java/edu/harvard/iq/dataverse/dataverse/featured/DataverseFeaturedItem.java similarity index 95% rename from src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java rename to src/main/java/edu/harvard/iq/dataverse/dataverse/featured/DataverseFeaturedItem.java index 852baf5b0db..53d09516789 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItem.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataverse/featured/DataverseFeaturedItem.java @@ -1,5 +1,6 @@ -package edu.harvard.iq.dataverse; +package edu.harvard.iq.dataverse.dataverse.featured; +import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.util.SystemConfig; import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/dataverse/featured/DataverseFeaturedItemServiceBean.java similarity index 97% rename from src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java rename to src/main/java/edu/harvard/iq/dataverse/dataverse/featured/DataverseFeaturedItemServiceBean.java index 89a970fae64..56cdaf5692e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseFeaturedItemServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataverse/featured/DataverseFeaturedItemServiceBean.java @@ -1,5 +1,6 @@ -package edu.harvard.iq.dataverse; +package edu.harvard.iq.dataverse.dataverse.featured; +import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.FileUtil; diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/CommandContext.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/CommandContext.java index b58f5f07ebc..42f2616cd80 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/CommandContext.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/CommandContext.java @@ -2,6 +2,7 @@ import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUserServiceBean; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItemServiceBean; import edu.harvard.iq.dataverse.search.IndexServiceBean; import edu.harvard.iq.dataverse.search.SearchServiceBean; import edu.harvard.iq.dataverse.actionlogging.ActionLogServiceBean; diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java index ae2db1c752a..8c4a8281345 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractWriteDataverseFeaturedItemCommand.java @@ -2,6 +2,8 @@ import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItem; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItemServiceBean; import edu.harvard.iq.dataverse.engine.command.AbstractCommand; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java index b21ed1403cd..24732d05c8b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommand.java @@ -1,7 +1,7 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.Dataverse; -import edu.harvard.iq.dataverse.DataverseFeaturedItem; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItem; import edu.harvard.iq.dataverse.api.dto.NewDataverseFeaturedItemDTO; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseCommand.java index 7fbb659ab96..84a0ab0f3f2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseCommand.java @@ -1,7 +1,7 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.Dataverse; -import edu.harvard.iq.dataverse.DataverseFeaturedItem; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItem; import edu.harvard.iq.dataverse.DataverseFieldTypeInputLevel; import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.RoleAssignment; diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseFeaturedItemCommand.java index 3a7504b8c77..215863a44da 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DeleteDataverseFeaturedItemCommand.java @@ -1,7 +1,7 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.Dataverse; -import edu.harvard.iq.dataverse.DataverseFeaturedItem; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItem; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.engine.command.*; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDataverseFeaturedItemCommand.java index 722c8050dbd..c594887b6ed 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDataverseFeaturedItemCommand.java @@ -1,6 +1,6 @@ package edu.harvard.iq.dataverse.engine.command.impl; -import edu.harvard.iq.dataverse.DataverseFeaturedItem; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItem; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.engine.command.AbstractCommand; import edu.harvard.iq.dataverse.engine.command.CommandContext; diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseFeaturedItemsCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseFeaturedItemsCommand.java index 474747ccb0d..0d4051fc7d5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseFeaturedItemsCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ListDataverseFeaturedItemsCommand.java @@ -1,7 +1,7 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.Dataverse; -import edu.harvard.iq.dataverse.DataverseFeaturedItem; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItem; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.engine.command.AbstractCommand; import edu.harvard.iq.dataverse.engine.command.CommandContext; diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemCommand.java index a54f0324a3e..ed6fe825b03 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemCommand.java @@ -1,7 +1,7 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.Dataverse; -import edu.harvard.iq.dataverse.DataverseFeaturedItem; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItem; import edu.harvard.iq.dataverse.api.dto.UpdatedDataverseFeaturedItemDTO; import edu.harvard.iq.dataverse.engine.command.*; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemsCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemsCommand.java index 12a972f2e47..0368efef6b0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemsCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UpdateDataverseFeaturedItemsCommand.java @@ -1,7 +1,7 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.Dataverse; -import edu.harvard.iq.dataverse.DataverseFeaturedItem; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItem; import edu.harvard.iq.dataverse.api.dto.NewDataverseFeaturedItemDTO; import edu.harvard.iq.dataverse.api.dto.UpdatedDataverseFeaturedItemDTO; import edu.harvard.iq.dataverse.authorization.Permission; diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index 1f27eef8e7f..b88dfaef4b5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -25,6 +25,7 @@ import edu.harvard.iq.dataverse.datavariable.VariableCategory; import edu.harvard.iq.dataverse.datavariable.VariableMetadata; import edu.harvard.iq.dataverse.datavariable.VariableRange; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItem; import edu.harvard.iq.dataverse.license.License; import edu.harvard.iq.dataverse.globus.FileDetailsHolder; import edu.harvard.iq.dataverse.harvest.client.HarvestingClient; diff --git a/src/test/java/edu/harvard/iq/dataverse/engine/TestCommandContext.java b/src/test/java/edu/harvard/iq/dataverse/engine/TestCommandContext.java index fd7e3f69bd2..4fec86c5b0b 100644 --- a/src/test/java/edu/harvard/iq/dataverse/engine/TestCommandContext.java +++ b/src/test/java/edu/harvard/iq/dataverse/engine/TestCommandContext.java @@ -9,6 +9,7 @@ import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailServiceBean; import edu.harvard.iq.dataverse.datacapturemodule.DataCaptureModuleServiceBean; import edu.harvard.iq.dataverse.dataset.DatasetTypeServiceBean; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItemServiceBean; import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.ingest.IngestServiceBean; diff --git a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java index 01c5f499b0f..f31299526bd 100644 --- a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseFeaturedItemCommandTest.java @@ -1,8 +1,8 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.Dataverse; -import edu.harvard.iq.dataverse.DataverseFeaturedItem; -import edu.harvard.iq.dataverse.DataverseFeaturedItemServiceBean; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItem; +import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItemServiceBean; import edu.harvard.iq.dataverse.api.dto.NewDataverseFeaturedItemDTO; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; From a986a674da6ce91128a67feea74b4bb8a701475b Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 27 Jan 2025 15:24:02 +0000 Subject: [PATCH 50/57] Added: new settings description to #10943 release notes --- doc/release-notes/10943-featured-items.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/release-notes/10943-featured-items.md b/doc/release-notes/10943-featured-items.md index e7778a2f9c9..fa61d4e4875 100644 --- a/doc/release-notes/10943-featured-items.md +++ b/doc/release-notes/10943-featured-items.md @@ -7,4 +7,9 @@ CRUD endpoints for Collection Featured Items have been implemented. In particula - Delete all featured items in a collection (DELETE /api/dataverses//featuredItems) - Update all featured items in a collection (PUT /api/dataverses//featuredItems) +New settings: + +- dataverse.files.featured-items.image-maxsize - It sets the maximum allowed size of the image that can be added to a featured item. +- dataverse.files.featured-items.image-uploads - It specifies the name of the subdirectory for saving featured item images within the docroot directory. + See also #10943 and #11124. From e0aeb2f31bd89f0ea22298b4e64a9f512c5eeeed Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 27 Jan 2025 15:34:19 +0000 Subject: [PATCH 51/57] Fixed: DataversesIT featured items imageFileUrl assertion --- .../java/edu/harvard/iq/dataverse/api/DataversesIT.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java index b87f3b5f6ea..825465fcd9e 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java @@ -1731,7 +1731,7 @@ public void testListFeaturedItems() { .body("data[1].displayOrder", equalTo(1)) .body("data[2].content", equalTo("Content 1")) .body("data[2].imageFileName", equalTo("coffeeshop.png")) - .body("data[2].imageFileUrl", containsString(UtilIT.getRestAssuredBaseUri() + "/api/access/dataverseFeatureItemImage/")) + .body("data[2].imageFileUrl", containsString("/api/access/dataverseFeaturedItemImage/")) .body("data[2].displayOrder", equalTo(2)) .statusCode(OK.getStatusCode()); @@ -1766,7 +1766,7 @@ public void testUpdateFeaturedItems() { .body("data.size()", equalTo(3)) .body("data[0].content", equalTo("Content 1")) .body("data[0].imageFileName", equalTo("coffeeshop.png")) - .body("data[0].imageFileUrl", containsString(baseUri + "/api/access/dataverseFeatureItemImage/")) + .body("data[0].imageFileUrl", containsString("/api/access/dataverseFeaturedItemImage/")) .body("data[0].displayOrder", equalTo(0)) .body("data[1].content", equalTo("Content 2")) .body("data[1].imageFileName", equalTo(null)) @@ -1799,7 +1799,7 @@ public void testUpdateFeaturedItems() { .body("data[0].displayOrder", equalTo(0)) .body("data[1].content", equalTo("Content 1 updated")) .body("data[1].imageFileName", equalTo("coffeeshop.png")) - .body("data[1].imageFileUrl", containsString(baseUri + "/api/access/dataverseFeatureItemImage/")) + .body("data[1].imageFileUrl", containsString("/api/access/dataverseFeaturedItemImage/")) .body("data[1].displayOrder", equalTo(1)) .body("data[2].content", equalTo("Content 3")) .body("data[2].imageFileName", equalTo(null)) @@ -1828,7 +1828,7 @@ public void testUpdateFeaturedItems() { .body("data.size()", equalTo(3)) .body("data[0].content", equalTo("Content 2")) .body("data[0].imageFileName", equalTo("coffeeshop.png")) - .body("data[0].imageFileUrl", containsString(baseUri + "/api/access/dataverseFeatureItemImage/")) + .body("data[0].imageFileUrl", containsString("/api/access/dataverseFeaturedItemImage/")) .body("data[0].displayOrder", equalTo(0)) .body("data[1].content", equalTo("Content 1 updated")) .body("data[1].imageFileName", equalTo(null)) From d3726f89781868441aef82b4a16ed3de587c1aa9 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 28 Jan 2025 08:31:00 +0000 Subject: [PATCH 52/57] Fixed: doc hierarchy --- doc/sphinx-guides/source/api/native-api.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 249da446c8c..6f2b425322a 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1286,9 +1286,6 @@ The following example creates one featured item and updates a second one, keepin -F "keepFile=false" -F "keepFile=true" \ "https://demo.dataverse.org/api/dataverses/root/featuredItems" -Dataverse Collection Featured Items ------------------------------------ - Update A Collection Featured Item ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 29944a0e2c22185c92abc44992e5cba7612401ba Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 28 Jan 2025 08:34:13 +0000 Subject: [PATCH 53/57] Changed: featured items docs hierarchy --- doc/sphinx-guides/source/api/native-api.rst | 90 ++++++++++----------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 6f2b425322a..c2714b6ac40 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1156,8 +1156,8 @@ Use the ``/settings`` API to enable or disable the enforcement of storage quotas curl -X PUT -d 'true' http://localhost:8080/api/admin/settings/:UseStorageQuotas -List Collection Featured Items -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +List All Collection Featured Items +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ List the featured items configured for a given Dataverse collection ``id``: @@ -1175,49 +1175,6 @@ The fully expanded example above (without environment variables) looks like this curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" "https://demo.dataverse.org/api/dataverses/root/featuredItems" -Delete All Collection Featured Items -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Deletes the featured items configured for a given Dataverse collection ``id``: - -.. code-block:: bash - - export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - export SERVER_URL=https://demo.dataverse.org - export ID=root - - curl -H "X-Dataverse-key: $API_TOKEN" -X DELETE "$SERVER_URL/api/dataverses/$ID/featuredItems" - -The fully expanded example above (without environment variables) looks like this: - -.. code-block:: bash - - curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE "https://demo.dataverse.org/api/dataverses/root/featuredItems" - -Create a Collection Featured Item -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Creates a featured item in the given Dataverse collection ``id``: - -.. code-block:: bash - - export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - export IMAGE_FILENAME='image.png' - export CONTENT='Content for featured item.' - export DISPLAY_ORDER=1 - export SERVER_URL=https://demo.dataverse.org - export ID=root - - curl -H "X-Dataverse-key:$API_TOKEN" -X POST -F "file=@$IMAGE_FILENAME" -F "content=$CONTENT" -F "displayOrder=$DISPLAY_ORDER" "$SERVER_URL/api/dataverses/$ID/featuredItems" - -The fully expanded example above (without environment variables) looks like this: - -.. code-block:: bash - - curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X POST -F "file=@image.png" -F "content=Content for featured item." -F "displayOrder=1" "https://demo.dataverse.org/api/dataverses/root/featuredItems" - -A featured item may or may not contain an image. If you wish to create it without an image, omit the file parameter in the request. - Update All Collection Featured Items ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1286,6 +1243,49 @@ The following example creates one featured item and updates a second one, keepin -F "keepFile=false" -F "keepFile=true" \ "https://demo.dataverse.org/api/dataverses/root/featuredItems" +Delete All Collection Featured Items +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Deletes the featured items configured for a given Dataverse collection ``id``: + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export SERVER_URL=https://demo.dataverse.org + export ID=root + + curl -H "X-Dataverse-key: $API_TOKEN" -X DELETE "$SERVER_URL/api/dataverses/$ID/featuredItems" + +The fully expanded example above (without environment variables) looks like this: + +.. code-block:: bash + + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE "https://demo.dataverse.org/api/dataverses/root/featuredItems" + +Create A Collection Featured Item +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Creates a featured item in the given Dataverse collection ``id``: + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export IMAGE_FILENAME='image.png' + export CONTENT='Content for featured item.' + export DISPLAY_ORDER=1 + export SERVER_URL=https://demo.dataverse.org + export ID=root + + curl -H "X-Dataverse-key:$API_TOKEN" -X POST -F "file=@$IMAGE_FILENAME" -F "content=$CONTENT" -F "displayOrder=$DISPLAY_ORDER" "$SERVER_URL/api/dataverses/$ID/featuredItems" + +The fully expanded example above (without environment variables) looks like this: + +.. code-block:: bash + + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X POST -F "file=@image.png" -F "content=Content for featured item." -F "displayOrder=1" "https://demo.dataverse.org/api/dataverses/root/featuredItems" + +A featured item may or may not contain an image. If you wish to create it without an image, omit the file parameter in the request. + Update A Collection Featured Item ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 497cc5d97458f25ef3abeb9a56c533d395fe0d17 Mon Sep 17 00:00:00 2001 From: Guillermo Portas Date: Tue, 28 Jan 2025 13:43:28 +0000 Subject: [PATCH 54/57] Update doc/sphinx-guides/source/api/native-api.rst Co-authored-by: Philip Durbin --- doc/sphinx-guides/source/api/native-api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index c2714b6ac40..ca389d366f9 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1262,7 +1262,7 @@ The fully expanded example above (without environment variables) looks like this curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE "https://demo.dataverse.org/api/dataverses/root/featuredItems" -Create A Collection Featured Item +Create a Collection Featured Item ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Creates a featured item in the given Dataverse collection ``id``: From 1799d70949bf6410c9672942bd04390e70025090 Mon Sep 17 00:00:00 2001 From: Guillermo Portas Date: Tue, 28 Jan 2025 13:43:35 +0000 Subject: [PATCH 55/57] Update doc/sphinx-guides/source/api/native-api.rst Co-authored-by: Philip Durbin --- doc/sphinx-guides/source/api/native-api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index ca389d366f9..10c42e1b76c 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1286,7 +1286,7 @@ The fully expanded example above (without environment variables) looks like this A featured item may or may not contain an image. If you wish to create it without an image, omit the file parameter in the request. -Update A Collection Featured Item +Update a Collection Featured Item ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Updates a featured item given its ``id``: From 1aac2d511c8f37f61aaa8930f77540434bc8bac9 Mon Sep 17 00:00:00 2001 From: Guillermo Portas Date: Tue, 28 Jan 2025 13:43:41 +0000 Subject: [PATCH 56/57] Update doc/sphinx-guides/source/api/native-api.rst Co-authored-by: Philip Durbin --- doc/sphinx-guides/source/api/native-api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 10c42e1b76c..212eec2603e 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1322,7 +1322,7 @@ Updating the featured item removing the existing image: curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X PUT -F "content=Content for featured item." -F "displayOrder=1" "https://demo.dataverse.org/api/dataverseFeaturedItems/1" -Delete A Collection Featured Item +Delete a Collection Featured Item ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Deletes a featured item given its ``id``: From a8d4a0526054567b150646195fed58d51a6b1e14 Mon Sep 17 00:00:00 2001 From: Guillermo Portas Date: Tue, 28 Jan 2025 13:43:48 +0000 Subject: [PATCH 57/57] Update doc/sphinx-guides/source/api/native-api.rst Co-authored-by: Philip Durbin --- doc/sphinx-guides/source/api/native-api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 212eec2603e..76682d1cec8 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1341,7 +1341,7 @@ The fully expanded example above (without environment variables) looks like this curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE "https://demo.dataverse.org/api/dataverseFeaturedItems/1" -Get A Collection Featured Item Image +Get a Collection Featured Item Image ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Returns the image of a featured item if one is assigned, given the featured item ``id``: