From b5fe4c75944deea540b807021e0f88401878118f Mon Sep 17 00:00:00 2001 From: "Balazs E. Pataki" Date: Fri, 17 Mar 2023 16:22:48 +0100 Subject: [PATCH 001/689] Fix placement of allowedApiCalls in example manifests allowedApiCalls should be at the top level, not inside toolParameters. --- .../external-tools/dynamicDatasetTool.json | 20 +++++++++---------- .../root/external-tools/fabulousFileTool.json | 18 ++++++++--------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/doc/sphinx-guides/source/_static/installation/files/root/external-tools/dynamicDatasetTool.json b/doc/sphinx-guides/source/_static/installation/files/root/external-tools/dynamicDatasetTool.json index 47413c8a625..22dd6477cb4 100644 --- a/doc/sphinx-guides/source/_static/installation/files/root/external-tools/dynamicDatasetTool.json +++ b/doc/sphinx-guides/source/_static/installation/files/root/external-tools/dynamicDatasetTool.json @@ -14,14 +14,14 @@ { "locale":"{localeCode}" } - ], - "allowedApiCalls": [ - { - "name":"retrieveDatasetJson", - "httpMethod":"GET", - "urlTemplate":"/api/v1/datasets/{datasetId}", - "timeOut":10 - } - ] - } + ] + }, + "allowedApiCalls": [ + { + "name":"retrieveDatasetJson", + "httpMethod":"GET", + "urlTemplate":"/api/v1/datasets/{datasetId}", + "timeOut":10 + } + ] } diff --git a/doc/sphinx-guides/source/_static/installation/files/root/external-tools/fabulousFileTool.json b/doc/sphinx-guides/source/_static/installation/files/root/external-tools/fabulousFileTool.json index 1c132576099..2b6a0b8e092 100644 --- a/doc/sphinx-guides/source/_static/installation/files/root/external-tools/fabulousFileTool.json +++ b/doc/sphinx-guides/source/_static/installation/files/root/external-tools/fabulousFileTool.json @@ -21,14 +21,14 @@ { "locale":"{localeCode}" } - ], - "allowedApiCalls": [ - { - "name":"retrieveDataFile", - "httpMethod":"GET", - "urlTemplate":"/api/v1/access/datafile/{fileId}", - "timeOut":270 - } ] - } + }, + "allowedApiCalls": [ + { + "name":"retrieveDataFile", + "httpMethod":"GET", + "urlTemplate":"/api/v1/access/datafile/{fileId}", + "timeOut":270 + } + ] } From d76092c1ec57a835920b8fd10e6883299f8b6d3a Mon Sep 17 00:00:00 2001 From: "Balazs E. Pataki" Date: Fri, 17 Mar 2023 16:24:41 +0100 Subject: [PATCH 002/689] Add missing break to DATASET case Without this it also evaluates the FILE case causing NPE when dataFile is accessed. --- .../harvard/iq/dataverse/externaltools/ExternalToolHandler.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandler.java b/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandler.java index 88a51017b75..dac046373ba 100644 --- a/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandler.java +++ b/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandler.java @@ -111,6 +111,7 @@ public String handleRequest(boolean preview) { case DATASET: callback=SystemConfig.getDataverseSiteUrlStatic() + "/api/v1/datasets/" + dataset.getId() + "/versions/:latest/toolparams/" + externalTool.getId(); + break; case FILE: callback= SystemConfig.getDataverseSiteUrlStatic() + "/api/v1/files/" + dataFile.getId() + "/metadata/" + fileMetadata.getId() + "/toolparams/" From 9c809c400d7e71a5cd682a892a20aa0dfa21c8ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Haarla=CC=88nder?= Date: Wed, 14 Jun 2023 10:03:23 +0200 Subject: [PATCH 003/689] #IQSS/3818 Delete temp thumbnail files --- .../dataverse/dataaccess/ImageThumbConverter.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/ImageThumbConverter.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/ImageThumbConverter.java index 2b4aed3a9a5..16003f6f32b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/ImageThumbConverter.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/ImageThumbConverter.java @@ -195,6 +195,7 @@ private static boolean generatePDFThumbnail(StorageIO storageIO, int s // will run the ImageMagick on it, and will save its output in another temp // file, and will save it as an "auxiliary" file via the driver. boolean tempFilesRequired = false; + File tempFile = null; try { Path pdfFilePath = storageIO.getFileSystemPath(); @@ -222,7 +223,7 @@ private static boolean generatePDFThumbnail(StorageIO storageIO, int s return false; } - File tempFile; + FileChannel tempFileChannel = null; try { tempFile = File.createTempFile("tempFileToRescale", ".tmp"); @@ -254,10 +255,14 @@ private static boolean generatePDFThumbnail(StorageIO storageIO, int s try { logger.fine("attempting to save generated pdf thumbnail, as AUX file " + THUMBNAIL_SUFFIX + size); storageIO.savePathAsAux(Paths.get(imageThumbFileName), THUMBNAIL_SUFFIX + size); + } catch (IOException ioex) { logger.warning("failed to save generated pdf thumbnail, as AUX file " + THUMBNAIL_SUFFIX + size + "!"); return false; } + finally { + tempFile.delete(); + } } return true; @@ -353,12 +358,18 @@ private static boolean generateImageThumbnailFromInputStream(StorageIO if (tempFileRequired) { storageIO.savePathAsAux(Paths.get(tempFile.getAbsolutePath()), THUMBNAIL_SUFFIX + size); + } } catch (Exception ioex) { logger.warning("Failed to rescale and/or save the image: " + ioex.getMessage()); return false; } + finally { + if(tempFileRequired) { + tempFile.delete(); + } + } return true; From ffad284142f64885a3ab748878a86e555a77d300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Haarla=CC=88nder?= Date: Wed, 14 Jun 2023 10:50:34 +0200 Subject: [PATCH 004/689] #IQSS/3818 Delete temp thumbnail files --- .../iq/dataverse/dataaccess/ImageThumbConverter.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/ImageThumbConverter.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/ImageThumbConverter.java index 16003f6f32b..134ae20de87 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/ImageThumbConverter.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/ImageThumbConverter.java @@ -261,7 +261,10 @@ private static boolean generatePDFThumbnail(StorageIO storageIO, int s return false; } finally { - tempFile.delete(); + try { + tempFile.delete(); + } + catch (Exception e) {} } } @@ -367,7 +370,10 @@ private static boolean generateImageThumbnailFromInputStream(StorageIO } finally { if(tempFileRequired) { - tempFile.delete(); + try { + tempFile.delete(); + } + catch (Exception e) {} } } From e73806a6907ec630d7b2389abda632727821f48e Mon Sep 17 00:00:00 2001 From: lubitchv Date: Thu, 27 Jul 2023 17:25:40 -0400 Subject: [PATCH 005/689] increase universe --- .../db/migration/V5.13.0.3__9728-universe-variablemetadata.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/main/resources/db/migration/V5.13.0.3__9728-universe-variablemetadata.sql diff --git a/src/main/resources/db/migration/V5.13.0.3__9728-universe-variablemetadata.sql b/src/main/resources/db/migration/V5.13.0.3__9728-universe-variablemetadata.sql new file mode 100644 index 00000000000..8e311c06b32 --- /dev/null +++ b/src/main/resources/db/migration/V5.13.0.3__9728-universe-variablemetadata.sql @@ -0,0 +1,2 @@ +-- increase field universe from 255 to text +ALTER TABLE variablemetadata ALTER COLUMN universe TYPE text; From 495594a2ed039b52951b7f1298426436b64a00f4 Mon Sep 17 00:00:00 2001 From: lubitchv Date: Fri, 28 Jul 2023 10:50:22 -0400 Subject: [PATCH 006/689] column text --- .../edu/harvard/iq/dataverse/datavariable/VariableMetadata.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/datavariable/VariableMetadata.java b/src/main/java/edu/harvard/iq/dataverse/datavariable/VariableMetadata.java index c18355c9979..08fcd14e0e6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/datavariable/VariableMetadata.java +++ b/src/main/java/edu/harvard/iq/dataverse/datavariable/VariableMetadata.java @@ -71,6 +71,7 @@ public class VariableMetadata implements Serializable { /** * universe: metadata variable field. */ + @Column(columnDefinition="TEXT") private String universe; /** From be56f48f469ce319c1e3cacc4e14e5bbb9c0ecb9 Mon Sep 17 00:00:00 2001 From: lubitchv Date: Fri, 28 Jul 2023 11:36:23 -0400 Subject: [PATCH 007/689] release note --- doc/release-notes/9728-universe-variablemetadata.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/release-notes/9728-universe-variablemetadata.md diff --git a/doc/release-notes/9728-universe-variablemetadata.md b/doc/release-notes/9728-universe-variablemetadata.md new file mode 100644 index 00000000000..66a2daf151b --- /dev/null +++ b/doc/release-notes/9728-universe-variablemetadata.md @@ -0,0 +1 @@ +universe field in variablemetadata table was changed from varchar(255) to text. The change was made to support longer strings in "universe" metadata field, similar to the rest of text fields in variablemetadata table. From 3d2255b963f869028b68576075462664f67a5888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20ROUCOU?= Date: Wed, 13 Sep 2023 18:35:40 +0200 Subject: [PATCH 008/689] Assign roles from email address Give a user a role from email address of the user's account --- .../iq/dataverse/authorization/users/AuthenticatedUser.java | 3 ++- src/main/webapp/roles-assign.xhtml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java b/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java index 89429b912f6..17db9e63e8b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java @@ -64,7 +64,8 @@ @NamedQuery( name="AuthenticatedUser.filter", query="select au from AuthenticatedUser au WHERE (" + "LOWER(au.userIdentifier) like LOWER(:query) OR " - + "lower(concat(au.firstName,' ',au.lastName)) like lower(:query))"), + + "lower(concat(au.firstName,' ',au.lastName)) like lower(:query) or " + + "lower(au.email) like lower(:query))"), @NamedQuery( name="AuthenticatedUser.findAdminUser", query="select au from AuthenticatedUser au WHERE " + "au.superuser = true " diff --git a/src/main/webapp/roles-assign.xhtml b/src/main/webapp/roles-assign.xhtml index 4b31f10dbfc..4b355c74d5c 100644 --- a/src/main/webapp/roles-assign.xhtml +++ b/src/main/webapp/roles-assign.xhtml @@ -31,7 +31,8 @@ styleClass="DropdownPopup" panelStyleClass="DropdownPopupPanel" var="roleAssignee" itemLabel="#{roleAssignee.displayInfo.title}" itemValue="#{roleAssignee}" converter="roleAssigneeConverter"> - + + From c6197b3bf23ad1dccb023ea668799e7a79805d93 Mon Sep 17 00:00:00 2001 From: Don Sizemore Date: Mon, 18 Sep 2023 10:40:05 -0400 Subject: [PATCH 009/689] #9920 support Postgres 16 --- pom.xml | 4 ++-- scripts/installer/install.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 7ba22d2a076..c5b7fc302f3 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ war 1.2.18.4 - 9.21.2 + 9.22.1 1.20.1 0.8.7 5.2.1 @@ -790,7 +790,7 @@ true docker-build - 13 + 16 gdcc/dataverse:${app.image.tag} unstable diff --git a/scripts/installer/install.py b/scripts/installer/install.py index 5a7b9f75696..18995695638 100644 --- a/scripts/installer/install.py +++ b/scripts/installer/install.py @@ -422,9 +422,13 @@ conn.close() if int(pg_major_version) >= 15: + admin_conn_string = "dbname='"+pgDb+"' user='postgres' password='"+pgAdminPassword+"' host='"+pgHost+"'" + conn = psycopg2.connect(admin_conn_string) + conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) + cur = conn.cursor() conn_cmd = "GRANT CREATE ON SCHEMA public TO "+pgUser+";" - print("PostgreSQL 15 or higher detected. Running " + conn_cmd) try: + print("PostgreSQL 15 or higher detected. Running " + conn_cmd) cur.execute(conn_cmd) except: if force: From d6c06004410a40405d55b6b00d059ed8338c284e Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 19 Sep 2023 22:54:39 +0200 Subject: [PATCH 010/689] feat(mail): add MTA settings for mail to JvmSettings #7424 --- .../edu/harvard/iq/dataverse/settings/JvmSettings.java | 9 +++++++++ 1 file changed, 9 insertions(+) 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 738d63e924f..2088dcbc5e1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java @@ -123,6 +123,15 @@ public enum JvmSettings { SCOPE_MAIL(PREFIX, "mail"), SUPPORT_EMAIL(SCOPE_MAIL, "support-email"), CC_SUPPORT_ON_CONTACT_EMAIL(SCOPE_MAIL, "cc-support-on-contact-email"), + MAIL_DEBUG(SCOPE_MAIL, "debug"), + // Mail Transfer Agent settings + SCOPE_MAIL_MTA(SCOPE_MAIL, "mta"), + MAIL_MTA_HOST(SCOPE_MAIL_MTA, "host"), + MAIL_MTA_AUTH(SCOPE_MAIL_MTA, "auth"), + MAIL_MTA_USER(SCOPE_MAIL_MTA, "user"), + MAIL_MTA_PASSWORD(SCOPE_MAIL_MTA, "password"), + // Placeholder setting for a large list of extra settings + MAIL_MTA_SETTING(SCOPE_MAIL_MTA), // UI SETTINGS SCOPE_UI(PREFIX, "ui"), From de759c1a23d6a997767e540ec8503cbdb0a6e26d Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 20 Sep 2023 12:58:34 +0200 Subject: [PATCH 011/689] feat(mail): add minimal implementation of a mail session factory #7424 --- .../dataverse/util/MailSessionProducer.java | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java diff --git a/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java b/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java new file mode 100644 index 00000000000..7728fa338ca --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java @@ -0,0 +1,87 @@ +package edu.harvard.iq.dataverse.util; + +import edu.harvard.iq.dataverse.settings.JvmSettings; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Named; +import jakarta.mail.Authenticator; +import jakarta.mail.PasswordAuthentication; +import jakarta.mail.Session; + +import java.util.List; +import java.util.Properties; + +@ApplicationScoped +public class MailSessionProducer { + + // NOTE: We do not allow "from" here, as we want the transport to get it from the message being sent, enabling + // matching addresses. If "from" in transport and "from" in the message differ, some MTAs may reject or + // classify as spam. + // NOTE: Complete list including descriptions at https://eclipse-ee4j.github.io/angus-mail/docs/api/org.eclipse.angus.mail/org/eclipse/angus/mail/smtp/package-summary.html + static final List smtpStringProps = List.of( + "localhost", "localaddress", "auth.mechanisms", "auth.ntlm.domain", "submitter", "dsn.notify", "dsn.ret", + "sasl.mechanisms", "sasl.authorizationid", "sasl.realm", "ssl.trust", "ssl.protocols", "ssl.ciphersuites", + "proxy.host", "proxy.port", "proxy.user", "proxy.password", "socks.host", "socks.port", "mailextension" + ); + static final List smtpIntProps = List.of( + "port", "connectiontimeout", "timeout", "writetimeout", "localport", "auth.ntlm.flag" + ); + static final List smtpBoolProps = List.of( + "ehlo", "auth.login.disable", "auth.plain.disable", "auth.digest-md5.disable", "auth.ntlm.disable", + "auth.xoauth2.disable", "allow8bitmime", "sendpartial", "sasl.enable", "sasl.usecanonicalhostname", + "quitwait", "quitonsessionreject", "ssl.enable", "ssl.checkserveridentity", "starttls.enable", + "starttls.required", "userset", "noop.strict" + ); + + private static final String PREFIX = "mail.stmp."; + + Session systemMailSession; + + @Produces + @Named("mail/systemSession") + public Session getSession() { + if (systemMailSession == null) { + // Initialize with null (= no authenticator) is a valid argument for the session factory method. + Authenticator authenticator = null; + + // In case we want auth, create an authenticator (default = false from microprofile-config.properties) + if (JvmSettings.MAIL_MTA_AUTH.lookup(Boolean.class)) { + authenticator = new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(JvmSettings.MAIL_MTA_USER.lookup(), JvmSettings.MAIL_MTA_PASSWORD.lookup()); + } + }; + } + + this.systemMailSession = Session.getInstance(getMailProperties(), authenticator); + } + return systemMailSession; + } + + Properties getMailProperties() { + Properties configuration = new Properties(); + + // See https://jakarta.ee/specifications/mail/2.1/apidocs/jakarta.mail/jakarta/mail/package-summary + configuration.put("mail.transport.protocol", "smtp"); + configuration.put("mail.debug", JvmSettings.MAIL_DEBUG.lookupOptional(Boolean.class).orElse(false).toString()); + + configuration.put("mail.smtp.host", JvmSettings.MAIL_MTA_HOST.lookup()); + // default = false from microprofile-config.properties + configuration.put("mail.smtp.auth", JvmSettings.MAIL_MTA_AUTH.lookup(Boolean.class).toString()); + + // Map properties 1:1 to mail.smtp properties for the mail session. + smtpStringProps.forEach( + prop -> JvmSettings.MAIL_MTA_SETTING.lookupOptional(prop).ifPresent( + string -> configuration.put(PREFIX + prop, string))); + smtpBoolProps.forEach( + prop -> JvmSettings.MAIL_MTA_SETTING.lookupOptional(Boolean.class, prop).ifPresent( + bool -> configuration.put(PREFIX + prop, bool.toString()))); + smtpIntProps.forEach( + prop -> JvmSettings.MAIL_MTA_SETTING.lookupOptional(Integer.class, prop).ifPresent( + number -> configuration.put(PREFIX + prop, number.toString()))); + + return configuration; + } + +} From 72cdde93acba7270c9cae55129162ecf8bcc2c0d Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 20 Sep 2023 12:59:35 +0200 Subject: [PATCH 012/689] feat(mail): add sane defaults for mail jvm settings at app level #7424 We only default to no authentication. We still require people to configure an SMTP host, only in containers we do default to "smtp" as a hostname for that (see our compose file). Username/password cannot have a default and all other special settings should not be done here. These are highly setup specific. --- src/main/resources/META-INF/microprofile-config.properties | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/resources/META-INF/microprofile-config.properties b/src/main/resources/META-INF/microprofile-config.properties index 7c16495f870..e71542c3c89 100644 --- a/src/main/resources/META-INF/microprofile-config.properties +++ b/src/main/resources/META-INF/microprofile-config.properties @@ -36,6 +36,11 @@ dataverse.rserve.user=rserve dataverse.rserve.password=rserve dataverse.rserve.tempdir=/tmp/Rserv +# MAIL +dataverse.mail.mta.auth=false +# In containers, default to hostname smtp, a container on the same network +%ct.dataverse.mail.mta.host=smtp + # OAI SERVER dataverse.oai.server.maxidentifiers=100 dataverse.oai.server.maxrecords=10 From 02f1c3d534822a0bd285a29949ae6ef26cb84f3d Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 20 Sep 2023 13:47:12 +0200 Subject: [PATCH 013/689] feat(mail): inject mail session via CDI from factory in MailServiceBean #7424 --- src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java index f17732df7b6..3463aa211bb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java @@ -26,9 +26,10 @@ import java.util.List; import java.util.Set; import java.util.logging.Logger; -import jakarta.annotation.Resource; import jakarta.ejb.EJB; import jakarta.ejb.Stateless; +import jakarta.inject.Inject; +import jakarta.inject.Named; import jakarta.mail.Address; import jakarta.mail.Message; import jakarta.mail.MessagingException; @@ -79,7 +80,8 @@ public class MailServiceBean implements java.io.Serializable { public MailServiceBean() { } - @Resource(name = "mail/notifyMailSession") + @Inject + @Named("mail/systemSession") private Session session; public boolean sendSystemEmail(String to, String subject, String messageText) { From 1f79f57556ec6b92de344e3a0a305a25f0196103 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 20 Sep 2023 13:47:44 +0200 Subject: [PATCH 014/689] fix(mail): make error logs about missing mapping file go away #7424 --- src/main/resources/META-INF/javamail.default.address.map | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/main/resources/META-INF/javamail.default.address.map diff --git a/src/main/resources/META-INF/javamail.default.address.map b/src/main/resources/META-INF/javamail.default.address.map new file mode 100644 index 00000000000..b1115c9dc8c --- /dev/null +++ b/src/main/resources/META-INF/javamail.default.address.map @@ -0,0 +1,2 @@ +# See https://jakartaee.github.io/mail-api/docs/api/jakarta.mail/jakarta/mail/Session.html +rfc822=smtp From 03b11bf7342f21b5486f4f473094de08eba26c3b Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 20 Sep 2023 13:48:31 +0200 Subject: [PATCH 015/689] feat(ct,mail): no longer configure mail in containers manually #7424 --- src/main/docker/scripts/init_2_configure.sh | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/docker/scripts/init_2_configure.sh b/src/main/docker/scripts/init_2_configure.sh index a98f08088c1..b31cfac37b7 100755 --- a/src/main/docker/scripts/init_2_configure.sh +++ b/src/main/docker/scripts/init_2_configure.sh @@ -31,10 +31,6 @@ echo "# Dataverse postboot configuration for Payara" > "${DV_POSTBOOT}" # EE 8 code annotations or at least glassfish-resources.xml # NOTE: postboot commands is not multi-line capable, thus spaghetti needed. -# JavaMail -echo "INFO: Defining JavaMail." -echo "create-javamail-resource --mailhost=${DATAVERSE_MAIL_HOST:-smtp} --mailuser=${DATAVERSE_MAIL_USER:-dataversenotify} --fromaddress=${DATAVERSE_MAIL_FROM:-dataverse@localhost} mail/notifyMailSession" >> "${DV_POSTBOOT}" - # 3. Domain based configuration options # Set Dataverse environment variables echo "INFO: Defining system properties for Dataverse configuration options." From ee88cfd8e3ec42d8244575a4f5a77cca914a8346 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 20 Sep 2023 13:51:10 +0200 Subject: [PATCH 016/689] doc(ct): remove mail env vars from app image docs #7424 --- doc/sphinx-guides/source/container/app-image.rst | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/doc/sphinx-guides/source/container/app-image.rst b/doc/sphinx-guides/source/container/app-image.rst index 29f6d6ac1d4..4720760544e 100644 --- a/doc/sphinx-guides/source/container/app-image.rst +++ b/doc/sphinx-guides/source/container/app-image.rst @@ -134,19 +134,6 @@ In addition, the application image provides the following tunables: 1. Simply pick a JVM option from the list and replace any ``.`` with ``_``. 2. Replace any ``-`` in the option name with ``__``. - * - ``DATAVERSE_MAIL_HOST`` - - ``smtp`` - - String - - A hostname (w/o port!) where to reach a Mail MTA on port 25. - * - ``DATAVERSE_MAIL_USER`` - - ``dataversenotify`` - - String - - A username to use with the Mail MTA - * - ``DATAVERSE_MAIL_FROM`` - - ``dataverse@localhost`` - - Mail address - - The "From" field for all outbound mail. Make sure to set :ref:`systemEmail` to the same value or no mail will - be sent. Note that the script ``init_2_configure.sh`` will apply a few very important defaults to enable quick usage From 600d209fbd7b70893fe29f7601ba8049cb415d8f Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 20 Sep 2023 13:53:29 +0200 Subject: [PATCH 017/689] build(test): enable using GenericContainer without JUnit4 around As a hack to work around testcontainers/testcontainers-java#970, we add these fake, empty classes. Copied from Spring project. See also: https://github.com/testcontainers/testcontainers-java/issues/970 --- src/test/java/org/junit/rules/TestRule.java | 11 +++++++++++ src/test/java/org/junit/runners/model/Statement.java | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/test/java/org/junit/rules/TestRule.java create mode 100644 src/test/java/org/junit/runners/model/Statement.java diff --git a/src/test/java/org/junit/rules/TestRule.java b/src/test/java/org/junit/rules/TestRule.java new file mode 100644 index 00000000000..4f94d8e6922 --- /dev/null +++ b/src/test/java/org/junit/rules/TestRule.java @@ -0,0 +1,11 @@ +package org.junit.rules; + +/** + * "Fake" class used as a replacement for Junit4-dependent classes. + * See more at: + * GenericContainer run from Jupiter tests shouldn't require JUnit 4.x library on runtime classpath + * . + */ +@SuppressWarnings("unused") +public interface TestRule { +} diff --git a/src/test/java/org/junit/runners/model/Statement.java b/src/test/java/org/junit/runners/model/Statement.java new file mode 100644 index 00000000000..b80ca0abc86 --- /dev/null +++ b/src/test/java/org/junit/runners/model/Statement.java @@ -0,0 +1,11 @@ +package org.junit.runners.model; + +/** + * "Fake" class used as a replacement for Junit4-dependent classes. + * See more at: + * GenericContainer run from Jupiter tests shouldn't require JUnit 4.x library on runtime classpath + * . + */ +@SuppressWarnings("unused") +public class Statement { +} From 6f6a9b7b4cbccf1cb1e6521c2b65a9eb81c85af6 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 20 Sep 2023 14:52:32 +0200 Subject: [PATCH 018/689] fix(mail): correct typo in mail.smtp prefix string #7424 --- .../edu/harvard/iq/dataverse/util/MailSessionProducer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java b/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java index 7728fa338ca..87475359215 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java @@ -33,7 +33,7 @@ public class MailSessionProducer { "starttls.required", "userset", "noop.strict" ); - private static final String PREFIX = "mail.stmp."; + private static final String PREFIX = "mail.smtp."; Session systemMailSession; @@ -66,9 +66,9 @@ Properties getMailProperties() { configuration.put("mail.transport.protocol", "smtp"); configuration.put("mail.debug", JvmSettings.MAIL_DEBUG.lookupOptional(Boolean.class).orElse(false).toString()); - configuration.put("mail.smtp.host", JvmSettings.MAIL_MTA_HOST.lookup()); + configuration.put(PREFIX + "host", JvmSettings.MAIL_MTA_HOST.lookup()); // default = false from microprofile-config.properties - configuration.put("mail.smtp.auth", JvmSettings.MAIL_MTA_AUTH.lookup(Boolean.class).toString()); + configuration.put(PREFIX + "auth", JvmSettings.MAIL_MTA_AUTH.lookup(Boolean.class).toString()); // Map properties 1:1 to mail.smtp properties for the mail session. smtpStringProps.forEach( From c23ccded546910b9175c35841930ee2901af1c10 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 20 Sep 2023 14:53:20 +0200 Subject: [PATCH 019/689] chore(build,test): upgrade to Testcontainers v1.19.0 --- modules/dataverse-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/dataverse-parent/pom.xml b/modules/dataverse-parent/pom.xml index c45d59e4f5f..29743aa3974 100644 --- a/modules/dataverse-parent/pom.xml +++ b/modules/dataverse-parent/pom.xml @@ -168,7 +168,7 @@ 5.1.0 - 1.15.0 + 1.19.0 2.10.1 5.10.0 From eb1664f08fa896013adb18be0171c1b5bddd5609 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 20 Sep 2023 14:53:57 +0200 Subject: [PATCH 020/689] feat(mail): add explicit injection constructor to MailServiceBean #7424 Necessary to add some integration testing, verifying sending mails actually should work. --- .../java/edu/harvard/iq/dataverse/MailServiceBean.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java index 3463aa211bb..df24099f60c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java @@ -79,6 +79,14 @@ public class MailServiceBean implements java.io.Serializable { */ public MailServiceBean() { } + + /** + * Creates a new instance of MailServiceBean with explicit injection, as used during testing. + */ + public MailServiceBean(Session session, SettingsServiceBean settingsService) { + this.session = session; + this.settingsService = settingsService; + } @Inject @Named("mail/systemSession") From 086f766c714feb61065ab4420783e31f57c91e99 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 20 Sep 2023 14:58:19 +0200 Subject: [PATCH 021/689] test(mail): add integration test for mail session configuration and usage #7424 --- .../dataverse/util/MailSessionProducerIT.java | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java diff --git a/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java b/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java new file mode 100644 index 00000000000..b41fa69ff52 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java @@ -0,0 +1,103 @@ +package edu.harvard.iq.dataverse.util; + +import edu.harvard.iq.dataverse.DataverseServiceBean; +import edu.harvard.iq.dataverse.MailServiceBean; +import edu.harvard.iq.dataverse.branding.BrandingUtil; +import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import io.restassured.RestAssured; +import jakarta.mail.Session; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * An integration test using a fake SMTP MTA to check for outgoing mails. + * LIMITATION: This test cannot possibly check if the production and injection of the session via CDI + * works, as it is not running within a servlet container. This would require usage of Arquillian + * or and end-to-end API test with a deployed application. + */ +@Testcontainers +@ExtendWith(MockitoExtension.class) +class MailSessionProducerIT { + + private static final Integer PORT_SMTP = 1025; + private static final Integer PORT_HTTP = 1080; + + Integer smtpPort; + String smtpHost; + + @Mock + SettingsServiceBean settingsServiceBean; + @Mock + DataverseServiceBean dataverseServiceBean; + + @Container + static GenericContainer maildev = new GenericContainer<>("maildev/maildev:2.1.0") + .withExposedPorts(PORT_HTTP, PORT_SMTP) + .waitingFor(Wait.forHttp("/")); + + @BeforeEach + void setUp() { + smtpHost = maildev.getHost(); + smtpPort = maildev.getMappedPort(PORT_SMTP); + Integer httpPort = maildev.getMappedPort(PORT_HTTP); + + RestAssured.baseURI = "http://" + smtpHost; + RestAssured.port = httpPort; + + // Setup mocks + Mockito.when(settingsServiceBean.getValueForKey(SettingsServiceBean.Key.SystemEmail)).thenReturn("noreply@example.org"); + BrandingUtil.injectServices(dataverseServiceBean, settingsServiceBean); + + // TODO: Once we merge PR 9273 (https://github.com/IQSS/dataverse/pull/9273), + // we can use methods to inject the settings in @JvmSetting + System.setProperty(JvmSettings.MAIL_MTA_HOST.getScopedKey(), smtpHost); + System.setProperty(JvmSettings.MAIL_MTA_SETTING.insert("port"), smtpPort.toString()); + } + + @AfterEach + void tearDown() { + System.clearProperty(JvmSettings.MAIL_MTA_HOST.getScopedKey()); + System.clearProperty(JvmSettings.MAIL_MTA_SETTING.insert("port")); + } + + @Test + //@JvmSetting(key = JvmSettings.MAIL_DEBUG, value = "true") + void createSessionWithoutAuth() { + given().when().get("/email") + .then() + .statusCode(200) + .body("size()", is(0)); + + // given + Session session = new MailSessionProducer().getSession(); + MailServiceBean mailer = new MailServiceBean(session, settingsServiceBean); + + // when + boolean sent = mailer.sendSystemEmail("test@example.org", "Test", "Test", false); + + // then + assertTrue(sent); + //RestAssured.get("/email").body().prettyPrint(); + given().when().get("/email") + .then() + .statusCode(200) + .body("size()", is(1)) + .body("[0].subject", equalTo("Test")); + } + +} \ No newline at end of file From 51af5e12051db26eebdbb7a4afe23173104c5043 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 20 Sep 2023 14:58:42 +0200 Subject: [PATCH 022/689] build(mail): exclude geronimo javamail spec from dependencies If not excluded, the very old Javamail 1.4 spec is being used during local testing, obviously incompatible with Jakarta EE Mail definition. Exclusion is the only way around this, as we cannot possibly change the upstream dependencies. --- pom.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pom.xml b/pom.xml index 7ba22d2a076..e781cca3b7c 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,17 @@ abdera-i18n 1.1.3 + + org.apache.abdera + abdera-parser + 1.1.3 + + + org.apache.geronimo.specs + geronimo-javamail_1.4_spec + + + - target/${project.artifactId}-${project.version} + target/${project.artifactId} app WEB-INF/lib/**/* @@ -11,7 +11,7 @@ - target/${project.artifactId}-${project.version}/WEB-INF/lib + target/${project.artifactId}/WEB-INF/lib deps From a7418cfe70c6e268254267d0aa9784fc3c27d8bc Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 26 Sep 2023 16:45:37 +0200 Subject: [PATCH 031/689] fix(ct): move fallback JRE version in base image to 17 --- modules/container-base/src/main/docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/container-base/src/main/docker/Dockerfile b/modules/container-base/src/main/docker/Dockerfile index 97aa4cd2792..11ad980f070 100644 --- a/modules/container-base/src/main/docker/Dockerfile +++ b/modules/container-base/src/main/docker/Dockerfile @@ -22,7 +22,7 @@ # # Make the Java base image and version configurable (useful for trying newer Java versions and flavors) -ARG JAVA_IMAGE="eclipse-temurin:11-jre" +ARG JAVA_IMAGE="eclipse-temurin:17-jre" FROM $JAVA_IMAGE # Default payara ports to expose From 68eb59d0d4de068c352d5a7b4889dd034f3dc296 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 26 Sep 2023 16:48:55 +0200 Subject: [PATCH 032/689] doc(ct): describe extended behaviour of ENABLE_RELOAD #9590 --- doc/sphinx-guides/source/container/base-image.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/container/base-image.rst b/doc/sphinx-guides/source/container/base-image.rst index 1a47a8fc413..1be0b992b2a 100644 --- a/doc/sphinx-guides/source/container/base-image.rst +++ b/doc/sphinx-guides/source/container/base-image.rst @@ -217,7 +217,9 @@ provides. These are mostly based on environment variables (very common with cont - ``0`` - Bool, ``0|1`` - Enable the dynamic "hot" reloads of files when changed in a deployment. Useful for development, - when new artifacts are copied into the running domain. + when new artifacts are copied into the running domain. Also, export Dataverse specific environment variables + ``DATAVERSE_JSF_PROJECT_STAGE=Development`` and ``DATAVERSE_JSF_REFRESH_PERIOD=0`` to enable dynamic JSF page + reloads. * - ``DATAVERSE_HTTP_TIMEOUT`` - ``900`` - Seconds From a8883981daa5d84d4553150804fe59942886d069 Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Thu, 28 Sep 2023 13:36:19 +0200 Subject: [PATCH 033/689] always_add_validity_field_to_solr_doc --- .../edu/harvard/iq/dataverse/search/IndexServiceBean.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java index d6d0be7a17b..04bc824c4b1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java @@ -811,9 +811,7 @@ public SolrInputDocuments toSolrDocs(IndexableDataset indexableDataset, Set Date: Wed, 4 Oct 2023 17:58:06 +0200 Subject: [PATCH 034/689] Revert print email on modal --- src/main/webapp/roles-assign.xhtml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/webapp/roles-assign.xhtml b/src/main/webapp/roles-assign.xhtml index 4b355c74d5c..93b9862c55d 100644 --- a/src/main/webapp/roles-assign.xhtml +++ b/src/main/webapp/roles-assign.xhtml @@ -32,7 +32,6 @@ var="roleAssignee" itemLabel="#{roleAssignee.displayInfo.title}" itemValue="#{roleAssignee}" converter="roleAssigneeConverter"> - From c367e09c8cfcdb1b368795145e62a0ec43e85544 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Thu, 5 Oct 2023 14:40:36 +0200 Subject: [PATCH 035/689] test(mail): refactor IT for mail sessions to use @JvmSettings --- .../dataverse/util/MailSessionProducerIT.java | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java b/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java index b41fa69ff52..382fb99dca3 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java @@ -5,6 +5,8 @@ import edu.harvard.iq.dataverse.branding.BrandingUtil; import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.testing.JvmSetting; +import edu.harvard.iq.dataverse.util.testing.LocalJvmSettings; import io.restassured.RestAssured; import jakarta.mail.Session; import org.junit.jupiter.api.AfterEach; @@ -32,14 +34,14 @@ */ @Testcontainers @ExtendWith(MockitoExtension.class) +@LocalJvmSettings +@JvmSetting(key = JvmSettings.MAIL_MTA_HOST, method = "tcSmtpHost") +@JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, method = "tcSmtpPort", varArgs = "port") class MailSessionProducerIT { private static final Integer PORT_SMTP = 1025; private static final Integer PORT_HTTP = 1080; - Integer smtpPort; - String smtpHost; - @Mock SettingsServiceBean settingsServiceBean; @Mock @@ -49,30 +51,23 @@ class MailSessionProducerIT { static GenericContainer maildev = new GenericContainer<>("maildev/maildev:2.1.0") .withExposedPorts(PORT_HTTP, PORT_SMTP) .waitingFor(Wait.forHttp("/")); + + static String tcSmtpHost() { + return maildev.getHost(); + } + + static String tcSmtpPort() { + return maildev.getMappedPort(PORT_SMTP).toString(); + } @BeforeEach void setUp() { - smtpHost = maildev.getHost(); - smtpPort = maildev.getMappedPort(PORT_SMTP); - Integer httpPort = maildev.getMappedPort(PORT_HTTP); - - RestAssured.baseURI = "http://" + smtpHost; - RestAssured.port = httpPort; + RestAssured.baseURI = "http://" + tcSmtpHost(); + RestAssured.port = maildev.getMappedPort(PORT_HTTP);; // Setup mocks Mockito.when(settingsServiceBean.getValueForKey(SettingsServiceBean.Key.SystemEmail)).thenReturn("noreply@example.org"); BrandingUtil.injectServices(dataverseServiceBean, settingsServiceBean); - - // TODO: Once we merge PR 9273 (https://github.com/IQSS/dataverse/pull/9273), - // we can use methods to inject the settings in @JvmSetting - System.setProperty(JvmSettings.MAIL_MTA_HOST.getScopedKey(), smtpHost); - System.setProperty(JvmSettings.MAIL_MTA_SETTING.insert("port"), smtpPort.toString()); - } - - @AfterEach - void tearDown() { - System.clearProperty(JvmSettings.MAIL_MTA_HOST.getScopedKey()); - System.clearProperty(JvmSettings.MAIL_MTA_SETTING.insert("port")); } @Test From 4c6405195497d8520f48de1f2f3f108b19fce24d Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Thu, 5 Oct 2023 15:16:50 +0200 Subject: [PATCH 036/689] test(mail): include in regular IT test suite --- .../harvard/iq/dataverse/util/MailSessionProducerIT.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java b/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java index 382fb99dca3..b6a77cc830e 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java @@ -7,10 +7,12 @@ import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.testing.JvmSetting; import edu.harvard.iq.dataverse.util.testing.LocalJvmSettings; +import edu.harvard.iq.dataverse.util.testing.Tags; import io.restassured.RestAssured; import jakarta.mail.Session; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -32,7 +34,10 @@ * works, as it is not running within a servlet container. This would require usage of Arquillian * or and end-to-end API test with a deployed application. */ -@Testcontainers + +@Tag(Tags.INTEGRATION_TEST) +@Tag(Tags.USES_TESTCONTAINERS) +@Testcontainers(disabledWithoutDocker = true) @ExtendWith(MockitoExtension.class) @LocalJvmSettings @JvmSetting(key = JvmSettings.MAIL_MTA_HOST, method = "tcSmtpHost") From 2178c839a2fceb2e678c7a58d30ab91e19ea536c Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Thu, 5 Oct 2023 23:10:16 +0200 Subject: [PATCH 037/689] test(mail): add test scenario including necessary SMTP authentication #7424 --- .../dataverse/util/MailSessionProducerIT.java | 158 +++++++++++++----- 1 file changed, 113 insertions(+), 45 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java b/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java index b6a77cc830e..8bf02697022 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java @@ -11,7 +11,9 @@ import io.restassured.RestAssured; import jakarta.mail.Session; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -23,6 +25,8 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; +import java.util.Map; + import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; @@ -39,65 +43,129 @@ @Tag(Tags.USES_TESTCONTAINERS) @Testcontainers(disabledWithoutDocker = true) @ExtendWith(MockitoExtension.class) -@LocalJvmSettings -@JvmSetting(key = JvmSettings.MAIL_MTA_HOST, method = "tcSmtpHost") -@JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, method = "tcSmtpPort", varArgs = "port") class MailSessionProducerIT { private static final Integer PORT_SMTP = 1025; private static final Integer PORT_HTTP = 1080; - @Mock - SettingsServiceBean settingsServiceBean; - @Mock - DataverseServiceBean dataverseServiceBean; - - @Container - static GenericContainer maildev = new GenericContainer<>("maildev/maildev:2.1.0") - .withExposedPorts(PORT_HTTP, PORT_SMTP) - .waitingFor(Wait.forHttp("/")); + static SettingsServiceBean settingsServiceBean = Mockito.mock(SettingsServiceBean.class);; + static DataverseServiceBean dataverseServiceBean = Mockito.mock(DataverseServiceBean.class);; - static String tcSmtpHost() { - return maildev.getHost(); + @BeforeAll + static void setUp() { + // Setup mocks behavior, inject as deps + Mockito.when(settingsServiceBean.getValueForKey(SettingsServiceBean.Key.SystemEmail)).thenReturn("noreply@example.org"); + BrandingUtil.injectServices(dataverseServiceBean, settingsServiceBean); } - static String tcSmtpPort() { - return maildev.getMappedPort(PORT_SMTP).toString(); - } - - @BeforeEach - void setUp() { - RestAssured.baseURI = "http://" + tcSmtpHost(); - RestAssured.port = maildev.getMappedPort(PORT_HTTP);; + @Nested + @LocalJvmSettings + @JvmSetting(key = JvmSettings.MAIL_MTA_HOST, method = "tcSmtpHost") + @JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, method = "tcSmtpPort", varArgs = "port") + class WithoutAuthentication { + @Container + static GenericContainer maildev = new GenericContainer<>("maildev/maildev:2.1.0") + .withExposedPorts(PORT_HTTP, PORT_SMTP) + .waitingFor(Wait.forHttp("/")); + + static String tcSmtpHost() { + return maildev.getHost(); + } + + static String tcSmtpPort() { + return maildev.getMappedPort(PORT_SMTP).toString(); + } + + @BeforeAll + static void setup() { + RestAssured.baseURI = "http://" + tcSmtpHost(); + RestAssured.port = maildev.getMappedPort(PORT_HTTP); + } + + @Test + void createSession() { + given().when().get("/email") + .then() + .statusCode(200) + .body("size()", is(0)); + + // given + Session session = new MailSessionProducer().getSession(); + MailServiceBean mailer = new MailServiceBean(session, settingsServiceBean); + + // when + boolean sent = mailer.sendSystemEmail("test@example.org", "Test", "Test", false); + + // then + assertTrue(sent); + //RestAssured.get("/email").body().prettyPrint(); + given().when().get("/email") + .then() + .statusCode(200) + .body("size()", is(1)) + .body("[0].subject", equalTo("Test")); + } - // Setup mocks - Mockito.when(settingsServiceBean.getValueForKey(SettingsServiceBean.Key.SystemEmail)).thenReturn("noreply@example.org"); - BrandingUtil.injectServices(dataverseServiceBean, settingsServiceBean); } - @Test - //@JvmSetting(key = JvmSettings.MAIL_DEBUG, value = "true") - void createSessionWithoutAuth() { - given().when().get("/email") - .then() - .statusCode(200) - .body("size()", is(0)); + static final String username = "testuser"; + static final String password = "supersecret"; + + @Nested + @LocalJvmSettings + @JvmSetting(key = JvmSettings.MAIL_MTA_HOST, method = "tcSmtpHost") + @JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, method = "tcSmtpPort", varArgs = "port") + @JvmSetting(key = JvmSettings.MAIL_MTA_AUTH, value = "yes") + @JvmSetting(key = JvmSettings.MAIL_MTA_USER, value = username) + @JvmSetting(key = JvmSettings.MAIL_MTA_PASSWORD, value = password) + class WithAuthentication { + @Container + static GenericContainer maildev = new GenericContainer<>("maildev/maildev:2.1.0") + .withExposedPorts(PORT_HTTP, PORT_SMTP) + .withEnv(Map.of( + "MAILDEV_INCOMING_USER", username, + "MAILDEV_INCOMING_PASS", password + )) + .waitingFor(Wait.forHttp("/")); + + static String tcSmtpHost() { + return maildev.getHost(); + } + + static String tcSmtpPort() { + return maildev.getMappedPort(PORT_SMTP).toString(); + } - // given - Session session = new MailSessionProducer().getSession(); - MailServiceBean mailer = new MailServiceBean(session, settingsServiceBean); + @BeforeAll + static void setup() { + RestAssured.baseURI = "http://" + tcSmtpHost(); + RestAssured.port = maildev.getMappedPort(PORT_HTTP); + } - // when - boolean sent = mailer.sendSystemEmail("test@example.org", "Test", "Test", false); + @Test + void createSession() { + given().when().get("/email") + .then() + .statusCode(200) + .body("size()", is(0)); + + // given + Session session = new MailSessionProducer().getSession(); + MailServiceBean mailer = new MailServiceBean(session, settingsServiceBean); + + // when + boolean sent = mailer.sendSystemEmail("test@example.org", "Test", "Test", false); + + // then + assertTrue(sent); + //RestAssured.get("/email").body().prettyPrint(); + given().when().get("/email") + .then() + .statusCode(200) + .body("size()", is(1)) + .body("[0].subject", equalTo("Test")); + } - // then - assertTrue(sent); - //RestAssured.get("/email").body().prettyPrint(); - given().when().get("/email") - .then() - .statusCode(200) - .body("size()", is(1)) - .body("[0].subject", equalTo("Test")); } } \ No newline at end of file From 17aa5ad267b82a623dab10bfbd0bf9b20fef47d9 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 6 Oct 2023 17:54:21 +0200 Subject: [PATCH 038/689] feat(mail): make system email address configurable via MPCONFIG #7424 Besides adding the JVM option, the logic to receive the setting in MailServiceBean has changed. The method signature is now returning an optional to enforce the optional nature of the setting. This replaces the "null" contract from before and requires more changes to code using the lookup. --- .../harvard/iq/dataverse/MailServiceBean.java | 44 ++++++++++++++++--- .../iq/dataverse/settings/JvmSettings.java | 1 + 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java index 6b8ae3a34d9..dabe6964ccf 100644 --- a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java @@ -11,6 +11,7 @@ import edu.harvard.iq.dataverse.branding.BrandingUtil; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailServiceBean; import edu.harvard.iq.dataverse.dataset.DatasetUtil; +import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.settings.SettingsServiceBean.Key; import edu.harvard.iq.dataverse.util.BundleUtil; @@ -24,7 +25,10 @@ import java.util.Arrays; import java.util.Date; import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.Set; +import java.util.logging.Level; import java.util.logging.Logger; import jakarta.ejb.EJB; import jakarta.ejb.Stateless; @@ -147,10 +151,37 @@ public boolean sendSystemEmail(String to, String subject, String messageText, bo } return sent; } - - public InternetAddress getSystemAddress() { - String systemEmail = settingsService.getValueForKey(Key.SystemEmail); - return MailUtil.parseSystemAddress(systemEmail); + + /** + * Lookup the system mail address ({@code InternetAddress} may contain personal and actual address). + * @return The system mail address or an empty {@code Optional} if not configured. + */ + public Optional getSystemAddress() { + boolean providedByDB = false; + String mailAddress = JvmSettings.SYSTEM_EMAIL.lookupOptional().orElse(null); + + // Try lookup of (deprecated) database setting only if not configured via MPCONFIG + if (mailAddress == null) { + mailAddress = settingsService.getValueForKey(Key.SystemEmail); + // Encourage people to migrate from deprecated setting + if (mailAddress != null) { + providedByDB = true; + logger.warning("The :SystemMail DB setting has been deprecated, please reconfigure using JVM option " + JvmSettings.SYSTEM_EMAIL.getScopedKey()); + } + } + + try { + // Parse and return. + return Optional.of(new InternetAddress(Objects.requireNonNull(mailAddress), true)); + } catch (AddressException e) { + logger.log(Level.WARNING, "Could not parse system mail address '%s' provided by %s: " + .formatted(providedByDB ? "DB setting" : "JVM option", mailAddress), e); + } catch (NullPointerException e) { + logger.warning("Could not find a system mail setting in database (key :SystemEmail, deprecated) or JVM option '" + JvmSettings.SYSTEM_EMAIL.getScopedKey() + "'"); + } + // We define the system email address as an optional setting, in case people do not want to enable mail + // notifications (like in a development context, but might be useful elsewhere, too). + return Optional.empty(); } //@Resource(name="mail/notifyMailSession") @@ -515,13 +546,12 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio messageText += MessageFormat.format(pattern, paramArrayStatus); return messageText; case CREATEACC: - InternetAddress systemAddress = getSystemAddress(); String accountCreatedMessage = BundleUtil.getStringFromBundle("notification.email.welcome", Arrays.asList( BrandingUtil.getInstallationBrandName(), systemConfig.getGuidesBaseUrl(), systemConfig.getGuidesVersion(), - BrandingUtil.getSupportTeamName(systemAddress), - BrandingUtil.getSupportTeamEmailAddress(systemAddress) + BrandingUtil.getSupportTeamName(getSystemAddress().orElse(null)), + BrandingUtil.getSupportTeamEmailAddress(getSystemAddress().orElse(null)) )); String optionalConfirmEmailAddon = confirmEmailService.optionalConfirmEmailAddonMsg(userNotification.getUser()); accountCreatedMessage += optionalConfirmEmailAddon; 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 f7c1ef906b6..2f249ad119e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java @@ -120,6 +120,7 @@ public enum JvmSettings { // MAIL SETTINGS SCOPE_MAIL(PREFIX, "mail"), + SYSTEM_EMAIL(SCOPE_MAIL, "system-email"), SUPPORT_EMAIL(SCOPE_MAIL, "support-email"), CC_SUPPORT_ON_CONTACT_EMAIL(SCOPE_MAIL, "cc-support-on-contact-email"), MAIL_DEBUG(SCOPE_MAIL, "debug"), From fd41607751fc48bfb3b4690a7be61ba515a74d9b Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 6 Oct 2023 17:56:42 +0200 Subject: [PATCH 039/689] refactor(mail): make MailServiceBean use new lookup API for system address #7424 As we changed the lookup function to use Optional to enforce the optional nature of the setting, we now have to change the code using the function. --- .../harvard/iq/dataverse/MailServiceBean.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java index dabe6964ccf..6fd2a9e35a6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java @@ -101,9 +101,14 @@ public boolean sendSystemEmail(String to, String subject, String messageText) { } public boolean sendSystemEmail(String to, String subject, String messageText, boolean isHtmlContent) { + Optional optionalAddress = getSystemAddress(); + if (optionalAddress.isEmpty()) { + logger.fine(() -> "Skipping sending mail to " + to + ", because no system address has been set."); + return false; + } + InternetAddress systemAddress = optionalAddress.get(); boolean sent = false; - InternetAddress systemAddress = getSystemAddress(); String body = messageText + (isHtmlContent ? BundleUtil.getStringFromBundle("notification.email.closing.html", Arrays.asList(BrandingUtil.getSupportTeamEmailAddress(systemAddress), BrandingUtil.getSupportTeamName(systemAddress))) @@ -186,10 +191,17 @@ public Optional getSystemAddress() { //@Resource(name="mail/notifyMailSession") public void sendMail(String reply, String to, String cc, String subject, String messageText) { + Optional optionalAddress = getSystemAddress(); + if (optionalAddress.isEmpty()) { + logger.fine(() -> "Skipping sending mail to " + to + ", because no system address has been set."); + return; + } + // Always send from system address to avoid email being blocked + InternetAddress fromAddress = optionalAddress.get(); + try { MimeMessage msg = new MimeMessage(session); - // Always send from system address to avoid email being blocked - InternetAddress fromAddress = getSystemAddress(); + try { setContactDelegation(reply, fromAddress); } catch (UnsupportedEncodingException ex) { From 2bff977e1742178a8fd76d7acb642280c7d85c2e Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 6 Oct 2023 17:58:58 +0200 Subject: [PATCH 040/689] feat(mail): provide lookup function for support mail address #7424 To ease looking up the (also optional) setting of a support team mail address, the mail service is extended with another lookup function. This is intended to replace many manual, error prone lookups, also streamlining the fall-through behavior when not set, etc. --- .../harvard/iq/dataverse/MailServiceBean.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java index 6fd2a9e35a6..83fafbd3b50 100644 --- a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java @@ -188,6 +188,23 @@ public Optional getSystemAddress() { // notifications (like in a development context, but might be useful elsewhere, too). return Optional.empty(); } + + /** + * Lookup the support team mail address ({@code InternetAddress} may contain personal and actual address). + * Will default to return {@code #getSystemAddress} if not configured. + * @return Support team mail address + */ + public Optional getSupportAddress() { + Optional supportMailAddress = JvmSettings.SUPPORT_EMAIL.lookupOptional(); + if (supportMailAddress.isPresent()) { + try { + return Optional.of(new InternetAddress(supportMailAddress.get(), true)); + } catch (AddressException e) { + logger.log(Level.WARNING, "Could not parse support mail address '%s', defaulting to system address: ".formatted(supportMailAddress.get()), e); + } + } + return getSystemAddress(); + } //@Resource(name="mail/notifyMailSession") public void sendMail(String reply, String to, String cc, String subject, String messageText) { From de2f42363726858218628b309dbd8012bc790fcc Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 6 Oct 2023 18:01:58 +0200 Subject: [PATCH 041/689] refactor(mail): replace manual parsing with mail service lookups #7424 As we now have proper functions to lookup the mail addresses, replace manual lookup and parsing with them. --- .../harvard/iq/dataverse/SendFeedbackDialog.java | 4 +--- .../harvard/iq/dataverse/SettingsWrapper.java | 11 +++++++---- .../harvard/iq/dataverse/api/FeedbackApi.java | 8 ++------ .../harvest/server/web/servlet/OAIServlet.java | 16 +++++++++++----- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/SendFeedbackDialog.java b/src/main/java/edu/harvard/iq/dataverse/SendFeedbackDialog.java index 6be768321c4..0f3e712676c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/SendFeedbackDialog.java +++ b/src/main/java/edu/harvard/iq/dataverse/SendFeedbackDialog.java @@ -6,7 +6,6 @@ import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; -import edu.harvard.iq.dataverse.util.MailUtil; import edu.harvard.iq.dataverse.util.SystemConfig; import java.util.Optional; import java.util.Random; @@ -101,8 +100,7 @@ public void initUserInput(ActionEvent ae) { op1 = Long.valueOf(random.nextInt(10)); op2 = Long.valueOf(random.nextInt(10)); userSum = null; - String supportEmail = JvmSettings.SUPPORT_EMAIL.lookupOptional().orElse(settingsService.getValueForKey(SettingsServiceBean.Key.SystemEmail)); - systemAddress = MailUtil.parseSystemAddress(supportEmail); + systemAddress = mailService.getSupportAddress().orElse(null); } public Long getOp1() { diff --git a/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java b/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java index 0a1d0effc03..02554ee832d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java +++ b/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java @@ -63,6 +63,9 @@ public class SettingsWrapper implements java.io.Serializable { @EJB MetadataBlockServiceBean mdbService; + + @EJB + MailServiceBean mailServiceBean; private Map settingsMap; @@ -388,14 +391,14 @@ public boolean isDataFilePIDSequentialDependent(){ } public String getSupportTeamName() { - String systemEmail = getValueForKey(SettingsServiceBean.Key.SystemEmail); - InternetAddress systemAddress = MailUtil.parseSystemAddress(systemEmail); + // TODO: should this be replaced with mailServiceBean.getSupportAddress() to expose a configured support team? + InternetAddress systemAddress = mailServiceBean.getSystemAddress().orElse(null); return BrandingUtil.getSupportTeamName(systemAddress); } public String getSupportTeamEmail() { - String systemEmail = getValueForKey(SettingsServiceBean.Key.SystemEmail); - InternetAddress systemAddress = MailUtil.parseSystemAddress(systemEmail); + // TODO: should this be replaced with mailServiceBean.getSupportAddress() to expose a configured support team? + InternetAddress systemAddress = mailServiceBean.getSystemAddress().orElse(null); return BrandingUtil.getSupportTeamEmailAddress(systemAddress) != null ? BrandingUtil.getSupportTeamEmailAddress(systemAddress) : BrandingUtil.getSupportTeamName(systemAddress); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/FeedbackApi.java b/src/main/java/edu/harvard/iq/dataverse/api/FeedbackApi.java index 8a178f8da62..56c5ca95ce6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/FeedbackApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/FeedbackApi.java @@ -7,9 +7,6 @@ import edu.harvard.iq.dataverse.branding.BrandingUtil; import edu.harvard.iq.dataverse.feedback.Feedback; import edu.harvard.iq.dataverse.feedback.FeedbackUtil; -import edu.harvard.iq.dataverse.settings.JvmSettings; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; -import edu.harvard.iq.dataverse.util.MailUtil; import jakarta.ejb.EJB; import jakarta.json.Json; @@ -40,7 +37,7 @@ public class FeedbackApi extends AbstractApiBean { * user input (e.g. to strip potentially malicious html, etc.)!!!! **/ @POST - public Response submitFeedback(JsonObject jsonObject) throws AddressException { + public Response submitFeedback(JsonObject jsonObject) { JsonNumber jsonNumber = jsonObject.getJsonNumber("targetId"); DvObject feedbackTarget = null; if (jsonNumber != null) { @@ -51,8 +48,7 @@ public Response submitFeedback(JsonObject jsonObject) throws AddressException { } DataverseSession dataverseSession = null; String userMessage = jsonObject.getString("body"); - String systemEmail = JvmSettings.SUPPORT_EMAIL.lookupOptional().orElse(settingsSvc.getValueForKey(SettingsServiceBean.Key.SystemEmail)); - InternetAddress systemAddress = MailUtil.parseSystemAddress(systemEmail); + InternetAddress systemAddress = mailService.getSupportAddress().orElse(null); String userEmail = jsonObject.getString("fromEmail"); String messageSubject = jsonObject.getString("subject"); String baseUrl = systemConfig.getDataverseSiteUrl(); diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java index 96a19acc0e8..b3e2025946b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java @@ -5,6 +5,7 @@ */ package edu.harvard.iq.dataverse.harvest.server.web.servlet; +import edu.harvard.iq.dataverse.MailServiceBean; import io.gdcc.xoai.dataprovider.DataProvider; import io.gdcc.xoai.dataprovider.repository.Repository; import io.gdcc.xoai.dataprovider.repository.RepositoryConfiguration; @@ -38,6 +39,7 @@ import java.io.IOException; +import java.util.Optional; import java.util.logging.Logger; import jakarta.ejb.EJB; import jakarta.inject.Inject; @@ -65,14 +67,14 @@ public class OAIServlet extends HttpServlet { @EJB OAIRecordServiceBean recordService; @EJB - SettingsServiceBean settingsService; - @EJB DataverseServiceBean dataverseService; @EJB DatasetServiceBean datasetService; @EJB SystemConfig systemConfig; + @EJB + MailServiceBean mailServiceBean; @Inject @ConfigProperty(name = "dataverse.oai.server.maxidentifiers", defaultValue="100") @@ -192,9 +194,13 @@ private RepositoryConfiguration createRepositoryConfiguration() { // (Note: if the setting does not exist, we are going to assume that they // have a reason not to want to configure their email address, if it is // a developer's instance, for example; or a reason not to want to - // advertise it to the world.) - InternetAddress systemEmailAddress = MailUtil.parseSystemAddress(settingsService.getValueForKey(SettingsServiceBean.Key.SystemEmail)); - String systemEmailLabel = systemEmailAddress != null ? systemEmailAddress.getAddress() : "donotreply@localhost"; + // advertise it to the world.) + String systemEmailLabel = "donotreply@localhost"; + // TODO: should we expose the support team's address if configured? + Optional systemAddress = mailServiceBean.getSystemAddress(); + if (systemAddress.isPresent()) { + systemEmailLabel = systemAddress.get().getAddress(); + } RepositoryConfiguration configuration = new RepositoryConfiguration.RepositoryConfigurationBuilder() .withAdminEmail(systemEmailLabel) From 81da403baaa0ae571a8591f42172b3caa7217fe3 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 6 Oct 2023 18:05:56 +0200 Subject: [PATCH 042/689] style(mail): deprecate db setting for system email #7424 Document in code how to replace usages, too. (There aren't any, but in case someone is adding it again in the future, it helps to have docs) --- .../harvard/iq/dataverse/settings/SettingsServiceBean.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java index 0aa403a5116..10c19ef6069 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java @@ -230,7 +230,12 @@ public enum Key { /* the number of files the GUI user is allowed to upload in one batch, via drag-and-drop, or through the file select dialog */ MultipleUploadFilesLimit, - /* return email address for system emails such as notifications */ + /** + * Return email address for system emails such as notifications + * @deprecated Please replace usages with {@link edu.harvard.iq.dataverse.MailServiceBean#getSystemAddress}, + * which is backward compatible with this setting. + */ + @Deprecated(since = "6.1", forRemoval = true) SystemEmail, /* size limit for Tabular data file ingests */ /* (can be set separately for specific ingestable formats; in which From 59b09cb5a000097e2d45de4964180859c726acae Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 6 Oct 2023 18:09:01 +0200 Subject: [PATCH 043/689] test(mail): use InternetAddress parsing directly, no need for MailUtil --- .../java/edu/harvard/iq/dataverse/MailServiceBeanTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java index 32bf9702ee7..83eba7e6480 100644 --- a/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java @@ -1,7 +1,7 @@ package edu.harvard.iq.dataverse; import edu.harvard.iq.dataverse.branding.BrandingUtilTest; -import edu.harvard.iq.dataverse.util.MailUtil; +import jakarta.mail.internet.AddressException; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; @@ -39,11 +39,11 @@ private static void tearDown() { // without name, without root dataverse name, without installation name -> default to bundle string. "dataverse@dataverse.org, NULL, NULL, Dataverse Installation Admin" }, nullValues = {"NULL"}) - void setContactDelegation(String fromMail, String rootDataverseName, String installationName, String expectedStartsWith) { + void setContactDelegation(String fromMail, String rootDataverseName, String installationName, String expectedStartsWith) throws AddressException { BrandingUtilTest.setRootDataverseName(rootDataverseName); BrandingUtilTest.setInstallationName(installationName); - InternetAddress fromAddress = MailUtil.parseSystemAddress(fromMail); + InternetAddress fromAddress = new InternetAddress(fromMail); MailServiceBean mailServiceBean = new MailServiceBean(); try { mailServiceBean.setContactDelegation("user@example.edu", fromAddress); From 7fc613f7a870951a088ebbf7238895318fb057c5 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 6 Oct 2023 18:10:14 +0200 Subject: [PATCH 044/689] refactor(mail): remove unused MailUtil.parseSystemAddress #7424 As we replaced the lookups and parsing with a streamlined version of it all in MailServiceBean, we don't need this helper function anymore. --- .../harvard/iq/dataverse/util/MailUtil.java | 18 ------------------ .../iq/dataverse/util/MailUtilTest.java | 2 ++ 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java index 0724e53700b..ccec3e5f09b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java @@ -5,32 +5,14 @@ import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.UserNotification; import edu.harvard.iq.dataverse.branding.BrandingUtil; -import static edu.harvard.iq.dataverse.settings.SettingsServiceBean.Key.SystemEmail; import java.util.Arrays; import java.util.List; import java.util.logging.Logger; -import jakarta.mail.internet.AddressException; -import jakarta.mail.internet.InternetAddress; public class MailUtil { private static final Logger logger = Logger.getLogger(MailUtil.class.getCanonicalName()); - public static InternetAddress parseSystemAddress(String systemEmail) { - if (systemEmail != null) { - try { - InternetAddress parsedSystemEmail = new InternetAddress(systemEmail); - logger.fine("parsed system email: " + parsedSystemEmail); - return parsedSystemEmail; - } catch (AddressException ex) { - logger.info("Email will not be sent due to invalid value in " + SystemEmail + " setting: " + ex); - return null; - } - } - logger.fine("Email will not be sent because the " + SystemEmail + " setting is null."); - return null; - } - public static String getSubjectTextBasedOnNotification(UserNotification userNotification, Object objectOfNotification) { List rootDvNameAsList = Arrays.asList(BrandingUtil.getInstallationBrandName()); String datasetDisplayName = ""; diff --git a/src/test/java/edu/harvard/iq/dataverse/util/MailUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/util/MailUtilTest.java index 205b1f0bfcf..0756c4650fb 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/MailUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/MailUtilTest.java @@ -33,6 +33,7 @@ public void setUp() { } + /* @Test public void testParseSystemAddress() { assertEquals("support@librascholar.edu", MailUtil.parseSystemAddress("support@librascholar.edu").getAddress()); @@ -46,6 +47,7 @@ public void testParseSystemAddress() { assertEquals(null, MailUtil.parseSystemAddress("\"LibraScholar Support Team ")); assertEquals(null, MailUtil.parseSystemAddress("support1@dataverse.org, support@librascholar.edu")); } + */ @Test @Order(1) From 5b418c0f6ef2157f81b64c5dab2032b39ec5766e Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 6 Oct 2023 18:19:36 +0200 Subject: [PATCH 045/689] test(mail): refactor IT with new system email JVM option #7424 --- .../edu/harvard/iq/dataverse/util/MailSessionProducerIT.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java b/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java index 8bf02697022..dcf04b7644a 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java @@ -54,12 +54,12 @@ class MailSessionProducerIT { @BeforeAll static void setUp() { // Setup mocks behavior, inject as deps - Mockito.when(settingsServiceBean.getValueForKey(SettingsServiceBean.Key.SystemEmail)).thenReturn("noreply@example.org"); BrandingUtil.injectServices(dataverseServiceBean, settingsServiceBean); } @Nested @LocalJvmSettings + @JvmSetting(key = JvmSettings.SYSTEM_EMAIL, value = "test@test.com") @JvmSetting(key = JvmSettings.MAIL_MTA_HOST, method = "tcSmtpHost") @JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, method = "tcSmtpPort", varArgs = "port") class WithoutAuthentication { @@ -113,6 +113,7 @@ void createSession() { @Nested @LocalJvmSettings + @JvmSetting(key = JvmSettings.SYSTEM_EMAIL, value = "test@test.com") @JvmSetting(key = JvmSettings.MAIL_MTA_HOST, method = "tcSmtpHost") @JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, method = "tcSmtpPort", varArgs = "port") @JvmSetting(key = JvmSettings.MAIL_MTA_AUTH, value = "yes") From 2c3e054599a95dd291d071c8c447b8d5e59d46fb Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 6 Oct 2023 18:48:25 +0200 Subject: [PATCH 046/689] test(mail): restructure and add test for mail address lookups working as expected #7424 --- .../iq/dataverse/MailServiceBeanTest.java | 147 ++++++++++++++---- 1 file changed, 114 insertions(+), 33 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java index 83eba7e6480..28d5721d667 100644 --- a/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java @@ -1,56 +1,137 @@ package edu.harvard.iq.dataverse; import edu.harvard.iq.dataverse.branding.BrandingUtilTest; +import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.testing.JvmSetting; +import edu.harvard.iq.dataverse.util.testing.LocalJvmSettings; import jakarta.mail.internet.AddressException; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import jakarta.mail.internet.InternetAddress; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; import java.io.UnsupportedEncodingException; +import static edu.harvard.iq.dataverse.settings.SettingsServiceBean.*; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +@ExtendWith(MockitoExtension.class) class MailServiceBeanTest { - - /** - * We need to reset the BrandingUtil mocks for every test, as we rely on them being set to default. - */ - @BeforeEach - private void setup() { - BrandingUtilTest.setupMocks(); - } - @AfterAll - private static void tearDown() { - BrandingUtilTest.tearDownMocks(); + @Nested + class Delegation { + /** + * We need to reset the BrandingUtil mocks for every test, as we rely on them being set to default. + */ + @BeforeEach + private void setup() { + BrandingUtilTest.setupMocks(); + } + @AfterAll + private static void tearDown() { + BrandingUtilTest.tearDownMocks(); + } + + @ParameterizedTest + @CsvSource(value = { + // with name in admin mail address + "Foo Bar , NULL, NULL, Foo Bar", + // without name, but installation branding name set + "dataverse@dataverse.org, NULL, LibraScholar Dataverse, LibraScholar Dataverse", + // without name, but root dataverse name available + "dataverse@dataverse.org, NotLibraScholar, NULL, NotLibraScholar", + // without name, without root dataverse name, without installation name -> default to bundle string. + "dataverse@dataverse.org, NULL, NULL, Dataverse Installation Admin" + }, nullValues = {"NULL"}) + void setContactDelegation(String fromMail, String rootDataverseName, String installationName, String expectedStartsWith) throws AddressException { + BrandingUtilTest.setRootDataverseName(rootDataverseName); + BrandingUtilTest.setInstallationName(installationName); + + InternetAddress fromAddress = new InternetAddress(fromMail); + MailServiceBean mailServiceBean = new MailServiceBean(); + try { + mailServiceBean.setContactDelegation("user@example.edu", fromAddress); + assertTrue(fromAddress.getPersonal().startsWith(expectedStartsWith)); + assertTrue(fromAddress.getPersonal().endsWith(" on behalf of user@example.edu")); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + } } - @ParameterizedTest - @CsvSource(value = { - // with name in admin mail address - "Foo Bar , NULL, NULL, Foo Bar", - // without name, but installation branding name set - "dataverse@dataverse.org, NULL, LibraScholar Dataverse, LibraScholar Dataverse", - // without name, but root dataverse name available - "dataverse@dataverse.org, NotLibraScholar, NULL, NotLibraScholar", - // without name, without root dataverse name, without installation name -> default to bundle string. - "dataverse@dataverse.org, NULL, NULL, Dataverse Installation Admin" - }, nullValues = {"NULL"}) - void setContactDelegation(String fromMail, String rootDataverseName, String installationName, String expectedStartsWith) throws AddressException { - BrandingUtilTest.setRootDataverseName(rootDataverseName); - BrandingUtilTest.setInstallationName(installationName); - - InternetAddress fromAddress = new InternetAddress(fromMail); + @Nested + @LocalJvmSettings + class LookupMailAddresses { + + @Mock + SettingsServiceBean settingsServiceBean; + + @InjectMocks MailServiceBean mailServiceBean = new MailServiceBean(); - try { - mailServiceBean.setContactDelegation("user@example.edu", fromAddress); - assertTrue(fromAddress.getPersonal().startsWith(expectedStartsWith)); - assertTrue(fromAddress.getPersonal().endsWith(" on behalf of user@example.edu")); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); + + private static final String email = "test@example.org"; + + @Test + void lookupSystemWithoutAnySetting() { + assertTrue(mailServiceBean.getSystemAddress().isEmpty()); + } + + @Test + void lookupSystemWithDBOnly() { + Mockito.when(settingsServiceBean.getValueForKey(Key.SystemEmail)).thenReturn(email); + assertEquals(email, mailServiceBean.getSystemAddress().get().getAddress()); + } + + @Test + @JvmSetting(key = JvmSettings.SYSTEM_EMAIL, value = email) + void lookupSystemWithMPConfig() { + assertEquals(email, mailServiceBean.getSystemAddress().get().getAddress()); + } + + @Test + @JvmSetting(key = JvmSettings.SYSTEM_EMAIL, value = email) + void lookupSystemWhereMPConfigTakesPrecedenceOverDB() { + Mockito.lenient().when(settingsServiceBean.getValueForKey(Key.SystemEmail)).thenReturn("foobar@example.org"); + assertEquals(email, mailServiceBean.getSystemAddress().get().getAddress()); + } + + @Test + void lookupSupportWithoutAnySetting() { + assertTrue(mailServiceBean.getSupportAddress().isEmpty()); + } + + @Test + @JvmSetting(key = JvmSettings.SYSTEM_EMAIL, value = email) + void lookupSupportNotSetButWithSystemPresent() { + assertEquals(email, mailServiceBean.getSupportAddress().get().getAddress()); } + + @Test + @JvmSetting(key = JvmSettings.SUPPORT_EMAIL, value = email) + void lookupSupportWithoutSystemSet() { + assertTrue(mailServiceBean.getSystemAddress().isEmpty()); + assertEquals(email, mailServiceBean.getSupportAddress().get().getAddress()); + } + + @Test + @JvmSetting(key = JvmSettings.SYSTEM_EMAIL, value = email) + @JvmSetting(key = JvmSettings.SUPPORT_EMAIL, value = "support@example.org") + void lookupSupportSetWithSystemPresent() { + assertEquals(email, mailServiceBean.getSystemAddress().get().getAddress()); + assertEquals("support@example.org", mailServiceBean.getSupportAddress().get().getAddress()); + } + } + } \ No newline at end of file From 8fac0f6ef489786768ee23ded1b27eccaa92a3be Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 6 Oct 2023 18:49:41 +0200 Subject: [PATCH 047/689] test,fix(branding): make mock adding methods lenient to avoid unnecessary stub exceptions --- .../harvard/iq/dataverse/branding/BrandingUtilTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/branding/BrandingUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/branding/BrandingUtilTest.java index 2b526b8a449..0a6d89ed490 100644 --- a/src/test/java/edu/harvard/iq/dataverse/branding/BrandingUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/branding/BrandingUtilTest.java @@ -41,8 +41,8 @@ public static void setupMocks() { BrandingUtil.injectServices(dataverseSvc, settingsSvc); // initial values (needed here for other tests where this method is reused!) - Mockito.when(settingsSvc.getValueForKey(SettingsServiceBean.Key.InstallationName)).thenReturn(DEFAULT_NAME); - Mockito.when(dataverseSvc.getRootDataverseName()).thenReturn(DEFAULT_NAME); + Mockito.lenient().when(settingsSvc.getValueForKey(SettingsServiceBean.Key.InstallationName)).thenReturn(DEFAULT_NAME); + Mockito.lenient().when(dataverseSvc.getRootDataverseName()).thenReturn(DEFAULT_NAME); } /** @@ -50,7 +50,7 @@ public static void setupMocks() { * @param installationName */ public static void setInstallationName(String installationName) { - Mockito.when(settingsSvc.getValueForKey(SettingsServiceBean.Key.InstallationName)).thenReturn(installationName); + Mockito.lenient().when(settingsSvc.getValueForKey(SettingsServiceBean.Key.InstallationName)).thenReturn(installationName); } /** @@ -58,7 +58,7 @@ public static void setInstallationName(String installationName) { * @param rootDataverseName */ public static void setRootDataverseName(String rootDataverseName) { - Mockito.when(dataverseSvc.getRootDataverseName()).thenReturn(rootDataverseName); + Mockito.lenient().when(dataverseSvc.getRootDataverseName()).thenReturn(rootDataverseName); } /** From aae043c31fbfd643c92969759c22acc041cbebbd Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 6 Oct 2023 17:34:50 -0400 Subject: [PATCH 048/689] add missing db constriants, add cvoc transaction and try/catch --- .../iq/dataverse/DatasetFieldServiceBean.java | 7 ++++++- ...V6.0.0.2__9983-missing-unique-constraints.sql | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/db/migration/V6.0.0.2__9983-missing-unique-constraints.sql diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetFieldServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetFieldServiceBean.java index 620d4bf3e09..4edc2160979 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetFieldServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetFieldServiceBean.java @@ -19,6 +19,8 @@ import jakarta.ejb.EJB; import jakarta.ejb.Stateless; +import jakarta.ejb.TransactionAttribute; +import jakarta.ejb.TransactionAttributeType; import jakarta.inject.Named; import jakarta.json.Json; import jakarta.json.JsonArray; @@ -34,6 +36,7 @@ import jakarta.persistence.NoResultException; import jakarta.persistence.NonUniqueResultException; import jakarta.persistence.PersistenceContext; +import jakarta.persistence.PersistenceException; import jakarta.persistence.TypedQuery; import org.apache.commons.codec.digest.DigestUtils; @@ -46,7 +49,6 @@ import org.apache.http.impl.client.HttpClients; import org.apache.http.protocol.HttpContext; import org.apache.http.util.EntityUtils; - import edu.harvard.iq.dataverse.settings.SettingsServiceBean; /** @@ -448,6 +450,7 @@ public JsonObject getExternalVocabularyValue(String termUri) { * @param cvocEntry - the configuration for the DatasetFieldType associated with this term * @param term - the term uri as a string */ + @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) public void registerExternalTerm(JsonObject cvocEntry, String term) { String retrievalUri = cvocEntry.getString("retrieval-uri"); String prefix = cvocEntry.getString("prefix", null); @@ -517,6 +520,8 @@ public void process(HttpResponse response, HttpContext context) throws HttpExcep logger.fine("Wrote value for term: " + term); } catch (JsonException je) { logger.severe("Error retrieving: " + retrievalUri + " : " + je.getMessage()); + } catch (PersistenceException e) { + logger.fine("Problem persisting: " + retrievalUri + " : " + e.getMessage()); } } else { logger.severe("Received response code : " + statusCode + " when retrieving " + retrievalUri diff --git a/src/main/resources/db/migration/V6.0.0.2__9983-missing-unique-constraints.sql b/src/main/resources/db/migration/V6.0.0.2__9983-missing-unique-constraints.sql new file mode 100644 index 00000000000..6cb3a455e4e --- /dev/null +++ b/src/main/resources/db/migration/V6.0.0.2__9983-missing-unique-constraints.sql @@ -0,0 +1,16 @@ +DO $$ +BEGIN + + BEGIN + ALTER TABLE externalvocabularyvalue ADD CONSTRAINT externalvocabularvalue_uri_key UNIQUE(uri); + EXCEPTION + WHEN duplicate_table THEN RAISE NOTICE 'Table unique constraint externalvocabularvalue_uri_key already exists'; + END; + + BEGIN + ALTER TABLE oaiset ADD CONSTRAINT oaiset_spec_key UNIQUE(spec); + EXCEPTION + WHEN duplicate_table THEN RAISE NOTICE 'Table unique constraint oaiset_spec_key already exists'; + END; + +END $$; \ No newline at end of file From e29b2b57254a04c631de639a1458a0ffd36ff9a8 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 6 Oct 2023 17:46:12 -0400 Subject: [PATCH 049/689] add truncate for vocab table --- .../db/migration/V6.0.0.2__9983-missing-unique-constraints.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/db/migration/V6.0.0.2__9983-missing-unique-constraints.sql b/src/main/resources/db/migration/V6.0.0.2__9983-missing-unique-constraints.sql index 6cb3a455e4e..d867dcde90d 100644 --- a/src/main/resources/db/migration/V6.0.0.2__9983-missing-unique-constraints.sql +++ b/src/main/resources/db/migration/V6.0.0.2__9983-missing-unique-constraints.sql @@ -2,6 +2,7 @@ DO $$ BEGIN BEGIN + TRUNCATE externalvocabularyvalue ALTER TABLE externalvocabularyvalue ADD CONSTRAINT externalvocabularvalue_uri_key UNIQUE(uri); EXCEPTION WHEN duplicate_table THEN RAISE NOTICE 'Table unique constraint externalvocabularvalue_uri_key already exists'; From 39a2cf5aabd52475bc89d30093c817f05d3d71d6 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 6 Oct 2023 17:59:17 -0400 Subject: [PATCH 050/689] add release note, remote truncate --- doc/release-notes/9983-unique-constraints.md | 9 +++++++++ .../V6.0.0.2__9983-missing-unique-constraints.sql | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 doc/release-notes/9983-unique-constraints.md diff --git a/doc/release-notes/9983-unique-constraints.md b/doc/release-notes/9983-unique-constraints.md new file mode 100644 index 00000000000..bb3ed200c62 --- /dev/null +++ b/doc/release-notes/9983-unique-constraints.md @@ -0,0 +1,9 @@ +This release adds two missing database constraints that will assure that the externalvocabularyvalue table only has one entry for each uri and that the oaiset table only has one set for each spec. (In the very unlikely case that your existing database has duplicate entries now, install would fail. This can be checked by running + +SELECT uri, count(*) FROM externalvocabularyvaluet group by uri; + +and + +SELECT spec, count(*) FROM oaiset group by spec; + +and then removing any duplicate rows (where count>1). diff --git a/src/main/resources/db/migration/V6.0.0.2__9983-missing-unique-constraints.sql b/src/main/resources/db/migration/V6.0.0.2__9983-missing-unique-constraints.sql index d867dcde90d..6cb3a455e4e 100644 --- a/src/main/resources/db/migration/V6.0.0.2__9983-missing-unique-constraints.sql +++ b/src/main/resources/db/migration/V6.0.0.2__9983-missing-unique-constraints.sql @@ -2,7 +2,6 @@ DO $$ BEGIN BEGIN - TRUNCATE externalvocabularyvalue ALTER TABLE externalvocabularyvalue ADD CONSTRAINT externalvocabularvalue_uri_key UNIQUE(uri); EXCEPTION WHEN duplicate_table THEN RAISE NOTICE 'Table unique constraint externalvocabularvalue_uri_key already exists'; From 9c7d9b54f66d99f09dff5bd49b64da608ab92c12 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Mon, 9 Oct 2023 17:12:08 +0200 Subject: [PATCH 051/689] refactor(mail): simplify MailServiceBean.sendSystemEmail #7424 - With JavaMail 1.6+, we have support for UTF-8 mail addresses and don't need to parse these ourselves - Remove some C-style coding and duplications - Make logging eat less cycles - Add missing Javadocs --- .../harvard/iq/dataverse/MailServiceBean.java | 73 ++++++++----------- .../iq/dataverse/MailServiceBeanTest.java | 14 ++++ 2 files changed, 45 insertions(+), 42 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java index 83fafbd3b50..e7fddf06d41 100644 --- a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java @@ -99,7 +99,17 @@ public MailServiceBean(Session session, SettingsServiceBean settingsService) { public boolean sendSystemEmail(String to, String subject, String messageText) { return sendSystemEmail(to, subject, messageText, false); } - + + /** + * Send a system notification to one or multiple recipients by email. + * Will skip sending when {@link #getSystemAddress()} doesn't return a configured "from" address. + * @param to A comma separated list of one or multiple recipients' addresses. May contain a "personal name" and + * the recipients address in <>. See also {@link InternetAddress}. + * @param subject The message's subject + * @param messageText The message's text + * @param isHtmlContent Determine if the message text is formatted using HTML or plain text. + * @return Status: true if sent successfully, false otherwise + */ public boolean sendSystemEmail(String to, String subject, String messageText, boolean isHtmlContent) { Optional optionalAddress = getSystemAddress(); if (optionalAddress.isEmpty()) { @@ -108,53 +118,32 @@ public boolean sendSystemEmail(String to, String subject, String messageText, bo } InternetAddress systemAddress = optionalAddress.get(); - boolean sent = false; - - String body = messageText - + (isHtmlContent ? BundleUtil.getStringFromBundle("notification.email.closing.html", Arrays.asList(BrandingUtil.getSupportTeamEmailAddress(systemAddress), BrandingUtil.getSupportTeamName(systemAddress))) - : BundleUtil.getStringFromBundle("notification.email.closing", Arrays.asList(BrandingUtil.getSupportTeamEmailAddress(systemAddress), BrandingUtil.getSupportTeamName(systemAddress)))); + String body = messageText + + BundleUtil.getStringFromBundle(isHtmlContent ? "notification.email.closing.html" : "notification.email.closing", + List.of(BrandingUtil.getSupportTeamEmailAddress(systemAddress), BrandingUtil.getSupportTeamName(systemAddress))); - logger.fine("Sending email to " + to + ". Subject: <<<" + subject + ">>>. Body: " + body); + logger.fine(() -> "Sending email to %s. Subject: <<<%s>>>. Body: %s".formatted(to, subject, body)); try { + // Since JavaMail 1.6, we have support for UTF-8 mail addresses and do not need to handle these ourselves. + InternetAddress[] recipients = InternetAddress.parse(to); + MimeMessage msg = new MimeMessage(session); - if (systemAddress != null) { - msg.setFrom(systemAddress); - msg.setSentDate(new Date()); - String[] recipientStrings = to.split(","); - InternetAddress[] recipients = new InternetAddress[recipientStrings.length]; - for (int i = 0; i < recipients.length; i++) { - try { - recipients[i] = new InternetAddress(recipientStrings[i], "", charset); - } catch (UnsupportedEncodingException ex) { - logger.severe(ex.getMessage()); - } - } - msg.setRecipients(Message.RecipientType.TO, recipients); - msg.setSubject(subject, charset); - if (isHtmlContent) { - msg.setText(body, charset, "html"); - } else { - msg.setText(body, charset); - } - - try { - Transport.send(msg, recipients); - sent = true; - } catch (MessagingException ssfe) { - logger.warning("Failed to send mail to: " + to); - logger.warning("MessagingException Message: " + ssfe); - } + msg.setFrom(systemAddress); + msg.setSentDate(new Date()); + msg.setRecipients(Message.RecipientType.TO, recipients); + msg.setSubject(subject, charset); + if (isHtmlContent) { + msg.setText(body, charset, "html"); } else { - logger.fine("Skipping sending mail to " + to + ", because the \"no-reply\" address not set (" + Key.SystemEmail + " setting)."); + msg.setText(body, charset); } - } catch (AddressException ae) { - logger.warning("Failed to send mail to " + to); - ae.printStackTrace(System.out); - } catch (MessagingException me) { - logger.warning("Failed to send mail to " + to); - me.printStackTrace(System.out); + + Transport.send(msg, recipients); + return true; + } catch (MessagingException ae) { + logger.log(Level.WARNING, "Failed to send mail to %s: %s".formatted(to, ae.getMessage()), ae); } - return sent; + return false; } /** diff --git a/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java index 28d5721d667..ea3f3c6e0a6 100644 --- a/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java @@ -24,6 +24,7 @@ import static edu.harvard.iq.dataverse.settings.SettingsServiceBean.*; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @ExtendWith(MockitoExtension.class) @@ -131,6 +132,19 @@ void lookupSupportSetWithSystemPresent() { assertEquals(email, mailServiceBean.getSystemAddress().get().getAddress()); assertEquals("support@example.org", mailServiceBean.getSupportAddress().get().getAddress()); } + } + + @Nested + @LocalJvmSettings + class SendSystemMail { + @InjectMocks + MailServiceBean mailServiceBean = new MailServiceBean(); + + @Test + @JvmSetting(key = JvmSettings.SYSTEM_EMAIL, value = "") + void skipIfNoSystemAddress() { + assertFalse(mailServiceBean.sendSystemEmail("target@example.org", "Test", "Test", false)); + } } From b970eb5499941126af2d99e24d17bf22e8223b62 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 10 Oct 2023 09:32:25 +0200 Subject: [PATCH 052/689] feat(mail): enable UTF-8 mail address following RFC 6530 #7424 - Make support configurable using new setting, defaulting to true (most MTAs today should support SMTPUTF8) - If need be and an admin disables the support, make email validator deny UTF-8 chars (otherwise no mails could be sent!) - Add logging message to send method to give hint about necessary UTF-8 support everywhere in the chain - Add (extensible) integration test for MailServiceBean to check sending mails actually works --- .../harvard/iq/dataverse/MailServiceBean.java | 1 + .../iq/dataverse/settings/JvmSettings.java | 1 + .../dataverse/util/MailSessionProducer.java | 5 + .../dataverse/validation/EMailValidator.java | 11 +- .../META-INF/microprofile-config.properties | 1 + .../iq/dataverse/MailServiceBeanIT.java | 143 ++++++++++++++++++ .../iq/dataverse/MailServiceBeanTest.java | 6 +- .../validation/EMailValidatorTest.java | 29 +++- 8 files changed, 191 insertions(+), 6 deletions(-) create mode 100644 src/test/java/edu/harvard/iq/dataverse/MailServiceBeanIT.java diff --git a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java index e7fddf06d41..47861ce3935 100644 --- a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java @@ -142,6 +142,7 @@ public boolean sendSystemEmail(String to, String subject, String messageText, bo return true; } catch (MessagingException ae) { logger.log(Level.WARNING, "Failed to send mail to %s: %s".formatted(to, ae.getMessage()), ae); + logger.info("When UTF-8 characters in recipients: make sure MTA supports it and JVM option " + JvmSettings.MAIL_MTA_SUPPORT_UTF8.getScopedKey() + "=true"); } return false; } 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 2f249ad119e..aa17f3f204a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java @@ -130,6 +130,7 @@ public enum JvmSettings { MAIL_MTA_AUTH(SCOPE_MAIL_MTA, "auth"), MAIL_MTA_USER(SCOPE_MAIL_MTA, "user"), MAIL_MTA_PASSWORD(SCOPE_MAIL_MTA, "password"), + MAIL_MTA_SUPPORT_UTF8(SCOPE_MAIL_MTA, "allow-utf8-addresses"), // Placeholder setting for a large list of extra settings MAIL_MTA_SETTING(SCOPE_MAIL_MTA), diff --git a/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java b/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java index dc5e0d68b4f..9a3615efea0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java @@ -84,6 +84,11 @@ Properties getMailProperties() { // See https://jakarta.ee/specifications/mail/2.1/apidocs/jakarta.mail/jakarta/mail/package-summary configuration.put("mail.transport.protocol", "smtp"); configuration.put("mail.debug", JvmSettings.MAIL_DEBUG.lookupOptional(Boolean.class).orElse(false).toString()); + // Only enable if your MTA properly supports UTF-8 mail addresses following RFC 6530/6531/6532. + // Before, we used a hack to put the raw UTF-8 mail address into the system. + // Now, make it proper, but make it possible to disable it - see also EMailValidator. + // Default = true from microprofile-config.properties as most MTAs these days support SMTPUTF8 extension + configuration.put("mail.mime.allowutf8", JvmSettings.MAIL_MTA_SUPPORT_UTF8.lookup(Boolean.class).toString()); configuration.put(PREFIX + "host", JvmSettings.MAIL_MTA_HOST.lookup()); // default = false from microprofile-config.properties diff --git a/src/main/java/edu/harvard/iq/dataverse/validation/EMailValidator.java b/src/main/java/edu/harvard/iq/dataverse/validation/EMailValidator.java index 624e49623f2..c431cd2ab0c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/validation/EMailValidator.java +++ b/src/main/java/edu/harvard/iq/dataverse/validation/EMailValidator.java @@ -1,16 +1,21 @@ package edu.harvard.iq.dataverse.validation; +import edu.harvard.iq.dataverse.settings.JvmSettings; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; import org.apache.commons.validator.routines.EmailValidator; +import java.nio.charset.StandardCharsets; + /** * * @author skraffmi */ public class EMailValidator implements ConstraintValidator { + private static final boolean MTA_SUPPORTS_UTF8 = JvmSettings.MAIL_MTA_SUPPORT_UTF8.lookup(Boolean.class); + @Override public boolean isValid(String value, ConstraintValidatorContext context) { return isEmailValid(value); @@ -23,6 +28,10 @@ public boolean isValid(String value, ConstraintValidatorContext context) { * @return true when valid, false when invalid (null = valid!) */ public static boolean isEmailValid(String value) { - return value == null || EmailValidator.getInstance().isValid(value); + return value == null || (EmailValidator.getInstance().isValid(value) && + // If the MTA isn't able to handle UTF-8 mail addresses following RFC 6530/6531/6532, we can only declare + // mail addresses using 7bit ASCII (RFC 821) as valid. + // Beyond scope for Apache Commons Validator, see also https://issues.apache.org/jira/browse/VALIDATOR-487 + (StandardCharsets.US_ASCII.newEncoder().canEncode(value) || MTA_SUPPORTS_UTF8) ); } } diff --git a/src/main/resources/META-INF/microprofile-config.properties b/src/main/resources/META-INF/microprofile-config.properties index 7598ccc9e27..af63ee0348f 100644 --- a/src/main/resources/META-INF/microprofile-config.properties +++ b/src/main/resources/META-INF/microprofile-config.properties @@ -38,6 +38,7 @@ dataverse.rserve.tempdir=/tmp/Rserv # MAIL dataverse.mail.mta.auth=false +dataverse.mail.mta.allow-utf8-addresses=true # In containers, default to hostname smtp, a container on the same network %ct.dataverse.mail.mta.host=smtp diff --git a/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanIT.java b/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanIT.java new file mode 100644 index 00000000000..08eed9fe295 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanIT.java @@ -0,0 +1,143 @@ +package edu.harvard.iq.dataverse; + +import edu.harvard.iq.dataverse.branding.BrandingUtil; +import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.MailSessionProducer; +import edu.harvard.iq.dataverse.util.testing.JvmSetting; +import edu.harvard.iq.dataverse.util.testing.LocalJvmSettings; +import edu.harvard.iq.dataverse.util.testing.Tags; +import io.restassured.RestAssured; +import jakarta.mail.Session; +import jakarta.mail.internet.AddressException; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.util.List; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * An integration test using a fake SMTP MTA to check for outgoing mails. + * LIMITATION: This test cannot possibly check if the production and injection of the session via CDI + * works, as it is not running within a servlet container. This would require usage of Arquillian + * or and end-to-end API test with a deployed application. + */ + +@Tag(Tags.INTEGRATION_TEST) +@Tag(Tags.USES_TESTCONTAINERS) +@Testcontainers(disabledWithoutDocker = true) +@ExtendWith(MockitoExtension.class) +@LocalJvmSettings +@JvmSetting(key = JvmSettings.MAIL_MTA_HOST, method = "tcSmtpHost") +@JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, method = "tcSmtpPort", varArgs = "port") +class MailServiceBeanIT { + + private static final Integer PORT_SMTP = 1025; + private static final Integer PORT_HTTP = 1080; + + static MailServiceBean mailer; + static Session session; + static SettingsServiceBean settingsServiceBean = Mockito.mock(SettingsServiceBean.class); + static DataverseServiceBean dataverseServiceBean = Mockito.mock(DataverseServiceBean.class); + + @BeforeAll + static void setUp() { + // Setup mocks behavior, inject as deps + Mockito.when(settingsServiceBean.getValueForKey(SettingsServiceBean.Key.SystemEmail)).thenReturn("noreply@example.org"); + BrandingUtil.injectServices(dataverseServiceBean, settingsServiceBean); + + // Must happen here, as we need Testcontainers to start the container first... + session = new MailSessionProducer().getSession(); + mailer = new MailServiceBean(session, settingsServiceBean); + } + + /* + Cannot use maildev/maildev here. Also MailCatcher doesn't provide official support for SMTPUTF8. + Also maildev does advertise the feature and everything is fine over the wire, both JSON API and Web UI + of maildev have an encoding problem - UTF-8 mail addresses following RFC 6530/6531/6532 are botched. + Neither MailCatcher nor MailHog have this problem, yet the API of MailCatcher is much simpler + to use during testing, which is why we are going with it. + */ + @Container + static GenericContainer maildev = new GenericContainer<>("dockage/mailcatcher") + .withExposedPorts(PORT_HTTP, PORT_SMTP) + .waitingFor(Wait.forHttp("/")); + + static String tcSmtpHost() { + return maildev.getHost(); + } + + static String tcSmtpPort() { + return maildev.getMappedPort(PORT_SMTP).toString(); + } + + @BeforeAll + static void setup() { + RestAssured.baseURI = "http://" + tcSmtpHost(); + RestAssured.port = maildev.getMappedPort(PORT_HTTP); + } + + static List mailTo() { + return List.of( + "pete@mailinator.com", // one example using ASCII only, make sure it works + "michélle.pereboom@example.com", + "begüm.vriezen@example.com", + "lótus.gonçalves@example.com", + "lótus.gonçalves@éxample.com", + "begüm.vriezen@example.cologne", + "رونیکا.محمدخان@example.com", + "lótus.gonçalves@example.cóm" + ); + } + + @ParameterizedTest + @MethodSource("mailTo") + @JvmSetting(key = JvmSettings.MAIL_MTA_SUPPORT_UTF8, value = "true") + void sendEmailIncludingUTF8(String mailAddress) { + given().when().get("/messages") + .then() + .statusCode(200); + + // given + Session session = new MailSessionProducer().getSession(); + MailServiceBean mailer = new MailServiceBean(session, settingsServiceBean); + + // when + boolean sent = mailer.sendSystemEmail(mailAddress, "Test", "Test üüü", false); + + // then + assertTrue(sent); + //RestAssured.get("/messages").body().prettyPrint(); + given().when().get("/messages") + .then() + .statusCode(200) + .body("last().recipients.first()", equalTo("<" + mailAddress + ">")); + } + + @Test + @JvmSetting(key = JvmSettings.SYSTEM_EMAIL, value = "test@example.org") + @JvmSetting(key = JvmSettings.MAIL_MTA_SUPPORT_UTF8, value = "false") + void mailRejectedWhenUTF8AddressButNoSupport() throws AddressException { + // given + Session session = new MailSessionProducer().getSession(); + MailServiceBean mailer = new MailServiceBean(session, settingsServiceBean); + String to = "michélle.pereboom@example.com"; + + assertFalse(mailer.sendSystemEmail(to, "Test", "Test", false)); + } + +} \ No newline at end of file diff --git a/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java index ea3f3c6e0a6..dad358cf8ee 100644 --- a/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java @@ -6,6 +6,7 @@ import edu.harvard.iq.dataverse.util.testing.JvmSetting; import edu.harvard.iq.dataverse.util.testing.LocalJvmSettings; import jakarta.mail.internet.AddressException; +import jakarta.mail.internet.InternetAddress; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -13,8 +14,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; - -import jakarta.mail.internet.InternetAddress; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; @@ -22,7 +21,7 @@ import java.io.UnsupportedEncodingException; -import static edu.harvard.iq.dataverse.settings.SettingsServiceBean.*; +import static edu.harvard.iq.dataverse.settings.SettingsServiceBean.Key; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -145,7 +144,6 @@ class SendSystemMail { void skipIfNoSystemAddress() { assertFalse(mailServiceBean.sendSystemEmail("target@example.org", "Test", "Test", false)); } - } } \ No newline at end of file diff --git a/src/test/java/edu/harvard/iq/dataverse/validation/EMailValidatorTest.java b/src/test/java/edu/harvard/iq/dataverse/validation/EMailValidatorTest.java index 0cbc9e52759..614cdae2310 100644 --- a/src/test/java/edu/harvard/iq/dataverse/validation/EMailValidatorTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/validation/EMailValidatorTest.java @@ -1,5 +1,8 @@ package edu.harvard.iq.dataverse.validation; +import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.util.testing.JvmSetting; +import edu.harvard.iq.dataverse.util.testing.LocalJvmSettings; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -14,7 +17,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -public class EMailValidatorTest { +@LocalJvmSettings +class EMailValidatorTest { private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); @@ -82,4 +86,27 @@ public void testConstraint(boolean expected, String mail) { violations.stream().findFirst().ifPresent( c -> { assertTrue(c.getMessage().contains(mail)); }); } + + public static Stream emailAsciiUtf8Examples() { + return Stream.of( + Arguments.of("false", "pete@mailinator.com"), + Arguments.of("false", "foobar@mail.science"), + Arguments.of("true", "lótus.gonçalves@éxample.com"), + Arguments.of("true", "begüm.vriezen@example.cologne") + ); + } + + @ParameterizedTest + @MethodSource("emailAsciiUtf8Examples") + @JvmSetting(key = JvmSettings.MAIL_MTA_SUPPORT_UTF8, value = "false") + void validateWhenMTADoesNotSupportUTF8(boolean needsUTF8Support, String mail) { + assertEquals(!needsUTF8Support, EMailValidator.isEmailValid(mail)); + } + + @ParameterizedTest + @MethodSource("emailAsciiUtf8Examples") + @JvmSetting(key = JvmSettings.MAIL_MTA_SUPPORT_UTF8, value = "true") + void validateWhenMTASupportsUTF8(boolean needsUTF8Support, String mail) { + assertTrue(EMailValidator.isEmailValid(mail)); + } } From 05870d124b05b84813e746d91ce7ace5ea086da8 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 10 Oct 2023 10:15:35 +0200 Subject: [PATCH 053/689] fix(mail): lookup UTF-8 support config in static method to pick up changed value during tests Also switch to safer lookup via codepoint comparison to 7bit = chars < 128 in favor over encoder --- .../harvard/iq/dataverse/validation/EMailValidator.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/validation/EMailValidator.java b/src/main/java/edu/harvard/iq/dataverse/validation/EMailValidator.java index c431cd2ab0c..446f55a193f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/validation/EMailValidator.java +++ b/src/main/java/edu/harvard/iq/dataverse/validation/EMailValidator.java @@ -6,15 +6,11 @@ import org.apache.commons.validator.routines.EmailValidator; -import java.nio.charset.StandardCharsets; - /** * * @author skraffmi */ public class EMailValidator implements ConstraintValidator { - - private static final boolean MTA_SUPPORTS_UTF8 = JvmSettings.MAIL_MTA_SUPPORT_UTF8.lookup(Boolean.class); @Override public boolean isValid(String value, ConstraintValidatorContext context) { @@ -28,10 +24,12 @@ public boolean isValid(String value, ConstraintValidatorContext context) { * @return true when valid, false when invalid (null = valid!) */ public static boolean isEmailValid(String value) { + // Must be looked up here - otherwise changes are not picked up (tests, live config, ...) + final boolean mtaSupportsUTF8 = JvmSettings.MAIL_MTA_SUPPORT_UTF8.lookup(Boolean.class); return value == null || (EmailValidator.getInstance().isValid(value) && // If the MTA isn't able to handle UTF-8 mail addresses following RFC 6530/6531/6532, we can only declare // mail addresses using 7bit ASCII (RFC 821) as valid. // Beyond scope for Apache Commons Validator, see also https://issues.apache.org/jira/browse/VALIDATOR-487 - (StandardCharsets.US_ASCII.newEncoder().canEncode(value) || MTA_SUPPORTS_UTF8) ); + (value.codePoints().noneMatch(c -> c > 127) || mtaSupportsUTF8) ); } } From e9abfd21b61fa1ca0cfab078f95d24bbdc984a1f Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 10 Oct 2023 10:18:21 +0200 Subject: [PATCH 054/689] fix(mail): add missing mock in MailServiceBeanTest --- src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java index dad358cf8ee..f8a01c53298 100644 --- a/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java @@ -136,6 +136,8 @@ void lookupSupportSetWithSystemPresent() { @Nested @LocalJvmSettings class SendSystemMail { + @Mock + SettingsServiceBean settingsServiceBean; @InjectMocks MailServiceBean mailServiceBean = new MailServiceBean(); From 7d1ba871838b69a52c62bc92ddfe53d1e3178186 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 10 Oct 2023 10:52:50 +0200 Subject: [PATCH 055/689] fix(mail): make mail configuration entirely optional #7424 Mail notification are optional (mostly to avoid setting up mail services in dev envs). Do not enforce MTA host config and do not pester logs about missing configuration. --- .../java/edu/harvard/iq/dataverse/MailServiceBean.java | 3 ++- .../harvard/iq/dataverse/util/MailSessionProducer.java | 8 ++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java index 47861ce3935..af6e01b94b9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java @@ -172,7 +172,8 @@ public Optional getSystemAddress() { logger.log(Level.WARNING, "Could not parse system mail address '%s' provided by %s: " .formatted(providedByDB ? "DB setting" : "JVM option", mailAddress), e); } catch (NullPointerException e) { - logger.warning("Could not find a system mail setting in database (key :SystemEmail, deprecated) or JVM option '" + JvmSettings.SYSTEM_EMAIL.getScopedKey() + "'"); + // Do not pester the logs - no configuration may mean someone wants to disable mail notifications + logger.fine("Could not find a system mail setting in database (key :SystemEmail, deprecated) or JVM option '" + JvmSettings.SYSTEM_EMAIL.getScopedKey() + "'"); } // We define the system email address as an optional setting, in case people do not want to enable mail // notifications (like in a development context, but might be useful elsewhere, too). diff --git a/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java b/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java index 9a3615efea0..93f3ac29e44 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java @@ -21,7 +21,7 @@ public class MailSessionProducer { // classify as spam. // NOTE: Complete list including descriptions at https://eclipse-ee4j.github.io/angus-mail/docs/api/org.eclipse.angus.mail/org/eclipse/angus/mail/smtp/package-summary.html static final List smtpStringProps = List.of( - "localhost", "localaddress", "auth.mechanisms", "auth.ntlm.domain", "submitter", "dsn.notify", "dsn.ret", + "host", "localhost", "localaddress", "auth.mechanisms", "auth.ntlm.domain", "submitter", "dsn.notify", "dsn.ret", "sasl.mechanisms", "sasl.authorizationid", "sasl.realm", "ssl.trust", "ssl.protocols", "ssl.ciphersuites", "proxy.host", "proxy.port", "proxy.user", "proxy.password", "socks.host", "socks.port", "mailextension" ); @@ -29,7 +29,7 @@ public class MailSessionProducer { "port", "connectiontimeout", "timeout", "writetimeout", "localport", "auth.ntlm.flag" ); static final List smtpBoolProps = List.of( - "ehlo", "auth.login.disable", "auth.plain.disable", "auth.digest-md5.disable", "auth.ntlm.disable", + "auth", "ehlo", "auth.login.disable", "auth.plain.disable", "auth.digest-md5.disable", "auth.ntlm.disable", "auth.xoauth2.disable", "allow8bitmime", "sendpartial", "sasl.enable", "sasl.usecanonicalhostname", "quitwait", "quitonsessionreject", "ssl.enable", "ssl.checkserveridentity", "starttls.enable", "starttls.required", "userset", "noop.strict" @@ -90,10 +90,6 @@ Properties getMailProperties() { // Default = true from microprofile-config.properties as most MTAs these days support SMTPUTF8 extension configuration.put("mail.mime.allowutf8", JvmSettings.MAIL_MTA_SUPPORT_UTF8.lookup(Boolean.class).toString()); - configuration.put(PREFIX + "host", JvmSettings.MAIL_MTA_HOST.lookup()); - // default = false from microprofile-config.properties - configuration.put(PREFIX + "auth", JvmSettings.MAIL_MTA_AUTH.lookup(Boolean.class).toString()); - // Map properties 1:1 to mail.smtp properties for the mail session. smtpStringProps.forEach( prop -> JvmSettings.MAIL_MTA_SETTING.lookupOptional(prop).ifPresent( From 1c7bc24b487625c805b666bfc1e218236808d33f Mon Sep 17 00:00:00 2001 From: Jan van Mansum Date: Tue, 10 Oct 2023 16:24:17 +0200 Subject: [PATCH 056/689] Changed DatasetFieldType.isControlledVocabulary() to return allowControlledVocabulary instead of looking up complete list of terms to see if it is empty --- src/main/java/edu/harvard/iq/dataverse/DatasetFieldType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetFieldType.java b/src/main/java/edu/harvard/iq/dataverse/DatasetFieldType.java index 824b486a42d..01785359e0e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetFieldType.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetFieldType.java @@ -284,7 +284,7 @@ public void setDisplayOnCreate(boolean displayOnCreate) { } public boolean isControlledVocabulary() { - return controlledVocabularyValues != null && !controlledVocabularyValues.isEmpty(); + return allowControlledVocabulary; } /** From 7a23d1a8ee951f9b84ed69c396b6bcc997241d77 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 11 Oct 2023 14:06:02 +0200 Subject: [PATCH 057/689] chore(build): update to Testcontainers 1.19.1 --- modules/dataverse-parent/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/dataverse-parent/pom.xml b/modules/dataverse-parent/pom.xml index 8e0ff2887df..ab5bd54c934 100644 --- a/modules/dataverse-parent/pom.xml +++ b/modules/dataverse-parent/pom.xml @@ -170,6 +170,7 @@ 1.19.0 2.10.1 + 1.19.1 5.10.0 5.4.0 0.8.10 From 4bfda6c86d9de90bc23026ce9f7dda146bcd710e Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 11 Oct 2023 14:06:23 +0200 Subject: [PATCH 058/689] chore(build): update to SmallRye Config 3.4.1 --- modules/dataverse-parent/pom.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/dataverse-parent/pom.xml b/modules/dataverse-parent/pom.xml index ab5bd54c934..4ff2a167e6c 100644 --- a/modules/dataverse-parent/pom.xml +++ b/modules/dataverse-parent/pom.xml @@ -168,9 +168,8 @@ 5.1.0 - 1.19.0 - 2.10.1 1.19.1 + 3.4.1 5.10.0 5.4.0 0.8.10 From b74d60f2af45148d3d59b459668733963915ec6a Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 11 Oct 2023 14:12:30 +0200 Subject: [PATCH 059/689] chore(build): update Mockito to v5.6.0 --- modules/dataverse-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/dataverse-parent/pom.xml b/modules/dataverse-parent/pom.xml index 4ff2a167e6c..94ee48bef9b 100644 --- a/modules/dataverse-parent/pom.xml +++ b/modules/dataverse-parent/pom.xml @@ -171,7 +171,7 @@ 1.19.1 3.4.1 5.10.0 - 5.4.0 + 5.6.0 0.8.10 9.3 From b8bc348636793d79e48830df9f8746a5c77d5b08 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 7 Nov 2023 10:19:36 -0500 Subject: [PATCH 060/689] stub out QA guide #10101 --- doc/sphinx-guides/source/index.rst | 1 + doc/sphinx-guides/source/qa/index.rst | 8 ++++++++ doc/sphinx-guides/source/qa/intro.rst | 12 ++++++++++++ 3 files changed, 21 insertions(+) create mode 100755 doc/sphinx-guides/source/qa/index.rst create mode 100755 doc/sphinx-guides/source/qa/intro.rst diff --git a/doc/sphinx-guides/source/index.rst b/doc/sphinx-guides/source/index.rst index e4eeea9b6d0..9d3d49ef4f2 100755 --- a/doc/sphinx-guides/source/index.rst +++ b/doc/sphinx-guides/source/index.rst @@ -20,6 +20,7 @@ These documentation guides are for the |version| version of Dataverse. To find g developers/index container/index style/index + qa/index How the Guides Are Organized ---------------------------- diff --git a/doc/sphinx-guides/source/qa/index.rst b/doc/sphinx-guides/source/qa/index.rst new file mode 100755 index 00000000000..40b8e2e1492 --- /dev/null +++ b/doc/sphinx-guides/source/qa/index.rst @@ -0,0 +1,8 @@ +QA Guide +======== + +**Contents:** + +.. toctree:: + + intro diff --git a/doc/sphinx-guides/source/qa/intro.rst b/doc/sphinx-guides/source/qa/intro.rst new file mode 100755 index 00000000000..12056bbeb91 --- /dev/null +++ b/doc/sphinx-guides/source/qa/intro.rst @@ -0,0 +1,12 @@ +Introduction +============ + +This is the QA Guide for Dataverse. + +.. contents:: |toctitle| + :local: + +Intended Audience +----------------- + +This guide is intended primarily for members of the Dataverse core team who are performing QA on pull requests. That said, the entire community is welcome to read and contribute back to what is written here. From f81bf27011d17e5a31d7cfb230bd8b19f95ddc35 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 7 Nov 2023 15:53:13 -0500 Subject: [PATCH 061/689] Add initial load for the pages of the guide --- .../source/qa/checklist-qa-pr.rst | 2 ++ .../source/qa/checklist-qa-release.rst | 2 ++ .../source/qa/deploying-jenkins.rst | 2 ++ doc/sphinx-guides/source/qa/index.rst | 21 ++++++++++++++- doc/sphinx-guides/source/qa/intro.rst | 12 --------- .../source/qa/manual-testing.rst | 2 ++ .../source/qa/other-approaches.rst | 2 ++ doc/sphinx-guides/source/qa/overview.rst | 27 +++++++++++++++++++ .../source/qa/performance-tests.rst | 2 ++ .../source/qa/test-automation-integration.rst | 2 ++ .../source/qa/testing-infrastructure.rst | 2 ++ doc/sphinx-guides/source/qa/tips-tricks.rst | 2 ++ .../source/qa/workflow-qa-pr.rst | 7 +++++ 13 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 doc/sphinx-guides/source/qa/checklist-qa-pr.rst create mode 100644 doc/sphinx-guides/source/qa/checklist-qa-release.rst create mode 100644 doc/sphinx-guides/source/qa/deploying-jenkins.rst delete mode 100755 doc/sphinx-guides/source/qa/intro.rst create mode 100644 doc/sphinx-guides/source/qa/manual-testing.rst create mode 100644 doc/sphinx-guides/source/qa/other-approaches.rst create mode 100644 doc/sphinx-guides/source/qa/overview.rst create mode 100644 doc/sphinx-guides/source/qa/performance-tests.rst create mode 100644 doc/sphinx-guides/source/qa/test-automation-integration.rst create mode 100644 doc/sphinx-guides/source/qa/testing-infrastructure.rst create mode 100644 doc/sphinx-guides/source/qa/tips-tricks.rst create mode 100644 doc/sphinx-guides/source/qa/workflow-qa-pr.rst diff --git a/doc/sphinx-guides/source/qa/checklist-qa-pr.rst b/doc/sphinx-guides/source/qa/checklist-qa-pr.rst new file mode 100644 index 00000000000..df60f4260fc --- /dev/null +++ b/doc/sphinx-guides/source/qa/checklist-qa-pr.rst @@ -0,0 +1,2 @@ +Checklist for QA on a PR +======================== \ No newline at end of file diff --git a/doc/sphinx-guides/source/qa/checklist-qa-release.rst b/doc/sphinx-guides/source/qa/checklist-qa-release.rst new file mode 100644 index 00000000000..34419fee8ae --- /dev/null +++ b/doc/sphinx-guides/source/qa/checklist-qa-release.rst @@ -0,0 +1,2 @@ +Checklist for QA on release +=========================== \ No newline at end of file diff --git a/doc/sphinx-guides/source/qa/deploying-jenkins.rst b/doc/sphinx-guides/source/qa/deploying-jenkins.rst new file mode 100644 index 00000000000..b15a6f88534 --- /dev/null +++ b/doc/sphinx-guides/source/qa/deploying-jenkins.rst @@ -0,0 +1,2 @@ +Building and deploying a PR from Jenkins to dataverse-internal +============================================================== \ No newline at end of file diff --git a/doc/sphinx-guides/source/qa/index.rst b/doc/sphinx-guides/source/qa/index.rst index 40b8e2e1492..019f5cdbd5c 100755 --- a/doc/sphinx-guides/source/qa/index.rst +++ b/doc/sphinx-guides/source/qa/index.rst @@ -5,4 +5,23 @@ QA Guide .. toctree:: - intro + overview + testing-infrastructure + performance-tests + manual-testing + test-automation-integration + deploying-jenkins + other-approaches + tips-tricks + workflow-qa-pr + checklist-qa-pr + checklist-qa-release + + + + + + + + + diff --git a/doc/sphinx-guides/source/qa/intro.rst b/doc/sphinx-guides/source/qa/intro.rst deleted file mode 100755 index 12056bbeb91..00000000000 --- a/doc/sphinx-guides/source/qa/intro.rst +++ /dev/null @@ -1,12 +0,0 @@ -Introduction -============ - -This is the QA Guide for Dataverse. - -.. contents:: |toctitle| - :local: - -Intended Audience ------------------ - -This guide is intended primarily for members of the Dataverse core team who are performing QA on pull requests. That said, the entire community is welcome to read and contribute back to what is written here. diff --git a/doc/sphinx-guides/source/qa/manual-testing.rst b/doc/sphinx-guides/source/qa/manual-testing.rst new file mode 100644 index 00000000000..5c3393af546 --- /dev/null +++ b/doc/sphinx-guides/source/qa/manual-testing.rst @@ -0,0 +1,2 @@ +Manual testing approach +======================= \ No newline at end of file diff --git a/doc/sphinx-guides/source/qa/other-approaches.rst b/doc/sphinx-guides/source/qa/other-approaches.rst new file mode 100644 index 00000000000..420d40fa09c --- /dev/null +++ b/doc/sphinx-guides/source/qa/other-approaches.rst @@ -0,0 +1,2 @@ +Other approaches to deploying and testing +========================================= \ No newline at end of file diff --git a/doc/sphinx-guides/source/qa/overview.rst b/doc/sphinx-guides/source/qa/overview.rst new file mode 100644 index 00000000000..d9e21a2f0ab --- /dev/null +++ b/doc/sphinx-guides/source/qa/overview.rst @@ -0,0 +1,27 @@ +Overview +======== + +.. contents:: |toctitle| + :local: + + +What is an API? +--------------- + +This document describes the testing process used by QA at IQSS and provides a guide for others filling in for that role. Please note that many variations are possible, and the main thing is to catch bugs and provide a good quality product to the user community. + +The basic workflow is bugs or feature requests are submitted to GitHub by the community or by team members as issues. These issues are prioritized and added to a two-week sprint that is reflected on the GitHub Kanban board. As developers work on these issues, a GitHub branch is produced, code is contributed, and a pull request is made to merge these new changes back into the common develop branch and ultimately released as part of the product. Before a pull request is merged it must be reviewed by a member of the development team from a coding perspective, it must pass automated integration tests before moving to QA. There it is tested manually, exercising the UI using three common browser types and any business logic it implements. Depending on whether the code modifies existing code or is completely new, a smoke test of core functionality is performed and some basic regression testing of modified or related code is performed. Any documentation provided is used to understand the feature and any assertions are tested. Once these pass and any bugs found corrected, the automated integration tests are confirmed to be passing, the pr is merged into develop, the pr closed, and the branch deleted. At this point the pr moves from the QA column automatically into the Done column and the process repeats with the next pr until it is decided to make a release. + +A release likely spans multiple two week sprints. Each sprint represents the priorities for that time and is sized so that the team can reasonably complete most of the work on time. This is a goal to help with planning, it is not a strict requirement. Some issues from the previous sprint may remain and likely be included in the next sprint but occasionally may be deprioritized and deferred to another time. + +The decision to make a release can be based on time since last release, some important feature needed by the community or contractual deadline or some other logical reason to package the work completed into a named release and posted to the releases section on GitHub. + +The final testing activity before producing a release is performance testing. This could be done throughout the release cycle but since it is time consuming it is done once near the end. Using a load generating tool named Locust, it loads the statistically most loaded pages, according to Google Analytics, that is 50% homepage and 50% some type of dataset page. Since dataset page weight also varies by number of files, a selection of about 10 datasets with varying file counts is used. The pages are called randomly as a guest user with increasing levels of user load, from 1 user to 250 users. Typical daily loads in production are around the 50 user level. Though the simulated user level does have a modest amount of random think time before repeated calls, from 5-20 seconds (I believe), it is not a real world load so direct comparisons to production are not reliable. Instead, we compare performance to prior versions of the product and based on how that performed in production we have some idea whether this might be similar in performance or whether there is some undetected issue that appears under load, such as inefficient or too many DB queries per page. + +Once performance has been tested and recorded in a google spreadsheet for this proposed version, the release will be prepared and posted. + +Preparing the release consists of writing and reviewing the release notes compiled from individual notes in prs that have been merged for this release. A pr is made for the notes and merged. Next, increment the version numbers in certain code files, produce a pr with those changes and merge that into the common develop branch. Last, a pr is made to merge develop into the master branch. Once that is merged a guides build with the new release version is made from the master branch. Last, a release war file is built from master and an installer is built from the master branch and includes the newly built war file. + +Publishing the release consists of creating a new draft release on Github, posting the release notes, uploading the .war file and the installer .zip file and any ancillary files used to configure this release. The latest link for the guides should be updated on the guides server to point to the newest version. Once that is all in place, specify the version name and the master branch at the top of the GitHub draft release and publish. This will tag the master branch with the version number and make the release notes and files available to the public. + +Once released, post to dv general about the release and when possible, deploy to demo and production. diff --git a/doc/sphinx-guides/source/qa/performance-tests.rst b/doc/sphinx-guides/source/qa/performance-tests.rst new file mode 100644 index 00000000000..c3df2dc7951 --- /dev/null +++ b/doc/sphinx-guides/source/qa/performance-tests.rst @@ -0,0 +1,2 @@ +Checklist for QA Release +======================== \ No newline at end of file diff --git a/doc/sphinx-guides/source/qa/test-automation-integration.rst b/doc/sphinx-guides/source/qa/test-automation-integration.rst new file mode 100644 index 00000000000..c3df2dc7951 --- /dev/null +++ b/doc/sphinx-guides/source/qa/test-automation-integration.rst @@ -0,0 +1,2 @@ +Checklist for QA Release +======================== \ No newline at end of file diff --git a/doc/sphinx-guides/source/qa/testing-infrastructure.rst b/doc/sphinx-guides/source/qa/testing-infrastructure.rst new file mode 100644 index 00000000000..5b3de602bc7 --- /dev/null +++ b/doc/sphinx-guides/source/qa/testing-infrastructure.rst @@ -0,0 +1,2 @@ +Testing Infrastructure +====================== \ No newline at end of file diff --git a/doc/sphinx-guides/source/qa/tips-tricks.rst b/doc/sphinx-guides/source/qa/tips-tricks.rst new file mode 100644 index 00000000000..738f701d33b --- /dev/null +++ b/doc/sphinx-guides/source/qa/tips-tricks.rst @@ -0,0 +1,2 @@ +Tips and tricks +=============== \ No newline at end of file diff --git a/doc/sphinx-guides/source/qa/workflow-qa-pr.rst b/doc/sphinx-guides/source/qa/workflow-qa-pr.rst new file mode 100644 index 00000000000..b2fae01da68 --- /dev/null +++ b/doc/sphinx-guides/source/qa/workflow-qa-pr.rst @@ -0,0 +1,7 @@ +Workflow for completing on a PR +=============================== + +.. contents:: |toctitle| + :local: + +1. Assign the PR you are working on to yourself. \ No newline at end of file From 1093892e2fdeb60ec4d21e9c86ac34332decee18 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 7 Nov 2023 16:13:08 -0500 Subject: [PATCH 062/689] Change to introduction title on overview --- doc/sphinx-guides/source/qa/overview.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/sphinx-guides/source/qa/overview.rst b/doc/sphinx-guides/source/qa/overview.rst index d9e21a2f0ab..9059fa87c5e 100644 --- a/doc/sphinx-guides/source/qa/overview.rst +++ b/doc/sphinx-guides/source/qa/overview.rst @@ -5,8 +5,8 @@ Overview :local: -What is an API? ---------------- +Introduction +------------ This document describes the testing process used by QA at IQSS and provides a guide for others filling in for that role. Please note that many variations are possible, and the main thing is to catch bugs and provide a good quality product to the user community. From 0e01b4eaf4276da01fd28204244ff57917861999 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 7 Nov 2023 16:57:19 -0500 Subject: [PATCH 063/689] Add content to "performance testing" and "manual testing" and fix the header on the other pages. --- .../source/qa/manual-testing.rst | 43 ++++++++++++++++++- doc/sphinx-guides/source/qa/overview.rst | 24 +++++++---- .../source/qa/performance-tests.rst | 25 ++++++++++- .../source/qa/test-automation-integration.rst | 7 ++- .../source/qa/testing-infrastructure.rst | 18 +++++++- 5 files changed, 101 insertions(+), 16 deletions(-) diff --git a/doc/sphinx-guides/source/qa/manual-testing.rst b/doc/sphinx-guides/source/qa/manual-testing.rst index 5c3393af546..27ee40d849e 100644 --- a/doc/sphinx-guides/source/qa/manual-testing.rst +++ b/doc/sphinx-guides/source/qa/manual-testing.rst @@ -1,2 +1,43 @@ Manual testing approach -======================= \ No newline at end of file +======================= + +.. contents:: |toctitle| + :local: + +Introduction +------------ +We use a risk-based, manual testing approach to achieve the most benefit with limited resources. This means we want to catch bugs where they are likely to exist, ensure core functions work, and failures do not have catastrophic results. In practice this means we do a brief positive check of core functions on each build called a smoke test, we test the most likely place for new bugs to exist, the area where things have changed, and attempt to prevent catastrophic failure by asking about the scope and reach of the code and how failures may occur. + +If it seems possible through user error or some other occurrence that such a serious failure will occur, we try to make it happen in the test environment. If the code has a UI component, we also do a limited amount of browser compatibility testing using Chrome, Firefox, and Safari browsers. We do not currently do UX or accessibility testing on a regular basis, though both have been done product-wide by the Design group and by the community. + +Examining a pull request for test cases: +---------------------------------------- +What does it do? What problem does it solve? +++++++++++++++++++++++++++++++++++++++++++++ +Read the top part of the pull request for a description, notes for reviewers, and usually a how-to test section. Does it make sense? If not, read the underlying ticket it closes, and any release notes or documentation. Knowing in general what it does helps you to think about how to approach it. +How is it configured? ++++++++++++++++++++++ +Most pull requests do not have any special configuration and are enabled on deployment, but some do. Configuration is part of testing. An admin will need to follow these instructions so try them out. Plus, that is the only way you will get it working to test it! + +Identify test cases by examining the problem report or feature description and any documentation of functionality. Look for statements or assertions about functions, what it does, as well as conditions or conditional behavior. These become your test cases. Think about how someone might make a mistake using it and try it. Does it fail gracefully or in a confusing or worse, damaging manner? Also, consider whether this pull request may interact with other functionality and try some spot checks there. For instance, if new metadata fields are added, try the export feature. Of course, try the suggestions under how to test. Those may be sufficient, but you should always think about it based on what it does. + +Try adding, modifying, and deleting any objects involved. This is probably covered by using the feature but a good basic approach to keep in mind. + +Make sure any server logging is appropriate. You should tail the server log while running your tests. Watch for unreported errors or stack traces especially chatty logging. If you do find a bug you will need to report the stack trace from the server.log + +Exercise the UI if there is one. I tend to use Chrome for most of my basic testing as it’s used twice as much as the next most commonly used browser, according to our site’s Google Analytics. I first go through all the options in the UI. Then, if all works, I’ll spot-check using Firefox and Safari. + +Check permissions. Is this feature limited to a specific set of users? Can it be accessed by a guest or by a non-privileged user? How about pasting a privileged page URL into a non-privileged user’s browser? + +Think about risk. Is the feature or function part of a critical area such as permissions? Does the functionality modify data? You may do more testing when the risk is higher. + +Smoke test +----------- + +1. Go to the homepage on https://dataverse-internal.iq.harvard.edu. Scroll to the bottom to ensure the build number is the one you intend to test from Jenkins. +2. Create a new user: I use a formulaic name with my initials and date and make the username and password the same, eg. kc080622. +3. Create a dataverse: I use the same username +4. Create a dataset: I use the same username; I fill in the required fields (I do not use a template). +5. Upload 3 different types of files: I use a tabular file, 50by1000.dta, an image file, and a text file. +6. Publish the dataset. +7. Download a file, done. diff --git a/doc/sphinx-guides/source/qa/overview.rst b/doc/sphinx-guides/source/qa/overview.rst index 9059fa87c5e..2c934564fcb 100644 --- a/doc/sphinx-guides/source/qa/overview.rst +++ b/doc/sphinx-guides/source/qa/overview.rst @@ -7,21 +7,27 @@ Overview Introduction ------------ - This document describes the testing process used by QA at IQSS and provides a guide for others filling in for that role. Please note that many variations are possible, and the main thing is to catch bugs and provide a good quality product to the user community. -The basic workflow is bugs or feature requests are submitted to GitHub by the community or by team members as issues. These issues are prioritized and added to a two-week sprint that is reflected on the GitHub Kanban board. As developers work on these issues, a GitHub branch is produced, code is contributed, and a pull request is made to merge these new changes back into the common develop branch and ultimately released as part of the product. Before a pull request is merged it must be reviewed by a member of the development team from a coding perspective, it must pass automated integration tests before moving to QA. There it is tested manually, exercising the UI using three common browser types and any business logic it implements. Depending on whether the code modifies existing code or is completely new, a smoke test of core functionality is performed and some basic regression testing of modified or related code is performed. Any documentation provided is used to understand the feature and any assertions are tested. Once these pass and any bugs found corrected, the automated integration tests are confirmed to be passing, the pr is merged into develop, the pr closed, and the branch deleted. At this point the pr moves from the QA column automatically into the Done column and the process repeats with the next pr until it is decided to make a release. +Workflow +-------- +The basic workflow is bugs or feature requests are submitted to GitHub by the community or by team members as issues. These issues are prioritized and added to a two-week sprint that is reflected on the GitHub Kanban board. As developers work on these issues, a GitHub branch is produced, code is contributed, and a pull request is made to merge these new changes back into the common develop branch and ultimately released as part of the product. Before a pull request is merged it must be reviewed by a member of the development team from a coding perspective, it must pass automated integration tests before moving to QA. There it is tested manually, exercising the UI using three common browser types and any business logic it implements. Depending on whether the code modifies existing code or is completely new, a smoke test of core functionality is performed and some basic regression testing of modified or related code is performed. Any documentation provided is used to understand the feature and any assertions are tested. Once this passes and any bugs that are found are corrected, the automated integration tests are confirmed to be passing, the PR is merged into development, the PR is closed, and the branch is deleted. At this point, the pr moves from the QA column automatically into the Done column and the process repeats with the next pr until it is decided to make a release. + +Release cadence and sprints +--------------------------- +A release likely spans multiple two-week sprints. Each sprint represents the priorities for that time and is sized so that the team can reasonably complete most of the work on time. This is a goal to help with planning, it is not a strict requirement. Some issues from the previous sprint may remain and likely be included in the next sprint but occasionally may be deprioritized and deferred to another time. -A release likely spans multiple two week sprints. Each sprint represents the priorities for that time and is sized so that the team can reasonably complete most of the work on time. This is a goal to help with planning, it is not a strict requirement. Some issues from the previous sprint may remain and likely be included in the next sprint but occasionally may be deprioritized and deferred to another time. +The decision to make a release can be based on the time since the last release, some important feature needed by the community or contractual deadline, or some other logical reason to package the work completed into a named release and posted to the releases section on GitHub. -The decision to make a release can be based on time since last release, some important feature needed by the community or contractual deadline or some other logical reason to package the work completed into a named release and posted to the releases section on GitHub. +Performance testing and deployment +---------------------------------- +The final testing activity before producing a release is performance testing. This could be done throughout the release cycle but since it is time-consuming it is done once near the end. Using a load-generating tool named Locust, it loads the statistically most loaded pages, according to Google Analytics, that is 50% homepage and 50% some type of dataset page. Since dataset page weight also varies by the number of files, a selection of about 10 datasets with varying file counts is used. The pages are called randomly as a guest user with increasing levels of user load, from 1 user to 250 users. Typical daily loads in production are around the 50-user level. Though the simulated user level does have a modest amount of random think time before repeated calls, from 5-20 seconds (I believe), it is not a real-world load so direct comparisons to production are not reliable. Instead, we compare performance to prior versions of the product and based on how that performed in production we have some idea whether this might be similar in performance or whether there is some undetected issue that appears under load, such as inefficient or too many DB queries per page. -The final testing activity before producing a release is performance testing. This could be done throughout the release cycle but since it is time consuming it is done once near the end. Using a load generating tool named Locust, it loads the statistically most loaded pages, according to Google Analytics, that is 50% homepage and 50% some type of dataset page. Since dataset page weight also varies by number of files, a selection of about 10 datasets with varying file counts is used. The pages are called randomly as a guest user with increasing levels of user load, from 1 user to 250 users. Typical daily loads in production are around the 50 user level. Though the simulated user level does have a modest amount of random think time before repeated calls, from 5-20 seconds (I believe), it is not a real world load so direct comparisons to production are not reliable. Instead, we compare performance to prior versions of the product and based on how that performed in production we have some idea whether this might be similar in performance or whether there is some undetected issue that appears under load, such as inefficient or too many DB queries per page. +Once the performance has been tested and recorded in a Google spreadsheet for this proposed version, the release will be prepared and posted. -Once performance has been tested and recorded in a google spreadsheet for this proposed version, the release will be prepared and posted. +Preparing the release consists of writing and reviewing the release notes compiled from individual notes in PRs that have been merged for this release. A PR is made for the notes and merged. Next, increment the version numbers in certain code files, produce a PR with those changes, and merge that into the common development branch. Last, a PR is made to merge and develop into the master branch. Once that is merged a guide build with the new release version is made from the master branch. Last, a release war file is built from the master and an installer is built from the master branch and includes the newly built war file. -Preparing the release consists of writing and reviewing the release notes compiled from individual notes in prs that have been merged for this release. A pr is made for the notes and merged. Next, increment the version numbers in certain code files, produce a pr with those changes and merge that into the common develop branch. Last, a pr is made to merge develop into the master branch. Once that is merged a guides build with the new release version is made from the master branch. Last, a release war file is built from master and an installer is built from the master branch and includes the newly built war file. +Publishing the release consists of creating a new draft release on GitHub, posting the release notes, uploading the .war file and the installer .zip file, and any ancillary files used to configure this release. The latest link for the guides should be updated on the guides server to point to the newest version. Once that is all in place, specify the version name and the master branch at the top of the GitHub draft release and publish. This will tag the master branch with the version number and make the release notes and files available to the public. -Publishing the release consists of creating a new draft release on Github, posting the release notes, uploading the .war file and the installer .zip file and any ancillary files used to configure this release. The latest link for the guides should be updated on the guides server to point to the newest version. Once that is all in place, specify the version name and the master branch at the top of the GitHub draft release and publish. This will tag the master branch with the version number and make the release notes and files available to the public. +Once released, post to Dataverse general about the release and when possible, deploy to demo and production. -Once released, post to dv general about the release and when possible, deploy to demo and production. diff --git a/doc/sphinx-guides/source/qa/performance-tests.rst b/doc/sphinx-guides/source/qa/performance-tests.rst index c3df2dc7951..673f797ed94 100644 --- a/doc/sphinx-guides/source/qa/performance-tests.rst +++ b/doc/sphinx-guides/source/qa/performance-tests.rst @@ -1,2 +1,23 @@ -Checklist for QA Release -======================== \ No newline at end of file +Performance testing +=================== + +.. contents:: |toctitle| + :local: + +Introduction +------------ +To run performance tests, we have a performance test cluster on AWS that employs web, database, and Solr. The database contains a copy of production that is updated weekly on Sundays. To ensure the homepage content is consistent between test runs across releases, two scripts set the datasets that will appear on the homepage. There is a script on the web server in the default CentOS user dir and one on the database server in the default CentOS user dir. Run these scripts before conducting the tests. + +Access +------ +Access to performance cluster instances requires ssh keys, see Leonid. The cluster itself is normally not running to reduce costs. To turn on the cluster, log on to the demo server and run the perfenv scripts from the centos default user dir. Access to the demo requires an ssh key, see Leonid. + +Special notes +------------- +Please note the performance database is also used occasionally by Julian and the Curation team to generate prod reports so a courtesy check with Julian would be good before taking over the env. + +Executing the performance script +-------------------------------- +To execute the performance test script, you need to install a local copy of the database-helper-scripts project (https://github.com/IQSS/dataverse-helper-scripts), written by Raman. I have since produced a stripped-down script that calls just the DB and ds and works with python3. + +The automated integration test runs happen on each commit to a PR on an AWS instance and should be reviewed to be passing before merging into development. Their status can be seen on the PR page near the bottom, above the merge button. See Don Sizemore or Phil for questions. diff --git a/doc/sphinx-guides/source/qa/test-automation-integration.rst b/doc/sphinx-guides/source/qa/test-automation-integration.rst index c3df2dc7951..050013cd9af 100644 --- a/doc/sphinx-guides/source/qa/test-automation-integration.rst +++ b/doc/sphinx-guides/source/qa/test-automation-integration.rst @@ -1,2 +1,5 @@ -Checklist for QA Release -======================== \ No newline at end of file +Test automation and integration test +==================================== + +.. contents:: |toctitle| + :local: \ No newline at end of file diff --git a/doc/sphinx-guides/source/qa/testing-infrastructure.rst b/doc/sphinx-guides/source/qa/testing-infrastructure.rst index 5b3de602bc7..98c4c6b2faf 100644 --- a/doc/sphinx-guides/source/qa/testing-infrastructure.rst +++ b/doc/sphinx-guides/source/qa/testing-infrastructure.rst @@ -1,2 +1,16 @@ -Testing Infrastructure -====================== \ No newline at end of file +Infrastructure for testing +========================== + +.. contents:: |toctitle| + :local: + + +Dataverse internal +------------------- +To build and test a PR, we use a build named IQSS_Dataverse_Internal on jenkins.dataverse.org, which deploys the .war file to an AWS instance named dataverse-internal.iq.harvard.edu. +Login to Jenkins requires a username and password. Check with Don Sizemore. Login to the dataverse-internal server requires a key, see Leonid. + +Guides server +------------- +There is also a guides build project named guides.dataverse.org. Any test builds of guides are deployed to a named directory** on guides.dataverse.org and can be found and tested by going to the existing guides, removing the part of the URL that contains the version, and browsing the resulting directory listing for the latest change. +Login to the guides server requires a key, see Don Sizemore. From 998cfe6c356dd8049e1e508dbc77696e7a7787a8 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 7 Nov 2023 17:07:35 -0500 Subject: [PATCH 064/689] Fix an issue with an item on the manual testing --- doc/sphinx-guides/source/qa/manual-testing.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/sphinx-guides/source/qa/manual-testing.rst b/doc/sphinx-guides/source/qa/manual-testing.rst index 27ee40d849e..645d8182fcd 100644 --- a/doc/sphinx-guides/source/qa/manual-testing.rst +++ b/doc/sphinx-guides/source/qa/manual-testing.rst @@ -15,6 +15,7 @@ Examining a pull request for test cases: What does it do? What problem does it solve? ++++++++++++++++++++++++++++++++++++++++++++ Read the top part of the pull request for a description, notes for reviewers, and usually a how-to test section. Does it make sense? If not, read the underlying ticket it closes, and any release notes or documentation. Knowing in general what it does helps you to think about how to approach it. + How is it configured? +++++++++++++++++++++ Most pull requests do not have any special configuration and are enabled on deployment, but some do. Configuration is part of testing. An admin will need to follow these instructions so try them out. Plus, that is the only way you will get it working to test it! From f3a0e324ac5ef89a63e3b5c303e44175a8cb5301 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 8 Nov 2023 13:55:53 +0100 Subject: [PATCH 065/689] fix(ct): remove nginx from modules --- modules/nginx/Dockerfile | 9 --------- modules/nginx/README.md | 7 ------- modules/nginx/default.conf | 12 ------------ 3 files changed, 28 deletions(-) delete mode 100644 modules/nginx/Dockerfile delete mode 100644 modules/nginx/README.md delete mode 100644 modules/nginx/default.conf diff --git a/modules/nginx/Dockerfile b/modules/nginx/Dockerfile deleted file mode 100644 index 3900076599f..00000000000 --- a/modules/nginx/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM nginx:latest - -# Remove the default NGINX configuration file -RUN rm /etc/nginx/conf.d/default.conf - -# Copy the contents of the local default.conf to the container -COPY default.conf /etc/nginx/conf.d/ - -EXPOSE 4849 \ No newline at end of file diff --git a/modules/nginx/README.md b/modules/nginx/README.md deleted file mode 100644 index 9d2ff785577..00000000000 --- a/modules/nginx/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# nginx proxy - -nginx can be used to proxy various services at other ports/protocols from docker. - -Currently, this is used to work around a problem with the IntelliJ Payara plugin, which doesn't allow remote redeployment in case the Payara admin is served via HTTPS using a self-signed certificate, which is the case of the default dataverse container installation. This configuration provides an HTTP endpoint at port 4849, and proxies requests to the Payara admin console's HTTPS 4848 endpoint. From the IntelliJ Payara plugin one has to specify the localhost 4849 port (without SSL). - -![img.png](img.png) diff --git a/modules/nginx/default.conf b/modules/nginx/default.conf deleted file mode 100644 index 8381a66c19a..00000000000 --- a/modules/nginx/default.conf +++ /dev/null @@ -1,12 +0,0 @@ -server { - listen 4849; - - # Make it big, so that .war files can be submitted - client_max_body_size 300M; - - location / { - proxy_pass https://dataverse:4848; - proxy_ssl_verify off; - proxy_ssl_server_name on; - } -} From 43fcf1dfcfaf867d6bb2ef5b6c6c4c9554123936 Mon Sep 17 00:00:00 2001 From: Jan van Mansum Date: Wed, 8 Nov 2023 15:21:06 +0100 Subject: [PATCH 066/689] Amend metadata block TSVs --- doc/release-notes/9983-unique-constraints.md | 5 +++++ scripts/api/data/metadatablocks/astrophysics.tsv | 6 +++--- scripts/api/data/metadatablocks/biomedical.tsv | 2 +- scripts/api/data/metadatablocks/citation.tsv | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/doc/release-notes/9983-unique-constraints.md b/doc/release-notes/9983-unique-constraints.md index bb3ed200c62..1e37d75d88d 100644 --- a/doc/release-notes/9983-unique-constraints.md +++ b/doc/release-notes/9983-unique-constraints.md @@ -7,3 +7,8 @@ and SELECT spec, count(*) FROM oaiset group by spec; and then removing any duplicate rows (where count>1). + + + + +TODO: Add note about reloading metadata blocks after upgrade. \ No newline at end of file diff --git a/scripts/api/data/metadatablocks/astrophysics.tsv b/scripts/api/data/metadatablocks/astrophysics.tsv index 4039d32cb75..92792d404c9 100644 --- a/scripts/api/data/metadatablocks/astrophysics.tsv +++ b/scripts/api/data/metadatablocks/astrophysics.tsv @@ -2,13 +2,13 @@ astrophysics Astronomy and Astrophysics Metadata #datasetField name title description watermark fieldType displayOrder displayFormat advancedSearchField allowControlledVocabulary allowmultiples facetable displayoncreate required parent metadatablock_id astroType Type The nature or genre of the content of the files in the dataset. text 0 TRUE TRUE TRUE TRUE FALSE FALSE astrophysics - astroFacility Facility The observatory or facility where the data was obtained. text 1 TRUE TRUE TRUE TRUE FALSE FALSE astrophysics - astroInstrument Instrument The instrument used to collect the data. text 2 TRUE TRUE TRUE TRUE FALSE FALSE astrophysics + astroFacility Facility The observatory or facility where the data was obtained. text 1 TRUE FALSE TRUE TRUE FALSE FALSE astrophysics + astroInstrument Instrument The instrument used to collect the data. text 2 TRUE FALSE TRUE TRUE FALSE FALSE astrophysics astroObject Object Astronomical Objects represented in the data (Given as SIMBAD recognizable names preferred). text 3 TRUE FALSE TRUE TRUE FALSE FALSE astrophysics resolution.Spatial Spatial Resolution The spatial (angular) resolution that is typical of the observations, in decimal degrees. text 4 TRUE FALSE FALSE TRUE FALSE FALSE astrophysics resolution.Spectral Spectral Resolution The spectral resolution that is typical of the observations, given as the ratio \u03bb/\u0394\u03bb. text 5 TRUE FALSE FALSE TRUE FALSE FALSE astrophysics resolution.Temporal Time Resolution The temporal resolution that is typical of the observations, given in seconds. text 6 FALSE FALSE FALSE FALSE FALSE FALSE astrophysics - coverage.Spectral.Bandpass Bandpass Conventional bandpass name text 7 TRUE TRUE TRUE TRUE FALSE FALSE astrophysics + coverage.Spectral.Bandpass Bandpass Conventional bandpass name text 7 TRUE FALSE TRUE TRUE FALSE FALSE astrophysics coverage.Spectral.CentralWavelength Central Wavelength (m) The central wavelength of the spectral bandpass, in meters. Enter a floating-point number. float 8 TRUE FALSE TRUE TRUE FALSE FALSE astrophysics coverage.Spectral.Wavelength Wavelength Range The minimum and maximum wavelength of the spectral bandpass. Enter a floating-point number. none 9 FALSE FALSE TRUE FALSE FALSE FALSE astrophysics coverage.Spectral.MinimumWavelength Minimum (m) The minimum wavelength of the spectral bandpass, in meters. Enter a floating-point number. float 10 TRUE FALSE FALSE TRUE FALSE FALSE coverage.Spectral.Wavelength astrophysics diff --git a/scripts/api/data/metadatablocks/biomedical.tsv b/scripts/api/data/metadatablocks/biomedical.tsv index 28d59130c34..d70f754336a 100644 --- a/scripts/api/data/metadatablocks/biomedical.tsv +++ b/scripts/api/data/metadatablocks/biomedical.tsv @@ -13,7 +13,7 @@ studyAssayOtherTechnologyType Other Technology Type If Other was selected in Technology Type, list any other technology types that were used in this Dataset. text 9 TRUE FALSE TRUE TRUE FALSE FALSE biomedical studyAssayPlatform Technology Platform The manufacturer and name of the technology platform used in the assay (e.g. Bruker AVANCE). text 10 TRUE TRUE TRUE TRUE FALSE FALSE biomedical studyAssayOtherPlatform Other Technology Platform If Other was selected in Technology Platform, list any other technology platforms that were used in this Dataset. text 11 TRUE FALSE TRUE TRUE FALSE FALSE biomedical - studyAssayCellType Cell Type The name of the cell line from which the source or sample derives. text 12 TRUE TRUE TRUE TRUE FALSE FALSE biomedical + studyAssayCellType Cell Type The name of the cell line from which the source or sample derives. text 12 TRUE FALSE TRUE TRUE FALSE FALSE biomedical #controlledVocabulary DatasetField Value identifier displayOrder studyDesignType Case Control EFO_0001427 0 studyDesignType Cross Sectional EFO_0001428 1 diff --git a/scripts/api/data/metadatablocks/citation.tsv b/scripts/api/data/metadatablocks/citation.tsv index b21b6bcce57..c5af05927dc 100644 --- a/scripts/api/data/metadatablocks/citation.tsv +++ b/scripts/api/data/metadatablocks/citation.tsv @@ -70,7 +70,7 @@ seriesName Name The name of the dataset series text 66 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE series citation seriesInformation Information Can include 1) a history of the series and 2) a summary of features that apply to the series textbox 67 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE series citation software Software Information about the software used to generate the Dataset none 68 , FALSE FALSE TRUE FALSE FALSE FALSE citation https://www.w3.org/TR/prov-o/#wasGeneratedBy - softwareName Name The name of software used to generate the Dataset text 69 #VALUE FALSE TRUE FALSE FALSE FALSE FALSE software citation + softwareName Name The name of software used to generate the Dataset text 69 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE software citation softwareVersion Version The version of the software used to generate the Dataset, e.g. 4.11 text 70 #NAME: #VALUE FALSE FALSE FALSE FALSE FALSE FALSE software citation relatedMaterial Related Material Information, such as a persistent ID or citation, about the material related to the Dataset, such as appendices or sampling information available outside of the Dataset textbox 71 FALSE FALSE TRUE FALSE FALSE FALSE citation relatedDatasets Related Dataset Information, such as a persistent ID or citation, about a related dataset, such as previous research on the Dataset's subject textbox 72 FALSE FALSE TRUE FALSE FALSE FALSE citation http://purl.org/dc/terms/relation From 65fb4dc0be3d519b80ee4e44007d720787042802 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 8 Nov 2023 15:46:10 +0100 Subject: [PATCH 067/689] feat(ct): replace nginx with caddy #9590 - Use caddy as a faster and smaller alternative to NGINX. - Remove unnecessary pom.xml entry for container. - Migrate config to Caddyfile in /conf instead of /modules (we do not create a new image here...) - Add dependency on Dataverse container to proxy container - Slight renaming of containers --- conf/proxy/Caddyfile | 12 ++++++++++++ docker-compose-dev.yml | 17 +++++++++++++---- pom.xml | 8 -------- 3 files changed, 25 insertions(+), 12 deletions(-) create mode 100644 conf/proxy/Caddyfile diff --git a/conf/proxy/Caddyfile b/conf/proxy/Caddyfile new file mode 100644 index 00000000000..70e6904d26e --- /dev/null +++ b/conf/proxy/Caddyfile @@ -0,0 +1,12 @@ +# This configuration is intended to be used with Caddy, a very small high perf proxy. +# It will serve the application containers Payara Admin GUI via HTTP instead of HTTPS, +# avoiding the trouble of self signed certificates for local development. + +:4848 { + reverse_proxy https://dataverse:4848 { + transport http { + tls_insecure_skip_verify + } + header_down Location "^https://" "http://" + } +} diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index dc245a88847..93a8c49ec54 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -145,12 +145,21 @@ services: volumes: - './conf/keycloak/test-realm.json:/opt/keycloak/data/import/test-realm.json' - dev_nginx: - container_name: dev_nginx - image: gdcc/dev_nginx:unstable + # This proxy configuration is only intended to be used for development purposes! + # DO NOT USE IN PRODUCTION! HIGH SECURITY RISK! + dev_proxy: + image: caddy:2-alpine + # The command below is enough to enable using the admin gui, but it will not rewrite location headers to HTTP. + # To achieve rewriting from https:// to http://, we need a simple configuration file + #command: ["caddy", "reverse-proxy", "-f", ":4848", "-t", "https://dataverse:4848", "--insecure"] + command: ["caddy", "run", "-c", "/Caddyfile"] ports: - - "4849:4849" + - "4848:4848" # Will expose Payara Admin Console (HTTPS) as HTTP restart: always + volumes: + - ./conf/proxy/Caddyfile:/Caddyfile:ro + depends_on: + - dev_dataverse networks: - dataverse diff --git a/pom.xml b/pom.xml index 473b7bd1bf7..c78d540e103 100644 --- a/pom.xml +++ b/pom.xml @@ -1008,14 +1008,6 @@ true - - - gdcc/dev_nginx:unstable - - ${project.basedir}/modules/nginx - - - true From 5630e7699149cbd4731dc460d49dd3e30b069168 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 8 Nov 2023 15:47:48 +0100 Subject: [PATCH 068/689] style(ct): expose payara admin gui as HTTPS on port 4949 #9590 It might be good to keep it available on localhost in addition to the HTTP variant on port 4848 via proxy. --- docker-compose-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 93a8c49ec54..4e0899595e1 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -21,7 +21,7 @@ services: - DATAVERSE_AUTH_OIDC_AUTH_SERVER_URL=http://keycloak.mydomain.com:8090/realms/test ports: - "8080:8080" # HTTP (Dataverse Application) - - "4848:4848" # HTTP (Payara Admin Console) + - "4949:4848" # HTTPS (Payara Admin Console) - "9009:9009" # JDWP - "8686:8686" # JMX networks: From 76b87b0606f3ffff9c7ca6742926678a539a168b Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 8 Nov 2023 15:50:06 +0100 Subject: [PATCH 069/689] feat(ct): enable skipping all deployments on app server start #9590 With using the env var "SKIP_DEPLOY" or the Maven property "-Dapp.deploy.skip" we can make the application server not deploy Dataverse on container start. This is necessary to save on time and manual undeploy when using Payara IDE tools to hot deploy changes. --- .env | 1 + docker-compose-dev.yml | 1 + modules/container-base/src/main/docker/Dockerfile | 3 ++- .../docker/scripts/init_1_generate_deploy_commands.sh | 10 ++++++++-- pom.xml | 2 ++ 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.env b/.env index e3ececc2e54..ae266af80da 100644 --- a/.env +++ b/.env @@ -2,3 +2,4 @@ APP_IMAGE=gdcc/dataverse:unstable POSTGRES_VERSION=13 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.3.0 +SKIP_DEPLOY=0 \ No newline at end of file diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 4e0899595e1..1b507b72fe1 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -14,6 +14,7 @@ services: - DATAVERSE_DB_USER=${DATAVERSE_DB_USER} - ENABLE_JDWP=1 - ENABLE_RELOAD=1 + - SKIP_DEPLOY=${SKIP_DEPLOY} - DATAVERSE_FEATURE_API_BEARER_AUTH=1 - DATAVERSE_AUTH_OIDC_ENABLED=1 - DATAVERSE_AUTH_OIDC_CLIENT_ID=test diff --git a/modules/container-base/src/main/docker/Dockerfile b/modules/container-base/src/main/docker/Dockerfile index 11ad980f070..f2c193beafd 100644 --- a/modules/container-base/src/main/docker/Dockerfile +++ b/modules/container-base/src/main/docker/Dockerfile @@ -65,7 +65,8 @@ ENV PATH="${PATH}:${PAYARA_DIR}/bin:${SCRIPT_DIR}" \ JVM_DUMPS_ARG="-XX:+HeapDumpOnOutOfMemoryError" \ ENABLE_JMX=0 \ ENABLE_JDWP=0 \ - ENABLE_RELOAD=0 + ENABLE_RELOAD=0 \ + SKIP_DEPLOY=0 ### PART 1: SYSTEM ### ARG UID=1000 diff --git a/modules/container-base/src/main/docker/scripts/init_1_generate_deploy_commands.sh b/modules/container-base/src/main/docker/scripts/init_1_generate_deploy_commands.sh index 8729f78e466..161f10caebf 100644 --- a/modules/container-base/src/main/docker/scripts/init_1_generate_deploy_commands.sh +++ b/modules/container-base/src/main/docker/scripts/init_1_generate_deploy_commands.sh @@ -31,6 +31,8 @@ # ########################################################################################################## +set -euo pipefail + # Check required variables are set if [ -z "$DEPLOY_DIR" ]; then echo "Variable DEPLOY_DIR is not set."; exit 1; fi if [ -z "$PREBOOT_COMMANDS" ]; then echo "Variable PREBOOT_COMMANDS is not set."; exit 1; fi @@ -51,8 +53,12 @@ deploy() { if grep -q "$1" "$POSTBOOT_COMMANDS"; then echo "post boot commands already deploys $1"; else - echo "Adding deployment target $1 to post boot commands"; - echo "$DEPLOY_STATEMENT" >> "$POSTBOOT_COMMANDS"; + if [ -n "$SKIP_DEPLOY" ] && { [ "$SKIP_DEPLOY" = "1" ] || [ "$SKIP_DEPLOY" = "true" ]; }; then + echo "Skipping deployment of $1 as requested."; + else + echo "Adding deployment target $1 to post boot commands"; + echo "$DEPLOY_STATEMENT" >> "$POSTBOOT_COMMANDS"; + fi fi } diff --git a/pom.xml b/pom.xml index c78d540e103..52bef929c1c 100644 --- a/pom.xml +++ b/pom.xml @@ -911,6 +911,7 @@ gdcc/dataverse:${app.image.tag} unstable + false gdcc/base:${base.image.tag} unstable gdcc/configbaker:${conf.image.tag} @@ -923,6 +924,7 @@ ${postgresql.server.version} ${solr.version} dataverse + ${app.deploy.skip} From b73c77a55d5a753beab080db35d01b78fef2a8a9 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Wed, 8 Nov 2023 12:09:29 -0500 Subject: [PATCH 070/689] Page restructure and additional info load --- .../source/qa/checklist-qa-pr.rst | 2 - .../source/qa/checklist-qa-release.rst | 2 - .../source/qa/deploying-jenkins.rst | 2 - doc/sphinx-guides/source/qa/index.rst | 5 - .../source/qa/manual-testing.rst | 12 +- .../source/qa/other-approaches.rst | 125 +++++++++++++++++- doc/sphinx-guides/source/qa/overview.rst | 4 +- .../source/qa/performance-tests.rst | 8 +- .../source/qa/test-automation-integration.rst | 32 ++++- .../source/qa/testing-infrastructure.rst | 6 +- doc/sphinx-guides/source/qa/tips-tricks.rst | 2 - .../source/qa/workflow-qa-pr.rst | 7 - 12 files changed, 170 insertions(+), 37 deletions(-) delete mode 100644 doc/sphinx-guides/source/qa/checklist-qa-pr.rst delete mode 100644 doc/sphinx-guides/source/qa/checklist-qa-release.rst delete mode 100644 doc/sphinx-guides/source/qa/deploying-jenkins.rst delete mode 100644 doc/sphinx-guides/source/qa/tips-tricks.rst delete mode 100644 doc/sphinx-guides/source/qa/workflow-qa-pr.rst diff --git a/doc/sphinx-guides/source/qa/checklist-qa-pr.rst b/doc/sphinx-guides/source/qa/checklist-qa-pr.rst deleted file mode 100644 index df60f4260fc..00000000000 --- a/doc/sphinx-guides/source/qa/checklist-qa-pr.rst +++ /dev/null @@ -1,2 +0,0 @@ -Checklist for QA on a PR -======================== \ No newline at end of file diff --git a/doc/sphinx-guides/source/qa/checklist-qa-release.rst b/doc/sphinx-guides/source/qa/checklist-qa-release.rst deleted file mode 100644 index 34419fee8ae..00000000000 --- a/doc/sphinx-guides/source/qa/checklist-qa-release.rst +++ /dev/null @@ -1,2 +0,0 @@ -Checklist for QA on release -=========================== \ No newline at end of file diff --git a/doc/sphinx-guides/source/qa/deploying-jenkins.rst b/doc/sphinx-guides/source/qa/deploying-jenkins.rst deleted file mode 100644 index b15a6f88534..00000000000 --- a/doc/sphinx-guides/source/qa/deploying-jenkins.rst +++ /dev/null @@ -1,2 +0,0 @@ -Building and deploying a PR from Jenkins to dataverse-internal -============================================================== \ No newline at end of file diff --git a/doc/sphinx-guides/source/qa/index.rst b/doc/sphinx-guides/source/qa/index.rst index 019f5cdbd5c..c0c617d561d 100755 --- a/doc/sphinx-guides/source/qa/index.rst +++ b/doc/sphinx-guides/source/qa/index.rst @@ -10,12 +10,7 @@ QA Guide performance-tests manual-testing test-automation-integration - deploying-jenkins other-approaches - tips-tricks - workflow-qa-pr - checklist-qa-pr - checklist-qa-release diff --git a/doc/sphinx-guides/source/qa/manual-testing.rst b/doc/sphinx-guides/source/qa/manual-testing.rst index 645d8182fcd..8e50e6b6b08 100644 --- a/doc/sphinx-guides/source/qa/manual-testing.rst +++ b/doc/sphinx-guides/source/qa/manual-testing.rst @@ -1,4 +1,4 @@ -Manual testing approach +Manual Testing Approach ======================= .. contents:: |toctitle| @@ -10,13 +10,13 @@ We use a risk-based, manual testing approach to achieve the most benefit with li If it seems possible through user error or some other occurrence that such a serious failure will occur, we try to make it happen in the test environment. If the code has a UI component, we also do a limited amount of browser compatibility testing using Chrome, Firefox, and Safari browsers. We do not currently do UX or accessibility testing on a regular basis, though both have been done product-wide by the Design group and by the community. -Examining a pull request for test cases: +Examining a Pull Pequest for Test Cases: ---------------------------------------- -What does it do? What problem does it solve? +What Problem Does it Solve? ++++++++++++++++++++++++++++++++++++++++++++ Read the top part of the pull request for a description, notes for reviewers, and usually a how-to test section. Does it make sense? If not, read the underlying ticket it closes, and any release notes or documentation. Knowing in general what it does helps you to think about how to approach it. -How is it configured? +How is it Configured? +++++++++++++++++++++ Most pull requests do not have any special configuration and are enabled on deployment, but some do. Configuration is part of testing. An admin will need to follow these instructions so try them out. Plus, that is the only way you will get it working to test it! @@ -32,7 +32,7 @@ Check permissions. Is this feature limited to a specific set of users? Can it be Think about risk. Is the feature or function part of a critical area such as permissions? Does the functionality modify data? You may do more testing when the risk is higher. -Smoke test +Smoke Test ----------- 1. Go to the homepage on https://dataverse-internal.iq.harvard.edu. Scroll to the bottom to ensure the build number is the one you intend to test from Jenkins. @@ -41,4 +41,4 @@ Smoke test 4. Create a dataset: I use the same username; I fill in the required fields (I do not use a template). 5. Upload 3 different types of files: I use a tabular file, 50by1000.dta, an image file, and a text file. 6. Publish the dataset. -7. Download a file, done. +7. Download a file. diff --git a/doc/sphinx-guides/source/qa/other-approaches.rst b/doc/sphinx-guides/source/qa/other-approaches.rst index 420d40fa09c..bd92e7d22d8 100644 --- a/doc/sphinx-guides/source/qa/other-approaches.rst +++ b/doc/sphinx-guides/source/qa/other-approaches.rst @@ -1,2 +1,125 @@ Other approaches to deploying and testing -========================================= \ No newline at end of file +========================================= + +.. contents:: |toctitle| + :local: + +This workflow is fine for a single person testing a PR, one at a time. It would be awkward or impossible if there were multiple people wanting to test different PRs at the same time. I’m assuming if a developer is testing, they would likely just deploy to their dev environment. That might be ok but not sure the env is fully configured enough to offer a real-world testing scenario. An alternative might be to spin an EC2 branch on AWS, potentially using sample data. This can take some time so another option might be to spin up a few, persistent AWS instances with sample data this way, one per tester, and just deploy new builds there when you want to test. You could even configure Jenkins projects for each if desired to maintain consistency in how they’re built. + +Tips and tricks +--------------- + +- Start testing simply, with the most obvious test. You don’t need to know all your tests upfront. As you gain comfort and understanding of how it works, try more tests until you are done. If it is a complex feature, jot down your tests in an outline format, some beforehand as a guide, and some after as things occur to you. Save the doc in a testing folder (I have one on Google Drive). This potentially will help with future testing. +- When in doubt, ask someone. If you are confused about how something is working, it may be something you have missed, or it could be a documentation issue, or it could be a bug! Talk to the code reviewer and the contributor/developer for their opinion and advice. +- Always tail the server.log file while testing. Open a terminal window to the test instance and tail -F server.log. This helps you get a real-time sense of what the server is doing when you act and makes it easier to identify any stack trace on failure. +- When overloaded, do the simple pull requests first to reduce the queue. It gives you a mental boost to complete something and reduces the perception of the amount of work still to be done. +- When testing a bug fix, try reproducing the bug on the demo before testing the fix, that way you know you are taking the correct steps to verify that the fix worked. +- When testing an optional feature that requires configuration, do a smoke test without the feature configured and then with it configured. That way you know that folks using the standard config are unaffected by the option if they choose not to configure it. +- Back up your DB before applying an irreversible DB update and you are using a persistent/reusable platform. Just in case it fails, and you need to carry on testing something else you can use the backup. + +Workflow for Completing QA on a PR +----------------------------------- + +1. Assign the PR you are working on to yourself. + +2. What does it do? + + Read the description at the top of the PR, any release notes, documentation, and the original issue. + +3. Does it address the issue it closes? + + The PR should address the issue entirely unless otherwise noted. + +4. How do you test it? + + Look at the “how to test section” at the top of the pull request. Does it make sense? This likely won’t be the only testing you perform. You can develop further tests from the original issue or problem description, from the description of functionality, the documentation, configuration, and release notes. Also consider trying to reveal bugs by trying to break it: try bad or missing data, very large values or volume of data, exceed any place that may have a limit or boundary. + +5. Does it have or need documentation? + + Small changes or fixes usually don’t have doc but new features or extensions of a feature or new configuration options should have documentation. + +6. Does it have or need release notes? + + Same as for doc, just a heads up to an admin for something of note or especially upgrade instructions as needed. + +7. Does it use a DB, flyway script? + + Good to know since it may collide with another existing one by version or it could be a one way transform of your DB so back up your test DB before. Also, happens during deployment so be on the lookout for any issues. + +8. Validate the documentation. + + Build the doc using Jenkins, does it build without errors? + Read it through for sense. + Use it for test cases and to understand the feature. + +9. Build and deploy the pull request. + + Normally this is done using Jenkins and automatically deployed to the QA test machine. + +10. Configure if required + + If needed to operate and everyone installing or upgrading will use this, configure now as all testing will use it. + +11. Smoke test the branch. + + Standard, minimal test of core functionality. + +12. Regression test-related or potentially affected features + + If config is optional and testing without config turned on, do some spot checks/ regression tests of related or potentially affected areas. + +13. Configure if optional + + What is the default, enabled or disabled? Is that clearly indicated? Test both. + By config here we mean enabling the functionality versus choosing a particular config option. Some complex features have config options in addition to enabling. Those will also need to be tested. + +14. Test all the new or changed functionality. + + The heart of the PR, what is this PR adding or fixing? Is it all there and working? + +15. Regression test related or potentially affected features. + + Sometimes new stuff modifies and extends other functionality or functionality that is shared with other aspects of the system, e.g. Export, Import. Check the underlying functionality that was also modified but in a spot check or briefer manner. + +16. Report any issues found within the PR + + It can be easy to lose track of what you’ve found, steps to reproduce, and any errors or stack traces from the server log. Add these in a numbered list to a comment in the pr. Easier to check off when fixed and to work on. Add large amounts of text as in the server log as attached, meaningfully named files. + +17. Retest all fixes, spot check feature functionality, smoke test + + Similar to your initial testing, it is only narrower. + +18. Test Upgrade Instructions, if required + + Some features build upon the existing architecture but require modifications, such as adding a new column to the DB or changing or adding data. It is crucial that this works properly for our 100+ installations. This testing should be performed at the least on the prior version with basic data objects (collection, dataset, files) and any other data that will be updated by this feature. Using the sample data from the prior version would be good or deploying to dataverse-internal and upgrading there would be a good test. Remember to back up your DB before doing a transformative upgrade so that you can repeat it later if you find a bug. + +19. Make sure the integration tests in the PR have been completed and passed. + + They are run with each commit to the PR and take approximately 42 minutes to run. + +20. Merge PR + + Click merge to include this PR into the common develop branch. + +21. Delete merged branch + + Just a housekeeping move if the PR is from IQSS. Click the delete branch button where the merge button had been. There is no deletion for outside contributions. + + +Checklist for Completing QA on a PR +------------------------------------ + +1. Build the docs +2. Smoke test the pr +3. Test the new functionality +4. Regression test +5. Test any upgrade instructions + +Checklist for QA on Release +--------------------------- + +1. Review Consolidated Release Notes, in particular upgrade instructions. +2. Conduct performance testing and compare with the previous release. +3. Perform clean install and smoke test. +4. Potentially follow upgrade instructions. Though they have been performed incrementally for each PR, the sequence may need checking + diff --git a/doc/sphinx-guides/source/qa/overview.rst b/doc/sphinx-guides/source/qa/overview.rst index 2c934564fcb..153fab1a28f 100644 --- a/doc/sphinx-guides/source/qa/overview.rst +++ b/doc/sphinx-guides/source/qa/overview.rst @@ -13,13 +13,13 @@ Workflow -------- The basic workflow is bugs or feature requests are submitted to GitHub by the community or by team members as issues. These issues are prioritized and added to a two-week sprint that is reflected on the GitHub Kanban board. As developers work on these issues, a GitHub branch is produced, code is contributed, and a pull request is made to merge these new changes back into the common develop branch and ultimately released as part of the product. Before a pull request is merged it must be reviewed by a member of the development team from a coding perspective, it must pass automated integration tests before moving to QA. There it is tested manually, exercising the UI using three common browser types and any business logic it implements. Depending on whether the code modifies existing code or is completely new, a smoke test of core functionality is performed and some basic regression testing of modified or related code is performed. Any documentation provided is used to understand the feature and any assertions are tested. Once this passes and any bugs that are found are corrected, the automated integration tests are confirmed to be passing, the PR is merged into development, the PR is closed, and the branch is deleted. At this point, the pr moves from the QA column automatically into the Done column and the process repeats with the next pr until it is decided to make a release. -Release cadence and sprints +Release Cadence and Sprints --------------------------- A release likely spans multiple two-week sprints. Each sprint represents the priorities for that time and is sized so that the team can reasonably complete most of the work on time. This is a goal to help with planning, it is not a strict requirement. Some issues from the previous sprint may remain and likely be included in the next sprint but occasionally may be deprioritized and deferred to another time. The decision to make a release can be based on the time since the last release, some important feature needed by the community or contractual deadline, or some other logical reason to package the work completed into a named release and posted to the releases section on GitHub. -Performance testing and deployment +Performance Testing and Deployment ---------------------------------- The final testing activity before producing a release is performance testing. This could be done throughout the release cycle but since it is time-consuming it is done once near the end. Using a load-generating tool named Locust, it loads the statistically most loaded pages, according to Google Analytics, that is 50% homepage and 50% some type of dataset page. Since dataset page weight also varies by the number of files, a selection of about 10 datasets with varying file counts is used. The pages are called randomly as a guest user with increasing levels of user load, from 1 user to 250 users. Typical daily loads in production are around the 50-user level. Though the simulated user level does have a modest amount of random think time before repeated calls, from 5-20 seconds (I believe), it is not a real-world load so direct comparisons to production are not reliable. Instead, we compare performance to prior versions of the product and based on how that performed in production we have some idea whether this might be similar in performance or whether there is some undetected issue that appears under load, such as inefficient or too many DB queries per page. diff --git a/doc/sphinx-guides/source/qa/performance-tests.rst b/doc/sphinx-guides/source/qa/performance-tests.rst index 673f797ed94..1bfde798100 100644 --- a/doc/sphinx-guides/source/qa/performance-tests.rst +++ b/doc/sphinx-guides/source/qa/performance-tests.rst @@ -1,4 +1,4 @@ -Performance testing +Performance Testing =================== .. contents:: |toctitle| @@ -12,11 +12,11 @@ Access ------ Access to performance cluster instances requires ssh keys, see Leonid. The cluster itself is normally not running to reduce costs. To turn on the cluster, log on to the demo server and run the perfenv scripts from the centos default user dir. Access to the demo requires an ssh key, see Leonid. -Special notes -------------- +Special Notes ⚠️ +----------------- Please note the performance database is also used occasionally by Julian and the Curation team to generate prod reports so a courtesy check with Julian would be good before taking over the env. -Executing the performance script +Executing the Performance Script -------------------------------- To execute the performance test script, you need to install a local copy of the database-helper-scripts project (https://github.com/IQSS/dataverse-helper-scripts), written by Raman. I have since produced a stripped-down script that calls just the DB and ds and works with python3. diff --git a/doc/sphinx-guides/source/qa/test-automation-integration.rst b/doc/sphinx-guides/source/qa/test-automation-integration.rst index 050013cd9af..13c48105f91 100644 --- a/doc/sphinx-guides/source/qa/test-automation-integration.rst +++ b/doc/sphinx-guides/source/qa/test-automation-integration.rst @@ -2,4 +2,34 @@ Test automation and integration test ==================================== .. contents:: |toctitle| - :local: \ No newline at end of file + :local: + +This test suite is added to and maintained by development. It is generally advisable for code contributors to add integration tests when adding new functionality. The approach here is one of code coverage: exercise as much of the code base’s code paths as possible, every time to catch bugs. + +This type of approach is often used to give contributing developers confidence that their code didn’t introduce any obvious, major issues and is run on each commit. Since it is a broad set of tests, it is not clear whether any specific, conceivable test is run but it does add a lot of confidence that the code base is functioning due to its reach and consistency. + +Building and Deploying a Pull Request from Jenkins to Dataverse-Internal: +------------------------------------------------------------------------- + +1. Log on to GitHub, go to projects, dataverse to see Kanban board, select a pull request to test from the QA queue. + +2. From the pull request page, click the copy icon next to the pull request branch name. + +3. Log on to jenkins.dataverse.org, select the IQSS_Dataverse_Internal project, and configure the repository URL and branch specifier to match the ones from the pull request. For example: + + - 8372-gdcc-xoai-library has IQSS implied + | **Repository URL:** https://github.com/IQSS/dataverse.git + | **Branch specifier:** \*/8372-gdcc-xoai-library + - GlobalDataverseCommunityConsortium:GDCC/DC-3B + | **Repository URL:** https://github.com/GlobalDataverseCommunityConsortium/dataverse.git + | **Branch specifier:** \*/GDCC/DC-3B. + +4. Click Build Now and note the build number in progress. + +5. Once complete, go to https://dataverse-internal.iq.harvard.edu and check that the deployment succeeded, and that the homepage displays the latest build number. + +6. If for some reason it didn’t deploy, check the server.log file. It may just be a caching issue so try un-deploying, deleting cache, restarting, and re-deploying on the server (su - dataverse, /usr/local/payara5/bin/asadmin list-applications, /usr/local/payara5/bin/asadmin undeploy dataverse-5.11.1, /usr/local/payara5/bin/asadmin deploy /tmp/dataverse-5.11.1.war) + +7. If that didn’t work, you may have run into a flyway DB script collision error but that should be indicated by the server.log + +8. Assuming the above steps worked, and they should 99% of the time, test away! Note: be sure to tail -F server.log in a terminal window while you are doing any testing. This way you can spot problems that may not appear in the UI and have easier access to any stack traces for easier reporting. \ No newline at end of file diff --git a/doc/sphinx-guides/source/qa/testing-infrastructure.rst b/doc/sphinx-guides/source/qa/testing-infrastructure.rst index 98c4c6b2faf..d35bc6e9a23 100644 --- a/doc/sphinx-guides/source/qa/testing-infrastructure.rst +++ b/doc/sphinx-guides/source/qa/testing-infrastructure.rst @@ -1,16 +1,16 @@ -Infrastructure for testing +Infrastructure for Testing ========================== .. contents:: |toctitle| :local: -Dataverse internal +Dataverse Internal ------------------- To build and test a PR, we use a build named IQSS_Dataverse_Internal on jenkins.dataverse.org, which deploys the .war file to an AWS instance named dataverse-internal.iq.harvard.edu. Login to Jenkins requires a username and password. Check with Don Sizemore. Login to the dataverse-internal server requires a key, see Leonid. -Guides server +Guides Server ------------- There is also a guides build project named guides.dataverse.org. Any test builds of guides are deployed to a named directory** on guides.dataverse.org and can be found and tested by going to the existing guides, removing the part of the URL that contains the version, and browsing the resulting directory listing for the latest change. Login to the guides server requires a key, see Don Sizemore. diff --git a/doc/sphinx-guides/source/qa/tips-tricks.rst b/doc/sphinx-guides/source/qa/tips-tricks.rst deleted file mode 100644 index 738f701d33b..00000000000 --- a/doc/sphinx-guides/source/qa/tips-tricks.rst +++ /dev/null @@ -1,2 +0,0 @@ -Tips and tricks -=============== \ No newline at end of file diff --git a/doc/sphinx-guides/source/qa/workflow-qa-pr.rst b/doc/sphinx-guides/source/qa/workflow-qa-pr.rst deleted file mode 100644 index b2fae01da68..00000000000 --- a/doc/sphinx-guides/source/qa/workflow-qa-pr.rst +++ /dev/null @@ -1,7 +0,0 @@ -Workflow for completing on a PR -=============================== - -.. contents:: |toctitle| - :local: - -1. Assign the PR you are working on to yourself. \ No newline at end of file From 7d33c2b89c39b6de23c4db0ab1635073d5666af6 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 8 Nov 2023 19:41:38 +0100 Subject: [PATCH 071/689] doc: add sphinx-tabs to enable variants of docs --- doc/sphinx-guides/requirements.txt | 1 + doc/sphinx-guides/source/_static/docsdataverse_org.css | 8 ++++++++ doc/sphinx-guides/source/conf.py | 1 + 3 files changed, 10 insertions(+) diff --git a/doc/sphinx-guides/requirements.txt b/doc/sphinx-guides/requirements.txt index 028f07d11cb..e369536ba4e 100755 --- a/doc/sphinx-guides/requirements.txt +++ b/doc/sphinx-guides/requirements.txt @@ -10,3 +10,4 @@ Jinja2>=3.0.2,<3.1 # Sphinx - Additional modules sphinx-icon==0.1.2 +sphinx-tabs==3.4.4 diff --git a/doc/sphinx-guides/source/_static/docsdataverse_org.css b/doc/sphinx-guides/source/_static/docsdataverse_org.css index da4ba06ddd4..726abcc42bd 100755 --- a/doc/sphinx-guides/source/_static/docsdataverse_org.css +++ b/doc/sphinx-guides/source/_static/docsdataverse_org.css @@ -182,3 +182,11 @@ div.form-group .glyphicon.glyphicon-asterisk {font-size: .5em; vertical-align: t pre { white-space: pre-wrap; } + +div.sphinx-tabs { + width: 100%; +} + +li div.sphinx-tabs { + padding-left: 0; +} \ No newline at end of file diff --git a/doc/sphinx-guides/source/conf.py b/doc/sphinx-guides/source/conf.py index 0660ec3b071..6f7d7aff722 100755 --- a/doc/sphinx-guides/source/conf.py +++ b/doc/sphinx-guides/source/conf.py @@ -43,6 +43,7 @@ 'sphinx.ext.viewcode', 'sphinx.ext.graphviz', 'sphinxcontrib.icon', + 'sphinx_tabs.tabs', ] # Add any paths that contain templates here, relative to this directory. From 7a7bbec34b17991f268a991f6c8e2834739aacca Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 8 Nov 2023 19:42:20 +0100 Subject: [PATCH 072/689] doc(ct): document using IntelliJ Payara tools --- .../source/container/dev-usage.rst | 100 ++++++++++++++---- .../img/intellij-payara-add-new-config.png | Bin 0 -> 67013 bytes .../img/intellij-payara-config-add-server.png | Bin 0 -> 95876 bytes .../img/intellij-payara-config-deployment.png | Bin 0 -> 95101 bytes ...ntellij-payara-config-server-behaviour.png | Bin 0 -> 29639 bytes .../img/intellij-payara-config-server.png | Bin 0 -> 122860 bytes .../img/intellij-payara-config-startup.png | Bin 0 -> 81423 bytes .../img/intellij-payara-plugin-install.png | Bin 0 -> 98114 bytes .../img/intellij-payara-run-menu-reload.png | Bin 0 -> 111271 bytes .../img/intellij-payara-run-output.png | Bin 0 -> 49972 bytes .../img/intellij-payara-run-toolbar.png | Bin 0 -> 2550 bytes 11 files changed, 77 insertions(+), 23 deletions(-) create mode 100644 doc/sphinx-guides/source/container/img/intellij-payara-add-new-config.png create mode 100644 doc/sphinx-guides/source/container/img/intellij-payara-config-add-server.png create mode 100644 doc/sphinx-guides/source/container/img/intellij-payara-config-deployment.png create mode 100644 doc/sphinx-guides/source/container/img/intellij-payara-config-server-behaviour.png create mode 100644 doc/sphinx-guides/source/container/img/intellij-payara-config-server.png create mode 100644 doc/sphinx-guides/source/container/img/intellij-payara-config-startup.png create mode 100644 doc/sphinx-guides/source/container/img/intellij-payara-plugin-install.png create mode 100644 doc/sphinx-guides/source/container/img/intellij-payara-run-menu-reload.png create mode 100644 doc/sphinx-guides/source/container/img/intellij-payara-run-output.png create mode 100644 doc/sphinx-guides/source/container/img/intellij-payara-run-toolbar.png diff --git a/doc/sphinx-guides/source/container/dev-usage.rst b/doc/sphinx-guides/source/container/dev-usage.rst index b2547306b03..9fc9058eada 100644 --- a/doc/sphinx-guides/source/container/dev-usage.rst +++ b/doc/sphinx-guides/source/container/dev-usage.rst @@ -149,38 +149,92 @@ Rebuild and Running Images The safest way to redeploy code is to stop the running containers (with Ctrl-c if you started them in the foreground) and then build and run them again with ``mvn -Pct clean package docker:run``. -IntelliJ IDEA Ultimate and Payara Platform Tools -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +IDE-triggered re-deployments +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -If you have IntelliJ IDEA Ultimate (note that `free educational licenses `_ are available), you can install `Payara Platform Tools `_ which can dramatically improve your feedback loop when iterating on code. +You have at least two options: -The following steps are suggested: +1. Use plugins for different IDEs by Payara to ease the burden of redeploying an application during development to a running Payara application server. + Their guides contain `documentation on Payara IDE plugins `_. +2. Use a paid product like `JRebel `_. -- Go to the Payara admin console (either at https://localhost:4848 or http://localhost:4849) and undeploy the dataverse application under "Applications". -- Install Payara Platform Tools. -- Under "Server": +The main difference between the first and the second option is support for hot deploys of non-class files plus limitations in what the JVM HotswapAgent can do for you. +Find more `details in a blog article by JRebel `_. - - Click "Run" then "Edit Configurations". - - Click the plus sign and scroll down to Payara Server and click "Remote". - - For "Name" put "Payara in Docker" or something reasonable. - - Under "Application server" select a local directory that has the same version of Payara used in the container. This should match the version of Payara mentioned in the Installation Guide under :ref:`payara`. - - Change "Admin Server Port" to 4849. - - For username, put "admin". - - For password, put "admin". +When opting for Payara tools, please follow these steps: -- Under "Deployment": +1. | Download the Payara appserver to your machine, unzip and note the location for later. + | - See this guide for which version, in doubt lookup using + | ``mvn help:evaluate -Dexpression=payara.version -q -DforceStdout`` + | - Can be downloaded from `Maven Central `_. - - Click the plus button and clien "Artifact" then "dataverse:war". +2. Install Payara tools plugin in your IDE: -- Under "Startup/Connection": + .. tabs:: + .. group-tab:: IntelliJ + **Requires IntelliJ Ultimate!** + (Note that `free educational licenses `_ are available) - - Click "Debug" and change the port to 9009. + .. image:: img/intellij-payara-plugin-install.png -- Click "Run" and then "Debug Payara in Docker". This initial deployment will take some time. -- Go to http://localhost:8080/api/info/version and make sure the API is responding. -- Edit ``Info.java`` and make a small change to the ``/api/info/version`` code. -- Click "Run" then "Debugging Actions" then "Reload Changed Classes". The deployment should only take a few seconds. -- Go to http://localhost:8080/api/info/version and verify the change you made. +3. Configure a connection to the application server: + + .. tabs:: + .. group-tab:: IntelliJ + Create a new running configuration with a "Remote Payara". + (Open dialog by clicking "Run", then "Edit Configurations") + + .. image:: img/intellij-payara-add-new-config.png + + Click on "Configure" next to "Application Server". + Add an application server and select unzipped local directory. + + .. image:: img/intellij-payara-config-add-server.png + + Add admin password "admin" and add "building artifact" before launch. + Make sure to select the WAR, *not* exploded! + + .. image:: img/intellij-payara-config-server.png + + Go to "Deployment" tab and add the Dataverse WAR, *not* exploded!. + + .. image:: img/intellij-payara-config-deployment.png + + Go to "Startup/Connection" tab, select "Debug" and change port to ``9009``. + + .. image:: img/intellij-payara-config-startup.png + + You might want to tweak the hot deploy behaviour in the "Server" tab now. + "Update action" can be found in the run window (see below). + "Frame deactivation" means switching from IntelliJ window to something else, e.g. your browser. + *Note: static resources like properties, XHTML etc will only update when redeploying!* + + .. image:: img/intellij-payara-config-server-behaviour.png + +4. | Start all the containers. Follow the cheat sheet above, but take care to skip application deployment: + | - When using the Maven commands, append ``-Dapp.deploy.skip``. + | - When using Docker Compose, prepend the command with ``SKIP_DEPLOY=1``. + | - Note: the Admin Console can be reached at http://localhost:4848 or https://localhost:4949 +5. To deploy the application to the running server, use the configured tools to deploy. + Using the "Run" configuration only deploys and enables redeploys, while running "Debug" enables hot swapping of classes via JDWP. + + .. tabs:: + .. group-tab:: IntelliJ + Choose "Run" or "Debug" in the toolbar. + + .. image:: img/intellij-payara-run-toolbar.png + + Watch the WAR build and the deployment unfold. + Note the "Update" action button (see config to change its behaviour). + + .. image:: img/intellij-payara-run-output.png + + Manually hotswap classes in "Debug" mode via "Run" > "Debugging Actions" > "Reload Changed Classes". + + .. image:: img/intellij-payara-run-menu-reload.png + +Note: in the background, the bootstrap job will wait for Dataverse to be deployed and responsive. +When your IDE automatically opens the URL a newly deployed, not bootstrapped Dataverse application, it might take some more time and page refreshes until the job finishes. Using a Debugger ---------------- diff --git a/doc/sphinx-guides/source/container/img/intellij-payara-add-new-config.png b/doc/sphinx-guides/source/container/img/intellij-payara-add-new-config.png new file mode 100644 index 0000000000000000000000000000000000000000..d1c7a8f2777fc1b61c636d6b8751d9c3f38f580e GIT binary patch literal 67013 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%0DVzlRCP_ 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 F{{iUWK#c$Z literal 0 HcmV?d00001 diff --git a/doc/sphinx-guides/source/container/img/intellij-payara-config-add-server.png b/doc/sphinx-guides/source/container/img/intellij-payara-config-add-server.png new file mode 100644 index 0000000000000000000000000000000000000000..54ffbd1b713c855c3316ef61524ba18851e7dd9e GIT binary patch literal 95876 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 literal 0 HcmV?d00001 diff --git a/doc/sphinx-guides/source/container/img/intellij-payara-config-server-behaviour.png b/doc/sphinx-guides/source/container/img/intellij-payara-config-server-behaviour.png new file mode 100644 index 0000000000000000000000000000000000000000..5d23672e61415026943dddfb4998b2ae1e82707e GIT binary patch literal 29639 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 z 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 literal 0 HcmV?d00001 diff --git a/doc/sphinx-guides/source/container/img/intellij-payara-plugin-install.png b/doc/sphinx-guides/source/container/img/intellij-payara-plugin-install.png new file mode 100644 index 0000000000000000000000000000000000000000..7c6896574de27de558f7d40449a8080a7a6804fa GIT binary patch literal 98114 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 literal 0 HcmV?d00001 diff --git a/doc/sphinx-guides/source/container/img/intellij-payara-run-menu-reload.png b/doc/sphinx-guides/source/container/img/intellij-payara-run-menu-reload.png new file mode 100644 index 0000000000000000000000000000000000000000..b1fd8bea260af87759210b7393afb9cb18f32e24 GIT binary patch literal 111271 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

jj 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 EIH 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 literal 0 HcmV?d00001 From f386a1f375670902d13074c840a2ef421f4269bc Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 8 Nov 2023 19:48:24 +0100 Subject: [PATCH 073/689] doc(ct): add SKIP_DEPLOY to base image doc page #9590 --- doc/sphinx-guides/source/container/base-image.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/container/base-image.rst b/doc/sphinx-guides/source/container/base-image.rst index 1be0b992b2a..5f9ba31ce48 100644 --- a/doc/sphinx-guides/source/container/base-image.rst +++ b/doc/sphinx-guides/source/container/base-image.rst @@ -220,6 +220,11 @@ provides. These are mostly based on environment variables (very common with cont when new artifacts are copied into the running domain. Also, export Dataverse specific environment variables ``DATAVERSE_JSF_PROJECT_STAGE=Development`` and ``DATAVERSE_JSF_REFRESH_PERIOD=0`` to enable dynamic JSF page reloads. + * - ``SKIP_DEPLOY`` + - ``0`` + - Bool, ``0|1`` or ``false|true`` + - When active, do not deploy applications from ``DEPLOY_DIR`` (see below), just start the application server. + Will still execute any provided init scripts and only skip deployments within the default init scripts. * - ``DATAVERSE_HTTP_TIMEOUT`` - ``900`` - Seconds @@ -274,7 +279,8 @@ building upon it. You can also use these for references in scripts, etc. (Might be reused for Dataverse one day) * - ``DEPLOY_DIR`` - ``${HOME_DIR}/deployments`` - - Any EAR or WAR file, exploded WAR directory etc are autodeployed on start + - Any EAR or WAR file, exploded WAR directory etc are autodeployed on start. + See also ``SKIP_DEPLOY`` above. * - ``DOMAIN_DIR`` - ``${PAYARA_DIR}/glassfish`` ``/domains/${DOMAIN_NAME}`` - Path to root of the Payara domain applications will be deployed into. Usually ``${DOMAIN_NAME}`` will be ``domain1``. From 07e1c737dd538d254d43f57132a9b8939472d979 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 8 Nov 2023 19:53:21 +0100 Subject: [PATCH 074/689] doc(ct): leave note about fs perms for STORAGE_DIR --- doc/sphinx-guides/source/container/base-image.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/sphinx-guides/source/container/base-image.rst b/doc/sphinx-guides/source/container/base-image.rst index 5f9ba31ce48..b43c201fe9f 100644 --- a/doc/sphinx-guides/source/container/base-image.rst +++ b/doc/sphinx-guides/source/container/base-image.rst @@ -307,9 +307,9 @@ named Docker volume in these places to avoid data loss, gain performance and/or - Description * - ``STORAGE_DIR`` - ``/dv`` - - This place is writeable by the Payara user, making it usable as a place to store research data, customizations - or other. Images inheriting the base image should create distinct folders here, backed by different - mounted volumes. + - This place is writeable by the Payara user, making it usable as a place to store research data, customizations or other. + Images inheriting the base image should create distinct folders here, backed by different mounted volumes. + Enforce correct filesystem permissions on the mounted volume using ``fix-fs-perms.sh`` from :doc:`configbaker-image` or similar scripts. * - ``SECRETS_DIR`` - ``/secrets`` - Mount secrets or other here, being picked up automatically by From b7e079c86a9a8e15b3d68de3446db6ea449a23ad Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 8 Nov 2023 20:59:27 +0100 Subject: [PATCH 075/689] fix(docs): downgrade sphinx-tabs to be compatible with Sphinx 3 To be compatible with Python <3.10 and >=3.10 as well as the Sphinx dependencies, also upgrading to Sphinx 4. --- doc/sphinx-guides/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/sphinx-guides/requirements.txt b/doc/sphinx-guides/requirements.txt index e369536ba4e..63be499f741 100755 --- a/doc/sphinx-guides/requirements.txt +++ b/doc/sphinx-guides/requirements.txt @@ -2,12 +2,12 @@ # For your convenience, a solution for Python 3.10 is provided below # but we would prefer that you use the same version of Sphinx # (below on the < 3.10 line) that is used to build the production guides. -Sphinx==3.5.4 ; python_version < '3.10' -Sphinx==5.3.0 ; python_version >= '3.10' +Sphinx==4.5.0; python_version < '3.10' +Sphinx==5.3.0; python_version >= '3.10' # Necessary workaround for ReadTheDocs for Sphinx 3.x - unnecessary as of Sphinx 4.5+ Jinja2>=3.0.2,<3.1 # Sphinx - Additional modules sphinx-icon==0.1.2 -sphinx-tabs==3.4.4 +sphinx-tabs==3.4.0 From 9397cc38690fe1b8adcffd7bb82080488e6e85ad Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 8 Nov 2023 22:58:16 +0100 Subject: [PATCH 076/689] fix(mail): lookup legacy mail session programmatically #7424 Using @Resource on the field triggers deployments to fail if the resource is not provided by the app server. Using a programmatic lookup, we can catch and ignore the exception. --- .../dataverse/util/MailSessionProducer.java | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java b/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java index 93f3ac29e44..25f5970274e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java @@ -9,8 +9,12 @@ import jakarta.mail.PasswordAuthentication; import jakarta.mail.Session; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; import java.util.List; import java.util.Properties; +import java.util.logging.Level; import java.util.logging.Logger; @ApplicationScoped @@ -41,13 +45,24 @@ public class MailSessionProducer { Session systemMailSession; /** - * Inject the application server provided (user defined) javamail resource to enable backwards compatibility. + * Cache the application server provided (user defined) javamail resource to enable backwards compatibility. + * No direct JNDI lookup on the field to avoid deployment failures when not present. * @deprecated This should be removed with the next major release of Dataverse, as it would be a breaking change. */ @Deprecated(forRemoval = true, since = "6.1") - @Resource(name = "mail/notifyMailSession") Session appserverProvidedSession; + public MailSessionProducer() { + try { + // Do JNDI lookup of legacy mail session programmatically to avoid deployment errors when not found. + Context initialContext = new InitialContext(); + this.appserverProvidedSession = (Session)initialContext.lookup("mail/notifyMailSession"); + } catch (NamingException e) { + // This exception simply means the appserver did not provide the legacy mail session. + // Debug level output is just fine. + logger.log(Level.FINE, "Error during mail resource lookup", e); + } + } @Produces @Named("mail/systemSession") @@ -104,4 +119,14 @@ Properties getMailProperties() { return configuration; } + /** + * Determine if the session returned by {@link #getSession()} has been provided by the application server + * @return True if injected as resource from app server, false otherwise + * @deprecated This is supposed to be removed when {@link #appserverProvidedSession} is removed. + */ + @Deprecated(forRemoval = true, since = "6.1") + public boolean hasSessionFromAppServer() { + return this.appserverProvidedSession != null; + } + } From d650725f794ae1e471939e3b48b18a24e5456ffa Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 8 Nov 2023 23:08:26 +0100 Subject: [PATCH 077/689] build(mail): add .map files to be included in resources #7424 Without this change, the javamail maps would not be included in the artifact and trigger error messages in the logs about them being missed. The error message will still be present as long as payara/Payara#6254 is not fixed, released and we updated to a newer version of Payara. --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 843afc22acb..75b0cf1100f 100644 --- a/pom.xml +++ b/pom.xml @@ -688,6 +688,7 @@ **/firstNames/*.* **/*.xsl **/services/* + **/*.map From 11826d9a5decf521daba183035bfd02ec248fc89 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Thu, 9 Nov 2023 00:07:08 +0100 Subject: [PATCH 078/689] feat(mail): add startup checks for mail configuration #7424 During the deployment of Dataverse we check for conditions of the mail system that might not be done as people intend to use it. We'll only issue warnings in the log messages, nothing critical here. --- .../settings/ConfigCheckService.java | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/ConfigCheckService.java b/src/main/java/edu/harvard/iq/dataverse/settings/ConfigCheckService.java index a2c3f53d59d..c4f4fc6610b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/ConfigCheckService.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/ConfigCheckService.java @@ -1,16 +1,22 @@ package edu.harvard.iq.dataverse.settings; +import edu.harvard.iq.dataverse.MailServiceBean; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean.Key; import edu.harvard.iq.dataverse.util.FileUtil; - +import edu.harvard.iq.dataverse.util.MailSessionProducer; import jakarta.annotation.PostConstruct; import jakarta.ejb.DependsOn; import jakarta.ejb.Singleton; import jakarta.ejb.Startup; +import jakarta.inject.Inject; +import jakarta.mail.internet.InternetAddress; + import java.io.IOException; import java.nio.file.FileSystemException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; +import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; @@ -20,6 +26,11 @@ public class ConfigCheckService { private static final Logger logger = Logger.getLogger(ConfigCheckService.class.getCanonicalName()); + + @Inject + MailSessionProducer mailSessionProducer; + @Inject + MailServiceBean mailService; public static class ConfigurationError extends RuntimeException { public ConfigurationError(String message) { @@ -32,6 +43,9 @@ public void startup() { if (!checkSystemDirectories()) { throw new ConfigurationError("Not all configuration checks passed successfully. See logs above."); } + + // Only checks resulting in warnings, nothing critical that needs to stop deployment + checkSystemMailSetup(); } /** @@ -77,5 +91,36 @@ public boolean checkSystemDirectories() { } return success; } + + /** + * This method is not expected to make a deployment fail, but send out clear warning messages about missing or + * wrong configuration settings. + */ + public void checkSystemMailSetup() { + // Check if a system mail setting has been provided or issue warning about disabled mail notifications + Optional mailAddress = mailService.getSystemAddress(); + + // Not present -> warning + if (mailAddress.isEmpty()) { + logger.warning("Could not find a system mail setting in database (key :" + Key.SystemEmail + ", deprecated) or JVM option '" + JvmSettings.SYSTEM_EMAIL.getScopedKey() + "'"); + logger.warning("Mail notifications and system messages are deactivated until you provide a configuration"); + } + + // If there is an app server provided mail config, let's determine if the setup is matching + // TODO: when support for appserver provided mail session goes away, this code can be deleted + if (mailSessionProducer.hasSessionFromAppServer()) { + if (mailAddress.isEmpty()) { + logger.warning("Found a mail session provided by app server, but no system mail address (see logs above)"); + // Check if the "from" in the session is the same as the system mail address (see issue 4210) + } else { + String sessionFrom = mailSessionProducer.getSession().getProperty("mail.from"); + if (! mailAddress.get().toString().equals(sessionFrom)) { + logger.warning(() -> String.format( + "Found app server mail session provided 'from' (%s) does not match system mail setting (%s)", + sessionFrom, mailAddress.get())); + } + } + } + } } From d32446ec1a72f23234136f322d0d955c7ef8d77b Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 9 Nov 2023 12:33:36 -0500 Subject: [PATCH 079/689] upgrade Sphinx, add Markdown support #10101 #10111 We don't seem to be using intersphinx and I got an error about it so i simply removed it. I also got rid of the Python 3.10 vs 3.11 divide. It's no longer relevant and some older servers were holding us back. Everyone should use the same (latest!) Sphinx. I included a tiny conclusion.md file as a demo, but it can be removed, of course. --- doc/sphinx-guides/requirements.txt | 15 +++++---------- doc/sphinx-guides/source/conf.py | 5 +---- doc/sphinx-guides/source/qa/conclusion.md | 11 +++++++++++ doc/sphinx-guides/source/qa/index.rst | 10 +--------- 4 files changed, 18 insertions(+), 23 deletions(-) create mode 100644 doc/sphinx-guides/source/qa/conclusion.md diff --git a/doc/sphinx-guides/requirements.txt b/doc/sphinx-guides/requirements.txt index 028f07d11cb..f55d2b9e518 100755 --- a/doc/sphinx-guides/requirements.txt +++ b/doc/sphinx-guides/requirements.txt @@ -1,12 +1,7 @@ -# Developers, please use Python 3.9 or lower to build the guides. -# For your convenience, a solution for Python 3.10 is provided below -# but we would prefer that you use the same version of Sphinx -# (below on the < 3.10 line) that is used to build the production guides. -Sphinx==3.5.4 ; python_version < '3.10' -Sphinx==5.3.0 ; python_version >= '3.10' +Sphinx==7.2.6 -# Necessary workaround for ReadTheDocs for Sphinx 3.x - unnecessary as of Sphinx 4.5+ -Jinja2>=3.0.2,<3.1 - -# Sphinx - Additional modules +# inline icons sphinx-icon==0.1.2 + +# Markdown support +myst-parser==2.0.0 diff --git a/doc/sphinx-guides/source/conf.py b/doc/sphinx-guides/source/conf.py index 0660ec3b071..a264ff23db0 100755 --- a/doc/sphinx-guides/source/conf.py +++ b/doc/sphinx-guides/source/conf.py @@ -38,11 +38,11 @@ # ones. extensions = [ 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', 'sphinx.ext.graphviz', 'sphinxcontrib.icon', + 'myst_parser', ] # Add any paths that contain templates here, relative to this directory. @@ -430,9 +430,6 @@ # If false, no index is generated. #epub_use_index = True - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} # Suppress "WARNING: unknown mimetype for ..." https://github.com/IQSS/dataverse/issues/3391 suppress_warnings = ['epub.unknown_project_files'] rst_prolog = """ diff --git a/doc/sphinx-guides/source/qa/conclusion.md b/doc/sphinx-guides/source/qa/conclusion.md new file mode 100644 index 00000000000..233dc3cdf3d --- /dev/null +++ b/doc/sphinx-guides/source/qa/conclusion.md @@ -0,0 +1,11 @@ +Conclusion +========== + +QA is awesome. Do you know what else is awesome? Markdown. + +It's easy to create a [link](https://dataverse.org), for example, and nested bullets don't need extra indentation: + +- foo + - one + - two +- bar diff --git a/doc/sphinx-guides/source/qa/index.rst b/doc/sphinx-guides/source/qa/index.rst index c0c617d561d..dd8c046fddc 100755 --- a/doc/sphinx-guides/source/qa/index.rst +++ b/doc/sphinx-guides/source/qa/index.rst @@ -11,12 +11,4 @@ QA Guide manual-testing test-automation-integration other-approaches - - - - - - - - - + conclusion From 6beafcef4855c2a35cfe6d61408a5625a285885e Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Sat, 11 Nov 2023 22:09:22 -0500 Subject: [PATCH 080/689] Change format to MD of the QA guide --- doc/sphinx-guides/source/index.rst | 2 +- doc/sphinx-guides/source/qa/conclusion.md | 11 -------- doc/sphinx-guides/source/qa/index.md | 10 +++++++ doc/sphinx-guides/source/qa/index.rst | 14 ---------- .../{manual-testing.rst => manual-testing.md} | 27 +++++++++---------- ...her-approaches.rst => other-approaches.md} | 24 ++++++++--------- .../source/qa/{overview.rst => overview.md} | 23 ++++++++-------- ...ormance-tests.rst => performance-tests.md} | 21 ++++++++------- ...ion.rst => test-automation-integration.md} | 24 ++++++++--------- ...tructure.rst => testing-infrastructure.md} | 15 +++++------ 10 files changed, 77 insertions(+), 94 deletions(-) delete mode 100644 doc/sphinx-guides/source/qa/conclusion.md create mode 100644 doc/sphinx-guides/source/qa/index.md delete mode 100755 doc/sphinx-guides/source/qa/index.rst rename doc/sphinx-guides/source/qa/{manual-testing.rst => manual-testing.md} (92%) rename doc/sphinx-guides/source/qa/{other-approaches.rst => other-approaches.md} (95%) rename doc/sphinx-guides/source/qa/{overview.rst => overview.md} (95%) rename doc/sphinx-guides/source/qa/{performance-tests.rst => performance-tests.md} (91%) rename doc/sphinx-guides/source/qa/{test-automation-integration.rst => test-automation-integration.md} (78%) rename doc/sphinx-guides/source/qa/{testing-infrastructure.rst => testing-infrastructure.md} (82%) diff --git a/doc/sphinx-guides/source/index.rst b/doc/sphinx-guides/source/index.rst index 9d3d49ef4f2..3184160b387 100755 --- a/doc/sphinx-guides/source/index.rst +++ b/doc/sphinx-guides/source/index.rst @@ -20,7 +20,7 @@ These documentation guides are for the |version| version of Dataverse. To find g developers/index container/index style/index - qa/index + qa/index.md How the Guides Are Organized ---------------------------- diff --git a/doc/sphinx-guides/source/qa/conclusion.md b/doc/sphinx-guides/source/qa/conclusion.md deleted file mode 100644 index 233dc3cdf3d..00000000000 --- a/doc/sphinx-guides/source/qa/conclusion.md +++ /dev/null @@ -1,11 +0,0 @@ -Conclusion -========== - -QA is awesome. Do you know what else is awesome? Markdown. - -It's easy to create a [link](https://dataverse.org), for example, and nested bullets don't need extra indentation: - -- foo - - one - - two -- bar diff --git a/doc/sphinx-guides/source/qa/index.md b/doc/sphinx-guides/source/qa/index.md new file mode 100644 index 00000000000..c190d823bef --- /dev/null +++ b/doc/sphinx-guides/source/qa/index.md @@ -0,0 +1,10 @@ +# QA Guide + +```{toctree} +overview.md +testing-infrastructure.md +performance-tests.md +manual-testing.md +test-automation-integration.md +other-approaches.md +``` \ No newline at end of file diff --git a/doc/sphinx-guides/source/qa/index.rst b/doc/sphinx-guides/source/qa/index.rst deleted file mode 100755 index dd8c046fddc..00000000000 --- a/doc/sphinx-guides/source/qa/index.rst +++ /dev/null @@ -1,14 +0,0 @@ -QA Guide -======== - -**Contents:** - -.. toctree:: - - overview - testing-infrastructure - performance-tests - manual-testing - test-automation-integration - other-approaches - conclusion diff --git a/doc/sphinx-guides/source/qa/manual-testing.rst b/doc/sphinx-guides/source/qa/manual-testing.md similarity index 92% rename from doc/sphinx-guides/source/qa/manual-testing.rst rename to doc/sphinx-guides/source/qa/manual-testing.md index 8e50e6b6b08..bf6f16f7911 100644 --- a/doc/sphinx-guides/source/qa/manual-testing.rst +++ b/doc/sphinx-guides/source/qa/manual-testing.md @@ -1,23 +1,22 @@ -Manual Testing Approach -======================= +# Manual Testing Approach -.. contents:: |toctitle| - :local: +```{contents} +:depth: 3 +``` +## Introduction -Introduction ------------- We use a risk-based, manual testing approach to achieve the most benefit with limited resources. This means we want to catch bugs where they are likely to exist, ensure core functions work, and failures do not have catastrophic results. In practice this means we do a brief positive check of core functions on each build called a smoke test, we test the most likely place for new bugs to exist, the area where things have changed, and attempt to prevent catastrophic failure by asking about the scope and reach of the code and how failures may occur. If it seems possible through user error or some other occurrence that such a serious failure will occur, we try to make it happen in the test environment. If the code has a UI component, we also do a limited amount of browser compatibility testing using Chrome, Firefox, and Safari browsers. We do not currently do UX or accessibility testing on a regular basis, though both have been done product-wide by the Design group and by the community. -Examining a Pull Pequest for Test Cases: ----------------------------------------- -What Problem Does it Solve? -++++++++++++++++++++++++++++++++++++++++++++ +## Examining a Pull Pequest for Test Cases: + +### What Problem Does it Solve? + Read the top part of the pull request for a description, notes for reviewers, and usually a how-to test section. Does it make sense? If not, read the underlying ticket it closes, and any release notes or documentation. Knowing in general what it does helps you to think about how to approach it. -How is it Configured? -+++++++++++++++++++++ +### How is it Configured? + Most pull requests do not have any special configuration and are enabled on deployment, but some do. Configuration is part of testing. An admin will need to follow these instructions so try them out. Plus, that is the only way you will get it working to test it! Identify test cases by examining the problem report or feature description and any documentation of functionality. Look for statements or assertions about functions, what it does, as well as conditions or conditional behavior. These become your test cases. Think about how someone might make a mistake using it and try it. Does it fail gracefully or in a confusing or worse, damaging manner? Also, consider whether this pull request may interact with other functionality and try some spot checks there. For instance, if new metadata fields are added, try the export feature. Of course, try the suggestions under how to test. Those may be sufficient, but you should always think about it based on what it does. @@ -32,8 +31,8 @@ Check permissions. Is this feature limited to a specific set of users? Can it be Think about risk. Is the feature or function part of a critical area such as permissions? Does the functionality modify data? You may do more testing when the risk is higher. -Smoke Test ------------ +## Smoke Test + 1. Go to the homepage on https://dataverse-internal.iq.harvard.edu. Scroll to the bottom to ensure the build number is the one you intend to test from Jenkins. 2. Create a new user: I use a formulaic name with my initials and date and make the username and password the same, eg. kc080622. diff --git a/doc/sphinx-guides/source/qa/other-approaches.rst b/doc/sphinx-guides/source/qa/other-approaches.md similarity index 95% rename from doc/sphinx-guides/source/qa/other-approaches.rst rename to doc/sphinx-guides/source/qa/other-approaches.md index bd92e7d22d8..b50d9d0cf11 100644 --- a/doc/sphinx-guides/source/qa/other-approaches.rst +++ b/doc/sphinx-guides/source/qa/other-approaches.md @@ -1,13 +1,13 @@ -Other approaches to deploying and testing -========================================= +# Other approaches to deploying and testing -.. contents:: |toctitle| - :local: +```{contents} +:depth: 3 +``` This workflow is fine for a single person testing a PR, one at a time. It would be awkward or impossible if there were multiple people wanting to test different PRs at the same time. I’m assuming if a developer is testing, they would likely just deploy to their dev environment. That might be ok but not sure the env is fully configured enough to offer a real-world testing scenario. An alternative might be to spin an EC2 branch on AWS, potentially using sample data. This can take some time so another option might be to spin up a few, persistent AWS instances with sample data this way, one per tester, and just deploy new builds there when you want to test. You could even configure Jenkins projects for each if desired to maintain consistency in how they’re built. -Tips and tricks ---------------- +## Tips and tricks + - Start testing simply, with the most obvious test. You don’t need to know all your tests upfront. As you gain comfort and understanding of how it works, try more tests until you are done. If it is a complex feature, jot down your tests in an outline format, some beforehand as a guide, and some after as things occur to you. Save the doc in a testing folder (I have one on Google Drive). This potentially will help with future testing. - When in doubt, ask someone. If you are confused about how something is working, it may be something you have missed, or it could be a documentation issue, or it could be a bug! Talk to the code reviewer and the contributor/developer for their opinion and advice. @@ -17,8 +17,8 @@ Tips and tricks - When testing an optional feature that requires configuration, do a smoke test without the feature configured and then with it configured. That way you know that folks using the standard config are unaffected by the option if they choose not to configure it. - Back up your DB before applying an irreversible DB update and you are using a persistent/reusable platform. Just in case it fails, and you need to carry on testing something else you can use the backup. -Workflow for Completing QA on a PR ------------------------------------ +## Workflow for Completing QA on a PR + 1. Assign the PR you are working on to yourself. @@ -106,8 +106,8 @@ Workflow for Completing QA on a PR Just a housekeeping move if the PR is from IQSS. Click the delete branch button where the merge button had been. There is no deletion for outside contributions. -Checklist for Completing QA on a PR ------------------------------------- +## Checklist for Completing QA on a PR + 1. Build the docs 2. Smoke test the pr @@ -115,8 +115,8 @@ Checklist for Completing QA on a PR 4. Regression test 5. Test any upgrade instructions -Checklist for QA on Release ---------------------------- +## Checklist for QA on Release + 1. Review Consolidated Release Notes, in particular upgrade instructions. 2. Conduct performance testing and compare with the previous release. diff --git a/doc/sphinx-guides/source/qa/overview.rst b/doc/sphinx-guides/source/qa/overview.md similarity index 95% rename from doc/sphinx-guides/source/qa/overview.rst rename to doc/sphinx-guides/source/qa/overview.md index 153fab1a28f..51b38ee0921 100644 --- a/doc/sphinx-guides/source/qa/overview.rst +++ b/doc/sphinx-guides/source/qa/overview.md @@ -1,26 +1,25 @@ -Overview -======== +# Overview -.. contents:: |toctitle| - :local: +```{contents} +:depth: 3 +``` +## Introduction -Introduction ------------- This document describes the testing process used by QA at IQSS and provides a guide for others filling in for that role. Please note that many variations are possible, and the main thing is to catch bugs and provide a good quality product to the user community. -Workflow --------- +## Workflow + The basic workflow is bugs or feature requests are submitted to GitHub by the community or by team members as issues. These issues are prioritized and added to a two-week sprint that is reflected on the GitHub Kanban board. As developers work on these issues, a GitHub branch is produced, code is contributed, and a pull request is made to merge these new changes back into the common develop branch and ultimately released as part of the product. Before a pull request is merged it must be reviewed by a member of the development team from a coding perspective, it must pass automated integration tests before moving to QA. There it is tested manually, exercising the UI using three common browser types and any business logic it implements. Depending on whether the code modifies existing code or is completely new, a smoke test of core functionality is performed and some basic regression testing of modified or related code is performed. Any documentation provided is used to understand the feature and any assertions are tested. Once this passes and any bugs that are found are corrected, the automated integration tests are confirmed to be passing, the PR is merged into development, the PR is closed, and the branch is deleted. At this point, the pr moves from the QA column automatically into the Done column and the process repeats with the next pr until it is decided to make a release. -Release Cadence and Sprints ---------------------------- +## Release Cadence and Sprints + A release likely spans multiple two-week sprints. Each sprint represents the priorities for that time and is sized so that the team can reasonably complete most of the work on time. This is a goal to help with planning, it is not a strict requirement. Some issues from the previous sprint may remain and likely be included in the next sprint but occasionally may be deprioritized and deferred to another time. The decision to make a release can be based on the time since the last release, some important feature needed by the community or contractual deadline, or some other logical reason to package the work completed into a named release and posted to the releases section on GitHub. -Performance Testing and Deployment ----------------------------------- +## Performance Testing and Deployment + The final testing activity before producing a release is performance testing. This could be done throughout the release cycle but since it is time-consuming it is done once near the end. Using a load-generating tool named Locust, it loads the statistically most loaded pages, according to Google Analytics, that is 50% homepage and 50% some type of dataset page. Since dataset page weight also varies by the number of files, a selection of about 10 datasets with varying file counts is used. The pages are called randomly as a guest user with increasing levels of user load, from 1 user to 250 users. Typical daily loads in production are around the 50-user level. Though the simulated user level does have a modest amount of random think time before repeated calls, from 5-20 seconds (I believe), it is not a real-world load so direct comparisons to production are not reliable. Instead, we compare performance to prior versions of the product and based on how that performed in production we have some idea whether this might be similar in performance or whether there is some undetected issue that appears under load, such as inefficient or too many DB queries per page. Once the performance has been tested and recorded in a Google spreadsheet for this proposed version, the release will be prepared and posted. diff --git a/doc/sphinx-guides/source/qa/performance-tests.rst b/doc/sphinx-guides/source/qa/performance-tests.md similarity index 91% rename from doc/sphinx-guides/source/qa/performance-tests.rst rename to doc/sphinx-guides/source/qa/performance-tests.md index 1bfde798100..7075d7f1776 100644 --- a/doc/sphinx-guides/source/qa/performance-tests.rst +++ b/doc/sphinx-guides/source/qa/performance-tests.md @@ -1,21 +1,22 @@ -Performance Testing -=================== +# Performance Testing -.. contents:: |toctitle| - :local: +```{contents} +:depth: 3 +``` + +## Introduction -Introduction ------------- To run performance tests, we have a performance test cluster on AWS that employs web, database, and Solr. The database contains a copy of production that is updated weekly on Sundays. To ensure the homepage content is consistent between test runs across releases, two scripts set the datasets that will appear on the homepage. There is a script on the web server in the default CentOS user dir and one on the database server in the default CentOS user dir. Run these scripts before conducting the tests. -Access ------- +## Access + Access to performance cluster instances requires ssh keys, see Leonid. The cluster itself is normally not running to reduce costs. To turn on the cluster, log on to the demo server and run the perfenv scripts from the centos default user dir. Access to the demo requires an ssh key, see Leonid. -Special Notes ⚠️ ------------------ +## Special Notes ⚠️ + Please note the performance database is also used occasionally by Julian and the Curation team to generate prod reports so a courtesy check with Julian would be good before taking over the env. + Executing the Performance Script -------------------------------- To execute the performance test script, you need to install a local copy of the database-helper-scripts project (https://github.com/IQSS/dataverse-helper-scripts), written by Raman. I have since produced a stripped-down script that calls just the DB and ds and works with python3. diff --git a/doc/sphinx-guides/source/qa/test-automation-integration.rst b/doc/sphinx-guides/source/qa/test-automation-integration.md similarity index 78% rename from doc/sphinx-guides/source/qa/test-automation-integration.rst rename to doc/sphinx-guides/source/qa/test-automation-integration.md index 13c48105f91..5e9d00cd461 100644 --- a/doc/sphinx-guides/source/qa/test-automation-integration.rst +++ b/doc/sphinx-guides/source/qa/test-automation-integration.md @@ -1,15 +1,15 @@ -Test automation and integration test -==================================== +# Test automation and integration test -.. contents:: |toctitle| - :local: +```{contents} +:depth: 3 +``` This test suite is added to and maintained by development. It is generally advisable for code contributors to add integration tests when adding new functionality. The approach here is one of code coverage: exercise as much of the code base’s code paths as possible, every time to catch bugs. This type of approach is often used to give contributing developers confidence that their code didn’t introduce any obvious, major issues and is run on each commit. Since it is a broad set of tests, it is not clear whether any specific, conceivable test is run but it does add a lot of confidence that the code base is functioning due to its reach and consistency. -Building and Deploying a Pull Request from Jenkins to Dataverse-Internal: -------------------------------------------------------------------------- +## Building and Deploying a Pull Request from Jenkins to Dataverse-Internal: + 1. Log on to GitHub, go to projects, dataverse to see Kanban board, select a pull request to test from the QA queue. @@ -17,12 +17,12 @@ Building and Deploying a Pull Request from Jenkins to Dataverse-Internal: 3. Log on to jenkins.dataverse.org, select the IQSS_Dataverse_Internal project, and configure the repository URL and branch specifier to match the ones from the pull request. For example: - - 8372-gdcc-xoai-library has IQSS implied - | **Repository URL:** https://github.com/IQSS/dataverse.git - | **Branch specifier:** \*/8372-gdcc-xoai-library - - GlobalDataverseCommunityConsortium:GDCC/DC-3B - | **Repository URL:** https://github.com/GlobalDataverseCommunityConsortium/dataverse.git - | **Branch specifier:** \*/GDCC/DC-3B. + * 8372-gdcc-xoai-library has IQSS implied + - **Repository URL:** https://github.com/IQSS/dataverse.git + - **Branch specifier:** */8372-gdcc-xoai-library + * GlobalDataverseCommunityConsortium:GDCC/DC-3B + - **Repository URL:** https://github.com/GlobalDataverseCommunityConsortium/dataverse.git + - **Branch specifier:** */GDCC/DC-3B. 4. Click Build Now and note the build number in progress. diff --git a/doc/sphinx-guides/source/qa/testing-infrastructure.rst b/doc/sphinx-guides/source/qa/testing-infrastructure.md similarity index 82% rename from doc/sphinx-guides/source/qa/testing-infrastructure.rst rename to doc/sphinx-guides/source/qa/testing-infrastructure.md index d35bc6e9a23..fb66bc4d099 100644 --- a/doc/sphinx-guides/source/qa/testing-infrastructure.rst +++ b/doc/sphinx-guides/source/qa/testing-infrastructure.md @@ -1,16 +1,15 @@ -Infrastructure for Testing -========================== +# Infrastructure for Testing -.. contents:: |toctitle| - :local: +```{contents} +:depth: 3 +``` +## Dataverse Internal -Dataverse Internal -------------------- To build and test a PR, we use a build named IQSS_Dataverse_Internal on jenkins.dataverse.org, which deploys the .war file to an AWS instance named dataverse-internal.iq.harvard.edu. Login to Jenkins requires a username and password. Check with Don Sizemore. Login to the dataverse-internal server requires a key, see Leonid. -Guides Server -------------- +## Guides Server + There is also a guides build project named guides.dataverse.org. Any test builds of guides are deployed to a named directory** on guides.dataverse.org and can be found and tested by going to the existing guides, removing the part of the URL that contains the version, and browsing the resulting directory listing for the latest change. Login to the guides server requires a key, see Don Sizemore. From a3d323599be4bcc6ad688a8b99135bd4447fbb02 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Mon, 13 Nov 2023 16:07:53 -0500 Subject: [PATCH 081/689] various improvements to the QA Guide #10101 --- doc/sphinx-guides/source/developers/intro.rst | 2 + .../source/developers/testing.rst | 4 + .../source/developers/version-control.rst | 2 + doc/sphinx-guides/source/qa/index.md | 4 +- doc/sphinx-guides/source/qa/manual-testing.md | 31 +++---- .../source/qa/other-approaches.md | 91 +++++++++---------- doc/sphinx-guides/source/qa/overview.md | 15 ++- .../source/qa/performance-tests.md | 6 +- .../source/qa/test-automation-integration.md | 35 ------- .../source/qa/test-automation.md | 35 +++++++ .../source/qa/testing-infrastructure.md | 12 ++- 11 files changed, 119 insertions(+), 118 deletions(-) delete mode 100644 doc/sphinx-guides/source/qa/test-automation-integration.md create mode 100644 doc/sphinx-guides/source/qa/test-automation.md diff --git a/doc/sphinx-guides/source/developers/intro.rst b/doc/sphinx-guides/source/developers/intro.rst index a01a8066897..3eddfbe8d2d 100755 --- a/doc/sphinx-guides/source/developers/intro.rst +++ b/doc/sphinx-guides/source/developers/intro.rst @@ -37,6 +37,8 @@ Roadmap For the Dataverse Software development roadmap, please see https://www.iq.harvard.edu/roadmap-dataverse-project +.. _kanban-board: + Kanban Board ------------ diff --git a/doc/sphinx-guides/source/developers/testing.rst b/doc/sphinx-guides/source/developers/testing.rst index abecaa09fad..57733f25406 100755 --- a/doc/sphinx-guides/source/developers/testing.rst +++ b/doc/sphinx-guides/source/developers/testing.rst @@ -426,6 +426,10 @@ target/coverage-it/index.html is the place to start reading the code coverage re Load/Performance Testing ------------------------ +See also :doc:`/qa/performance-tests` in the QA Guide. + +.. _locust: + Locust ~~~~~~ diff --git a/doc/sphinx-guides/source/developers/version-control.rst b/doc/sphinx-guides/source/developers/version-control.rst index 31fc0a4e602..f46411ebd7f 100644 --- a/doc/sphinx-guides/source/developers/version-control.rst +++ b/doc/sphinx-guides/source/developers/version-control.rst @@ -34,6 +34,8 @@ The "master" Branch The "`master `_" branch represents released versions of the Dataverse Software. As mentioned in the :doc:`making-releases` section, at release time we update the master branch to include all the code for that release. Commits are never made directly to master. Rather, master is updated only when we merge code into it from the "develop" branch. +.. _develop-branch: + The "develop" Branch ******************** diff --git a/doc/sphinx-guides/source/qa/index.md b/doc/sphinx-guides/source/qa/index.md index c190d823bef..08deb7ee27d 100644 --- a/doc/sphinx-guides/source/qa/index.md +++ b/doc/sphinx-guides/source/qa/index.md @@ -5,6 +5,6 @@ overview.md testing-infrastructure.md performance-tests.md manual-testing.md -test-automation-integration.md +test-automation.md other-approaches.md -``` \ No newline at end of file +``` diff --git a/doc/sphinx-guides/source/qa/manual-testing.md b/doc/sphinx-guides/source/qa/manual-testing.md index bf6f16f7911..9f365aae59f 100644 --- a/doc/sphinx-guides/source/qa/manual-testing.md +++ b/doc/sphinx-guides/source/qa/manual-testing.md @@ -9,23 +9,23 @@ We use a risk-based, manual testing approach to achieve the most benefit with li If it seems possible through user error or some other occurrence that such a serious failure will occur, we try to make it happen in the test environment. If the code has a UI component, we also do a limited amount of browser compatibility testing using Chrome, Firefox, and Safari browsers. We do not currently do UX or accessibility testing on a regular basis, though both have been done product-wide by the Design group and by the community. -## Examining a Pull Pequest for Test Cases: +## Examining a Pull Request for Test Cases -### What Problem Does it Solve? +### What Problem Does It Solve? -Read the top part of the pull request for a description, notes for reviewers, and usually a how-to test section. Does it make sense? If not, read the underlying ticket it closes, and any release notes or documentation. Knowing in general what it does helps you to think about how to approach it. +Read the top part of the pull request for a description, notes for reviewers, and usually a "how to test" section. Does it make sense? If not, read the underlying issue it closes, and any release notes or documentation. Knowing in general what it does helps you to think about how to approach it. -### How is it Configured? +### How is It Configured? -Most pull requests do not have any special configuration and are enabled on deployment, but some do. Configuration is part of testing. An admin will need to follow these instructions so try them out. Plus, that is the only way you will get it working to test it! +Most pull requests do not have any special configuration and are enabled on deployment, but some do. Configuration is part of testing. A sysadmin or superuser will need to follow these instructions so try them out. Plus, that is the only way you will get it working to test it! -Identify test cases by examining the problem report or feature description and any documentation of functionality. Look for statements or assertions about functions, what it does, as well as conditions or conditional behavior. These become your test cases. Think about how someone might make a mistake using it and try it. Does it fail gracefully or in a confusing or worse, damaging manner? Also, consider whether this pull request may interact with other functionality and try some spot checks there. For instance, if new metadata fields are added, try the export feature. Of course, try the suggestions under how to test. Those may be sufficient, but you should always think about it based on what it does. +Identify test cases by examining the problem report or feature description and any documentation of functionality. Look for statements or assertions about functions, what it does, as well as conditions or conditional behavior. These become your test cases. Think about how someone might make a mistake using it and try it. Does it fail gracefully or in a confusing or worse, damaging manner? Also, consider whether this pull request may interact with other functionality and try some spot checks there. For instance, if new metadata fields are added, try the export feature. Of course, try the suggestions under "how to test." Those may be sufficient, but you should always think about the pull request based on what it does. Try adding, modifying, and deleting any objects involved. This is probably covered by using the feature but a good basic approach to keep in mind. -Make sure any server logging is appropriate. You should tail the server log while running your tests. Watch for unreported errors or stack traces especially chatty logging. If you do find a bug you will need to report the stack trace from the server.log +Make sure any server logging is appropriate. You should tail the server log while running your tests. Watch for unreported errors or stack traces especially chatty logging. If you do find a bug you will need to report the stack trace from the server.log. Err on the side of providing the developer too much of server.log rather than too little. -Exercise the UI if there is one. I tend to use Chrome for most of my basic testing as it’s used twice as much as the next most commonly used browser, according to our site’s Google Analytics. I first go through all the options in the UI. Then, if all works, I’ll spot-check using Firefox and Safari. +Exercise the UI if there is one. We tend to use Chrome for most of my basic testing as it's used twice as much as the next most commonly used browser, according to our site's Google Analytics. First go through all the options in the UI. Then, if all works, spot-check using Firefox and Safari. Check permissions. Is this feature limited to a specific set of users? Can it be accessed by a guest or by a non-privileged user? How about pasting a privileged page URL into a non-privileged user’s browser? @@ -33,11 +33,10 @@ Think about risk. Is the feature or function part of a critical area such as per ## Smoke Test - -1. Go to the homepage on https://dataverse-internal.iq.harvard.edu. Scroll to the bottom to ensure the build number is the one you intend to test from Jenkins. -2. Create a new user: I use a formulaic name with my initials and date and make the username and password the same, eg. kc080622. -3. Create a dataverse: I use the same username -4. Create a dataset: I use the same username; I fill in the required fields (I do not use a template). -5. Upload 3 different types of files: I use a tabular file, 50by1000.dta, an image file, and a text file. -6. Publish the dataset. -7. Download a file. +1. Go to the homepage on . Scroll to the bottom to ensure the build number is the one you intend to test from Jenkins. +1. Create a new user: It's fine to use a formulaic name with your initials and date and make the username and password the same, eg. kc080622. +1. Create a dataverse: You can use the same username. +1. Create a dataset: You can use the same username; fill in the required fields (do not use a template). +1. Upload 3 different types of files: You can use a tabular file, 50by1000.dta, an image file, and a text file. +1. Publish the dataset. +1. Download a file. diff --git a/doc/sphinx-guides/source/qa/other-approaches.md b/doc/sphinx-guides/source/qa/other-approaches.md index b50d9d0cf11..cf679c3f442 100644 --- a/doc/sphinx-guides/source/qa/other-approaches.md +++ b/doc/sphinx-guides/source/qa/other-approaches.md @@ -1,125 +1,120 @@ -# Other approaches to deploying and testing +# Other Approaches to Deploying and Testing ```{contents} :depth: 3 ``` -This workflow is fine for a single person testing a PR, one at a time. It would be awkward or impossible if there were multiple people wanting to test different PRs at the same time. I’m assuming if a developer is testing, they would likely just deploy to their dev environment. That might be ok but not sure the env is fully configured enough to offer a real-world testing scenario. An alternative might be to spin an EC2 branch on AWS, potentially using sample data. This can take some time so another option might be to spin up a few, persistent AWS instances with sample data this way, one per tester, and just deploy new builds there when you want to test. You could even configure Jenkins projects for each if desired to maintain consistency in how they’re built. +This workflow is fine for a single person testing a PR, one at a time. It would be awkward or impossible if there were multiple people wanting to test different PRs at the same time. If a developer is testing, they would likely just deploy to their dev environment. That might be ok, but is the env is fully configured enough to offer a real-world testing scenario? An alternative might be to spin an EC2 branch on AWS, potentially using sample data. This can take some time so another option might be to spin up a few, persistent AWS instances with sample data this way, one per tester, and just deploy new builds there when you want to test. You could even configure Jenkins projects for each if desired to maintain consistency in how they’re built. -## Tips and tricks +## Tips and Tricks - -- Start testing simply, with the most obvious test. You don’t need to know all your tests upfront. As you gain comfort and understanding of how it works, try more tests until you are done. If it is a complex feature, jot down your tests in an outline format, some beforehand as a guide, and some after as things occur to you. Save the doc in a testing folder (I have one on Google Drive). This potentially will help with future testing. -- When in doubt, ask someone. If you are confused about how something is working, it may be something you have missed, or it could be a documentation issue, or it could be a bug! Talk to the code reviewer and the contributor/developer for their opinion and advice. -- Always tail the server.log file while testing. Open a terminal window to the test instance and tail -F server.log. This helps you get a real-time sense of what the server is doing when you act and makes it easier to identify any stack trace on failure. -- When overloaded, do the simple pull requests first to reduce the queue. It gives you a mental boost to complete something and reduces the perception of the amount of work still to be done. -- When testing a bug fix, try reproducing the bug on the demo before testing the fix, that way you know you are taking the correct steps to verify that the fix worked. -- When testing an optional feature that requires configuration, do a smoke test without the feature configured and then with it configured. That way you know that folks using the standard config are unaffected by the option if they choose not to configure it. -- Back up your DB before applying an irreversible DB update and you are using a persistent/reusable platform. Just in case it fails, and you need to carry on testing something else you can use the backup. +- Start testing simply, with the most obvious test. You don’t need to know all your tests upfront. As you gain comfort and understanding of how it works, try more tests until you are done. If it is a complex feature, jot down your tests in an outline format, some beforehand as a guide, and some after as things occur to you. Save the doc in a testing folder (on Google Drive). This potentially will help with future testing. +- When in doubt, ask someone. If you are confused about how something is working, it may be something you have missed, or it could be a documentation issue, or it could be a bug! Talk to the code reviewer and the contributor/developer for their opinion and advice. +- Always tail the server.log file while testing. Open a terminal window to the test instance and `tail -F server.log`. This helps you get a real-time sense of what the server is doing when you act and makes it easier to identify any stack trace on failure. +- When overloaded, do the simple pull requests first to reduce the queue. It gives you a mental boost to complete something and reduces the perception of the amount of work still to be done. +- When testing a bug fix, try reproducing the bug on the demo before testing the fix, that way you know you are taking the correct steps to verify that the fix worked. +- When testing an optional feature that requires configuration, do a smoke test without the feature configured and then with it configured. That way you know that folks using the standard config are unaffected by the option if they choose not to configure it. +- Back up your DB before applying an irreversible DB update and you are using a persistent/reusable platform. Just in case it fails, and you need to carry on testing something else you can use the backup. ## Workflow for Completing QA on a PR +1. Assign the PR you are working on to yourself. -1. Assign the PR you are working on to yourself. - -2. What does it do? +1. What does it do? Read the description at the top of the PR, any release notes, documentation, and the original issue. -3. Does it address the issue it closes? +1. Does it address the issue it closes? The PR should address the issue entirely unless otherwise noted. -4. How do you test it? +1. How do you test it? - Look at the “how to test section” at the top of the pull request. Does it make sense? This likely won’t be the only testing you perform. You can develop further tests from the original issue or problem description, from the description of functionality, the documentation, configuration, and release notes. Also consider trying to reveal bugs by trying to break it: try bad or missing data, very large values or volume of data, exceed any place that may have a limit or boundary. + Look at the “how to test" section at the top of the pull request. Does it make sense? This likely won’t be the only testing you perform. You can develop further tests from the original issue or problem description, from the description of functionality, the documentation, configuration, and release notes. Also consider trying to reveal bugs by trying to break it: try bad or missing data, very large values or volume of data, exceed any place that may have a limit or boundary. -5. Does it have or need documentation? +1. Does it have or need documentation? - Small changes or fixes usually don’t have doc but new features or extensions of a feature or new configuration options should have documentation. + Small changes or fixes usually don’t have docs but new features or extensions of a feature or new configuration options should have documentation. -6. Does it have or need release notes? +1. Does it have or need release notes? Same as for doc, just a heads up to an admin for something of note or especially upgrade instructions as needed. -7. Does it use a DB, flyway script? +1. Does it use a DB, Flyway script? Good to know since it may collide with another existing one by version or it could be a one way transform of your DB so back up your test DB before. Also, happens during deployment so be on the lookout for any issues. -8. Validate the documentation. +1. Validate the documentation. Build the doc using Jenkins, does it build without errors? Read it through for sense. Use it for test cases and to understand the feature. -9. Build and deploy the pull request. +1. Build and deploy the pull request. Normally this is done using Jenkins and automatically deployed to the QA test machine. -10. Configure if required +1. Configure if required If needed to operate and everyone installing or upgrading will use this, configure now as all testing will use it. -11. Smoke test the branch. +1. Smoke test the branch. Standard, minimal test of core functionality. -12. Regression test-related or potentially affected features +1. Regression test-related or potentially affected features If config is optional and testing without config turned on, do some spot checks/ regression tests of related or potentially affected areas. -13. Configure if optional +1. Configure if optional What is the default, enabled or disabled? Is that clearly indicated? Test both. By config here we mean enabling the functionality versus choosing a particular config option. Some complex features have config options in addition to enabling. Those will also need to be tested. -14. Test all the new or changed functionality. +1. Test all the new or changed functionality. The heart of the PR, what is this PR adding or fixing? Is it all there and working? -15. Regression test related or potentially affected features. +1. Regression test related or potentially affected features. - Sometimes new stuff modifies and extends other functionality or functionality that is shared with other aspects of the system, e.g. Export, Import. Check the underlying functionality that was also modified but in a spot check or briefer manner. + Sometimes new stuff modifies and extends other functionality or functionality that is shared with other aspects of the system, e.g. export, import. Check the underlying functionality that was also modified but in a spot check or briefer manner. -16. Report any issues found within the PR +1. Report any issues found within the PR It can be easy to lose track of what you’ve found, steps to reproduce, and any errors or stack traces from the server log. Add these in a numbered list to a comment in the pr. Easier to check off when fixed and to work on. Add large amounts of text as in the server log as attached, meaningfully named files. -17. Retest all fixes, spot check feature functionality, smoke test +1. Retest all fixes, spot check feature functionality, smoke test Similar to your initial testing, it is only narrower. -18. Test Upgrade Instructions, if required +1. Test upgrade instructions, if required Some features build upon the existing architecture but require modifications, such as adding a new column to the DB or changing or adding data. It is crucial that this works properly for our 100+ installations. This testing should be performed at the least on the prior version with basic data objects (collection, dataset, files) and any other data that will be updated by this feature. Using the sample data from the prior version would be good or deploying to dataverse-internal and upgrading there would be a good test. Remember to back up your DB before doing a transformative upgrade so that you can repeat it later if you find a bug. -19. Make sure the integration tests in the PR have been completed and passed. - +1. Make sure the API tests in the PR have been completed and passed. + They are run with each commit to the PR and take approximately 42 minutes to run. -20. Merge PR +1. Merge PR Click merge to include this PR into the common develop branch. -21. Delete merged branch +1. Delete merged branch Just a housekeeping move if the PR is from IQSS. Click the delete branch button where the merge button had been. There is no deletion for outside contributions. ## Checklist for Completing QA on a PR - 1. Build the docs -2. Smoke test the pr -3. Test the new functionality -4. Regression test -5. Test any upgrade instructions +1. Smoke test the pr +1. Test the new functionality +1. Regression test +1. Test any upgrade instructions ## Checklist for QA on Release - -1. Review Consolidated Release Notes, in particular upgrade instructions. -2. Conduct performance testing and compare with the previous release. -3. Perform clean install and smoke test. -4. Potentially follow upgrade instructions. Though they have been performed incrementally for each PR, the sequence may need checking - +1. Review Consolidated Release Notes, in particular upgrade instructions. +1. Conduct performance testing and compare with the previous release. +1. Perform clean install and smoke test. +1. Potentially follow upgrade instructions. Though they have been performed incrementally for each PR, the sequence may need checking diff --git a/doc/sphinx-guides/source/qa/overview.md b/doc/sphinx-guides/source/qa/overview.md index 51b38ee0921..d3364fbbbf9 100644 --- a/doc/sphinx-guides/source/qa/overview.md +++ b/doc/sphinx-guides/source/qa/overview.md @@ -6,11 +6,11 @@ ## Introduction -This document describes the testing process used by QA at IQSS and provides a guide for others filling in for that role. Please note that many variations are possible, and the main thing is to catch bugs and provide a good quality product to the user community. +This guide describes the testing process used by QA at IQSS and provides a reference for others filling in for that role. Please note that many variations are possible, and the main thing is to catch bugs and provide a good quality product to the user community. ## Workflow -The basic workflow is bugs or feature requests are submitted to GitHub by the community or by team members as issues. These issues are prioritized and added to a two-week sprint that is reflected on the GitHub Kanban board. As developers work on these issues, a GitHub branch is produced, code is contributed, and a pull request is made to merge these new changes back into the common develop branch and ultimately released as part of the product. Before a pull request is merged it must be reviewed by a member of the development team from a coding perspective, it must pass automated integration tests before moving to QA. There it is tested manually, exercising the UI using three common browser types and any business logic it implements. Depending on whether the code modifies existing code or is completely new, a smoke test of core functionality is performed and some basic regression testing of modified or related code is performed. Any documentation provided is used to understand the feature and any assertions are tested. Once this passes and any bugs that are found are corrected, the automated integration tests are confirmed to be passing, the PR is merged into development, the PR is closed, and the branch is deleted. At this point, the pr moves from the QA column automatically into the Done column and the process repeats with the next pr until it is decided to make a release. +The basic workflow is as follows. Bugs or feature requests are submitted to GitHub by the community or by team members as issues. These issues are prioritized and added to a two-week sprint that is reflected on the GitHub {ref}`kanban-board`. As developers work on these issues, a GitHub branch is produced, code is contributed, and a pull request is made to merge these new changes back into the common {ref}`develop branch ` and ultimately released as part of the product. Before a pull request is moved to QA, it must be reviewed by a member of the development team from a coding perspective, and it must pass automated tests. There it is tested manually, exercising the UI (using three common browsers) and any business logic it implements. Depending on whether the code modifies existing code or is completely new, a smoke test of core functionality is performed and some basic regression testing of modified or related code is performed. Any documentation provided is used to understand the feature and any assertions made in that documentation are tested. Once this passes and any bugs that are found are corrected, and the automated tests are confirmed to be passing, the PR is merged into the develop, the PR is closed, and the branch is deleted (if it is local). At this point, the PR moves from the QA column automatically into the Done column and the process repeats with the next PR until it is decided to {doc}`make a release `. ## Release Cadence and Sprints @@ -20,13 +20,10 @@ The decision to make a release can be based on the time since the last release, ## Performance Testing and Deployment -The final testing activity before producing a release is performance testing. This could be done throughout the release cycle but since it is time-consuming it is done once near the end. Using a load-generating tool named Locust, it loads the statistically most loaded pages, according to Google Analytics, that is 50% homepage and 50% some type of dataset page. Since dataset page weight also varies by the number of files, a selection of about 10 datasets with varying file counts is used. The pages are called randomly as a guest user with increasing levels of user load, from 1 user to 250 users. Typical daily loads in production are around the 50-user level. Though the simulated user level does have a modest amount of random think time before repeated calls, from 5-20 seconds (I believe), it is not a real-world load so direct comparisons to production are not reliable. Instead, we compare performance to prior versions of the product and based on how that performed in production we have some idea whether this might be similar in performance or whether there is some undetected issue that appears under load, such as inefficient or too many DB queries per page. +The final testing activity before producing a release is performance testing. This could be done throughout the release cycle but since it is time-consuming it is done once near the end. Using a load-generating tool named {ref}`Locust `, it loads the statistically most loaded pages, according to Google Analytics, that is 50% homepage and 50% some type of dataset page. Since dataset page weight also varies by the number of files, a selection of about 10 datasets with varying file counts is used. The pages are called randomly as a guest user with increasing levels of user load, from 1 user to 250 users. Typical daily loads in production are around the 50-user level. Though the simulated user level does have a modest amount of random think time before repeated calls, from 5-20 seconds, it is not a real-world load so direct comparisons to production are not reliable. Instead, we compare performance to prior versions of the product, and based on how that performed in production we have some idea whether this might be similar in performance or whether there is some undetected issue that appears under load, such as inefficient or too many DB queries per page. -Once the performance has been tested and recorded in a Google spreadsheet for this proposed version, the release will be prepared and posted. +Once the performance has been tested and recorded in a [Google spreadsheet](https://docs.google.com/spreadsheets/d/1lwPlifvgu3-X_6xLwq6Zr6sCOervr1mV_InHIWjh5KA/edit?usp=sharing) for this proposed version, the release will be prepared and posted. -Preparing the release consists of writing and reviewing the release notes compiled from individual notes in PRs that have been merged for this release. A PR is made for the notes and merged. Next, increment the version numbers in certain code files, produce a PR with those changes, and merge that into the common development branch. Last, a PR is made to merge and develop into the master branch. Once that is merged a guide build with the new release version is made from the master branch. Last, a release war file is built from the master and an installer is built from the master branch and includes the newly built war file. - -Publishing the release consists of creating a new draft release on GitHub, posting the release notes, uploading the .war file and the installer .zip file, and any ancillary files used to configure this release. The latest link for the guides should be updated on the guides server to point to the newest version. Once that is all in place, specify the version name and the master branch at the top of the GitHub draft release and publish. This will tag the master branch with the version number and make the release notes and files available to the public. - -Once released, post to Dataverse general about the release and when possible, deploy to demo and production. +## Making a Release +See {doc}`/developers/making-releases` in the Developer Guide. diff --git a/doc/sphinx-guides/source/qa/performance-tests.md b/doc/sphinx-guides/source/qa/performance-tests.md index 7075d7f1776..a5981dcfbe9 100644 --- a/doc/sphinx-guides/source/qa/performance-tests.md +++ b/doc/sphinx-guides/source/qa/performance-tests.md @@ -10,7 +10,7 @@ To run performance tests, we have a performance test cluster on AWS that employs ## Access -Access to performance cluster instances requires ssh keys, see Leonid. The cluster itself is normally not running to reduce costs. To turn on the cluster, log on to the demo server and run the perfenv scripts from the centos default user dir. Access to the demo requires an ssh key, see Leonid. +Access to performance cluster instances requires ssh keys. The cluster itself is normally not running to reduce costs. To turn on the cluster, log on to the demo server and run the perfenv scripts from the centos default user dir. Access to the demo requires an ssh key, see Leonid. ## Special Notes ⚠️ @@ -19,6 +19,4 @@ Please note the performance database is also used occasionally by Julian and the Executing the Performance Script -------------------------------- -To execute the performance test script, you need to install a local copy of the database-helper-scripts project (https://github.com/IQSS/dataverse-helper-scripts), written by Raman. I have since produced a stripped-down script that calls just the DB and ds and works with python3. - -The automated integration test runs happen on each commit to a PR on an AWS instance and should be reviewed to be passing before merging into development. Their status can be seen on the PR page near the bottom, above the merge button. See Don Sizemore or Phil for questions. +To execute the performance test script, you need to install a local copy of the database-helper-scripts project at . We have since produced a stripped-down script that calls just the DB and ds and works with python3. diff --git a/doc/sphinx-guides/source/qa/test-automation-integration.md b/doc/sphinx-guides/source/qa/test-automation-integration.md deleted file mode 100644 index 5e9d00cd461..00000000000 --- a/doc/sphinx-guides/source/qa/test-automation-integration.md +++ /dev/null @@ -1,35 +0,0 @@ -# Test automation and integration test - -```{contents} -:depth: 3 -``` - -This test suite is added to and maintained by development. It is generally advisable for code contributors to add integration tests when adding new functionality. The approach here is one of code coverage: exercise as much of the code base’s code paths as possible, every time to catch bugs. - -This type of approach is often used to give contributing developers confidence that their code didn’t introduce any obvious, major issues and is run on each commit. Since it is a broad set of tests, it is not clear whether any specific, conceivable test is run but it does add a lot of confidence that the code base is functioning due to its reach and consistency. - -## Building and Deploying a Pull Request from Jenkins to Dataverse-Internal: - - -1. Log on to GitHub, go to projects, dataverse to see Kanban board, select a pull request to test from the QA queue. - -2. From the pull request page, click the copy icon next to the pull request branch name. - -3. Log on to jenkins.dataverse.org, select the IQSS_Dataverse_Internal project, and configure the repository URL and branch specifier to match the ones from the pull request. For example: - - * 8372-gdcc-xoai-library has IQSS implied - - **Repository URL:** https://github.com/IQSS/dataverse.git - - **Branch specifier:** */8372-gdcc-xoai-library - * GlobalDataverseCommunityConsortium:GDCC/DC-3B - - **Repository URL:** https://github.com/GlobalDataverseCommunityConsortium/dataverse.git - - **Branch specifier:** */GDCC/DC-3B. - -4. Click Build Now and note the build number in progress. - -5. Once complete, go to https://dataverse-internal.iq.harvard.edu and check that the deployment succeeded, and that the homepage displays the latest build number. - -6. If for some reason it didn’t deploy, check the server.log file. It may just be a caching issue so try un-deploying, deleting cache, restarting, and re-deploying on the server (su - dataverse, /usr/local/payara5/bin/asadmin list-applications, /usr/local/payara5/bin/asadmin undeploy dataverse-5.11.1, /usr/local/payara5/bin/asadmin deploy /tmp/dataverse-5.11.1.war) - -7. If that didn’t work, you may have run into a flyway DB script collision error but that should be indicated by the server.log - -8. Assuming the above steps worked, and they should 99% of the time, test away! Note: be sure to tail -F server.log in a terminal window while you are doing any testing. This way you can spot problems that may not appear in the UI and have easier access to any stack traces for easier reporting. \ No newline at end of file diff --git a/doc/sphinx-guides/source/qa/test-automation.md b/doc/sphinx-guides/source/qa/test-automation.md new file mode 100644 index 00000000000..ba8e5296d47 --- /dev/null +++ b/doc/sphinx-guides/source/qa/test-automation.md @@ -0,0 +1,35 @@ +# Test Automation + +```{contents} +:depth: 3 +``` + +The API test suite is added to and maintained by development. (See {doc}`/developers/testing` in the Developer Guide.) It is generally advisable for code contributors to add API tests when adding new functionality. The approach here is one of code coverage: exercise as much of the code base's code paths as possible, every time to catch bugs. + +This type of approach is often used to give contributing developers confidence that their code didn’t introduce any obvious, major issues and is run on each commit. Since it is a broad set of tests, it is not clear whether any specific, conceivable test is run but it does add a lot of confidence that the code base is functioning due to its reach and consistency. + +## Building and Deploying a Pull Request from Jenkins to Dataverse-Internal + + +1. Log on to GitHub, go to projects, dataverse to see Kanban board, select a pull request to test from the QA queue. + +1. From the pull request page, click the copy icon next to the pull request branch name. + +1. Log on to , select the `IQSS_Dataverse_Internal` project, and configure the repository URL and branch specifier to match the ones from the pull request. For example: + + * 8372-gdcc-xoai-library has IQSS implied + - **Repository URL:** https://github.com/IQSS/dataverse.git + - **Branch specifier:** */8372-gdcc-xoai-library + * GlobalDataverseCommunityConsortium:GDCC/DC-3B + - **Repository URL:** https://github.com/GlobalDataverseCommunityConsortium/dataverse.git + - **Branch specifier:** */GDCC/DC-3B. + +1. Click "Build Now" and note the build number in progress. + +1. Once complete, go to and check that the deployment succeeded, and that the homepage displays the latest build number. + +1. If for some reason it didn’t deploy, check the server.log file. It may just be a caching issue so try un-deploying, deleting cache, restarting, and re-deploying on the server (`su - dataverse` then `/usr/local/payara5/bin/asadmin list-applications; /usr/local/payara5/bin/asadmin undeploy dataverse-5.11.1; /usr/local/payara5/bin/asadmin deploy /tmp/dataverse-5.11.1.war`) + +1. If that didn't work, you may have run into a Flyway DB script collision error but that should be indicated by the server.log. See {doc}`/developers/sql-upgrade-scripts` in the Developer Guide. + +1. Assuming the above steps worked, and they should 99% of the time, test away! Note: be sure to `tail -F server.log` in a terminal window while you are doing any testing. This way you can spot problems that may not appear in the UI and have easier access to any stack traces for easier reporting. diff --git a/doc/sphinx-guides/source/qa/testing-infrastructure.md b/doc/sphinx-guides/source/qa/testing-infrastructure.md index fb66bc4d099..45b3b360ac7 100644 --- a/doc/sphinx-guides/source/qa/testing-infrastructure.md +++ b/doc/sphinx-guides/source/qa/testing-infrastructure.md @@ -6,10 +6,14 @@ ## Dataverse Internal -To build and test a PR, we use a build named IQSS_Dataverse_Internal on jenkins.dataverse.org, which deploys the .war file to an AWS instance named dataverse-internal.iq.harvard.edu. -Login to Jenkins requires a username and password. Check with Don Sizemore. Login to the dataverse-internal server requires a key, see Leonid. +To build and test a PR, we use a build named `IQSS_Dataverse_Internal` on , which deploys the .war file to an AWS instance named . ## Guides Server -There is also a guides build project named guides.dataverse.org. Any test builds of guides are deployed to a named directory** on guides.dataverse.org and can be found and tested by going to the existing guides, removing the part of the URL that contains the version, and browsing the resulting directory listing for the latest change. -Login to the guides server requires a key, see Don Sizemore. +There is also a guides build project named `guides.dataverse.org`. Any test builds of guides are deployed to a named directory on guides.dataverse.org and can be found and tested by going to the existing guides, removing the part of the URL that contains the version, and browsing the resulting directory listing for the latest change. + +Note that changes to guides can also be previewed on Read the Docs. In the pull request, look for a link like . This Read the Docs preview is also mentioned under also {doc}`/developers/documentation`. + +## Other Servers + +We can spin up additional AWS EC2 instances as needed. See {doc}`/developers/deployment` in the Developer Guide for the scripts we use. From 7650eb308ed5cb8805981e77b252ceb2e3c760c2 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Mon, 13 Nov 2023 16:35:25 -0500 Subject: [PATCH 082/689] Removes the title from content and add label --- doc/sphinx-guides/source/qa/manual-testing.md | 3 ++- doc/sphinx-guides/source/qa/other-approaches.md | 3 ++- doc/sphinx-guides/source/qa/overview.md | 3 ++- doc/sphinx-guides/source/qa/performance-tests.md | 3 ++- doc/sphinx-guides/source/qa/test-automation.md | 3 ++- doc/sphinx-guides/source/qa/testing-infrastructure.md | 3 ++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/doc/sphinx-guides/source/qa/manual-testing.md b/doc/sphinx-guides/source/qa/manual-testing.md index 9f365aae59f..580e5153394 100644 --- a/doc/sphinx-guides/source/qa/manual-testing.md +++ b/doc/sphinx-guides/source/qa/manual-testing.md @@ -1,6 +1,7 @@ # Manual Testing Approach -```{contents} +```{contents} Contents: +:local: :depth: 3 ``` ## Introduction diff --git a/doc/sphinx-guides/source/qa/other-approaches.md b/doc/sphinx-guides/source/qa/other-approaches.md index cf679c3f442..2e2ef906191 100644 --- a/doc/sphinx-guides/source/qa/other-approaches.md +++ b/doc/sphinx-guides/source/qa/other-approaches.md @@ -1,6 +1,7 @@ # Other Approaches to Deploying and Testing -```{contents} +```{contents} Contents: +:local: :depth: 3 ``` diff --git a/doc/sphinx-guides/source/qa/overview.md b/doc/sphinx-guides/source/qa/overview.md index d3364fbbbf9..c4f66446ca3 100644 --- a/doc/sphinx-guides/source/qa/overview.md +++ b/doc/sphinx-guides/source/qa/overview.md @@ -1,6 +1,7 @@ # Overview -```{contents} +```{contents} Contents: +:local: :depth: 3 ``` diff --git a/doc/sphinx-guides/source/qa/performance-tests.md b/doc/sphinx-guides/source/qa/performance-tests.md index a5981dcfbe9..f433226d4ff 100644 --- a/doc/sphinx-guides/source/qa/performance-tests.md +++ b/doc/sphinx-guides/source/qa/performance-tests.md @@ -1,6 +1,7 @@ # Performance Testing -```{contents} +```{contents} Contents: +:local: :depth: 3 ``` diff --git a/doc/sphinx-guides/source/qa/test-automation.md b/doc/sphinx-guides/source/qa/test-automation.md index ba8e5296d47..c2b649df498 100644 --- a/doc/sphinx-guides/source/qa/test-automation.md +++ b/doc/sphinx-guides/source/qa/test-automation.md @@ -1,6 +1,7 @@ # Test Automation -```{contents} +```{contents} Contents: +:local: :depth: 3 ``` diff --git a/doc/sphinx-guides/source/qa/testing-infrastructure.md b/doc/sphinx-guides/source/qa/testing-infrastructure.md index 45b3b360ac7..7a4bda626fc 100644 --- a/doc/sphinx-guides/source/qa/testing-infrastructure.md +++ b/doc/sphinx-guides/source/qa/testing-infrastructure.md @@ -1,6 +1,7 @@ # Infrastructure for Testing -```{contents} +```{contents} Contents: +:local: :depth: 3 ``` From 74d36b64d0fc36afafa5382952050239737ebe1a Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Thu, 16 Nov 2023 11:24:30 -0500 Subject: [PATCH 083/689] #9686 preliminary check in --- .../java/edu/harvard/iq/dataverse/Dataset.java | 14 +------------- .../java/edu/harvard/iq/dataverse/DvObject.java | 17 +++++++++++++++++ .../V6.0.0.3__9686-move-harvestingclient-id.sql | 8 ++++++++ 3 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 src/main/resources/db/migration/V6.0.0.3__9686-move-harvestingclient-id.sql diff --git a/src/main/java/edu/harvard/iq/dataverse/Dataset.java b/src/main/java/edu/harvard/iq/dataverse/Dataset.java index 245bdf0efd2..ad72ada20e9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Dataset.java +++ b/src/main/java/edu/harvard/iq/dataverse/Dataset.java @@ -742,21 +742,9 @@ public void setDatasetExternalCitations(List datasetEx this.datasetExternalCitations = datasetExternalCitations; } - @ManyToOne - @JoinColumn(name="harvestingClient_id") - private HarvestingClient harvestedFrom; - - public HarvestingClient getHarvestedFrom() { - return this.harvestedFrom; - } - public void setHarvestedFrom(HarvestingClient harvestingClientConfig) { - this.harvestedFrom = harvestingClientConfig; - } - public boolean isHarvested() { - return this.harvestedFrom != null; - } + private String harvestIdentifier; diff --git a/src/main/java/edu/harvard/iq/dataverse/DvObject.java b/src/main/java/edu/harvard/iq/dataverse/DvObject.java index 9e7f3f3fe96..16237203d78 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DvObject.java +++ b/src/main/java/edu/harvard/iq/dataverse/DvObject.java @@ -1,6 +1,7 @@ package edu.harvard.iq.dataverse; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.harvest.client.HarvestingClient; import edu.harvard.iq.dataverse.pidproviders.PidUtil; import java.sql.Timestamp; @@ -351,6 +352,22 @@ public GlobalId getGlobalId() { return globalId; } + @ManyToOne + @JoinColumn(name="harvestingClient_id") + private HarvestingClient harvestedFrom; + + public HarvestingClient getHarvestedFrom() { + return this.harvestedFrom; + } + + public void setHarvestedFrom(HarvestingClient harvestingClientConfig) { + this.harvestedFrom = harvestingClientConfig; + } + + public boolean isHarvested() { + return this.harvestedFrom != null; + } + public abstract T accept(Visitor v); @Override diff --git a/src/main/resources/db/migration/V6.0.0.3__9686-move-harvestingclient-id.sql b/src/main/resources/db/migration/V6.0.0.3__9686-move-harvestingclient-id.sql new file mode 100644 index 00000000000..23d66701b99 --- /dev/null +++ b/src/main/resources/db/migration/V6.0.0.3__9686-move-harvestingclient-id.sql @@ -0,0 +1,8 @@ +ALTER TABLE dvobject ADD COLUMN IF NOT EXISTS harvestingclient_id BIGINT; + +update dvobject dvo set harvestingclient_id = s.harvestingclient_id from +(select id, harvestingclient_id from dataset d) s +where s.id = dvo.id; + +--ALTER TABLE dataset drop COLUMN IF EXISTS harvestingclient_id; + From 5c045120d6660ee0b07501cadfb06aaf9f083f6b Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Thu, 16 Nov 2023 13:42:51 -0500 Subject: [PATCH 084/689] #9686 rename migration script --- ...lient-id.sql => V6.0.0.4__9686-move-harvestingclient-id.sql} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/main/resources/db/migration/{V6.0.0.3__9686-move-harvestingclient-id.sql => V6.0.0.4__9686-move-harvestingclient-id.sql} (72%) diff --git a/src/main/resources/db/migration/V6.0.0.3__9686-move-harvestingclient-id.sql b/src/main/resources/db/migration/V6.0.0.4__9686-move-harvestingclient-id.sql similarity index 72% rename from src/main/resources/db/migration/V6.0.0.3__9686-move-harvestingclient-id.sql rename to src/main/resources/db/migration/V6.0.0.4__9686-move-harvestingclient-id.sql index 23d66701b99..0e4c9a58a93 100644 --- a/src/main/resources/db/migration/V6.0.0.3__9686-move-harvestingclient-id.sql +++ b/src/main/resources/db/migration/V6.0.0.4__9686-move-harvestingclient-id.sql @@ -1,7 +1,7 @@ ALTER TABLE dvobject ADD COLUMN IF NOT EXISTS harvestingclient_id BIGINT; update dvobject dvo set harvestingclient_id = s.harvestingclient_id from -(select id, harvestingclient_id from dataset d) s +(select id, harvestingclient_id from dataset d where d.harvestingclient_id is not null) s where s.id = dvo.id; --ALTER TABLE dataset drop COLUMN IF EXISTS harvestingclient_id; From 5ecfd49c7397f04003c745fc78074e1fb1a9b0aa Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Mon, 20 Nov 2023 09:30:16 -0500 Subject: [PATCH 085/689] #9686 update metrics queries --- .../dataverse/metrics/MetricsServiceBean.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/metrics/MetricsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/metrics/MetricsServiceBean.java index 79369207963..6b540595e77 100644 --- a/src/main/java/edu/harvard/iq/dataverse/metrics/MetricsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/metrics/MetricsServiceBean.java @@ -138,8 +138,8 @@ public JsonArray getDatasetsTimeSeries(UriInfo uriInfo, String dataLocation, Dat + "from datasetversion\n" + "where versionstate='RELEASED' \n" + (((d == null)&&(DATA_LOCATION_ALL.equals(dataLocation))) ? "" : "and dataset_id in (select dataset.id from dataset, dvobject where dataset.id=dvobject.id\n") - + ((DATA_LOCATION_LOCAL.equals(dataLocation)) ? "and dataset.harvestingclient_id IS NULL and publicationdate is not null\n " : "") - + ((DATA_LOCATION_REMOTE.equals(dataLocation)) ? "and dataset.harvestingclient_id IS NOT NULL\n " : "") + + ((DATA_LOCATION_LOCAL.equals(dataLocation)) ? "and dvobject.harvestingclient_id IS NULL and publicationdate is not null\n " : "") + + ((DATA_LOCATION_REMOTE.equals(dataLocation)) ? "and dvobject.harvestingclient_id IS NOT NULL\n " : "") + ((d == null) ? "" : "and dvobject.owner_id in (" + getCommaSeparatedIdStringForSubtree(d, "Dataverse") + ")\n ") + (((d == null)&&(DATA_LOCATION_ALL.equals(dataLocation))) ? "" : ")\n") + "group by dataset_id) as subq group by subq.date order by date;" @@ -156,11 +156,13 @@ public JsonArray getDatasetsTimeSeries(UriInfo uriInfo, String dataLocation, Dat * @param d */ public long datasetsToMonth(String yyyymm, String dataLocation, Dataverse d) { - String dataLocationLine = "(date_trunc('month', releasetime) <= to_date('" + yyyymm + "','YYYY-MM') and dataset.harvestingclient_id IS NULL)\n"; + + System.out.print("datasets to month..."); + String dataLocationLine = "(date_trunc('month', releasetime) <= to_date('" + yyyymm + "','YYYY-MM') and dvobject.harvestingclient_id IS NULL)\n"; if (!DATA_LOCATION_LOCAL.equals(dataLocation)) { // Default api state is DATA_LOCATION_LOCAL //we have to use createtime for harvest as post dvn3 harvests do not have releasetime populated - String harvestBaseLine = "(date_trunc('month', createtime) <= to_date('" + yyyymm + "','YYYY-MM') and dataset.harvestingclient_id IS NOT NULL)\n"; + String harvestBaseLine = "(date_trunc('month', createtime) <= to_date('" + yyyymm + "','YYYY-MM') and dvobject.harvestingclient_id IS NOT NULL)\n"; if (DATA_LOCATION_REMOTE.equals(dataLocation)) { dataLocationLine = harvestBaseLine; // replace } else if (DATA_LOCATION_ALL.equals(dataLocation)) { @@ -189,7 +191,7 @@ public long datasetsToMonth(String yyyymm, String dataLocation, Dataverse d) { + "select datasetversion.dataset_id || ':' || max(datasetversion.versionnumber + (.1 * datasetversion.minorversionnumber))\n" + "from datasetversion\n" + "join dataset on dataset.id = datasetversion.dataset_id\n" - + ((d == null) ? "" : "join dvobject on dvobject.id = dataset.id\n") + + "join dvobject on dvobject.id = dataset.id\n" + "where versionstate='RELEASED' \n" + ((d == null) ? "" : "and dvobject.owner_id in (" + getCommaSeparatedIdStringForSubtree(d, "Dataverse") + ")\n ") + "and \n" @@ -198,7 +200,6 @@ public long datasetsToMonth(String yyyymm, String dataLocation, Dataverse d) { +") sub_temp" ); logger.log(Level.FINE, "Metric query: {0}", query); - return (long) query.getSingleResult(); } @@ -212,6 +213,7 @@ public List datasetsBySubjectToMonth(String yyyymm, String dataLocatio "select datasetversion.dataset_id || ':' || max(datasetversion.versionnumber + (.1 * datasetversion.minorversionnumber))\n" + " from datasetversion\n" + " join dataset on dataset.id = datasetversion.dataset_id\n" + + " join dvobject on dataset.id = dvobject.id \n" + " where versionstate='RELEASED'\n" + " and dataset.harvestingclient_id is null\n" + " and date_trunc('month', releasetime) <= to_date('" + yyyymm + "','YYYY-MM')\n" + @@ -225,6 +227,7 @@ public List datasetsBySubjectToMonth(String yyyymm, String dataLocatio // so the query is simpler: String harvestOriginClause = "(\n" + " datasetversion.dataset_id = dataset.id\n" + + " dvobject.id = dataset.id \n" + " AND dataset.harvestingclient_id IS NOT null \n" + " AND date_trunc('month', datasetversion.createtime) <= to_date('" + yyyymm + "','YYYY-MM')\n" + ")\n"; @@ -253,7 +256,7 @@ public List datasetsBySubjectToMonth(String yyyymm, String dataLocatio + "ORDER BY count(dataset.id) desc;" ); logger.log(Level.FINE, "Metric query: {0}", query); - + System.out.print("by sub to month: " + query); return query.getResultList(); } @@ -616,7 +619,7 @@ public String returnUnexpiredCacheDayBased(String metricName, String days, Strin public String returnUnexpiredCacheMonthly(String metricName, String yyyymm, String dataLocation, Dataverse d) { Metric queriedMetric = getMetric(metricName, dataLocation, yyyymm, d); - + System.out.print("returnUnexpiredCacheMonthly: " + queriedMetric); if (!doWeQueryAgainMonthly(queriedMetric)) { return queriedMetric.getValueJson(); } From f69c22982aeae57fdfb57607e06dfad628123b45 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Mon, 20 Nov 2023 09:33:06 -0500 Subject: [PATCH 086/689] #9686 update metrics IT --- src/test/java/edu/harvard/iq/dataverse/api/MetricsIT.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/MetricsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/MetricsIT.java index e3328eefb4a..fa05a23b675 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/MetricsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/MetricsIT.java @@ -30,7 +30,7 @@ public static void cleanUpClass() { @Test public void testGetDataversesToMonth() { - String yyyymm = "2018-04"; + String yyyymm = "2023-11"; // yyyymm = null; Response response = UtilIT.metricsDataversesToMonth(yyyymm, null); String precache = response.prettyPrint(); @@ -54,7 +54,7 @@ public void testGetDataversesToMonth() { @Test public void testGetDatasetsToMonth() { - String yyyymm = "2018-04"; + String yyyymm = "2023-11"; // yyyymm = null; Response response = UtilIT.metricsDatasetsToMonth(yyyymm, null); String precache = response.prettyPrint(); @@ -77,7 +77,7 @@ public void testGetDatasetsToMonth() { @Test public void testGetFilesToMonth() { - String yyyymm = "2018-04"; + String yyyymm = "2023-11"; // yyyymm = null; Response response = UtilIT.metricsFilesToMonth(yyyymm, null); String precache = response.prettyPrint(); @@ -100,7 +100,7 @@ public void testGetFilesToMonth() { @Test public void testGetDownloadsToMonth() { - String yyyymm = "2018-04"; + String yyyymm = "2023-11"; // yyyymm = null; Response response = UtilIT.metricsDownloadsToMonth(yyyymm, null); String precache = response.prettyPrint(); From 0cbdb1c4bad298d310c5d46429adb30e61689cde Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Mon, 27 Nov 2023 09:15:53 -0500 Subject: [PATCH 087/689] #9686 fix metrics service bean --- .../dataverse/metrics/MetricsServiceBean.java | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/metrics/MetricsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/metrics/MetricsServiceBean.java index 6b540595e77..f83819cd34d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/metrics/MetricsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/metrics/MetricsServiceBean.java @@ -156,8 +156,6 @@ public JsonArray getDatasetsTimeSeries(UriInfo uriInfo, String dataLocation, Dat * @param d */ public long datasetsToMonth(String yyyymm, String dataLocation, Dataverse d) { - - System.out.print("datasets to month..."); String dataLocationLine = "(date_trunc('month', releasetime) <= to_date('" + yyyymm + "','YYYY-MM') and dvobject.harvestingclient_id IS NULL)\n"; if (!DATA_LOCATION_LOCAL.equals(dataLocation)) { // Default api state is DATA_LOCATION_LOCAL @@ -208,17 +206,17 @@ public List datasetsBySubjectToMonth(String yyyymm, String dataLocatio // A published local datasets may have more than one released version! // So that's why we have to jump through some extra hoops below // in order to select the latest one: - String originClause = "(datasetversion.dataset_id || ':' || datasetversion.versionnumber + (.1 * datasetversion.minorversionnumber) in\n" + - "(\n" + - "select datasetversion.dataset_id || ':' || max(datasetversion.versionnumber + (.1 * datasetversion.minorversionnumber))\n" + - " from datasetversion\n" + - " join dataset on dataset.id = datasetversion.dataset_id\n" + - " join dvobject on dataset.id = dvobject.id \n" + - " where versionstate='RELEASED'\n" + - " and dataset.harvestingclient_id is null\n" + - " and date_trunc('month', releasetime) <= to_date('" + yyyymm + "','YYYY-MM')\n" + - " group by dataset_id\n" + - "))\n"; + String originClause = "(datasetversion.dataset_id || ':' || datasetversion.versionnumber + (.1 * datasetversion.minorversionnumber) in\n" + + "(\n" + + "select datasetversion.dataset_id || ':' || max(datasetversion.versionnumber + (.1 * datasetversion.minorversionnumber))\n" + + " from datasetversion\n" + + " join dataset on dataset.id = datasetversion.dataset_id\n" + + " join dvobject on dataset.id = dvobject.id\n" + + " where versionstate='RELEASED'\n" + + " and dvobject.harvestingclient_id is null" + + " and date_trunc('month', releasetime) <= to_date('" + yyyymm + "','YYYY-MM')\n" + + " group by dataset_id\n" + + "))\n"; if (!DATA_LOCATION_LOCAL.equals(dataLocation)) { // Default api state is DATA_LOCATION_LOCAL //we have to use createtime for harvest as post dvn3 harvests do not have releasetime populated @@ -227,8 +225,7 @@ public List datasetsBySubjectToMonth(String yyyymm, String dataLocatio // so the query is simpler: String harvestOriginClause = "(\n" + " datasetversion.dataset_id = dataset.id\n" + - " dvobject.id = dataset.id \n" + - " AND dataset.harvestingclient_id IS NOT null \n" + + " AND dvobject.harvestingclient_id IS NOT null \n" + " AND date_trunc('month', datasetversion.createtime) <= to_date('" + yyyymm + "','YYYY-MM')\n" + ")\n"; @@ -247,7 +244,7 @@ public List datasetsBySubjectToMonth(String yyyymm, String dataLocatio + "JOIN datasetfieldtype ON datasetfieldtype.id = controlledvocabularyvalue.datasetfieldtype_id\n" + "JOIN datasetversion ON datasetversion.id = datasetfield.datasetversion_id\n" + "JOIN dataset ON dataset.id = datasetversion.dataset_id\n" - + ((d == null) ? "" : "JOIN dvobject ON dvobject.id = dataset.id\n") + + "JOIN dvobject ON dvobject.id = dataset.id\n" + "WHERE\n" + originClause + "AND datasetfieldtype.name = 'subject'\n" @@ -256,16 +253,16 @@ public List datasetsBySubjectToMonth(String yyyymm, String dataLocatio + "ORDER BY count(dataset.id) desc;" ); logger.log(Level.FINE, "Metric query: {0}", query); - System.out.print("by sub to month: " + query); + return query.getResultList(); } public long datasetsPastDays(int days, String dataLocation, Dataverse d) { - String dataLocationLine = "(releasetime > current_date - interval '" + days + "' day and dataset.harvestingclient_id IS NULL)\n"; + String dataLocationLine = "(releasetime > current_date - interval '" + days + "' day and dvobject.harvestingclient_id IS NULL)\n"; if (!DATA_LOCATION_LOCAL.equals(dataLocation)) { // Default api state is DATA_LOCATION_LOCAL //we have to use createtime for harvest as post dvn3 harvests do not have releasetime populated - String harvestBaseLine = "(createtime > current_date - interval '" + days + "' day and dataset.harvestingclient_id IS NOT NULL)\n"; + String harvestBaseLine = "(createtime > current_date - interval '" + days + "' day and dvobject.harvestingclient_id IS NOT NULL)\n"; if (DATA_LOCATION_REMOTE.equals(dataLocation)) { dataLocationLine = harvestBaseLine; // replace } else if (DATA_LOCATION_ALL.equals(dataLocation)) { @@ -279,7 +276,7 @@ public long datasetsPastDays(int days, String dataLocation, Dataverse d) { + "select datasetversion.dataset_id || ':' || max(datasetversion.versionnumber + (.1 * datasetversion.minorversionnumber)) as max\n" + "from datasetversion\n" + "join dataset on dataset.id = datasetversion.dataset_id\n" - + ((d == null) ? "" : "join dvobject on dvobject.id = dataset.id\n") + + "join dvobject on dvobject.id = dataset.id\n" + "where versionstate='RELEASED' \n" + ((d == null) ? "" : "and dvobject.owner_id in (" + getCommaSeparatedIdStringForSubtree(d, "Dataverse") + ")\n") + "and \n" @@ -307,7 +304,7 @@ public JsonArray filesTimeSeries(Dataverse d) { + "where datasetversion.id=filemetadata.datasetversion_id\n" + "and versionstate='RELEASED' \n" + "and dataset_id in (select dataset.id from dataset, dvobject where dataset.id=dvobject.id\n" - + "and dataset.harvestingclient_id IS NULL and publicationdate is not null\n " + + "and dvobject.harvestingclient_id IS NULL and publicationdate is not null\n " + ((d == null) ? ")" : "and dvobject.owner_id in (" + getCommaSeparatedIdStringForSubtree(d, "Dataverse") + "))\n ") + "group by filemetadata.id) as subq group by subq.date order by date;"); logger.log(Level.FINE, "Metric query: {0}", query); @@ -330,11 +327,11 @@ public long filesToMonth(String yyyymm, Dataverse d) { + "select datasetversion.dataset_id || ':' || max(datasetversion.versionnumber + (.1 * datasetversion.minorversionnumber)) as max \n" + "from datasetversion\n" + "join dataset on dataset.id = datasetversion.dataset_id\n" - + ((d == null) ? "" : "join dvobject on dvobject.id = dataset.id\n") + + "join dvobject on dvobject.id = dataset.id\n" + "where versionstate='RELEASED'\n" + ((d == null) ? "" : "and dvobject.owner_id in (" + getCommaSeparatedIdStringForSubtree(d, "Dataverse") + ")\n") + "and date_trunc('month', releasetime) <= to_date('" + yyyymm + "','YYYY-MM')\n" - + "and dataset.harvestingclient_id is null\n" + + "and dvobject.harvestingclient_id is null\n" + "group by dataset_id \n" + ");" ); @@ -353,11 +350,11 @@ public long filesPastDays(int days, Dataverse d) { + "select datasetversion.dataset_id || ':' || max(datasetversion.versionnumber + (.1 * datasetversion.minorversionnumber)) as max \n" + "from datasetversion\n" + "join dataset on dataset.id = datasetversion.dataset_id\n" - + ((d == null) ? "" : "join dvobject on dvobject.id = dataset.id\n") + + "join dvobject on dvobject.id = dataset.id\n" + "where versionstate='RELEASED'\n" + "and releasetime > current_date - interval '" + days + "' day\n" + ((d == null) ? "" : "AND dvobject.owner_id in (" + getCommaSeparatedIdStringForSubtree(d, "Dataverse") + ")\n") - + "and dataset.harvestingclient_id is null\n" + + "and dvobject.harvestingclient_id is null\n" + "group by dataset_id \n" + ");" ); @@ -619,7 +616,7 @@ public String returnUnexpiredCacheDayBased(String metricName, String days, Strin public String returnUnexpiredCacheMonthly(String metricName, String yyyymm, String dataLocation, Dataverse d) { Metric queriedMetric = getMetric(metricName, dataLocation, yyyymm, d); - System.out.print("returnUnexpiredCacheMonthly: " + queriedMetric); + if (!doWeQueryAgainMonthly(queriedMetric)) { return queriedMetric.getValueJson(); } From 6ce88a98717662558b9abf4a6e0af8acdda4df1c Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Mon, 27 Nov 2023 15:25:52 -0500 Subject: [PATCH 088/689] #9686 rename migration script --- ...gclient-id.sql => V6.0.0.5__9686-move-harvestingclient-id.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V6.0.0.4__9686-move-harvestingclient-id.sql => V6.0.0.5__9686-move-harvestingclient-id.sql} (100%) diff --git a/src/main/resources/db/migration/V6.0.0.4__9686-move-harvestingclient-id.sql b/src/main/resources/db/migration/V6.0.0.5__9686-move-harvestingclient-id.sql similarity index 100% rename from src/main/resources/db/migration/V6.0.0.4__9686-move-harvestingclient-id.sql rename to src/main/resources/db/migration/V6.0.0.5__9686-move-harvestingclient-id.sql From 5bf52f06499427ef0c2c56b4170a6dc920b08865 Mon Sep 17 00:00:00 2001 From: sbondka Date: Wed, 29 Nov 2023 18:34:23 +0100 Subject: [PATCH 089/689] Add JupyterHub Connector to Dataverse guide --- doc/sphinx-guides/source/admin/integrations.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/sphinx-guides/source/admin/integrations.rst b/doc/sphinx-guides/source/admin/integrations.rst index 9a24cf0715c..a9b962f33ca 100644 --- a/doc/sphinx-guides/source/admin/integrations.rst +++ b/doc/sphinx-guides/source/admin/integrations.rst @@ -185,6 +185,16 @@ Avgidea Data Search Researchers can use a Google Sheets add-on to search for Dataverse installation's CSV data and then import that data into a sheet. See `Avgidea Data Search `_ for details. +JupyterHub +++++++++++ + +The Dataverse-to-JupyterHub Data Transfer Connector streamlines data transfer between Dataverse repositories and the cloud-based platform JupyterHub, enhancing collaborative research. +This connector facilitates seamless two-way transfer of datasets and files, emphasizing the potential of an integrated research environment. +It is a lightweight client-side web application built using React and relying on the Dataverse External Tool feature, allowing for easy deployment on modern integration systems. Currently, it supports small to medium-sized files, with plans to enable support for large files and signed Dataverse endpoints in the future. + +What kind of user is the feature intended for? +The feature is intended for reasearchers, scientists and data analyst working with Dataverse instances and JupyterHub looking to ease the data transfer process. + .. _integrations-discovery: Discoverability From a6fd3f5a715f3717941d1997813b9b4f7313bab1 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Thu, 30 Nov 2023 09:49:56 -0500 Subject: [PATCH 090/689] #9686 changes from CR --- .../V6.0.0.5__9686-move-harvestingclient-id.sql | 2 +- .../edu/harvard/iq/dataverse/api/MetricsIT.java | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/resources/db/migration/V6.0.0.5__9686-move-harvestingclient-id.sql b/src/main/resources/db/migration/V6.0.0.5__9686-move-harvestingclient-id.sql index 0e4c9a58a93..22142b8fc41 100644 --- a/src/main/resources/db/migration/V6.0.0.5__9686-move-harvestingclient-id.sql +++ b/src/main/resources/db/migration/V6.0.0.5__9686-move-harvestingclient-id.sql @@ -4,5 +4,5 @@ update dvobject dvo set harvestingclient_id = s.harvestingclient_id from (select id, harvestingclient_id from dataset d where d.harvestingclient_id is not null) s where s.id = dvo.id; ---ALTER TABLE dataset drop COLUMN IF EXISTS harvestingclient_id; +ALTER TABLE dataset drop COLUMN IF EXISTS harvestingclient_id; diff --git a/src/test/java/edu/harvard/iq/dataverse/api/MetricsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/MetricsIT.java index fa05a23b675..1425b7bc5d9 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/MetricsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/MetricsIT.java @@ -5,6 +5,8 @@ import edu.harvard.iq.dataverse.metrics.MetricsUtil; import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; import static jakarta.ws.rs.core.Response.Status.OK; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -16,10 +18,13 @@ //To improve these tests we should try adding data and see if the number DOESN'T //go up to show that the caching worked public class MetricsIT { + + private static String yyyymm; @BeforeAll public static void setUpClass() { RestAssured.baseURI = UtilIT.getRestAssuredBaseUri(); + yyyymm = LocalDate.now().format(DateTimeFormatter.ofPattern(MetricsUtil.YEAR_AND_MONTH_PATTERN)); UtilIT.clearMetricCache(); } @@ -30,8 +35,7 @@ public static void cleanUpClass() { @Test public void testGetDataversesToMonth() { - String yyyymm = "2023-11"; -// yyyymm = null; + Response response = UtilIT.metricsDataversesToMonth(yyyymm, null); String precache = response.prettyPrint(); response.then().assertThat() @@ -54,8 +58,7 @@ public void testGetDataversesToMonth() { @Test public void testGetDatasetsToMonth() { - String yyyymm = "2023-11"; -// yyyymm = null; + Response response = UtilIT.metricsDatasetsToMonth(yyyymm, null); String precache = response.prettyPrint(); response.then().assertThat() @@ -77,8 +80,7 @@ public void testGetDatasetsToMonth() { @Test public void testGetFilesToMonth() { - String yyyymm = "2023-11"; -// yyyymm = null; + Response response = UtilIT.metricsFilesToMonth(yyyymm, null); String precache = response.prettyPrint(); response.then().assertThat() @@ -100,8 +102,7 @@ public void testGetFilesToMonth() { @Test public void testGetDownloadsToMonth() { - String yyyymm = "2023-11"; -// yyyymm = null; + Response response = UtilIT.metricsDownloadsToMonth(yyyymm, null); String precache = response.prettyPrint(); response.then().assertThat() From 538921061604e4daacd864f8ec3865d6d0642561 Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 1 Dec 2023 14:21:35 +0000 Subject: [PATCH 091/689] Stash: working on new canDownloadAtLeastOneFile Datasets API endpoint --- .../iq/dataverse/PermissionServiceBean.java | 8 ++++++ .../harvard/iq/dataverse/api/Datasets.java | 14 +++++++++++ .../harvard/iq/dataverse/api/DatasetsIT.java | 25 +++++++++++++++++++ .../edu/harvard/iq/dataverse/api/UtilIT.java | 6 +++++ 4 files changed, 53 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java index a1de33a764e..9e6628617ce 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java @@ -837,4 +837,12 @@ public boolean isMatchingWorkflowLock(Dataset d, String userId, String invocatio return false; } + public boolean canDownloadAtLeastOneFile(User requestUser, DatasetVersion datasetVersion) { + for (FileMetadata fileMetadata : datasetVersion.getFileMetadatas()) { + if (userOn(requestUser, fileMetadata.getDataFile()).has(Permission.DownloadFile)) { + return true; + } + } + return false; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index af6059cf882..a9cfefc33d8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -4134,4 +4134,18 @@ public Response getUserPermissionsOnDataset(@Context ContainerRequestContext crc jsonObjectBuilder.add("canDeleteDatasetDraft", permissionService.userOn(requestUser, dataset).has(Permission.DeleteDatasetDraft)); return ok(jsonObjectBuilder); } + + @GET + @AuthRequired + @Path("{id}/versions/{versionId}/canDownloadAtLeastOneFile") + public Response getCanDownloadAtLeastOneFile(@Context ContainerRequestContext crc, + @PathParam("id") String datasetId, + @PathParam("versionId") String versionId, + @Context UriInfo uriInfo, + @Context HttpHeaders headers) { + return response(req -> { + DatasetVersion datasetVersion = getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers, false); + return ok(permissionService.canDownloadAtLeastOneFile(getRequestUser(crc), datasetVersion)); + }, getRequestUser(crc)); + } } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index d20f1e8a58b..945b741a94b 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -4121,4 +4121,29 @@ public void testGetUserPermissionsOnDataset() { Response getUserPermissionsOnDatasetInvalidIdResponse = UtilIT.getUserPermissionsOnDataset("testInvalidId", apiToken); getUserPermissionsOnDatasetInvalidIdResponse.then().assertThat().statusCode(BAD_REQUEST.getStatusCode()); } + + @Test + public void testGetCanDownloadAtLeastOneFile() { + Response createUser = UtilIT.createRandomUser(); + createUser.then().assertThat().statusCode(OK.getStatusCode()); + String apiToken = UtilIT.getApiTokenFromResponse(createUser); + + Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); + createDataverseResponse.then().assertThat().statusCode(CREATED.getStatusCode()); + String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + + Response createDatasetResponse = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, apiToken); + createDatasetResponse.then().assertThat().statusCode(CREATED.getStatusCode()); + int datasetId = JsonPath.from(createDatasetResponse.body().asString()).getInt("data.id"); + + // Call with valid dataset id + Response canDownloadAtLeastOneFileResponse = UtilIT.getCanDownloadAtLeastOneFile(Integer.toString(datasetId), DS_VERSION_LATEST, apiToken); + canDownloadAtLeastOneFileResponse.then().assertThat().statusCode(OK.getStatusCode()); + boolean canDownloadAtLeastOneFile = JsonPath.from(canDownloadAtLeastOneFileResponse.body().asString()).getBoolean("data"); + assertTrue(canDownloadAtLeastOneFile); + + // Call with invalid dataset id + Response getUserPermissionsOnDatasetInvalidIdResponse = UtilIT.getCanDownloadAtLeastOneFile("testInvalidId", DS_VERSION_LATEST, apiToken); + getUserPermissionsOnDatasetInvalidIdResponse.then().assertThat().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 9b264086c27..bf43733788a 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -3442,6 +3442,12 @@ static Response getUserPermissionsOnDataset(String datasetId, String apiToken) { .get("/api/datasets/" + datasetId + "/userPermissions"); } + static Response getCanDownloadAtLeastOneFile(String datasetId, String versionId, String apiToken) { + return given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .get("/api/datasets/" + datasetId + "/versions/" + versionId + "/canDownloadAtLeastOneFile"); + } + static Response createFileEmbargo(Integer datasetId, Integer fileId, String dateAvailable, String apiToken) { JsonObjectBuilder jsonBuilder = Json.createObjectBuilder(); jsonBuilder.add("dateAvailable", dateAvailable); From 8ec0984a663e4daa5b60049c1ee8d51004ca452c Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Fri, 1 Dec 2023 09:26:39 -0500 Subject: [PATCH 092/689] add page on Jenkins #10101 --- doc/sphinx-guides/source/qa/index.md | 1 + doc/sphinx-guides/source/qa/jenkins.md | 44 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 doc/sphinx-guides/source/qa/jenkins.md diff --git a/doc/sphinx-guides/source/qa/index.md b/doc/sphinx-guides/source/qa/index.md index 08deb7ee27d..6027f07574f 100644 --- a/doc/sphinx-guides/source/qa/index.md +++ b/doc/sphinx-guides/source/qa/index.md @@ -7,4 +7,5 @@ performance-tests.md manual-testing.md test-automation.md other-approaches.md +jenkins.md ``` diff --git a/doc/sphinx-guides/source/qa/jenkins.md b/doc/sphinx-guides/source/qa/jenkins.md new file mode 100644 index 00000000000..dbfec0d60d0 --- /dev/null +++ b/doc/sphinx-guides/source/qa/jenkins.md @@ -0,0 +1,44 @@ +# Jenkins + +```{contents} Contents: +:local: +:depth: 3 +``` + +## Introduction + +Jenkins is our primary tool for knowing if our API tests are passing. (Unit tests are executed locally by developers.) + +You can find our Jenkins installation at . + +Please note that while it has been open to the public in the past, it is currently firewalled off. We can poke a hole in the firewall for your IP address if necessary. Please get in touch. (You might also be interested in which is about restoring the ability of contributors to see if their pull requests are passing API tests or not.) + +## Jobs + +Jenkins is organized into jobs. We'll highlight a few. + +### IQSS-dataverse-develop + +, which we will refer to as the "develop" job runs after pull requests are merged. It is crucial that this job stays green (passing) because we always want to stay in a "release ready" state. If you notice that this job is failing, make noise about it! + +You can get to this job from the README at . + +### IQSS-Dataverse-Develop-PR + + can be thought of as "PR jobs". It's a collection of jobs run on pull requests. Typically, you will navigate directly into the job (and it's particular build number) from a pull request. For example, from , look for a check called "continuous-integration/jenkins/pr-merge". Clicking it will bring you to a particular build like (build #10). + +### guides.dataverse.org + + is what we use to build guides. See {doc}`/developers/making-releases` in the Developer Guide. + +## Checking if API Tests are Passing + +If API tests are failing, you should not merge the pull request. + +How can you know if API tests are passing? Here are the steps, by way of example. + +- From the pull request, navigate to the build. For example from , look for a check called "continuous-integration/jenkins/pr-merge". Clicking it will bring you to a particular build like (build #10). +- You are now on the new "blue" interface for Jenkins. Click the button in the header called "go to classic" which should take you to (for example) . +- Click "Test Result". +- Under "All Tests", look at the duration for "edu.harvard.iq.dataverse.api". It should be ten minutes or higher. If it was only a few seconds, tests did not run. +- Assuming tests ran, if there were failures, they should appear at the top under "All Failed Tests". Inform the author of the pull request about the error. From a142ac82e7315370755f11245c38f388f7580b12 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Fri, 1 Dec 2023 12:51:55 -0500 Subject: [PATCH 093/689] Adds description about the "go to classic" button --- doc/sphinx-guides/source/qa/jenkins.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/qa/jenkins.md b/doc/sphinx-guides/source/qa/jenkins.md index dbfec0d60d0..a4ca4d8688f 100644 --- a/doc/sphinx-guides/source/qa/jenkins.md +++ b/doc/sphinx-guides/source/qa/jenkins.md @@ -38,7 +38,7 @@ If API tests are failing, you should not merge the pull request. How can you know if API tests are passing? Here are the steps, by way of example. - From the pull request, navigate to the build. For example from , look for a check called "continuous-integration/jenkins/pr-merge". Clicking it will bring you to a particular build like (build #10). -- You are now on the new "blue" interface for Jenkins. Click the button in the header called "go to classic" which should take you to (for example) . +- You are now on the new "blue" interface for Jenkins. Click the button with an arrow on the right side of the header called "go to classic" which should take you to (for example) . - Click "Test Result". - Under "All Tests", look at the duration for "edu.harvard.iq.dataverse.api". It should be ten minutes or higher. If it was only a few seconds, tests did not run. - Assuming tests ran, if there were failures, they should appear at the top under "All Failed Tests". Inform the author of the pull request about the error. From 6f1cd087624fea70a1c37425aacaf05c9d7ba0bf Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 5 Dec 2023 15:53:21 +0000 Subject: [PATCH 094/689] Added: checks before calling getFileMetadatas on canDownloadAtLeastOneFile method in PermissionServiceBean --- .../iq/dataverse/PermissionServiceBean.java | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java index 9e6628617ce..2e4627576c6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java @@ -41,6 +41,9 @@ import java.util.stream.Collectors; import static java.util.stream.Collectors.toList; import jakarta.persistence.Query; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; /** * Your one-stop-shop for deciding which user can do what action on which @@ -837,12 +840,56 @@ public boolean isMatchingWorkflowLock(Dataset d, String userId, String invocatio return false; } - public boolean canDownloadAtLeastOneFile(User requestUser, DatasetVersion datasetVersion) { + /** + * Checks if a User can download at least one file of the target DatasetVersion. + * + * @param user User to check + * @param datasetVersion DatasetVersion to check + * @return boolean indicating whether the user can download at least one file or not + */ + public boolean canDownloadAtLeastOneFile(User user, DatasetVersion datasetVersion) { + if (user.isSuperuser()) { + return true; + } + if (hasReleasedFiles(datasetVersion)) { + return true; + } for (FileMetadata fileMetadata : datasetVersion.getFileMetadatas()) { - if (userOn(requestUser, fileMetadata.getDataFile()).has(Permission.DownloadFile)) { + if (userOn(user, fileMetadata.getDataFile()).has(Permission.DownloadFile)) { return true; } } return false; } + + /** + * Checks if a DatasetVersion has released files. + * + * This method is mostly based on {@link #isPublicallyDownloadable(DvObject)} although in this case, instead of basing + * the search on a particular file, it searches for the total number of files in the target version that are present + * in the released version. + * + * @param targetDatasetVersion DatasetVersion to check + * @return boolean indicating whether the dataset version has released files or not + */ + private boolean hasReleasedFiles(DatasetVersion targetDatasetVersion) { + Dataset targetDataset = targetDatasetVersion.getDataset(); + if (!targetDataset.isReleased()) { + return false; + } + CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder(); + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Long.class); + Root datasetVersionRoot = criteriaQuery.from(DatasetVersion.class); + Root fileMetadataRoot = criteriaQuery.from(FileMetadata.class); + criteriaQuery + .select(criteriaBuilder.count(fileMetadataRoot)) + .where(criteriaBuilder.and( + criteriaBuilder.equal(fileMetadataRoot.get("dataFile").get("restricted"), false), + criteriaBuilder.equal(datasetVersionRoot.get("dataset"), targetDataset), + criteriaBuilder.equal(datasetVersionRoot.get("versionState"), DatasetVersion.VersionState.RELEASED), + fileMetadataRoot.in(targetDatasetVersion.getFileMetadatas()), + fileMetadataRoot.in(datasetVersionRoot.get("fileMetadatas")))); + Long result = em.createQuery(criteriaQuery).getSingleResult(); + return result > 0; + } } From 6a4a9ab3d625f1e5835b3e119449f8fd88eaee23 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 5 Dec 2023 12:10:48 -0500 Subject: [PATCH 095/689] stub out diagnosing jenkins failures #10101 --- doc/sphinx-guides/source/qa/jenkins.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/doc/sphinx-guides/source/qa/jenkins.md b/doc/sphinx-guides/source/qa/jenkins.md index a4ca4d8688f..9259284beb9 100644 --- a/doc/sphinx-guides/source/qa/jenkins.md +++ b/doc/sphinx-guides/source/qa/jenkins.md @@ -42,3 +42,18 @@ How can you know if API tests are passing? Here are the steps, by way of example - Click "Test Result". - Under "All Tests", look at the duration for "edu.harvard.iq.dataverse.api". It should be ten minutes or higher. If it was only a few seconds, tests did not run. - Assuming tests ran, if there were failures, they should appear at the top under "All Failed Tests". Inform the author of the pull request about the error. + +## Diagnosing Failures + +API test failures can have multiple causes. As described above, from the "Test Result" page, you might see the failure under "All Failed Tests". However, the test could have failed because of some underlying system issue. + +If you have determined that the API tests have not run at all, your next step should be to click on "Console Output". For example, . Click "Full log" to see the full log in the browser or navigate to (for example) to get a plain text version. + +Go to the end of the log and then scroll up, looking for the failure. A failed Ansible task can look like this: + +``` +TASK [dataverse : download payara zip] ***************************************** +fatal: [localhost]: FAILED! => {"changed": false, "dest": "/tmp/payara.zip", "elapsed": 10, "msg": "Request failed: ", "url": "https://nexus.payara.fish/repository/payara-community/fish/payara/distributions/payara/6.2023.8/payara-6.2023.8.zip"} +``` + +In the example above, if Payara can't be downloaded, we're obviously going to have problems deploying Dataverse to it! From 03a4c77155934060c33c33ed27ea2f7628301e91 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 6 Dec 2023 10:58:33 +0000 Subject: [PATCH 096/689] Refactor: shortcut on datafile permission check --- .../harvard/iq/dataverse/PermissionServiceBean.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java index 2e4627576c6..107024bcfb9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java @@ -851,11 +851,13 @@ public boolean canDownloadAtLeastOneFile(User user, DatasetVersion datasetVersio if (user.isSuperuser()) { return true; } - if (hasReleasedFiles(datasetVersion)) { + if (hasUnrestrictedReleasedFiles(datasetVersion)) { return true; } for (FileMetadata fileMetadata : datasetVersion.getFileMetadatas()) { - if (userOn(user, fileMetadata.getDataFile()).has(Permission.DownloadFile)) { + DataFile dataFile = fileMetadata.getDataFile(); + Set ras = new HashSet<>(groupService.groupsFor(user, dataFile)); + if (hasGroupPermissionsFor(ras, dataFile, EnumSet.of(Permission.DownloadFile))) { return true; } } @@ -863,7 +865,7 @@ public boolean canDownloadAtLeastOneFile(User user, DatasetVersion datasetVersio } /** - * Checks if a DatasetVersion has released files. + * Checks if a DatasetVersion has unrestricted released files. * * This method is mostly based on {@link #isPublicallyDownloadable(DvObject)} although in this case, instead of basing * the search on a particular file, it searches for the total number of files in the target version that are present @@ -872,7 +874,7 @@ public boolean canDownloadAtLeastOneFile(User user, DatasetVersion datasetVersio * @param targetDatasetVersion DatasetVersion to check * @return boolean indicating whether the dataset version has released files or not */ - private boolean hasReleasedFiles(DatasetVersion targetDatasetVersion) { + private boolean hasUnrestrictedReleasedFiles(DatasetVersion targetDatasetVersion) { Dataset targetDataset = targetDatasetVersion.getDataset(); if (!targetDataset.isReleased()) { return false; From 326b784da752091bf4c7b3bf4112ebfc327acb69 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 6 Dec 2023 10:59:08 +0000 Subject: [PATCH 097/689] Refactor: variable extracted in isPublicallyDownloadable --- .../java/edu/harvard/iq/dataverse/PermissionServiceBean.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java index 107024bcfb9..1c568e83143 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java @@ -451,8 +451,9 @@ private boolean isPublicallyDownloadable(DvObject dvo) { if (!df.isRestricted()) { if (df.getOwner().getReleasedVersion() != null) { - if (df.getOwner().getReleasedVersion().getFileMetadatas() != null) { - for (FileMetadata fm : df.getOwner().getReleasedVersion().getFileMetadatas()) { + List fileMetadatas = df.getOwner().getReleasedVersion().getFileMetadatas(); + if (fileMetadatas != null) { + for (FileMetadata fm : fileMetadatas) { if (df.equals(fm.getDataFile())) { return true; } From 16c685dc30601d8a8b0140cec4b8621e1fe33a99 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 6 Dec 2023 11:22:06 +0000 Subject: [PATCH 098/689] Changed: passing DataverseRequest instead of User to canDownloadAtLeastOneFile --- .../harvard/iq/dataverse/PermissionServiceBean.java | 11 ++++++----- .../java/edu/harvard/iq/dataverse/api/Datasets.java | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java index 1c568e83143..e87809ada56 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java @@ -844,20 +844,21 @@ public boolean isMatchingWorkflowLock(Dataset d, String userId, String invocatio /** * Checks if a User can download at least one file of the target DatasetVersion. * - * @param user User to check + * @param dataverseRequest DataverseRequest to check * @param datasetVersion DatasetVersion to check * @return boolean indicating whether the user can download at least one file or not */ - public boolean canDownloadAtLeastOneFile(User user, DatasetVersion datasetVersion) { - if (user.isSuperuser()) { + public boolean canDownloadAtLeastOneFile(DataverseRequest dataverseRequest, DatasetVersion datasetVersion) { + if (dataverseRequest.getUser().isSuperuser()) { return true; } if (hasUnrestrictedReleasedFiles(datasetVersion)) { return true; } - for (FileMetadata fileMetadata : datasetVersion.getFileMetadatas()) { + List fileMetadatas = datasetVersion.getFileMetadatas(); + for (FileMetadata fileMetadata : fileMetadatas) { DataFile dataFile = fileMetadata.getDataFile(); - Set ras = new HashSet<>(groupService.groupsFor(user, dataFile)); + Set ras = new HashSet<>(groupService.groupsFor(dataverseRequest, dataFile)); if (hasGroupPermissionsFor(ras, dataFile, EnumSet.of(Permission.DownloadFile))) { return true; } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index a9cfefc33d8..6a1e11e690b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -4145,7 +4145,7 @@ public Response getCanDownloadAtLeastOneFile(@Context ContainerRequestContext cr @Context HttpHeaders headers) { return response(req -> { DatasetVersion datasetVersion = getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers, false); - return ok(permissionService.canDownloadAtLeastOneFile(getRequestUser(crc), datasetVersion)); + return ok(permissionService.canDownloadAtLeastOneFile(req, datasetVersion)); }, getRequestUser(crc)); } } From 8ca2338723a0ec1a57a9affc923fe65229009909 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 6 Dec 2023 11:22:51 +0000 Subject: [PATCH 099/689] Fixed: method doc --- .../java/edu/harvard/iq/dataverse/PermissionServiceBean.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java index e87809ada56..359e8823fce 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java @@ -842,7 +842,7 @@ public boolean isMatchingWorkflowLock(Dataset d, String userId, String invocatio } /** - * Checks if a User can download at least one file of the target DatasetVersion. + * Checks if a DataverseRequest can download at least one file of the target DatasetVersion. * * @param dataverseRequest DataverseRequest to check * @param datasetVersion DatasetVersion to check From 96cd5c9d55437180cfa256df38b0d5990c97ec6c Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 6 Dec 2023 11:24:49 +0000 Subject: [PATCH 100/689] Added: explanatory comment --- .../java/edu/harvard/iq/dataverse/PermissionServiceBean.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java index 359e8823fce..6dc943f1ca8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java @@ -852,6 +852,7 @@ public boolean canDownloadAtLeastOneFile(DataverseRequest dataverseRequest, Data if (dataverseRequest.getUser().isSuperuser()) { return true; } + // This is a shortcut to avoid having to check version files if the condition is met if (hasUnrestrictedReleasedFiles(datasetVersion)) { return true; } From 3c1820b060b303da2bfa97132667ceccb5d5e977 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 6 Dec 2023 11:48:09 +0000 Subject: [PATCH 101/689] Added: includeDeaccessioned query param to getCanDownloadAtLeastOneFile API endpoint --- src/main/java/edu/harvard/iq/dataverse/api/Datasets.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 6a1e11e690b..579f4f78fe1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -4141,10 +4141,11 @@ public Response getUserPermissionsOnDataset(@Context ContainerRequestContext crc public Response getCanDownloadAtLeastOneFile(@Context ContainerRequestContext crc, @PathParam("id") String datasetId, @PathParam("versionId") String versionId, + @QueryParam("includeDeaccessioned") boolean includeDeaccessioned, @Context UriInfo uriInfo, @Context HttpHeaders headers) { return response(req -> { - DatasetVersion datasetVersion = getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers, false); + DatasetVersion datasetVersion = getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers, includeDeaccessioned); return ok(permissionService.canDownloadAtLeastOneFile(req, datasetVersion)); }, getRequestUser(crc)); } From e9a670c8620c068419080aad25421afa04641958 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 6 Dec 2023 10:39:09 -0500 Subject: [PATCH 102/689] collection not DB #10101 --- doc/sphinx-guides/source/qa/performance-tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/qa/performance-tests.md b/doc/sphinx-guides/source/qa/performance-tests.md index f433226d4ff..447c4f6c54d 100644 --- a/doc/sphinx-guides/source/qa/performance-tests.md +++ b/doc/sphinx-guides/source/qa/performance-tests.md @@ -20,4 +20,4 @@ Please note the performance database is also used occasionally by Julian and the Executing the Performance Script -------------------------------- -To execute the performance test script, you need to install a local copy of the database-helper-scripts project at . We have since produced a stripped-down script that calls just the DB and ds and works with python3. +To execute the performance test script, you need to install a local copy of the database-helper-scripts project at . We have since produced a stripped-down script that calls just the collection and dataset and works with Python 3. From c2ad0092c545a41f071129bcd85c398775a53a1e Mon Sep 17 00:00:00 2001 From: sbondka Date: Wed, 6 Dec 2023 17:28:40 +0100 Subject: [PATCH 103/689] Add modifications --- .../source/_static/admin/dataverse-external-tools.tsv | 1 + doc/sphinx-guides/source/admin/integrations.rst | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv b/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv index 4f4c29d0670..ba60be59227 100644 --- a/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv +++ b/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv @@ -5,3 +5,4 @@ Binder explore dataset Binder allows you to spin up custom computing environment File Previewers explore file "A set of tools that display the content of files - including audio, html, `Hypothes.is `_ annotations, images, PDF, Markdown, text, video, tabular data, spreadsheets, GeoJSON, zip, and NcML files - allowing them to be viewed without downloading the file. The previewers can be run directly from github.io, so the only required step is using the Dataverse API to register the ones you want to use. Documentation, including how to optionally brand the previewers, and an invitation to contribute through github are in the README.md file. Initial development was led by the Qualitative Data Repository and the spreasdheet previewer was added by the Social Sciences and Humanities Open Cloud (SSHOC) project. https://github.com/gdcc/dataverse-previewers" Data Curation Tool configure file "A GUI for curating data by adding labels, groups, weights and other details to assist with informed reuse. See the README.md file at https://github.com/scholarsportal/Dataverse-Data-Curation-Tool for the installation instructions." Ask the Data query file Ask the Data is an experimental tool that allows you ask natural language questions about the data contained in Dataverse tables (tabular data). See the README.md file at https://github.com/IQSS/askdataverse/tree/main/askthedata for the instructions on adding Ask the Data to your Dataverse installation. +JupyterHub explore file The `Dataverse-to-JupyterHub Data Transfer Connector `_ is a tool that simplifies the transfer of data between Dataverse repositories and the cloud-based platform JupyterHub. It is designed for researchers, scientists, and data analysts, facilitating collaboration on projects by seamlessly moving datasets and files. The tool is a lightweight client-side web application built using React and relies on the Dataverse External Tool feature, allowing for easy deployment on modern integration systems. Currently optimized for small to medium-sized files, future plans include extending support for larger files and signed Dataverse endpoints. For more details, you can refer to the external tool manifest: https://forgemia.inra.fr/dipso/eosc-pillar/dataverse-jupyterhub-connector/-/blob/master/externalTools.json diff --git a/doc/sphinx-guides/source/admin/integrations.rst b/doc/sphinx-guides/source/admin/integrations.rst index a9b962f33ca..ed3860a9ca1 100644 --- a/doc/sphinx-guides/source/admin/integrations.rst +++ b/doc/sphinx-guides/source/admin/integrations.rst @@ -188,12 +188,12 @@ Researchers can use a Google Sheets add-on to search for Dataverse installation' JupyterHub ++++++++++ -The Dataverse-to-JupyterHub Data Transfer Connector streamlines data transfer between Dataverse repositories and the cloud-based platform JupyterHub, enhancing collaborative research. +The `Dataverse-to-JupyterHub Data Transfer Connector `_ streamlines data transfer between Dataverse repositories and the cloud-based platform JupyterHub, enhancing collaborative research. This connector facilitates seamless two-way transfer of datasets and files, emphasizing the potential of an integrated research environment. It is a lightweight client-side web application built using React and relying on the Dataverse External Tool feature, allowing for easy deployment on modern integration systems. Currently, it supports small to medium-sized files, with plans to enable support for large files and signed Dataverse endpoints in the future. What kind of user is the feature intended for? -The feature is intended for reasearchers, scientists and data analyst working with Dataverse instances and JupyterHub looking to ease the data transfer process. +The feature is intended for researchers, scientists and data analyst who are working with Dataverse instances and JupyterHub looking to ease the data transfer process. .. _integrations-discovery: From a9a8f0cadec9bc3b31f0546805c46cdbf578aef1 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 6 Dec 2023 11:37:06 -0500 Subject: [PATCH 104/689] clarify it's pages we're hitting #10101 --- doc/sphinx-guides/source/qa/performance-tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/qa/performance-tests.md b/doc/sphinx-guides/source/qa/performance-tests.md index 447c4f6c54d..ad7972bd75e 100644 --- a/doc/sphinx-guides/source/qa/performance-tests.md +++ b/doc/sphinx-guides/source/qa/performance-tests.md @@ -20,4 +20,4 @@ Please note the performance database is also used occasionally by Julian and the Executing the Performance Script -------------------------------- -To execute the performance test script, you need to install a local copy of the database-helper-scripts project at . We have since produced a stripped-down script that calls just the collection and dataset and works with Python 3. +To execute the performance test script, you need to install a local copy of the database-helper-scripts project at . We have since produced a stripped-down script that calls just the collection and dataset pages and works with Python 3. From 15e80aa4c847cb5ce8574fe600723c9cc81a5bc2 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 6 Dec 2023 16:56:37 +0000 Subject: [PATCH 105/689] Fixed: roleAssignees setup in canDownloadAtLeastOneFile --- .../edu/harvard/iq/dataverse/PermissionServiceBean.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java index 6dc943f1ca8..471cac31e77 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java @@ -849,7 +849,8 @@ public boolean isMatchingWorkflowLock(Dataset d, String userId, String invocatio * @return boolean indicating whether the user can download at least one file or not */ public boolean canDownloadAtLeastOneFile(DataverseRequest dataverseRequest, DatasetVersion datasetVersion) { - if (dataverseRequest.getUser().isSuperuser()) { + User user = dataverseRequest.getUser(); + if (user.isSuperuser()) { return true; } // This is a shortcut to avoid having to check version files if the condition is met @@ -859,8 +860,9 @@ public boolean canDownloadAtLeastOneFile(DataverseRequest dataverseRequest, Data List fileMetadatas = datasetVersion.getFileMetadatas(); for (FileMetadata fileMetadata : fileMetadatas) { DataFile dataFile = fileMetadata.getDataFile(); - Set ras = new HashSet<>(groupService.groupsFor(dataverseRequest, dataFile)); - if (hasGroupPermissionsFor(ras, dataFile, EnumSet.of(Permission.DownloadFile))) { + Set roleAssignees = new HashSet<>(groupService.groupsFor(dataverseRequest, dataFile)); + roleAssignees.add(user); + if (hasGroupPermissionsFor(roleAssignees, dataFile, EnumSet.of(Permission.DownloadFile))) { return true; } } From 4b71b36305fb6c18f7282530dc4491976a352936 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 6 Dec 2023 17:02:07 +0000 Subject: [PATCH 106/689] Added: IT for getCanDownloadAtLeastOneFile endpoint --- .../harvard/iq/dataverse/api/DatasetsIT.java | 71 +++++++++++++++---- 1 file changed, 58 insertions(+), 13 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index 945b741a94b..3510f2c06ef 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -80,7 +80,6 @@ import javax.xml.stream.XMLStreamReader; import static java.lang.Thread.sleep; -import static org.junit.jupiter.api.Assertions.assertEquals; import org.hamcrest.CoreMatchers; @@ -90,11 +89,7 @@ import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.Matchers.contains; - -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.*; public class DatasetsIT { @@ -4123,10 +4118,10 @@ public void testGetUserPermissionsOnDataset() { } @Test - public void testGetCanDownloadAtLeastOneFile() { - Response createUser = UtilIT.createRandomUser(); - createUser.then().assertThat().statusCode(OK.getStatusCode()); - String apiToken = UtilIT.getApiTokenFromResponse(createUser); + public void testGetCanDownloadAtLeastOneFile() throws InterruptedException { + Response createUserResponse = UtilIT.createRandomUser(); + createUserResponse.then().assertThat().statusCode(OK.getStatusCode()); + String apiToken = UtilIT.getApiTokenFromResponse(createUserResponse); Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); createDataverseResponse.then().assertThat().statusCode(CREATED.getStatusCode()); @@ -4135,15 +4130,65 @@ public void testGetCanDownloadAtLeastOneFile() { Response createDatasetResponse = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, apiToken); createDatasetResponse.then().assertThat().statusCode(CREATED.getStatusCode()); int datasetId = JsonPath.from(createDatasetResponse.body().asString()).getInt("data.id"); + String datasetPersistentId = JsonPath.from(createDatasetResponse.body().asString()).getString("data.persistentId"); - // Call with valid dataset id - Response canDownloadAtLeastOneFileResponse = UtilIT.getCanDownloadAtLeastOneFile(Integer.toString(datasetId), DS_VERSION_LATEST, apiToken); + // Upload file + String pathToTestFile = "src/test/resources/images/coffeeshop.png"; + Response uploadResponse = UtilIT.uploadFileViaNative(Integer.toString(datasetId), pathToTestFile, Json.createObjectBuilder().build(), apiToken); + uploadResponse.then().assertThat().statusCode(OK.getStatusCode()); + + String fileId = JsonPath.from(uploadResponse.body().asString()).getString("data.files[0].dataFile.id"); + + // Publish dataset version + Response publishDataverseResponse = UtilIT.publishDataverseViaNativeApi(dataverseAlias, apiToken); + publishDataverseResponse.then().assertThat().statusCode(OK.getStatusCode()); + Response publishDatasetResponse = UtilIT.publishDatasetViaNativeApi(datasetPersistentId, "major", apiToken); + publishDatasetResponse.then().assertThat().statusCode(OK.getStatusCode()); + + // Make sure the dataset is published + Thread.sleep(3000); + + // Create a second user to call the getCanDownloadAtLeastOneFile method + Response createSecondUserResponse = UtilIT.createRandomUser(); + createSecondUserResponse.then().assertThat().statusCode(OK.getStatusCode()); + String secondUserApiToken = UtilIT.getApiTokenFromResponse(createSecondUserResponse); + String secondUserUsername = UtilIT.getUsernameFromResponse(createSecondUserResponse); + + // Call with a valid dataset id when a file is released + Response canDownloadAtLeastOneFileResponse = UtilIT.getCanDownloadAtLeastOneFile(Integer.toString(datasetId), DS_VERSION_LATEST, secondUserApiToken); canDownloadAtLeastOneFileResponse.then().assertThat().statusCode(OK.getStatusCode()); boolean canDownloadAtLeastOneFile = JsonPath.from(canDownloadAtLeastOneFileResponse.body().asString()).getBoolean("data"); assertTrue(canDownloadAtLeastOneFile); + // Restrict file + Response restrictFileResponse = UtilIT.restrictFile(fileId, true, apiToken); + restrictFileResponse.then().assertThat().statusCode(OK.getStatusCode()); + + // Publish dataset version + publishDatasetResponse = UtilIT.publishDatasetViaNativeApi(datasetPersistentId, "major", apiToken); + publishDatasetResponse.then().assertThat().statusCode(OK.getStatusCode()); + + // Make sure the dataset is published + Thread.sleep(3000); + + // Call with a valid dataset id when a file is restricted and the user does not have access + canDownloadAtLeastOneFileResponse = UtilIT.getCanDownloadAtLeastOneFile(Integer.toString(datasetId), DS_VERSION_LATEST, secondUserApiToken); + canDownloadAtLeastOneFileResponse.then().assertThat().statusCode(OK.getStatusCode()); + canDownloadAtLeastOneFile = JsonPath.from(canDownloadAtLeastOneFileResponse.body().asString()).getBoolean("data"); + assertFalse(canDownloadAtLeastOneFile); + + // Grant restricted file access to the user + Response grantFileAccessResponse = UtilIT.grantFileAccess(fileId, "@" + secondUserUsername, apiToken); + grantFileAccessResponse.then().assertThat().statusCode(OK.getStatusCode()); + + // Call with a valid dataset id when a file is restricted and the user has access + canDownloadAtLeastOneFileResponse = UtilIT.getCanDownloadAtLeastOneFile(Integer.toString(datasetId), DS_VERSION_LATEST, secondUserApiToken); + canDownloadAtLeastOneFileResponse.then().assertThat().statusCode(OK.getStatusCode()); + canDownloadAtLeastOneFile = JsonPath.from(canDownloadAtLeastOneFileResponse.body().asString()).getBoolean("data"); + assertTrue(canDownloadAtLeastOneFile); + // Call with invalid dataset id - Response getUserPermissionsOnDatasetInvalidIdResponse = UtilIT.getCanDownloadAtLeastOneFile("testInvalidId", DS_VERSION_LATEST, apiToken); + Response getUserPermissionsOnDatasetInvalidIdResponse = UtilIT.getCanDownloadAtLeastOneFile("testInvalidId", DS_VERSION_LATEST, secondUserApiToken); getUserPermissionsOnDatasetInvalidIdResponse.then().assertThat().statusCode(BAD_REQUEST.getStatusCode()); } } From 9dd3f9785c6a5c8939bd9f023400f5f10c3ef58d Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 11 Dec 2023 09:28:16 +0000 Subject: [PATCH 107/689] Added: release notes for #10155 --- .../10155-datasets-can-download-at-least-one-file.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 doc/release-notes/10155-datasets-can-download-at-least-one-file.md diff --git a/doc/release-notes/10155-datasets-can-download-at-least-one-file.md b/doc/release-notes/10155-datasets-can-download-at-least-one-file.md new file mode 100644 index 00000000000..566d505f7ca --- /dev/null +++ b/doc/release-notes/10155-datasets-can-download-at-least-one-file.md @@ -0,0 +1,3 @@ +The getCanDownloadAtLeastOneFile (/api/datasets/{id}/versions/{versionId}/canDownloadAtLeastOneFile) endpoint has been created. + +This endpoint allows to know if the calling user can download at least one file of a particular dataset version. From 9fb44d3d45080a2e5c9de15ab0445cc052c956b3 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 11 Dec 2023 09:33:56 +0000 Subject: [PATCH 108/689] Added: docs for #10155 --- doc/sphinx-guides/source/api/native-api.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 56190dd342c..99438520120 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -2686,6 +2686,19 @@ In particular, the user permissions that this API call checks, returned as boole curl -H "X-Dataverse-key: $API_TOKEN" -X GET "$SERVER_URL/api/datasets/$ID/userPermissions" +Know if a User can download at least one File from a Dataset Version +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This API call allows to know if the calling user can download at least one file of a dataset version. + +.. code-block:: bash + + export SERVER_URL=https://demo.dataverse.org + export ID=24 + export VERSION=1.0 + + curl -H "X-Dataverse-key: $API_TOKEN" -X GET "$SERVER_URL/api/datasets/$ID/versions/$VERSION/canDownloadAtLeastOneFile" + Files ----- From fa32ef5a413f6b0fbfab7d6e96e602a31bc18ac4 Mon Sep 17 00:00:00 2001 From: Guillermo Portas Date: Tue, 12 Dec 2023 11:36:52 +0000 Subject: [PATCH 109/689] 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 99438520120..1e86f24356b 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -2686,7 +2686,7 @@ In particular, the user permissions that this API call checks, returned as boole curl -H "X-Dataverse-key: $API_TOKEN" -X GET "$SERVER_URL/api/datasets/$ID/userPermissions" -Know if a User can download at least one File from a Dataset Version +Know If a User Can Download at Least One File from a Dataset Version ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This API call allows to know if the calling user can download at least one file of a dataset version. From 476977b48925ae6eae4dabf69b0de0d7d40d6841 Mon Sep 17 00:00:00 2001 From: Guillermo Portas Date: Tue, 12 Dec 2023 11:37:01 +0000 Subject: [PATCH 110/689] 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 1e86f24356b..9ceeb4410ef 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -2689,7 +2689,7 @@ In particular, the user permissions that this API call checks, returned as boole Know If a User Can Download at Least One File from a Dataset Version ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This API call allows to know if the calling user can download at least one file of a dataset version. +This API endpoint indicates if the calling user can download at least one file from a dataset version. Note that Shibboleth group permissions are not considered. .. code-block:: bash From 64861afbc11c4475ca3d85e729f4b73e962d5efa Mon Sep 17 00:00:00 2001 From: Guillermo Portas Date: Tue, 12 Dec 2023 11:37:36 +0000 Subject: [PATCH 111/689] Update doc/release-notes/10155-datasets-can-download-at-least-one-file.md Co-authored-by: Philip Durbin --- .../10155-datasets-can-download-at-least-one-file.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/10155-datasets-can-download-at-least-one-file.md b/doc/release-notes/10155-datasets-can-download-at-least-one-file.md index 566d505f7ca..a0b0d02310a 100644 --- a/doc/release-notes/10155-datasets-can-download-at-least-one-file.md +++ b/doc/release-notes/10155-datasets-can-download-at-least-one-file.md @@ -1,3 +1,3 @@ The getCanDownloadAtLeastOneFile (/api/datasets/{id}/versions/{versionId}/canDownloadAtLeastOneFile) endpoint has been created. -This endpoint allows to know if the calling user can download at least one file of a particular dataset version. +This API endpoint indicates if the calling user can download at least one file from a dataset version. Note that Shibboleth group permissions are not considered. From 39e4bcee0f164854301b45f0ba6cbd4e11b4cf5c Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 12 Dec 2023 13:42:46 +0000 Subject: [PATCH 112/689] Fixed: minio storage volume mapping --- docker-compose-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 5265a6b7c2d..6f8decc0dfb 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -209,7 +209,7 @@ services: networks: - dataverse volumes: - - minio_storage:/data + - ./docker-dev-volumes/minio_storage:/data environment: MINIO_ROOT_USER: 4cc355_k3y MINIO_ROOT_PASSWORD: s3cr3t_4cc355_k3y From 0c279adc3e93bd09bedc08a3f1bda48876fc1de3 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 12 Dec 2023 13:50:08 +0000 Subject: [PATCH 113/689] Removed: sleep calls from testGetCanDownloadAtLeastOneFile IT --- .../java/edu/harvard/iq/dataverse/api/DatasetsIT.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index b2cf5c75467..f36b93b85ab 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -4225,7 +4225,7 @@ public void testGetGlobusUploadParameters() { } @Test - public void testGetCanDownloadAtLeastOneFile() throws InterruptedException { + public void testGetCanDownloadAtLeastOneFile() { Response createUserResponse = UtilIT.createRandomUser(); createUserResponse.then().assertThat().statusCode(OK.getStatusCode()); String apiToken = UtilIT.getApiTokenFromResponse(createUserResponse); @@ -4252,9 +4252,6 @@ public void testGetCanDownloadAtLeastOneFile() throws InterruptedException { Response publishDatasetResponse = UtilIT.publishDatasetViaNativeApi(datasetPersistentId, "major", apiToken); publishDatasetResponse.then().assertThat().statusCode(OK.getStatusCode()); - // Make sure the dataset is published - Thread.sleep(3000); - // Create a second user to call the getCanDownloadAtLeastOneFile method Response createSecondUserResponse = UtilIT.createRandomUser(); createSecondUserResponse.then().assertThat().statusCode(OK.getStatusCode()); @@ -4275,9 +4272,6 @@ public void testGetCanDownloadAtLeastOneFile() throws InterruptedException { publishDatasetResponse = UtilIT.publishDatasetViaNativeApi(datasetPersistentId, "major", apiToken); publishDatasetResponse.then().assertThat().statusCode(OK.getStatusCode()); - // Make sure the dataset is published - Thread.sleep(3000); - // Call with a valid dataset id when a file is restricted and the user does not have access canDownloadAtLeastOneFileResponse = UtilIT.getCanDownloadAtLeastOneFile(Integer.toString(datasetId), DS_VERSION_LATEST, secondUserApiToken); canDownloadAtLeastOneFileResponse.then().assertThat().statusCode(OK.getStatusCode()); From 960a20c79dc8a3292ff3d26973d8e35d8a4f481c Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Tue, 12 Dec 2023 14:06:21 -0500 Subject: [PATCH 114/689] #10168 fix error response status --- src/main/java/edu/harvard/iq/dataverse/api/Datasets.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index b3bfc476423..05355cbbc68 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -4288,7 +4288,7 @@ public Response getDatasetVersionArchivalStatus(@Context ContainerRequestContext headers); if (dsv.getArchivalCopyLocation() == null) { - return error(Status.NO_CONTENT, "This dataset version has not been archived"); + return error(Status.NOT_FOUND, "This dataset version has not been archived"); } else { JsonObject status = JsonUtil.getJsonObject(dsv.getArchivalCopyLocation()); return ok(status); From 40e5d39c73ec2097fb16d65e8fff33078168498b Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 12 Dec 2023 14:53:45 -0500 Subject: [PATCH 115/689] how to test Docker images made during a release --- .../source/developers/making-releases.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/doc/sphinx-guides/source/developers/making-releases.rst b/doc/sphinx-guides/source/developers/making-releases.rst index 23c4773a06e..432b4ca2672 100755 --- a/doc/sphinx-guides/source/developers/making-releases.rst +++ b/doc/sphinx-guides/source/developers/making-releases.rst @@ -67,6 +67,19 @@ Once important tests have passed (compile, unit tests, etc.), merge the pull req If this is a hotfix release, skip this whole "merge develop to master" step (the "develop" branch is not involved until later). +(Optional) Test Docker Images +----------------------------- + +After the "master" branch has been updated and the GitHub Action to build and push Docker images has run (see `PR #9776 `_), go to https://hub.docker.com/u/gdcc and make sure the "alpha" tag for the following images has been updated: + +- https://hub.docker.com/r/gdcc/base +- https://hub.docker.com/r/gdcc/dataverse +- https://hub.docker.com/r/gdcc/configbaker + +To test these images against our API test suite, go to the "alpha" workflow at https://github.com/gdcc/api-test-runner/actions/workflows/alpha.yml and run it. + +If there are failures, additional dependencies or settings may have been added to the "develop" workflow. Copy them over and try again. + Build the Guides for the Release -------------------------------- From a240bd0fa81cc4a9db0cc9c8ddb37ad733324fcd Mon Sep 17 00:00:00 2001 From: Don Sizemore Date: Tue, 12 Dec 2023 15:20:07 -0500 Subject: [PATCH 116/689] bump htmlunit to 3.9.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 34b0ad2e835..d690e509f46 100644 --- a/pom.xml +++ b/pom.xml @@ -650,7 +650,7 @@ org.htmlunit htmlunit - 3.2.0 + 3.9.0 test From b1f15bb95ff58dd62c7aaa1a2ababa1f44b83881 Mon Sep 17 00:00:00 2001 From: Don Sizemore Date: Tue, 12 Dec 2023 15:30:54 -0500 Subject: [PATCH 117/689] bump DuraCloud to 8.0.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 34b0ad2e835..be4fa605aab 100644 --- a/pom.xml +++ b/pom.xml @@ -466,7 +466,7 @@ org.duracloud common - 7.1.1 + 8.0.0 org.slf4j @@ -481,7 +481,7 @@ org.duracloud storeclient - 7.1.1 + 8.0.0 org.slf4j From daf89261174600b1db106974cc941213fa0b36bd Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Tue, 12 Dec 2023 15:37:27 -0500 Subject: [PATCH 118/689] #10168 update integration tests --- src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index 928574eb82b..7efd44b9533 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -3291,7 +3291,8 @@ public void testArchivalStatusAPI() throws IOException { //Verify the status is empty Response nullStatus = UtilIT.getDatasetVersionArchivalStatus(datasetId, "1.0", apiToken); - nullStatus.then().assertThat().statusCode(NO_CONTENT.getStatusCode()); + nullStatus.prettyPrint(); + nullStatus.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); //Set it Response setStatus = UtilIT.setDatasetVersionArchivalStatus(datasetId, "1.0", apiToken, "pending", @@ -3309,7 +3310,7 @@ public void testArchivalStatusAPI() throws IOException { //Make sure it's gone Response nullStatus2 = UtilIT.getDatasetVersionArchivalStatus(datasetId, "1.0", apiToken); - nullStatus2.then().assertThat().statusCode(NO_CONTENT.getStatusCode()); + nullStatus2.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); } From 2ce0fb8f083ef8dfedfb71feea0d58ff2f9c7647 Mon Sep 17 00:00:00 2001 From: Don Sizemore Date: Tue, 12 Dec 2023 16:06:52 -0500 Subject: [PATCH 119/689] bump google.cloud.version to 0.209.0 --- modules/dataverse-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/dataverse-parent/pom.xml b/modules/dataverse-parent/pom.xml index 7b305cad581..25d714b39ed 100644 --- a/modules/dataverse-parent/pom.xml +++ b/modules/dataverse-parent/pom.xml @@ -152,7 +152,7 @@ 42.6.0 9.3.0 1.12.290 - 0.177.0 + 0.209.0 8.0.0 From 349f7dbcaaaf260c00126567f9f4c6d32b0c367c Mon Sep 17 00:00:00 2001 From: sbondka Date: Wed, 13 Dec 2023 15:31:31 +0100 Subject: [PATCH 120/689] Add presentation link --- doc/sphinx-guides/source/admin/integrations.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/admin/integrations.rst b/doc/sphinx-guides/source/admin/integrations.rst index ed3860a9ca1..53a663b942e 100644 --- a/doc/sphinx-guides/source/admin/integrations.rst +++ b/doc/sphinx-guides/source/admin/integrations.rst @@ -193,7 +193,7 @@ This connector facilitates seamless two-way transfer of datasets and files, emph It is a lightweight client-side web application built using React and relying on the Dataverse External Tool feature, allowing for easy deployment on modern integration systems. Currently, it supports small to medium-sized files, with plans to enable support for large files and signed Dataverse endpoints in the future. What kind of user is the feature intended for? -The feature is intended for researchers, scientists and data analyst who are working with Dataverse instances and JupyterHub looking to ease the data transfer process. +The feature is intended for researchers, scientists and data analyst who are working with Dataverse instances and JupyterHub looking to ease the data transfer process. See `presentation `_ for details. .. _integrations-discovery: From ea644b89a3149ff8599fe3fcaa3a2bf6f5804e71 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 13 Dec 2023 14:16:47 -0500 Subject: [PATCH 121/689] add "message sent" success message #2638 --- src/main/java/edu/harvard/iq/dataverse/SendFeedbackDialog.java | 2 ++ src/main/java/propertyFiles/Bundle.properties | 1 + src/main/webapp/contactFormFragment.xhtml | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/SendFeedbackDialog.java b/src/main/java/edu/harvard/iq/dataverse/SendFeedbackDialog.java index 6be768321c4..68912969003 100644 --- a/src/main/java/edu/harvard/iq/dataverse/SendFeedbackDialog.java +++ b/src/main/java/edu/harvard/iq/dataverse/SendFeedbackDialog.java @@ -6,6 +6,7 @@ import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; +import edu.harvard.iq.dataverse.util.JsfHelper; import edu.harvard.iq.dataverse.util.MailUtil; import edu.harvard.iq.dataverse.util.SystemConfig; import java.util.Optional; @@ -217,6 +218,7 @@ public String sendMessage() { } logger.fine("sending feedback: " + feedback); mailService.sendMail(feedback.getFromEmail(), feedback.getToEmail(), feedback.getCcEmail(), feedback.getSubject(), feedback.getBody()); + JsfHelper.addSuccessMessage(BundleUtil.getStringFromBundle("contact.sent")); return null; } diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 10576c0c116..0c6ce979a94 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -184,6 +184,7 @@ contact.context.file.intro={0}\n\nYou have just been sent the following message contact.context.file.ending=\n\n---\n\n{0}\n{1}\n\nGo to file {2}/file.xhtml?fileId={3}\n\nYou received this email because you have been listed as a contact for the dataset. If you believe this was an error, please contact {4} at {5}. To respond directly to the individual who sent the message, simply reply to this email. contact.context.support.intro={0},\n\nThe following message was sent from {1}.\n\n---\n\n contact.context.support.ending=\n\n---\n\nMessage sent from Support contact form. +contact.sent=Message sent. # dataverseuser.xhtml account.info=Account Information diff --git a/src/main/webapp/contactFormFragment.xhtml b/src/main/webapp/contactFormFragment.xhtml index cb4eb3d0872..8950ec5acf8 100644 --- a/src/main/webapp/contactFormFragment.xhtml +++ b/src/main/webapp/contactFormFragment.xhtml @@ -81,7 +81,7 @@
+ update="@form,messagePanel" oncomplete="if (args && !args.validationFailed) PF('contactForm').hide();" actionListener="#{sendFeedbackDialog.sendMessage}">
From 057d1b926513a4716737a4b766a8fb46e709d44e Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 4 Jan 2024 09:05:21 -0500 Subject: [PATCH 154/689] add docker compose config to get HarvestingServerIT to pass #9275 --- docker-compose-dev.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 6f8decc0dfb..ce9f39a418a 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -19,6 +19,9 @@ services: DATAVERSE_AUTH_OIDC_CLIENT_SECRET: 94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8 DATAVERSE_AUTH_OIDC_AUTH_SERVER_URL: http://keycloak.mydomain.com:8090/realms/test DATAVERSE_JSF_REFRESH_PERIOD: "1" + # to get HarvestingServerIT to pass + dataverse_oai_server_maxidentifiers: "2" + dataverse_oai_server_maxrecords: "2" JVM_ARGS: -Ddataverse.files.storage-driver-id=file1 -Ddataverse.files.file1.type=file -Ddataverse.files.file1.label=Filesystem From 37d3d41a51867758cac611215f830ad2af1d31a1 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 4 Jan 2024 09:11:41 -0500 Subject: [PATCH 155/689] assert 500 error when invalid query params are passed #9275 --- .../harvard/iq/dataverse/api/HarvestingServerIT.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java index e02964ef28f..07788eca6db 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java @@ -860,7 +860,16 @@ public void testMultiRecordOaiSet() throws InterruptedException { logger.info("deleteResponse.getStatusCode(): " + deleteResponse.getStatusCode()); assertEquals(200, deleteResponse.getStatusCode(), "Failed to delete the control multi-record set"); } - + + @Test + public void testInvalidQueryParams() { + // "foo" is not a valid verb + String oaiVerbPath = "/oai?foo=bar"; + Response identifyResponse = given().get(oaiVerbPath); + // TODO Why is this 500? https://github.com/IQSS/dataverse/issues/9275 + identifyResponse.then().assertThat().statusCode(500); + } + // TODO: // What else can we test? // Some ideas: From 2ab5ba99a357fa88f44fe72201f827cb26cff448 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Thu, 4 Jan 2024 10:50:15 -0500 Subject: [PATCH 156/689] #9686 update migration script --- ...gclient-id.sql => V6.1.0.1__9686-move-harvestingclient-id.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V6.0.0.5__9686-move-harvestingclient-id.sql => V6.1.0.1__9686-move-harvestingclient-id.sql} (100%) diff --git a/src/main/resources/db/migration/V6.0.0.5__9686-move-harvestingclient-id.sql b/src/main/resources/db/migration/V6.1.0.1__9686-move-harvestingclient-id.sql similarity index 100% rename from src/main/resources/db/migration/V6.0.0.5__9686-move-harvestingclient-id.sql rename to src/main/resources/db/migration/V6.1.0.1__9686-move-harvestingclient-id.sql From 27fa15458cf9d68192a3e0eed53f43371990de8e Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 4 Jan 2024 16:21:16 -0500 Subject: [PATCH 157/689] show errors (in XML) for verb params #9275 --- .../9275-harvest-invalid-query-params.md | 4 +++ .../server/web/servlet/OAIServlet.java | 18 ++++++++-- .../iq/dataverse/api/HarvestingServerIT.java | 34 ++++++++++++++++--- 3 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 doc/release-notes/9275-harvest-invalid-query-params.md diff --git a/doc/release-notes/9275-harvest-invalid-query-params.md b/doc/release-notes/9275-harvest-invalid-query-params.md new file mode 100644 index 00000000000..33d7c7bac13 --- /dev/null +++ b/doc/release-notes/9275-harvest-invalid-query-params.md @@ -0,0 +1,4 @@ +OAI-PMH error handling has been improved to display a machine-readable error in XML rather than a 500 error with no further information. + +- /oai?foo=bar will show "No argument 'verb' found" +- /oai?verb=foo&verb=bar will show "Verb must be singular, given: '[foo, bar]'" diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java index 96a19acc0e8..34152a2d8bd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java @@ -31,8 +31,11 @@ import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.MailUtil; import edu.harvard.iq.dataverse.util.SystemConfig; +import io.gdcc.xoai.exceptions.BadArgumentException; +import io.gdcc.xoai.exceptions.BadVerbException; import io.gdcc.xoai.exceptions.OAIException; import io.gdcc.xoai.model.oaipmh.Granularity; +import io.gdcc.xoai.model.oaipmh.verbs.Verb; import io.gdcc.xoai.services.impl.SimpleResumptionTokenFormat; import org.apache.commons.lang3.StringUtils; @@ -256,9 +259,18 @@ private void processRequest(HttpServletRequest httpServletRequest, HttpServletRe "Sorry. OAI Service is disabled on this Dataverse node."); return; } - - RawRequest rawRequest = RequestBuilder.buildRawRequest(httpServletRequest.getParameterMap()); - + + RawRequest rawRequest = null; + try { + rawRequest = RequestBuilder.buildRawRequest(httpServletRequest.getParameterMap()); + } catch (BadVerbException bve) { + // Verb.Type is required. Hard-code one. + rawRequest = new RawRequest(Verb.Type.Identify); + // Ideally, withError would accept a BadVerbException. + BadArgumentException bae = new BadArgumentException(bve.getLocalizedMessage()); + rawRequest.withError(bae); + } + OAIPMH handle = dataProvider.handle(rawRequest); response.setContentType("text/xml;charset=UTF-8"); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java index 07788eca6db..3936a240826 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java @@ -863,11 +863,35 @@ public void testMultiRecordOaiSet() throws InterruptedException { @Test public void testInvalidQueryParams() { - // "foo" is not a valid verb - String oaiVerbPath = "/oai?foo=bar"; - Response identifyResponse = given().get(oaiVerbPath); - // TODO Why is this 500? https://github.com/IQSS/dataverse/issues/9275 - identifyResponse.then().assertThat().statusCode(500); + + // The query parameter "verb" must appear. + Response noVerbArg = given().get("/oai?foo=bar"); + noVerbArg.prettyPrint(); + noVerbArg.then().assertThat() + .statusCode(OK.getStatusCode()) + // This should be "badVerb" + .body("oai.error.@code", equalTo("badArgument")) + .body("oai.error", equalTo("No argument 'verb' found")); + + // The query parameter "verb" cannot appear more than once. + Response repeated = given().get( "/oai?verb=foo&verb=bar"); + repeated.prettyPrint(); + repeated.then().assertThat() + .statusCode(OK.getStatusCode()) + // This should be "badVerb" + .body("oai.error.@code", equalTo("badArgument")) + .body("oai.error", equalTo("Verb must be singular, given: '[foo, bar]'")); + + } + + @Test + public void testNoSuchSetError() { + Response noSuchSet = given().get("/oai?verb=ListIdentifiers&set=census&metadataPrefix=dc"); + noSuchSet.prettyPrint(); + noSuchSet.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("oai.error.@code", equalTo("noRecordsMatch")) + .body("oai.error", equalTo("Requested set 'census' does not exist")); } // TODO: From 6db3e3b9c64a0163c52b3cf988669d9bfd3a919f Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 4 Jan 2024 16:42:16 -0500 Subject: [PATCH 158/689] Fix for "latest" dataset version --- src/main/java/edu/harvard/iq/dataverse/api/Datasets.java | 2 +- .../impl/GetLatestAccessibleDatasetVersionCommand.java | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 094f2b88c92..83b1a4e861b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2796,7 +2796,7 @@ private DatasetVersion getDatasetVersionOrDie(final DataverseRequest req, String @Override public Command handleLatest() { - return new GetLatestAccessibleDatasetVersionCommand(req, ds, includeDeaccessioned); + return new GetLatestAccessibleDatasetVersionCommand(req, ds, includeDeaccessioned, checkPermsWhenDeaccessioned); } @Override diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleDatasetVersionCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleDatasetVersionCommand.java index 96e8ee73a50..7bcc851bde2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleDatasetVersionCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleDatasetVersionCommand.java @@ -25,15 +25,17 @@ public class GetLatestAccessibleDatasetVersionCommand extends AbstractCommand { private final Dataset ds; private final boolean includeDeaccessioned; + private boolean checkPerms; public GetLatestAccessibleDatasetVersionCommand(DataverseRequest aRequest, Dataset anAffectedDataset) { - this(aRequest, anAffectedDataset, false); + this(aRequest, anAffectedDataset, false, false); } - public GetLatestAccessibleDatasetVersionCommand(DataverseRequest aRequest, Dataset anAffectedDataset, boolean includeDeaccessioned) { + public GetLatestAccessibleDatasetVersionCommand(DataverseRequest aRequest, Dataset anAffectedDataset, boolean includeDeaccessioned, boolean checkPerms) { super(aRequest, anAffectedDataset); ds = anAffectedDataset; this.includeDeaccessioned = includeDeaccessioned; + this.checkPerms = checkPerms; } @Override @@ -41,6 +43,6 @@ public DatasetVersion execute(CommandContext ctxt) throws CommandException { if (ds.getLatestVersion().isDraft() && ctxt.permissions().requestOn(getRequest(), ds).has(Permission.ViewUnpublishedDataset)) { return ctxt.engine().submit(new GetDraftDatasetVersionCommand(getRequest(), ds)); } - return ctxt.engine().submit(new GetLatestPublishedDatasetVersionCommand(getRequest(), ds, includeDeaccessioned, true)); + return ctxt.engine().submit(new GetLatestPublishedDatasetVersionCommand(getRequest(), ds, includeDeaccessioned, checkPerms)); } } From d017bf6843189a0228ff1be229614ba7685fcf0b Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Fri, 5 Jan 2024 11:48:00 -0500 Subject: [PATCH 159/689] #9686 assign harvest client id to harvested files --- .../harvard/iq/dataverse/api/imports/ImportServiceBean.java | 5 +++++ .../harvest/client/HarvestingClientServiceBean.java | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportServiceBean.java index c17ba909230..c5812403f31 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportServiceBean.java @@ -332,6 +332,11 @@ public Dataset doImportHarvestedDataset(DataverseRequest dataverseRequest, Harve Dataset existingDs = datasetService.findByGlobalId(ds.getGlobalId().asString()); + //adding the harvesting client id to harvested files #9686 + for (DataFile df : ds.getFiles()){ + df.setHarvestedFrom(harvestingClient); + } + if (existingDs != null) { // If this dataset already exists IN ANOTHER DATAVERSE // we are just going to skip it! diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvestingClientServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvestingClientServiceBean.java index 7ec6d75a41c..5747c64d217 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvestingClientServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvestingClientServiceBean.java @@ -199,8 +199,8 @@ public void recordHarvestJobStatus(Long hcId, Date finishTime, int harvestedCoun public Long getNumberOfHarvestedDatasetsByAllClients() { try { - return (Long) em.createNativeQuery("SELECT count(d.id) FROM dataset d " - + " WHERE d.harvestingclient_id IS NOT NULL").getSingleResult(); + return (Long) em.createNativeQuery("SELECT count(d.id) FROM dvobject d " + + " WHERE d.harvestingclient_id IS NOT NULL and d.dtype = 'Dataset'").getSingleResult(); } catch (Exception ex) { logger.info("Warning: exception looking up the total number of harvested datasets: " + ex.getMessage()); From e085ca926274a4688faeb61f842c319ffc41b538 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Fri, 5 Jan 2024 15:27:06 -0500 Subject: [PATCH 160/689] Adds test to cover latest, latest published and specific scenarios. --- .../harvard/iq/dataverse/api/DatasetsIT.java | 302 +++++++++++++++--- 1 file changed, 249 insertions(+), 53 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index 200cfbaf1ff..9ac05ce5704 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -70,6 +70,7 @@ import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.Matchers.contains; @@ -613,6 +614,7 @@ public void testCreatePublishDestroyDataset() { */ @Test public void testDatasetVersionsAPI() { + // Create user String apiToken = UtilIT.createRandomUserGetToken(); @@ -650,6 +652,11 @@ public void testDatasetVersionsAPI() { .statusCode(OK.getStatusCode()) .body("data.files", equalTo(null)); + unpublishedDraft = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_DRAFT, apiTokenNoPerms, excludeFiles, false); + unpublishedDraft.prettyPrint(); + unpublishedDraft.then().assertThat() + .statusCode(UNAUTHORIZED.getStatusCode()); + excludeFiles = false; unpublishedDraft = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_DRAFT, apiToken, excludeFiles, false); unpublishedDraft.prettyPrint(); @@ -657,7 +664,11 @@ public void testDatasetVersionsAPI() { .statusCode(OK.getStatusCode()) .body("data.files.size()", equalTo(1)); - + unpublishedDraft = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_DRAFT, apiTokenNoPerms, excludeFiles, false); + unpublishedDraft.prettyPrint(); + unpublishedDraft.then().assertThat() + .statusCode(UNAUTHORIZED.getStatusCode()); + // Publish collection and dataset UtilIT.publishDataverseViaNativeApi(collectionAlias, apiToken).then().assertThat().statusCode(OK.getStatusCode()); @@ -680,7 +691,8 @@ public void testDatasetVersionsAPI() { .body("data.size()", equalTo(2)) .body("data[0].files.size()", equalTo(2)) .body("data[1].files.size()", equalTo(1)); - + + // Now call this api with the new (as of 6.1) pagination parameters Integer offset = 0; Integer howmany = 1; @@ -690,15 +702,16 @@ public void testDatasetVersionsAPI() { versionsResponse.then().assertThat() .statusCode(OK.getStatusCode()) .body("data.size()", equalTo(1)) + .body("data.versionState[0]", equalTo("DRAFT")) .body("data[0].files.size()", equalTo(2)); // And now call it with an un-privileged token, to make sure only one - // (the published) version is shown: - + // (the published) version is shown: versionsResponse = UtilIT.getDatasetVersions(datasetPid, apiTokenNoPerms); versionsResponse.prettyPrint(); versionsResponse.then().assertThat() .statusCode(OK.getStatusCode()) + .body("data.versionState[0]", not("DRAFT")) .body("data.size()", equalTo(1)); // And now call the "short", no-files version of the same api @@ -711,35 +724,98 @@ public void testDatasetVersionsAPI() { - //Set of tests on non-deaccesioned dataset - - boolean includeDeaccessioned = true; - excludeFiles = true; - UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiToken, excludeFiles, includeDeaccessioned). - then().assertThat().statusCode(OK.getStatusCode()).body("data[0].files", equalTo(null)); - UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiTokenNoPerms, excludeFiles, includeDeaccessioned). - then().assertThat().statusCode(OK.getStatusCode()).body("data[0].files", equalTo(null)); - - excludeFiles = false; - UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiToken, excludeFiles, includeDeaccessioned). - then().assertThat().statusCode(OK.getStatusCode()).body("data.files.size()", equalTo(1)); - UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiTokenNoPerms, excludeFiles, includeDeaccessioned). - then().assertThat().statusCode(OK.getStatusCode()).body("data.files.size()", equalTo(1)); + //Set of tests on non-deaccesioned dataset + String specificVersion = "1.0"; + boolean includeDeaccessioned = false; + Response datasetVersion = null; - includeDeaccessioned = false; excludeFiles = true; - UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiToken, excludeFiles, includeDeaccessioned). - then().assertThat().statusCode(OK.getStatusCode()).body("data[0].files", equalTo(null)); - UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiTokenNoPerms, excludeFiles, includeDeaccessioned). - then().assertThat().statusCode(OK.getStatusCode()).body("data[0].files", equalTo(null)); + //Latest published authorized token + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiToken, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("RELEASED")) + .body("data.files", equalTo(null)); + + //Latest published unauthorized token + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiTokenNoPerms, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("RELEASED")) + .body("data.files", equalTo(null)); + + //Latest authorized token + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST, apiToken, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("DRAFT")) + .body("data.files", equalTo(null)); + + //Latest unauthorized token + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST, apiTokenNoPerms, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("RELEASED")) + .body("data.files", equalTo(null)); + + //Specific version authorized token + datasetVersion = UtilIT.getDatasetVersion(datasetPid, specificVersion, apiToken, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("RELEASED")) + .body("data.files", equalTo(null)); + + //Specific version unauthorized token + datasetVersion = UtilIT.getDatasetVersion(datasetPid, specificVersion, apiTokenNoPerms, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("RELEASED")) + .body("data.files", equalTo(null)); excludeFiles = false; - UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiToken, excludeFiles, includeDeaccessioned). - then().assertThat().statusCode(OK.getStatusCode()).body("data.files.size()", equalTo(1)); - UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiTokenNoPerms, excludeFiles, includeDeaccessioned). - then().assertThat().statusCode(OK.getStatusCode()).body("data.files.size()", equalTo(1)); - + //Latest published authorized token + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiToken, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("RELEASED")) + .body("data.files.size()", equalTo(1)); + + //Latest published unauthorized token + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiTokenNoPerms, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("RELEASED")) + .body("data.files.size()", equalTo(1)); + + //Latest authorized token, user is authenticated should get the Draft version + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST, apiToken, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("DRAFT")) + .body("data.files.size()", equalTo(2)); + + //Latest unauthorized token, user has no permissions should get the latest Published version + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST, apiTokenNoPerms, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("RELEASED")) + .body("data.files.size()", equalTo(1)); + + //Specific version authorized token + datasetVersion = UtilIT.getDatasetVersion(datasetPid, specificVersion, apiToken, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("RELEASED")) + .body("data.files.size()", equalTo(1)); + + //Specific version unauthorized token + datasetVersion = UtilIT.getDatasetVersion(datasetPid, specificVersion, apiTokenNoPerms, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("RELEASED")) + .body("data.files.size()", equalTo(1)); + //We deaccession the dataset Response deaccessionDatasetResponse = UtilIT.deaccessionDataset(datasetId, DS_VERSION_LATEST_PUBLISHED, "Test deaccession reason.", null, apiToken); deaccessionDatasetResponse.then().assertThat().statusCode(OK.getStatusCode()); @@ -747,38 +823,158 @@ public void testDatasetVersionsAPI() { //Set of tests on deaccesioned dataset, only 3/9 should return OK message includeDeaccessioned = true; - excludeFiles = true; - UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiToken, excludeFiles, includeDeaccessioned). - then().assertThat().statusCode(OK.getStatusCode()).body("data[0].files", equalTo(null)); - UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiTokenNoPerms, excludeFiles, includeDeaccessioned). - then().assertThat().statusCode(OK.getStatusCode()).body("data[0].files", equalTo(null)); excludeFiles = false; - UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiToken, excludeFiles, includeDeaccessioned). - then().assertThat().statusCode(OK.getStatusCode()).body("data.files.size()", equalTo(1));; - UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiTokenNoPerms, excludeFiles, includeDeaccessioned). - then().assertThat().statusCode(404); - - includeDeaccessioned = false; - excludeFiles = true; - UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiToken, excludeFiles, includeDeaccessioned). - then().assertThat().statusCode(404); - UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiTokenNoPerms, excludeFiles, includeDeaccessioned). - then().assertThat().statusCode(404); - excludeFiles = false; - UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiToken, excludeFiles, includeDeaccessioned). - then().assertThat().statusCode(404); - UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiTokenNoPerms, excludeFiles, includeDeaccessioned). - then().assertThat().statusCode(404); - + //Latest published authorized token with deaccessioned dataset + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiToken, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("DEACCESSIONED")) + .body("data.files.size()", equalTo(1)); + + //Latest published requesting files, one version is DEACCESSIONED the second is DRAFT so shouldn't get any datasets + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiTokenNoPerms, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); + + //Latest authorized token should get the DRAFT version + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST, apiToken, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("DRAFT")) + .body("data.files.size()", equalTo(2)); + + //Latest unauthorized token requesting files, one version is DEACCESSIONED the second is DRAFT so shouldn't get any datasets + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST, apiTokenNoPerms, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); + + //Specific version authorized token + datasetVersion = UtilIT.getDatasetVersion(datasetPid, specificVersion, apiToken, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("DEACCESSIONED")) + .body("data.files.size()", equalTo(1)); + + //Specific version unauthorized token requesting files, one version is DEACCESSIONED the second is DRAFT so shouldn't get any datasets. + datasetVersion = UtilIT.getDatasetVersion(datasetPid, specificVersion, apiTokenNoPerms, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); + excludeFiles = true; + //Latest published exclude files authorized token with deaccessioned dataset + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiToken, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("DEACCESSIONED")) + .body("data.files", equalTo(null)); + + //Latest published exclude files, should get the DEACCESSIONED version + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiTokenNoPerms, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("DEACCESSIONED")) + .body("data.files", equalTo(null)); + + //Latest authorized token should get the DRAFT version with no files + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST, apiToken, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("DRAFT")) + .body("data.files", equalTo(null)); + + //Latest unauthorized token excluding files, one version is DEACCESSIONED the second is DRAFT so shouldn't get any datasets + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST, apiTokenNoPerms, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("DEACCESSIONED")) + .body("data.files", equalTo(null)); + + //Specific version authorized token + datasetVersion = UtilIT.getDatasetVersion(datasetPid, specificVersion, apiToken, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("DEACCESSIONED")) + .body("data.files", equalTo(null)); + + //Specific version unauthorized token requesting files, one version is DEACCESSIONED the second is DRAFT so shouldn't get any datasets. + datasetVersion = UtilIT.getDatasetVersion(datasetPid, specificVersion, apiTokenNoPerms, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("DEACCESSIONED")) + .body("data.files", equalTo(null)); + + //Set of test when we have a deaccessioned dataset but we don't include deaccessioned + includeDeaccessioned = false; + excludeFiles = false; + //Latest published authorized token with deaccessioned dataset not included + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiToken, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); + + //Latest published unauthorized token with deaccessioned dataset not included + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiTokenNoPerms, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); + + //Latest authorized token should get the DRAFT version + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST, apiToken, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("DRAFT")) + .body("data.files.size()", equalTo(2)); + + //Latest unauthorized token one version is DEACCESSIONED the second is DRAFT so shouldn't get any datasets + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST, apiTokenNoPerms, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); + + //Specific version authorized token, the version is DEACCESSIONED so shouldn't get any datasets + datasetVersion = UtilIT.getDatasetVersion(datasetPid, specificVersion, apiToken, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); + + //Specific version unauthorized token, the version is DEACCESSIONED so shouldn't get any datasets + datasetVersion = UtilIT.getDatasetVersion(datasetPid, specificVersion, apiTokenNoPerms, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); - - + excludeFiles = true; - + //Latest published authorized token with deaccessioned dataset not included + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiToken, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); + + //Latest published unauthorized token with deaccessioned dataset not included + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST_PUBLISHED, apiTokenNoPerms, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); + + //Latest authorized token should get the DRAFT version + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST, apiToken, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.versionState", equalTo("DRAFT")) + .body("data.files", equalTo(null)); + + //Latest unauthorized token one version is DEACCESSIONED the second is DRAFT so shouldn't get any datasets + datasetVersion = UtilIT.getDatasetVersion(datasetPid, DS_VERSION_LATEST, apiTokenNoPerms, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); + + //Specific version authorized token, the version is DEACCESSIONED so shouldn't get any datasets + datasetVersion = UtilIT.getDatasetVersion(datasetPid, specificVersion, apiToken, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); + + //Specific version unauthorized token, the version is DEACCESSIONED so shouldn't get any datasets + datasetVersion = UtilIT.getDatasetVersion(datasetPid, specificVersion, apiTokenNoPerms, excludeFiles, includeDeaccessioned); + datasetVersion.prettyPrint(); + datasetVersion.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); + } From 2001f5206c922062bdc4b419fe4022b2aaa33875 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Fri, 5 Jan 2024 16:06:37 -0500 Subject: [PATCH 161/689] quick preliminary fixes/work in progress #3437 --- .../harvest/server/OAIRecordServiceBean.java | 44 ++++++++++++------- .../harvest/server/OAISetServiceBean.java | 5 ++- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAIRecordServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAIRecordServiceBean.java index 1b4a7bc7db0..56c19e004dc 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAIRecordServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAIRecordServiceBean.java @@ -55,13 +55,8 @@ public class OAIRecordServiceBean implements java.io.Serializable { EntityManager em; private static final Logger logger = Logger.getLogger("edu.harvard.iq.dataverse.harvest.server.OAIRecordServiceBean"); - - public void updateOaiRecords(String setName, List datasetIds, Date updateTime, boolean doExport) { - updateOaiRecords(setName, datasetIds, updateTime, doExport, logger); - } - - public void updateOaiRecords(String setName, List datasetIds, Date updateTime, boolean doExport, Logger setUpdateLogger) { - + + public void updateOaiRecords(String setName, List datasetIds, Date updateTime, boolean doExport, boolean confirmed, Logger setUpdateLogger) { // create Map of OaiRecords List oaiRecords = findOaiRecordsBySetName(setName); Map recordMap = new HashMap<>(); @@ -101,9 +96,6 @@ public void updateOaiRecords(String setName, List datasetIds, Date updateT DatasetVersion releasedVersion = dataset.getReleasedVersion(); Date publicationDate = releasedVersion == null ? null : releasedVersion.getReleaseTime(); - //if (dataset.getPublicationDate() != null - // && (dataset.getLastExportTime() == null - // || dataset.getLastExportTime().before(dataset.getPublicationDate()))) { if (publicationDate != null && (dataset.getLastExportTime() == null || dataset.getLastExportTime().before(publicationDate))) { @@ -125,7 +117,9 @@ public void updateOaiRecords(String setName, List datasetIds, Date updateT } // anything left in the map should be marked as removed! - markOaiRecordsAsRemoved( recordMap.values(), updateTime, setUpdateLogger); + markOaiRecordsAsRemoved(recordMap.values(), updateTime, confirmed, setUpdateLogger); + + } @@ -162,7 +156,7 @@ record = new OAIRecord(setName, dataset.getGlobalId().asString(), new Date()); } } - + /* // Updates any existing OAI records for this dataset // Should be called whenever there's a change in the release status of the Dataset // (i.e., when it's published or deaccessioned), so that the timestamps and @@ -201,13 +195,31 @@ public void updateOaiRecordsForDataset(Dataset dataset) { logger.fine("Null returned - no records found."); } } +*/ - public void markOaiRecordsAsRemoved(Collection records, Date updateTime, Logger setUpdateLogger) { + public void markOaiRecordsAsRemoved(Collection records, Date updateTime, boolean confirmed, Logger setUpdateLogger) { for (OAIRecord oaiRecord : records) { if ( !oaiRecord.isRemoved() ) { - setUpdateLogger.fine("marking OAI record "+oaiRecord.getGlobalId()+" as removed"); - oaiRecord.setRemoved(true); - oaiRecord.setLastUpdateTime(updateTime); + boolean confirmedRemoved = confirmed; + if (!confirmedRemoved) { + Dataset lookedUp = datasetService.findByGlobalId(oaiRecord.getGlobalId()); + if (lookedUp == null) { + confirmedRemoved = true; + } else if (lookedUp.getLastExportTime() == null) { + confirmedRemoved = true; + } else { + boolean isReleased = lookedUp.getReleasedVersion() != null; + if (!isReleased) { + confirmedRemoved = true; + } + } + } + + if (confirmedRemoved) { + setUpdateLogger.fine("marking OAI record "+oaiRecord.getGlobalId()+" as removed"); + oaiRecord.setRemoved(true); + oaiRecord.setLastUpdateTime(updateTime); + } } else { setUpdateLogger.fine("OAI record "+oaiRecord.getGlobalId()+" is already marked as removed."); } diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAISetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAISetServiceBean.java index 2bd666401c7..9020a09abdd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAISetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAISetServiceBean.java @@ -151,6 +151,8 @@ public void exportOaiSet(OAISet oaiSet, Logger exportLogger) { String query = managedSet.getDefinition(); List datasetIds; + boolean databaseLookup = false; // As opposed to a search engine lookup + try { if (!oaiSet.isDefaultSet()) { datasetIds = expandSetQuery(query); @@ -161,6 +163,7 @@ public void exportOaiSet(OAISet oaiSet, Logger exportLogger) { // including the unpublished drafts and deaccessioned ones. // Those will be filtered out further down the line. datasetIds = datasetService.findAllLocalDatasetIds(); + databaseLookup = true; } } catch (OaiSetException ose) { datasetIds = null; @@ -171,7 +174,7 @@ public void exportOaiSet(OAISet oaiSet, Logger exportLogger) { // they will be properly marked as "deleted"! -- L.A. 4.5 //if (datasetIds != null && !datasetIds.isEmpty()) { exportLogger.info("Calling OAI Record Service to re-export " + datasetIds.size() + " datasets."); - oaiRecordService.updateOaiRecords(managedSet.getSpec(), datasetIds, new Date(), true, exportLogger); + oaiRecordService.updateOaiRecords(managedSet.getSpec(), datasetIds, new Date(), true, databaseLookup, exportLogger); //} managedSet.setUpdateInProgress(false); From 4db74b6e5ddd3cf7f2ee49b94b9b229e2746bd35 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Fri, 5 Jan 2024 16:20:27 -0500 Subject: [PATCH 162/689] how to write release note snippets #9264 --- .../source/developers/making-releases.rst | 10 ++-- .../source/developers/version-control.rst | 54 ++++++++++++++++--- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/doc/sphinx-guides/source/developers/making-releases.rst b/doc/sphinx-guides/source/developers/making-releases.rst index e73811a77e1..6b94282d55e 100755 --- a/doc/sphinx-guides/source/developers/making-releases.rst +++ b/doc/sphinx-guides/source/developers/making-releases.rst @@ -14,16 +14,18 @@ See :doc:`version-control` for background on our branching strategy. The steps below describe making both regular releases and hotfix releases. +.. _write-release-notes: + Write Release Notes ------------------- -Developers express the need for an addition to release notes by creating a file in ``/doc/release-notes`` containing the name of the issue they're working on. The name of the branch could be used for the filename with ".md" appended (release notes are written in Markdown) such as ``5053-apis-custom-homepage.md``. +Developers express the need for an addition to release notes by creating a "release note snippet" in ``/doc/release-notes`` containing the name of the issue they're working on. The name of the branch could be used for the filename with ".md" appended (release notes are written in Markdown) such as ``5053-apis-custom-homepage.md``. See :ref:`writing-release-note-snippets` for how this is described for contributors. -The task at or near release time is to collect these notes into a single doc. +The task at or near release time is to collect these snippets into a single file. - Create an issue in GitHub to track the work of creating release notes for the upcoming release. -- Create a branch, add a .md file for the release (ex. 5.10.1 Release Notes) in ``/doc/release-notes`` and write the release notes, making sure to pull content from the issue-specific release notes mentioned above. -- Delete the previously-created, issue-specific release notes as the content is added to the main release notes file. +- Create a branch, add a .md file for the release (ex. 5.10.1 Release Notes) in ``/doc/release-notes`` and write the release notes, making sure to pull content from the release note snippets mentioned above. +- Delete the release note snippets as the content is added to the main release notes file. - Include instructions to describe the steps required to upgrade the application from the previous version. These must be customized for release numbers and special circumstances such as changes to metadata blocks and infrastructure. - Take the release notes .md through the regular Code Review and QA process. diff --git a/doc/sphinx-guides/source/developers/version-control.rst b/doc/sphinx-guides/source/developers/version-control.rst index 91f59c76e61..12f3d5b81fd 100644 --- a/doc/sphinx-guides/source/developers/version-control.rst +++ b/doc/sphinx-guides/source/developers/version-control.rst @@ -65,23 +65,65 @@ The example of creating a pull request below has to do with fixing an important Find or Create a GitHub Issue ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Issue is a bug (unexpected behavior) or a new feature in Dataverse, to know how to find or create an issue in dataverse please see https://github.com/IQSS/dataverse/blob/develop/CONTRIBUTING.md +An issue represents a bug (unexpected behavior) or a new feature in Dataverse. We'll use the issue number in the branch we create for our pull request. -For guidance on which issue to work on, please ask! with email to support@dataverse.org +Finding GitHub Issues to Work On +******************************** -Let's say you want to tackle https://github.com/IQSS/dataverse/issues/3728 which points out a typo in a page of the Dataverse Software's documentation. +Assuming this is your first contribution to Dataverse, you should start with something small. The following issue labels might be helpful in your search: + +- `good first issue `_ (these appear at https://github.com/IQSS/dataverse/contribute ) +- `hacktoberfest `_ +- `Help Wanted: Code `_ +- `Help Wanted: Documentation `_ + +For guidance on which issue to work on, please ask! :ref:`getting-help-developers` explains how to get in touch. + +Creating GitHub Issues to Work On +********************************* + +You are very welcome to create a GitHub issue to work on. However, for significant changes, please reach out (see :ref:`getting-help-developers`) to make sure the team and community agree with the proposed change. + +For small changes and especially typo fixes, please don't worry about reaching out first. + +Communicate Which Issue You Are Working On +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In the issue you can simply leave a comment to say you're working on it. If you tell us your GitHub username we are happy to add you to the "read only" team at https://github.com/orgs/IQSS/teams/dataverse-readonly/members so that we can assign the issue to you while you're working on it. You can also tell us if you'd like to be added to the `Dataverse Community Contributors spreadsheet `_. -Create a New Branch off the develop Branch +Create a New Branch Off the develop Branch ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Always create your feature branch from the latest code in develop, pulling the latest code if necessary. As mentioned above, your branch should have a name like "3728-doc-apipolicy-fix" that starts with the issue number you are addressing, and ends with a short, descriptive name. Dashes ("-") and underscores ("_") in your branch name are ok, but please try to avoid other special characters such as ampersands ("&") that have special meaning in Unix shells. +Always create your feature branch from the latest code in develop, pulling the latest code if necessary. As mentioned above, your branch should have a name like "3728-doc-apipolicy-fix" that starts with the issue number you are addressing (e.g. `#3728 `_) and ends with a short, descriptive name. Dashes ("-") and underscores ("_") in your branch name are ok, but please try to avoid other special characters such as ampersands ("&") that have special meaning in Unix shells. Commit Your Change to Your New Branch ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Making a commit (or several commits) to that branch, enter a description of the changes you have made. Ideally the first line of your commit message includes the number of the issue you are addressing, such as ``Fixed BlockedApiPolicy #3728``. +For each commit to that branch, try to include the issue number along with a summary in the first line of the commit message, such as ``Fixed BlockedApiPolicy #3728``. You are welcome to write longer descriptions in the body as well! + +.. _writing-release-note-snippets: + +Writing a Release Note Snippet +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We highly value your insight as a contributor when in comes to describing your work in our release notes. Not every pull request will be mentioned in release notes but most are. + +As described at :ref:`write-release-notes`, at release time we compile together release note "snippets" into the final release notes. + +Here's how to add a release note snippet to your pull request: + +- Create a Markdown file under ``doc/release-notes``. You can reuse the name of your branch and append ".md" to it, e.g. ``3728-doc-apipolicy-fix.md`` +- Edit the snippet to include anything you think should be mentioned in the release notes, such as: + + - Descriptions of new features + - Explanations of bugs fixed + - New configuration settings + - Upgrade instructions + - Etc. + +Release note snippets do not need to be long. For a new feature, a single line description might be enough. Please note that your release note will likely be edited (expanded or shortened) when the final release notes are being created. Push Your Branch to GitHub ~~~~~~~~~~~~~~~~~~~~~~~~~~ From 1ab441c718b13198f51fd9d5eb6732fc919c6a64 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Fri, 5 Jan 2024 16:36:24 -0500 Subject: [PATCH 163/689] cosmetic #3437 --- .../harvest/server/OAIRecordServiceBean.java | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAIRecordServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAIRecordServiceBean.java index 56c19e004dc..902a52c7b97 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAIRecordServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAIRecordServiceBean.java @@ -40,10 +40,6 @@ @Stateless @Named public class OAIRecordServiceBean implements java.io.Serializable { - @EJB - OAISetServiceBean oaiSetService; - @EJB - IndexServiceBean indexService; @EJB DatasetServiceBean datasetService; @EJB @@ -55,7 +51,23 @@ public class OAIRecordServiceBean implements java.io.Serializable { EntityManager em; private static final Logger logger = Logger.getLogger("edu.harvard.iq.dataverse.harvest.server.OAIRecordServiceBean"); - + + /** + * Updates the OAI records for the set specified + * @param setName name of the OAI set + * @param datasetIds ids of the datasets that are candidates for this OAI set + * @param updateTime time stamp + * @param doExport attempt to export datasets that haven't been exported yet + * @param confirmed true if the datasetIds above were looked up in the database + * - as opposed to in the search engine. Meaning, that it is + * confirmed that any dataset not on this list that's currently + * in the set is no longer in the database and should be + * marked as deleted without any further checks. Otherwise + * we'll want to double-check if the dataset still exists + * as published. This is to prevent marking existing datasets + * as deleted during a full reindex and such. + * @param setUpdateLogger dedicated Logger + */ public void updateOaiRecords(String setName, List datasetIds, Date updateTime, boolean doExport, boolean confirmed, Logger setUpdateLogger) { // create Map of OaiRecords List oaiRecords = findOaiRecordsBySetName(setName); From 826d4bdcd2d0418c8d65c8409107de0d66f6dd19 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 5 Jan 2024 17:46:26 -0500 Subject: [PATCH 164/689] per QA --- doc/sphinx-guides/source/developers/globus-api.rst | 1 + .../java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/developers/globus-api.rst b/doc/sphinx-guides/source/developers/globus-api.rst index de9df06a798..2f922fb1fc0 100644 --- a/doc/sphinx-guides/source/developers/globus-api.rst +++ b/doc/sphinx-guides/source/developers/globus-api.rst @@ -2,6 +2,7 @@ Globus Transfer API =================== The Globus API addresses three use cases: + * Transfer to a Dataverse-managed Globus endpoint (File-based or using the Globus S3 Connector) * Reference of files that will remain in a remote Globus endpoint * Transfer from a Dataverse-managed Globus endpoint diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java index 61884045f35..3e60441850b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -240,7 +240,7 @@ private int makeDir(GlobusEndpoint endpoint, String dir) { MakeRequestResponse result = null; String body = "{\"DATA_TYPE\":\"mkdir\",\"path\":\"" + dir + "\"}"; try { - logger.info(body); + logger.fine(body); URL url = new URL( "https://transfer.api.globusonline.org/v0.10/operation/endpoint/" + endpoint.getId() + "/mkdir"); result = makeRequest(url, "Bearer", endpoint.getClientToken(), "POST", body); From dbab6ca9269a93bd7d292b37b00c42dc0fbad55f Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Mon, 8 Jan 2024 10:30:25 -0500 Subject: [PATCH 165/689] use name@email.xyz to match citation block #2638 From datasetfieldtype.datasetContactEmail.watermark --- 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 b1c38e52496..ece3f070cdd 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -155,7 +155,7 @@ contact.support=Support contact.from=From contact.from.required=User email is required. contact.from.invalid=Email is invalid. -contact.from.emailPlaceholder=valid@email.org +contact.from.emailPlaceholder=name@email.xyz contact.subject=Subject contact.subject.required=Subject is required. contact.subject.selectTab.top=Select subject... From 88af3d4ed1316df681ce53fc0d4c00d03ac56e7d Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Mon, 8 Jan 2024 12:16:51 -0500 Subject: [PATCH 166/689] clean up error handling #9275 dataProvider.handle(params) allows us to return the correct error. --- .../harvest/server/web/servlet/OAIServlet.java | 16 ++++++---------- .../iq/dataverse/api/HarvestingServerIT.java | 6 ++---- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java index 34152a2d8bd..233ca94f5fc 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java @@ -31,11 +31,9 @@ import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.MailUtil; import edu.harvard.iq.dataverse.util.SystemConfig; -import io.gdcc.xoai.exceptions.BadArgumentException; import io.gdcc.xoai.exceptions.BadVerbException; import io.gdcc.xoai.exceptions.OAIException; import io.gdcc.xoai.model.oaipmh.Granularity; -import io.gdcc.xoai.model.oaipmh.verbs.Verb; import io.gdcc.xoai.services.impl.SimpleResumptionTokenFormat; import org.apache.commons.lang3.StringUtils; @@ -51,6 +49,7 @@ import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.util.Map; import javax.xml.stream.XMLStreamException; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; @@ -260,18 +259,15 @@ private void processRequest(HttpServletRequest httpServletRequest, HttpServletRe return; } - RawRequest rawRequest = null; + Map params = httpServletRequest.getParameterMap(); + OAIPMH handle; try { - rawRequest = RequestBuilder.buildRawRequest(httpServletRequest.getParameterMap()); + RawRequest rawRequest = RequestBuilder.buildRawRequest(params); + handle = dataProvider.handle(rawRequest); } catch (BadVerbException bve) { - // Verb.Type is required. Hard-code one. - rawRequest = new RawRequest(Verb.Type.Identify); - // Ideally, withError would accept a BadVerbException. - BadArgumentException bae = new BadArgumentException(bve.getLocalizedMessage()); - rawRequest.withError(bae); + handle = dataProvider.handle(params); } - OAIPMH handle = dataProvider.handle(rawRequest); response.setContentType("text/xml;charset=UTF-8"); try (XmlWriter xmlWriter = new XmlWriter(response.getOutputStream(), repositoryConfiguration);) { diff --git a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java index 3936a240826..45dd0c08226 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java @@ -869,8 +869,7 @@ public void testInvalidQueryParams() { noVerbArg.prettyPrint(); noVerbArg.then().assertThat() .statusCode(OK.getStatusCode()) - // This should be "badVerb" - .body("oai.error.@code", equalTo("badArgument")) + .body("oai.error.@code", equalTo("badVerb")) .body("oai.error", equalTo("No argument 'verb' found")); // The query parameter "verb" cannot appear more than once. @@ -878,8 +877,7 @@ public void testInvalidQueryParams() { repeated.prettyPrint(); repeated.then().assertThat() .statusCode(OK.getStatusCode()) - // This should be "badVerb" - .body("oai.error.@code", equalTo("badArgument")) + .body("oai.error.@code", equalTo("badVerb")) .body("oai.error", equalTo("Verb must be singular, given: '[foo, bar]'")); } From 2b1e5dd4bda6788f644c2737cf56310e7eaefb7d Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Mon, 8 Jan 2024 16:10:58 -0500 Subject: [PATCH 167/689] Extend getVersionFiles API endpoint to include the total file count --- .../iq/dataverse/api/AbstractApiBean.java | 64 +++----- .../harvard/iq/dataverse/api/Datasets.java | 146 +++++------------- .../harvard/iq/dataverse/api/DatasetsIT.java | 98 ++++++------ 3 files changed, 108 insertions(+), 200 deletions(-) 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 58565bcc9d6..2a2843c0494 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -1,29 +1,6 @@ package edu.harvard.iq.dataverse.api; -import edu.harvard.iq.dataverse.DataFile; -import edu.harvard.iq.dataverse.DataFileServiceBean; -import edu.harvard.iq.dataverse.Dataset; -import edu.harvard.iq.dataverse.DatasetFieldServiceBean; -import edu.harvard.iq.dataverse.DatasetFieldType; -import edu.harvard.iq.dataverse.DatasetLinkingDataverse; -import edu.harvard.iq.dataverse.DatasetLinkingServiceBean; -import edu.harvard.iq.dataverse.DatasetServiceBean; -import edu.harvard.iq.dataverse.DatasetVersionServiceBean; -import edu.harvard.iq.dataverse.Dataverse; -import edu.harvard.iq.dataverse.DataverseLinkingDataverse; -import edu.harvard.iq.dataverse.DataverseLinkingServiceBean; -import edu.harvard.iq.dataverse.DataverseRoleServiceBean; -import edu.harvard.iq.dataverse.DataverseServiceBean; -import edu.harvard.iq.dataverse.DvObject; -import edu.harvard.iq.dataverse.DvObjectServiceBean; -import edu.harvard.iq.dataverse.EjbDataverseEngine; -import edu.harvard.iq.dataverse.GuestbookResponseServiceBean; -import edu.harvard.iq.dataverse.MetadataBlock; -import edu.harvard.iq.dataverse.MetadataBlockServiceBean; -import edu.harvard.iq.dataverse.PermissionServiceBean; -import edu.harvard.iq.dataverse.RoleAssigneeServiceBean; -import edu.harvard.iq.dataverse.UserNotificationServiceBean; -import edu.harvard.iq.dataverse.UserServiceBean; +import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.actionlogging.ActionLogServiceBean; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.DataverseRole; @@ -40,8 +17,8 @@ import edu.harvard.iq.dataverse.engine.command.exception.PermissionException; import edu.harvard.iq.dataverse.externaltools.ExternalToolServiceBean; import edu.harvard.iq.dataverse.license.LicenseServiceBean; -import edu.harvard.iq.dataverse.metrics.MetricsServiceBean; import edu.harvard.iq.dataverse.locality.StorageSiteServiceBean; +import edu.harvard.iq.dataverse.metrics.MetricsServiceBean; import edu.harvard.iq.dataverse.search.savedsearch.SavedSearchServiceBean; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; @@ -51,33 +28,30 @@ import edu.harvard.iq.dataverse.util.json.JsonUtil; import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; import edu.harvard.iq.dataverse.validation.PasswordValidatorServiceBean; -import java.io.InputStream; -import java.net.URI; -import java.util.Arrays; -import java.util.Collections; -import java.util.UUID; -import java.util.concurrent.Callable; -import java.util.logging.Level; -import java.util.logging.Logger; import jakarta.ejb.EJB; import jakarta.ejb.EJBException; -import jakarta.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonArrayBuilder; -import jakarta.json.JsonException; -import jakarta.json.JsonObject; -import jakarta.json.JsonObjectBuilder; -import jakarta.json.JsonValue; +import jakarta.json.*; import jakarta.json.JsonValue.ValueType; import jakarta.persistence.EntityManager; import jakarta.persistence.NoResultException; import jakarta.persistence.PersistenceContext; import jakarta.servlet.http.HttpServletRequest; import jakarta.ws.rs.container.ContainerRequestContext; -import jakarta.ws.rs.core.*; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.ResponseBuilder; import jakarta.ws.rs.core.Response.Status; +import java.io.InputStream; +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.logging.Level; +import java.util.logging.Logger; + import static org.apache.commons.lang3.StringUtils.isNumeric; /** @@ -661,7 +635,13 @@ protected Response ok( JsonArrayBuilder bld ) { .add("data", bld).build()) .type(MediaType.APPLICATION_JSON).build(); } - + protected Response ok( JsonArrayBuilder bld , long totalCount) { + return Response.ok(Json.createObjectBuilder() + .add("status", ApiConstants.STATUS_OK) + .add("total_count", totalCount) + .add("data", bld).build()) + .type(MediaType.APPLICATION_JSON).build(); + } protected Response ok( JsonArray ja ) { return Response.ok(Json.createObjectBuilder() .add("status", ApiConstants.STATUS_OK) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 094f2b88c92..56b9e8df319 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -1,9 +1,11 @@ package edu.harvard.iq.dataverse.api; +import com.amazonaws.services.s3.model.PartETag; import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.DatasetLock.Reason; import edu.harvard.iq.dataverse.actionlogging.ActionLogRecord; import edu.harvard.iq.dataverse.api.auth.AuthRequired; +import edu.harvard.iq.dataverse.api.dto.RoleAssignmentDTO; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.authorization.Permission; @@ -13,6 +15,7 @@ import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.batch.jobs.importer.ImportMode; +import edu.harvard.iq.dataverse.dataaccess.*; import edu.harvard.iq.dataverse.datacapturemodule.DataCaptureModuleUtil; import edu.harvard.iq.dataverse.datacapturemodule.ScriptRequestResponse; import edu.harvard.iq.dataverse.dataset.DatasetThumbnail; @@ -23,92 +26,47 @@ import edu.harvard.iq.dataverse.datasetutility.OptionalFileParams; import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; -import edu.harvard.iq.dataverse.engine.command.impl.AbstractSubmitToArchiveCommand; -import edu.harvard.iq.dataverse.engine.command.impl.AddLockCommand; -import edu.harvard.iq.dataverse.engine.command.impl.AssignRoleCommand; -import edu.harvard.iq.dataverse.engine.command.impl.CreateDatasetVersionCommand; -import edu.harvard.iq.dataverse.engine.command.impl.CreatePrivateUrlCommand; -import edu.harvard.iq.dataverse.engine.command.impl.CuratePublishedDatasetVersionCommand; -import edu.harvard.iq.dataverse.engine.command.impl.DeaccessionDatasetVersionCommand; -import edu.harvard.iq.dataverse.engine.command.impl.DeleteDatasetCommand; -import edu.harvard.iq.dataverse.engine.command.impl.DeleteDatasetVersionCommand; -import edu.harvard.iq.dataverse.engine.command.impl.DeleteDatasetLinkingDataverseCommand; -import edu.harvard.iq.dataverse.engine.command.impl.DeletePrivateUrlCommand; -import edu.harvard.iq.dataverse.engine.command.impl.DestroyDatasetCommand; -import edu.harvard.iq.dataverse.engine.command.impl.FinalizeDatasetPublicationCommand; -import edu.harvard.iq.dataverse.engine.command.impl.GetDatasetCommand; -import edu.harvard.iq.dataverse.engine.command.impl.GetSpecificPublishedDatasetVersionCommand; -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.GetPrivateUrlCommand; -import edu.harvard.iq.dataverse.engine.command.impl.ImportFromFileSystemCommand; -import edu.harvard.iq.dataverse.engine.command.impl.LinkDatasetCommand; -import edu.harvard.iq.dataverse.engine.command.impl.ListRoleAssignments; -import edu.harvard.iq.dataverse.engine.command.impl.ListVersionsCommand; -import edu.harvard.iq.dataverse.engine.command.impl.MoveDatasetCommand; -import edu.harvard.iq.dataverse.engine.command.impl.PublishDatasetCommand; -import edu.harvard.iq.dataverse.engine.command.impl.PublishDatasetResult; -import edu.harvard.iq.dataverse.engine.command.impl.RemoveLockCommand; -import edu.harvard.iq.dataverse.engine.command.impl.RequestRsyncScriptCommand; -import edu.harvard.iq.dataverse.engine.command.impl.ReturnDatasetToAuthorCommand; -import edu.harvard.iq.dataverse.engine.command.impl.SetDatasetCitationDateCommand; -import edu.harvard.iq.dataverse.engine.command.impl.SetCurationStatusCommand; -import edu.harvard.iq.dataverse.engine.command.impl.SubmitDatasetForReviewCommand; -import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand; -import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetTargetURLCommand; -import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetThumbnailCommand; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import edu.harvard.iq.dataverse.engine.command.exception.UnforcedCommandException; +import edu.harvard.iq.dataverse.engine.command.impl.*; import edu.harvard.iq.dataverse.export.DDIExportServiceBean; import edu.harvard.iq.dataverse.export.ExportService; import edu.harvard.iq.dataverse.externaltools.ExternalTool; import edu.harvard.iq.dataverse.externaltools.ExternalToolHandler; +import edu.harvard.iq.dataverse.globus.GlobusServiceBean; +import edu.harvard.iq.dataverse.globus.GlobusUtil; import edu.harvard.iq.dataverse.ingest.IngestServiceBean; -import edu.harvard.iq.dataverse.privateurl.PrivateUrl; -import edu.harvard.iq.dataverse.api.dto.RoleAssignmentDTO; -import edu.harvard.iq.dataverse.dataaccess.DataAccess; -import edu.harvard.iq.dataverse.dataaccess.GlobusAccessibleStore; -import edu.harvard.iq.dataverse.dataaccess.ImageThumbConverter; -import edu.harvard.iq.dataverse.dataaccess.S3AccessIO; -import edu.harvard.iq.dataverse.dataaccess.StorageIO; -import edu.harvard.iq.dataverse.engine.command.exception.CommandException; -import edu.harvard.iq.dataverse.engine.command.exception.UnforcedCommandException; -import edu.harvard.iq.dataverse.engine.command.impl.GetDatasetStorageSizeCommand; -import edu.harvard.iq.dataverse.engine.command.impl.RevokeRoleCommand; -import edu.harvard.iq.dataverse.engine.command.impl.UpdateDvObjectPIDMetadataCommand; -import edu.harvard.iq.dataverse.makedatacount.DatasetExternalCitations; -import edu.harvard.iq.dataverse.makedatacount.DatasetExternalCitationsServiceBean; -import edu.harvard.iq.dataverse.makedatacount.DatasetMetrics; -import edu.harvard.iq.dataverse.makedatacount.DatasetMetricsServiceBean; -import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean; +import edu.harvard.iq.dataverse.makedatacount.*; import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean.MakeDataCountEntry; import edu.harvard.iq.dataverse.metrics.MetricsUtil; -import edu.harvard.iq.dataverse.makedatacount.MakeDataCountUtil; +import edu.harvard.iq.dataverse.privateurl.PrivateUrl; import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean; +import edu.harvard.iq.dataverse.search.IndexServiceBean; +import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; -import edu.harvard.iq.dataverse.util.ArchiverUtil; -import edu.harvard.iq.dataverse.util.BundleUtil; -import edu.harvard.iq.dataverse.util.EjbUtil; -import edu.harvard.iq.dataverse.util.FileUtil; -import edu.harvard.iq.dataverse.util.MarkupChecker; -import edu.harvard.iq.dataverse.util.SystemConfig; -import edu.harvard.iq.dataverse.util.URLTokenUtil; +import edu.harvard.iq.dataverse.util.*; import edu.harvard.iq.dataverse.util.bagit.OREMap; -import edu.harvard.iq.dataverse.util.json.JSONLDUtil; -import edu.harvard.iq.dataverse.util.json.JsonLDTerm; -import edu.harvard.iq.dataverse.util.json.JsonParseException; -import edu.harvard.iq.dataverse.util.json.JsonUtil; -import edu.harvard.iq.dataverse.util.SignpostingResources; -import edu.harvard.iq.dataverse.search.IndexServiceBean; -import static edu.harvard.iq.dataverse.api.ApiConstants.*; -import static edu.harvard.iq.dataverse.util.json.JsonPrinter.*; -import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; -import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; +import edu.harvard.iq.dataverse.util.json.*; import edu.harvard.iq.dataverse.workflow.Workflow; import edu.harvard.iq.dataverse.workflow.WorkflowContext; -import edu.harvard.iq.dataverse.workflow.WorkflowServiceBean; import edu.harvard.iq.dataverse.workflow.WorkflowContext.TriggerType; -import edu.harvard.iq.dataverse.globus.GlobusServiceBean; -import edu.harvard.iq.dataverse.globus.GlobusUtil; +import edu.harvard.iq.dataverse.workflow.WorkflowServiceBean; +import jakarta.ejb.EJB; +import jakarta.ejb.EJBException; +import jakarta.inject.Inject; +import jakarta.json.*; +import jakarta.json.stream.JsonParsingException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.*; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.core.*; +import jakarta.ws.rs.core.Response.Status; +import org.apache.commons.lang3.StringUtils; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; +import org.glassfish.jersey.media.multipart.FormDataParam; + import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -117,45 +75,21 @@ import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.*; -import java.util.concurrent.*; -import java.util.function.Predicate; import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import java.util.*; import java.util.Map.Entry; +import java.util.concurrent.ExecutionException; +import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import java.util.stream.Collectors; -import jakarta.ejb.EJB; -import jakarta.ejb.EJBException; -import jakarta.inject.Inject; -import jakarta.json.*; -import jakarta.json.stream.JsonParsingException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.DefaultValue; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.NotAcceptableException; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.container.ContainerRequestContext; -import jakarta.ws.rs.core.*; -import jakarta.ws.rs.core.Response.Status; + +import static edu.harvard.iq.dataverse.api.ApiConstants.*; +import static edu.harvard.iq.dataverse.util.json.JsonPrinter.*; +import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; -import org.apache.commons.lang3.StringUtils; -import org.glassfish.jersey.media.multipart.FormDataBodyPart; -import org.glassfish.jersey.media.multipart.FormDataContentDisposition; -import org.glassfish.jersey.media.multipart.FormDataParam; -import com.amazonaws.services.s3.model.PartETag; -import edu.harvard.iq.dataverse.settings.JvmSettings; @Path("datasets") public class Datasets extends AbstractApiBean { @@ -546,7 +480,9 @@ public Response getVersionFiles(@Context ContainerRequestContext crc, } catch (IllegalArgumentException e) { return badRequest(BundleUtil.getStringFromBundle("datasets.api.version.files.invalid.access.status", List.of(accessStatus))); } - return ok(jsonFileMetadatas(datasetVersionFilesServiceBean.getFileMetadatas(datasetVersion, limit, offset, fileSearchCriteria, fileOrderCriteria))); + // TODO: should we count the total every time or only when offset = 0? + return ok(jsonFileMetadatas(datasetVersionFilesServiceBean.getFileMetadatas(datasetVersion, limit, offset, fileSearchCriteria, fileOrderCriteria)), + datasetVersionFilesServiceBean.getFileMetadataCount(datasetVersion, fileSearchCriteria)); }, getRequestUser(crc)); } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index 200cfbaf1ff..ace69a6c606 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -1,77 +1,66 @@ package edu.harvard.iq.dataverse.api; +import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.DatasetVersionFilesServiceBean; import edu.harvard.iq.dataverse.FileSearchCriteria; -import io.restassured.RestAssured; -import static edu.harvard.iq.dataverse.DatasetVersion.ARCHIVE_NOTE_MAX_LENGTH; -import static edu.harvard.iq.dataverse.api.ApiConstants.*; -import static io.restassured.RestAssured.given; -import io.restassured.path.json.JsonPath; -import io.restassured.http.ContentType; -import io.restassured.response.Response; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.logging.Logger; -import org.apache.commons.lang3.RandomStringUtils; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.skyscreamer.jsonassert.JSONAssert; -import org.junit.jupiter.api.Disabled; -import jakarta.json.JsonObject; -import static jakarta.ws.rs.core.Response.Status.CREATED; -import static jakarta.ws.rs.core.Response.Status.FORBIDDEN; -import static jakarta.ws.rs.core.Response.Status.OK; -import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED; -import static jakarta.ws.rs.core.Response.Status.NOT_FOUND; -import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; -import static jakarta.ws.rs.core.Response.Status.METHOD_NOT_ALLOWED; -import static jakarta.ws.rs.core.Response.Status.CONFLICT; -import static jakarta.ws.rs.core.Response.Status.NO_CONTENT; -import edu.harvard.iq.dataverse.DataFile; -import static edu.harvard.iq.dataverse.api.UtilIT.API_TOKEN_HTTP_HEADER; import edu.harvard.iq.dataverse.authorization.DataverseRole; +import edu.harvard.iq.dataverse.authorization.groups.impl.builtin.AuthenticatedUsers; import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; import edu.harvard.iq.dataverse.dataaccess.AbstractRemoteOverlayAccessIO; import edu.harvard.iq.dataverse.dataaccess.GlobusOverlayAccessIOTest; -import edu.harvard.iq.dataverse.dataaccess.StorageIO; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; -import io.restassured.parsing.Parser; -import static io.restassured.path.json.JsonPath.with; -import io.restassured.path.xml.XmlPath; -import static edu.harvard.iq.dataverse.api.UtilIT.equalToCI; -import edu.harvard.iq.dataverse.authorization.groups.impl.builtin.AuthenticatedUsers; import edu.harvard.iq.dataverse.datavariable.VarGroup; import edu.harvard.iq.dataverse.datavariable.VariableMetadata; import edu.harvard.iq.dataverse.datavariable.VariableMetadataDDIParser; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.SystemConfig; import edu.harvard.iq.dataverse.util.json.JSONLDUtil; import edu.harvard.iq.dataverse.util.json.JsonUtil; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.StringReader; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.Files; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import io.restassured.parsing.Parser; +import io.restassured.path.json.JsonPath; +import io.restassured.path.xml.XmlPath; +import io.restassured.response.Response; import jakarta.json.Json; import jakarta.json.JsonArray; +import jakarta.json.JsonObject; import jakarta.json.JsonObjectBuilder; import jakarta.ws.rs.core.Response.Status; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.logging.Logger; + +import static edu.harvard.iq.dataverse.DatasetVersion.ARCHIVE_NOTE_MAX_LENGTH; +import static edu.harvard.iq.dataverse.api.ApiConstants.*; +import static edu.harvard.iq.dataverse.api.UtilIT.API_TOKEN_HTTP_HEADER; +import static edu.harvard.iq.dataverse.api.UtilIT.equalToCI; +import static io.restassured.RestAssured.given; +import static io.restassured.path.json.JsonPath.with; +import static jakarta.ws.rs.core.Response.Status.*; import static java.lang.Thread.sleep; -import org.hamcrest.CoreMatchers; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.hasItems; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.Matchers.contains; import static org.junit.jupiter.api.Assertions.*; @@ -3548,7 +3537,9 @@ public void getVersionFiles() throws IOException, InterruptedException { getVersionFilesResponsePaginated.then().assertThat() .statusCode(OK.getStatusCode()) .body("data[0].label", equalTo(testFileName1)) - .body("data[1].label", equalTo(testFileName2)); + .body("data[1].label", equalTo(testFileName2)) + .body("total_count", equalTo(5)); + String x = getVersionFilesResponsePaginated.prettyPrint(); int fileMetadatasCount = getVersionFilesResponsePaginated.jsonPath().getList("data").size(); assertEquals(testPageSize, fileMetadatasCount); @@ -3562,7 +3553,8 @@ public void getVersionFiles() throws IOException, InterruptedException { getVersionFilesResponsePaginated.then().assertThat() .statusCode(OK.getStatusCode()) .body("data[0].label", equalTo(testFileName3)) - .body("data[1].label", equalTo(testFileName4)); + .body("data[1].label", equalTo(testFileName4)) + .body("total_count", equalTo(5)); fileMetadatasCount = getVersionFilesResponsePaginated.jsonPath().getList("data").size(); assertEquals(testPageSize, fileMetadatasCount); From 0807b1fd64b076ef92029a16b1c3a946802c56b7 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Mon, 8 Jan 2024 16:18:55 -0500 Subject: [PATCH 168/689] fix format --- src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java | 2 ++ 1 file changed, 2 insertions(+) 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 2a2843c0494..419132f7ba7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -635,6 +635,7 @@ protected Response ok( JsonArrayBuilder bld ) { .add("data", bld).build()) .type(MediaType.APPLICATION_JSON).build(); } + protected Response ok( JsonArrayBuilder bld , long totalCount) { return Response.ok(Json.createObjectBuilder() .add("status", ApiConstants.STATUS_OK) @@ -642,6 +643,7 @@ protected Response ok( JsonArrayBuilder bld , long totalCount) { .add("data", bld).build()) .type(MediaType.APPLICATION_JSON).build(); } + protected Response ok( JsonArray ja ) { return Response.ok(Json.createObjectBuilder() .add("status", ApiConstants.STATUS_OK) From 53e525d7ddddcc4fd055f45debc126f8b2340ffc Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Mon, 8 Jan 2024 16:24:21 -0500 Subject: [PATCH 169/689] fix format --- src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index ace69a6c606..91aa33f6b1f 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -3539,7 +3539,6 @@ public void getVersionFiles() throws IOException, InterruptedException { .body("data[0].label", equalTo(testFileName1)) .body("data[1].label", equalTo(testFileName2)) .body("total_count", equalTo(5)); - String x = getVersionFilesResponsePaginated.prettyPrint(); int fileMetadatasCount = getVersionFilesResponsePaginated.jsonPath().getList("data").size(); assertEquals(testPageSize, fileMetadatasCount); From b8a79a1d8a6240a8997abf2fe1332140a4bff62b Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Mon, 8 Jan 2024 21:08:53 -0500 Subject: [PATCH 170/689] adds a simple api for clearing a single dataset from Solr. I want to have it in order to be able to create an api test for for a specific OAI set export case. But I figure it could be useful otherwise. #3437 --- .../edu/harvard/iq/dataverse/api/Index.java | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Index.java b/src/main/java/edu/harvard/iq/dataverse/api/Index.java index 4910c460b6a..2516c05e634 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Index.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Index.java @@ -215,7 +215,7 @@ public Response clearSolrIndex() { return error(Status.INTERNAL_SERVER_ERROR, ex.getLocalizedMessage()); } } - + @GET @Path("{type}/{id}") public Response indexTypeById(@PathParam("type") String type, @PathParam("id") Long id) { @@ -326,6 +326,29 @@ public Response indexDatasetByPersistentId(@QueryParam("persistentId") String pe } } + /** + * Clears the entry for a dataset from Solr + * + * @param id numer id of the dataset + * @return response; + * will return 404 if no such dataset in the database; but will attempt to + * clear the entry from Solr regardless. + */ + @DELETE + @Path("datasets/clear/{id}") + public Response clearDatasetFromIndex(@PathParam("id") Long id) { + Dataset dataset = datasetService.find(id); + // We'll attempt to delete the Solr document regardless of whether the + // dataset exists in the database: + String response = indexService.removeSolrDocFromIndex(IndexServiceBean.solrDocIdentifierDataset + id); + if (dataset != null) { + return ok("Sent request to clear Solr document for dataset " + id + ": " + response); + } else { + return notFound("Could not find dataset " + id + " in the database. Requested to clear from Solr anyway: " + response); + } + } + + /** * This is just a demo of the modular math logic we use for indexAll. */ From 622a676681a336fd78e89d1f6d21e3e703eb7d7a Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 9 Jan 2024 10:32:12 -0500 Subject: [PATCH 171/689] updated per review comments --- ...-extend-getVersionFiles-api-to-include-total-file-count.md | 2 ++ doc/sphinx-guides/source/api/native-api.rst | 4 +++- .../java/edu/harvard/iq/dataverse/api/AbstractApiBean.java | 2 +- src/main/java/edu/harvard/iq/dataverse/api/Datasets.java | 1 - src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java | 4 ++-- 5 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 doc/release-notes/10202-extend-getVersionFiles-api-to-include-total-file-count.md diff --git a/doc/release-notes/10202-extend-getVersionFiles-api-to-include-total-file-count.md b/doc/release-notes/10202-extend-getVersionFiles-api-to-include-total-file-count.md new file mode 100644 index 00000000000..80a71e9bb7e --- /dev/null +++ b/doc/release-notes/10202-extend-getVersionFiles-api-to-include-total-file-count.md @@ -0,0 +1,2 @@ +The response for getVersionFiles (/api/datasets/{id}/versions/{versionId}/files) endpoint has been modified to include a total count of records available (totalCount:x). +This will aid in pagination by allowing the caller to know how many pages can be iterated through. The existing API (getVersionFileCounts) to return the count will still be available. \ No newline at end of file diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 6591c983824..48fc16bf141 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1066,7 +1066,9 @@ The fully expanded example above (without environment variables) looks like this curl "https://demo.dataverse.org/api/datasets/24/versions/1.0/files" -This endpoint supports optional pagination, through the ``limit`` and ``offset`` query parameters: +This endpoint supports optional pagination, through the ``limit`` and ``offset`` query parameters. +To aid in pagination the Json response also includes the total number of rows (totalCount) available. +Usage example: .. code-block:: bash 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 419132f7ba7..bc94d7f0bcc 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -639,7 +639,7 @@ protected Response ok( JsonArrayBuilder bld ) { protected Response ok( JsonArrayBuilder bld , long totalCount) { return Response.ok(Json.createObjectBuilder() .add("status", ApiConstants.STATUS_OK) - .add("total_count", totalCount) + .add("totalCount", totalCount) .add("data", bld).build()) .type(MediaType.APPLICATION_JSON).build(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 56b9e8df319..3a2497d9418 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -480,7 +480,6 @@ public Response getVersionFiles(@Context ContainerRequestContext crc, } catch (IllegalArgumentException e) { return badRequest(BundleUtil.getStringFromBundle("datasets.api.version.files.invalid.access.status", List.of(accessStatus))); } - // TODO: should we count the total every time or only when offset = 0? return ok(jsonFileMetadatas(datasetVersionFilesServiceBean.getFileMetadatas(datasetVersion, limit, offset, fileSearchCriteria, fileOrderCriteria)), datasetVersionFilesServiceBean.getFileMetadataCount(datasetVersion, fileSearchCriteria)); }, getRequestUser(crc)); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index 91aa33f6b1f..5753550d564 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -3538,7 +3538,7 @@ public void getVersionFiles() throws IOException, InterruptedException { .statusCode(OK.getStatusCode()) .body("data[0].label", equalTo(testFileName1)) .body("data[1].label", equalTo(testFileName2)) - .body("total_count", equalTo(5)); + .body("totalCount", equalTo(5)); int fileMetadatasCount = getVersionFilesResponsePaginated.jsonPath().getList("data").size(); assertEquals(testPageSize, fileMetadatasCount); @@ -3553,7 +3553,7 @@ public void getVersionFiles() throws IOException, InterruptedException { .statusCode(OK.getStatusCode()) .body("data[0].label", equalTo(testFileName3)) .body("data[1].label", equalTo(testFileName4)) - .body("total_count", equalTo(5)); + .body("totalCount", equalTo(5)); fileMetadatasCount = getVersionFilesResponsePaginated.jsonPath().getList("data").size(); assertEquals(testPageSize, fileMetadatasCount); From 4b6fb504873b6864060f0e15f1a6609b3f05a2d0 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Tue, 9 Jan 2024 10:32:50 -0500 Subject: [PATCH 172/689] cosmetic #3437 --- src/main/java/edu/harvard/iq/dataverse/api/Index.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Index.java b/src/main/java/edu/harvard/iq/dataverse/api/Index.java index 2516c05e634..a55ddad0fa0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Index.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Index.java @@ -335,7 +335,7 @@ public Response indexDatasetByPersistentId(@QueryParam("persistentId") String pe * clear the entry from Solr regardless. */ @DELETE - @Path("datasets/clear/{id}") + @Path("datasets/{id}/clear") public Response clearDatasetFromIndex(@PathParam("id") Long id) { Dataset dataset = datasetService.find(id); // We'll attempt to delete the Solr document regardless of whether the From 291811e3e3c6f0f8c54dcd6b980444259e247d70 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Tue, 9 Jan 2024 11:42:34 -0500 Subject: [PATCH 173/689] #9686 add migration to harvested files --- .../migration/V6.1.0.1__9686-move-harvestingclient-id.sql | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/resources/db/migration/V6.1.0.1__9686-move-harvestingclient-id.sql b/src/main/resources/db/migration/V6.1.0.1__9686-move-harvestingclient-id.sql index 22142b8fc41..67ba026745f 100644 --- a/src/main/resources/db/migration/V6.1.0.1__9686-move-harvestingclient-id.sql +++ b/src/main/resources/db/migration/V6.1.0.1__9686-move-harvestingclient-id.sql @@ -1,8 +1,14 @@ ALTER TABLE dvobject ADD COLUMN IF NOT EXISTS harvestingclient_id BIGINT; +--add harvesting client id to dvobject records of harvested datasets update dvobject dvo set harvestingclient_id = s.harvestingclient_id from (select id, harvestingclient_id from dataset d where d.harvestingclient_id is not null) s where s.id = dvo.id; +--add harvesting client id to dvobject records of harvested files +update dvobject dvo set harvestingclient_id = s.harvestingclient_id from +(select id, harvestingclient_id from dataset d where d.harvestingclient_id is not null) s +where s.id = dvo.owner_id; + ALTER TABLE dataset drop COLUMN IF EXISTS harvestingclient_id; From c6ec7faefb1101a29c1e89e4f40a9085c3234e93 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Tue, 9 Jan 2024 12:10:59 -0500 Subject: [PATCH 174/689] api tests (work in progress) #3437 --- .../iq/dataverse/api/HarvestingServerIT.java | 50 ++++++++++++++++++- .../edu/harvard/iq/dataverse/api/UtilIT.java | 7 ++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java index e02964ef28f..629e72aec06 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java @@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -39,6 +40,7 @@ public class HarvestingServerIT { private static String adminUserAPIKey; private static String singleSetDatasetIdentifier; private static String singleSetDatasetPersistentId; + private static Integer singleSetDatasetDatabaseId; private static List extraDatasetsIdentifiers = new ArrayList<>(); @BeforeAll @@ -84,7 +86,7 @@ private static void setupDatasets() { // create dataset: Response createDatasetResponse = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, adminUserAPIKey); createDatasetResponse.prettyPrint(); - Integer datasetId = UtilIT.getDatasetIdFromResponse(createDatasetResponse); + singleSetDatasetDatabaseId = UtilIT.getDatasetIdFromResponse(createDatasetResponse); // retrieve the global id: singleSetDatasetPersistentId = UtilIT.getDatasetPersistentIdFromResponse(createDatasetResponse); @@ -110,7 +112,7 @@ private static void setupDatasets() { // create dataset: createDatasetResponse = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, adminUserAPIKey); createDatasetResponse.prettyPrint(); - datasetId = UtilIT.getDatasetIdFromResponse(createDatasetResponse); + Integer datasetId = UtilIT.getDatasetIdFromResponse(createDatasetResponse); // retrieve the global id: String thisDatasetPersistentId = UtilIT.getDatasetPersistentIdFromResponse(createDatasetResponse); @@ -395,6 +397,11 @@ public void testSetEditAPIandOAIlistSets() { // OAI set with a single dataset, and attempt to retrieve // it and validate the OAI server responses of the corresponding // ListIdentifiers, ListRecords and GetRecord methods. + // Finally, we will make sure that the test reexport survives + // a reexport when the control dataset is dropped from the search + // index temporarily (if, for example, the site admin cleared their + // solr index in order to reindex everything from scratch - which + // can take a while on a large database). This is per #3437 @Test public void testSingleRecordOaiSet() throws InterruptedException { // Let's try and create an OAI set with the "single set dataset" that @@ -549,7 +556,46 @@ public void testSingleRecordOaiSet() throws InterruptedException { assertEquals("Medicine, Health and Life Sciences", responseXmlPath.getString("OAI-PMH.GetRecord.record.metadata.dc.subject")); // ok, looks legit! + + // Now, let's clear this dataset from Solr: + Response solrClearResponse = UtilIT.indexClearDataset(singleSetDatasetDatabaseId); + assertEquals(200, solrClearResponse.getStatusCode()); + + // Now, let's re-export the set. The search query that defines the set + // will no longer find it (todo: confirm this first?). However, since + // the dataset still exists in the database; and would in real life + // be reindexed again, we don't want to mark the OAI record for the + // dataset as "deleted" just yet. (this is a new feature, as of 6.2) + // So, let's re-export the set... + + exportSetResponse = UtilIT.exportOaiSet(setName); + assertEquals(200, exportSetResponse.getStatusCode()); + Thread.sleep(10000L); // wait for just a second, to be safe + + // OAI Test 5. Check ListIdentifiers again: + + Response listIdentifiersResponse = UtilIT.getOaiListIdentifiers(setName, "oai_dc"); + assertEquals(OK.getStatusCode(), listIdentifiersResponse.getStatusCode()); + // Validate the service section of the OAI response: + responseXmlPath = validateOaiVerbResponse(listIdentifiersResponse, "ListIdentifiers"); + + // ... and confirm that the record for our dataset is still listed + // as active: + List ret = responseXmlPath.getList("OAI-PMH.ListIdentifiers.header"); + + assertEquals(1, ret.size()); + assertEquals(singleSetDatasetPersistentId, responseXmlPath + .getString("OAI-PMH.ListIdentifiers.header.identifier")); + assertEquals(setName, responseXmlPath + .getString("OAI-PMH.ListIdentifiers.header.setSpec")); + // ... and, most importantly, make sure the record does not have a + // `status="deleted"` attribute: + assertNull(responseXmlPath.getString("OAI-PMH.ListIdentifiers.header.@status")); + + // TODO: (?) we could also destroy the dataset for real now, and make + // sure the "deleted" attribute has been added to the OAI record. + } // This test will attempt to create a set with multiple records (enough 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 b6dfc697f3c..cbc2f974fec 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -1494,6 +1494,11 @@ static Response reindexDataset(String persistentId) { return response; } + static Response indexClearDataset(Integer datasetId) { + return given() + .delete("/api/admin/index/datasets/"+datasetId+"/clear"); + } + static Response reindexDataverse(String dvId) { Response response = given() .get("/api/admin/index/dataverses/" + dvId); @@ -2066,7 +2071,7 @@ static Response indexClear() { return given() .get("/api/admin/index/clear"); } - + static Response index() { return given() .get("/api/admin/index"); From dfb1795e1318d058c4b614894ce9cd1039da38d3 Mon Sep 17 00:00:00 2001 From: Guillermo Portas Date: Tue, 9 Jan 2024 17:37:06 +0000 Subject: [PATCH 175/689] Added: minor docs formatting tweaks --- doc/sphinx-guides/source/api/native-api.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 48fc16bf141..09fc3c69693 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1067,7 +1067,9 @@ The fully expanded example above (without environment variables) looks like this curl "https://demo.dataverse.org/api/datasets/24/versions/1.0/files" This endpoint supports optional pagination, through the ``limit`` and ``offset`` query parameters. -To aid in pagination the Json response also includes the total number of rows (totalCount) available. + +To aid in pagination the JSON response also includes the total number of rows (totalCount) available. + Usage example: .. code-block:: bash From 03f4a06b5ed163d9252e6e868fa2e939fda0a2e0 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Tue, 9 Jan 2024 13:30:34 -0500 Subject: [PATCH 176/689] #9686 add a release note --- doc/release-notes/9686-move-harvesting-client-id.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/release-notes/9686-move-harvesting-client-id.md diff --git a/doc/release-notes/9686-move-harvesting-client-id.md b/doc/release-notes/9686-move-harvesting-client-id.md new file mode 100644 index 00000000000..110fcc6ca6e --- /dev/null +++ b/doc/release-notes/9686-move-harvesting-client-id.md @@ -0,0 +1 @@ +With this release the harvesting client id will be available for harvested files. A database update will copy the id to previously harvested files./ From 7c920676611ecabd932067e8124b2df2e166a18b Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Tue, 9 Jan 2024 13:56:59 -0500 Subject: [PATCH 177/689] API test finished. #3437 --- .../harvest/server/OAIRecordServiceBean.java | 2 +- .../iq/dataverse/api/HarvestingServerIT.java | 42 ++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAIRecordServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAIRecordServiceBean.java index 902a52c7b97..cc15d4c978b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAIRecordServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAIRecordServiceBean.java @@ -65,7 +65,7 @@ public class OAIRecordServiceBean implements java.io.Serializable { * marked as deleted without any further checks. Otherwise * we'll want to double-check if the dataset still exists * as published. This is to prevent marking existing datasets - * as deleted during a full reindex and such. + * as deleted during a full reindex etc. * @param setUpdateLogger dedicated Logger */ public void updateOaiRecords(String setName, List datasetIds, Date updateTime, boolean doExport, boolean confirmed, Logger setUpdateLogger) { diff --git a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java index 629e72aec06..f076f819f6f 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java @@ -560,6 +560,7 @@ public void testSingleRecordOaiSet() throws InterruptedException { // Now, let's clear this dataset from Solr: Response solrClearResponse = UtilIT.indexClearDataset(singleSetDatasetDatabaseId); assertEquals(200, solrClearResponse.getStatusCode()); + solrClearResponse.prettyPrint(); // Now, let's re-export the set. The search query that defines the set // will no longer find it (todo: confirm this first?). However, since @@ -570,7 +571,7 @@ public void testSingleRecordOaiSet() throws InterruptedException { exportSetResponse = UtilIT.exportOaiSet(setName); assertEquals(200, exportSetResponse.getStatusCode()); - Thread.sleep(10000L); // wait for just a second, to be safe + Thread.sleep(1000L); // wait for just a second, to be safe // OAI Test 5. Check ListIdentifiers again: @@ -596,6 +597,43 @@ public void testSingleRecordOaiSet() throws InterruptedException { // TODO: (?) we could also destroy the dataset for real now, and make // sure the "deleted" attribute has been added to the OAI record. + // While we are at it, let's now destroy this dataset for real, and + // make sure the "deleted" attribute is actually added once the set + // is re-exported: + + Response destroyDatasetResponse = UtilIT.destroyDataset(singleSetDatasetPersistentId, adminUserAPIKey); + assertEquals(200, destroyDatasetResponse.getStatusCode()); + destroyDatasetResponse.prettyPrint(); + + // Confirm that it no longer exists: + Response datasetNotFoundResponse = UtilIT.nativeGet(singleSetDatasetDatabaseId, adminUserAPIKey); + assertEquals(404, datasetNotFoundResponse.getStatusCode()); + + // Repeat the whole production with re-exporting set and checking + // ListIdentifiers: + + exportSetResponse = UtilIT.exportOaiSet(setName); + assertEquals(200, exportSetResponse.getStatusCode()); + Thread.sleep(1000L); // wait for just a second, to be safe + System.out.println("re-exported the dataset again, with the control dataset destroyed"); + + // OAI Test 6. Check ListIdentifiers again: + + listIdentifiersResponse = UtilIT.getOaiListIdentifiers(setName, "oai_dc"); + assertEquals(OK.getStatusCode(), listIdentifiersResponse.getStatusCode()); + + // Validate the service section of the OAI response: + responseXmlPath = validateOaiVerbResponse(listIdentifiersResponse, "ListIdentifiers"); + + // ... and confirm that the record for our dataset is still listed... + ret = responseXmlPath.getList("OAI-PMH.ListIdentifiers.header"); + assertEquals(1, ret.size()); + assertEquals(singleSetDatasetPersistentId, responseXmlPath + .getString("OAI-PMH.ListIdentifiers.header.identifier")); + + // ... BUT, it should be marked as "deleted" now: + assertEquals(responseXmlPath.getString("OAI-PMH.ListIdentifiers.header.@status"), "deleted"); + } // This test will attempt to create a set with multiple records (enough @@ -910,7 +948,7 @@ public void testMultiRecordOaiSet() throws InterruptedException { // TODO: // What else can we test? // Some ideas: - // - Test handling of deleted dataset records + // - Test handling of deleted dataset records - DONE! // - Test "from" and "until" time parameters // - Validate full verb response records against XML schema // (for each supported metadata format, possibly?) From 37b9a8cb0e24b9b83780c9c1ab7c0bf32272b0d2 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Tue, 9 Jan 2024 14:23:54 -0500 Subject: [PATCH 178/689] Added a guide entry for the new api call/renamed the call itself #3437 --- .../source/admin/solr-search-index.rst | 14 ++++++++++++-- .../java/edu/harvard/iq/dataverse/api/Index.java | 2 +- .../java/edu/harvard/iq/dataverse/api/UtilIT.java | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/doc/sphinx-guides/source/admin/solr-search-index.rst b/doc/sphinx-guides/source/admin/solr-search-index.rst index e6f7b588ede..4c71ef9d4a8 100644 --- a/doc/sphinx-guides/source/admin/solr-search-index.rst +++ b/doc/sphinx-guides/source/admin/solr-search-index.rst @@ -26,8 +26,8 @@ Remove all Solr documents that are orphaned (i.e. not associated with objects in ``curl http://localhost:8080/api/admin/index/clear-orphans`` -Clearing Data from Solr -~~~~~~~~~~~~~~~~~~~~~~~ +Clearing ALL Data from Solr +~~~~~~~~~~~~~~~~~~~~~~~~~~~ Please note that the moment you issue this command, it will appear to end users looking at the root Dataverse installation page that all data is gone! This is because the root Dataverse installation page is powered by the search index. @@ -86,6 +86,16 @@ To re-index a dataset by its database ID: ``curl http://localhost:8080/api/admin/index/datasets/7504557`` +Clearing a Dataset from Solr +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This API will clear the Solr entry for the dataset specified. It can be useful if you have reasons to stop showing a published dataset in search results and/or on Collection pages, but don't want to destroy and purge it from the database just yet. + +``curl -X DELETE http://localhost:8080/api/admin/index/datasets/`` + +This can be reversed of course by re-indexing the dataset with the API above. + + Manually Querying Solr ---------------------- diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Index.java b/src/main/java/edu/harvard/iq/dataverse/api/Index.java index a55ddad0fa0..c30a77acb58 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Index.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Index.java @@ -335,7 +335,7 @@ public Response indexDatasetByPersistentId(@QueryParam("persistentId") String pe * clear the entry from Solr regardless. */ @DELETE - @Path("datasets/{id}/clear") + @Path("datasets/{id}") public Response clearDatasetFromIndex(@PathParam("id") Long id) { Dataset dataset = datasetService.find(id); // We'll attempt to delete the Solr document regardless of whether the 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 cbc2f974fec..49da0445e52 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -1496,7 +1496,7 @@ static Response reindexDataset(String persistentId) { static Response indexClearDataset(Integer datasetId) { return given() - .delete("/api/admin/index/datasets/"+datasetId+"/clear"); + .delete("/api/admin/index/datasets/"+datasetId); } static Response reindexDataverse(String dvId) { From b9bcf995b42889af3333368b3264f49264df52ef Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Tue, 9 Jan 2024 14:58:32 -0500 Subject: [PATCH 179/689] Update Kanban Board URL The URL was pointing to the old board. --- doc/sphinx-guides/source/developers/intro.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/developers/intro.rst b/doc/sphinx-guides/source/developers/intro.rst index a01a8066897..f446b73de09 100755 --- a/doc/sphinx-guides/source/developers/intro.rst +++ b/doc/sphinx-guides/source/developers/intro.rst @@ -40,7 +40,7 @@ For the Dataverse Software development roadmap, please see https://www.iq.harvar Kanban Board ------------ -You can get a sense of what's currently in flight (in dev, in QA, etc.) by looking at https://github.com/orgs/IQSS/projects/2 +You can get a sense of what's currently in flight (in dev, in QA, etc.) by looking at https://github.com/orgs/IQSS/projects/34 Issue Tracker ------------- From 94570f0c670e6d39594c5cfb9ca5233962834de0 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 10 Jan 2024 10:59:21 -0500 Subject: [PATCH 180/689] add toc to docs #10200 --- doc/sphinx-guides/source/developers/globus-api.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/sphinx-guides/source/developers/globus-api.rst b/doc/sphinx-guides/source/developers/globus-api.rst index 2f922fb1fc0..b5d420467aa 100644 --- a/doc/sphinx-guides/source/developers/globus-api.rst +++ b/doc/sphinx-guides/source/developers/globus-api.rst @@ -1,6 +1,9 @@ Globus Transfer API =================== +.. contents:: |toctitle| + :local: + The Globus API addresses three use cases: * Transfer to a Dataverse-managed Globus endpoint (File-based or using the Globus S3 Connector) From 67292840e9b6e2f701fd6bc0e09522b0b2d0ef07 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Wed, 10 Jan 2024 13:16:27 -0500 Subject: [PATCH 181/689] Add comments and makes the loop easier to understand. --- ...tLatestPublishedDatasetVersionCommand.java | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedDatasetVersionCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedDatasetVersionCommand.java index a4952bbf524..dd9a8112afe 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedDatasetVersionCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedDatasetVersionCommand.java @@ -17,33 +17,51 @@ public class GetLatestPublishedDatasetVersionCommand extends AbstractCommand { private final Dataset ds; private final boolean includeDeaccessioned; - private boolean checkPerms; + private boolean checkPermsWhenDeaccessioned; public GetLatestPublishedDatasetVersionCommand(DataverseRequest aRequest, Dataset anAffectedDataset) { this(aRequest, anAffectedDataset, false, false); } - public GetLatestPublishedDatasetVersionCommand(DataverseRequest aRequest, Dataset anAffectedDataset, boolean includeDeaccessioned, boolean checkPerms) { + public GetLatestPublishedDatasetVersionCommand(DataverseRequest aRequest, Dataset anAffectedDataset, boolean includeDeaccessioned, boolean checkPermsWhenDeaccessioned) { super(aRequest, anAffectedDataset); ds = anAffectedDataset; this.includeDeaccessioned = includeDeaccessioned; - this.checkPerms = checkPerms; + this.checkPermsWhenDeaccessioned = checkPermsWhenDeaccessioned; } + /* + * This command depending on the requested parameters will return: + * + * If the user requested to include a deaccessioned dataset with the files, the command will return the deaccessioned version if the user has permissions to view the files. Otherwise, it will return null. + * If the user requested to include a deaccessioned dataset but did not request the files, the command will return the deaccessioned version. + * If the user did not request to include a deaccessioned dataset, the command will return the latest published version. + * + */ @Override public DatasetVersion execute(CommandContext ctxt) throws CommandException { - for (DatasetVersion dsv : ds.getVersions()) { - if (dsv.isReleased() || (includeDeaccessioned && dsv.isDeaccessioned())) { - - if(dsv.isDeaccessioned() && checkPerms){ - if(!ctxt.permissions().requestOn(getRequest(), ds).has(Permission.EditDataset)){ - return null; - } - } - return dsv; + DatasetVersion dsv = null; + + //We search of a released or deaccessioned version if it is requested. + for (DatasetVersion next : ds.getVersions()) { + if (next.isReleased() || (includeDeaccessioned && next.isDeaccessioned())){ + dsv = next; + break; + } + } + + //Checking permissions if the deaccessionedVersion was found and we are checking permissions because files were requested. + if(dsv != null && (dsv.isDeaccessioned() && checkPermsWhenDeaccessioned)){ + //If the user has no permissions we return null + if(!ctxt.permissions().requestOn(getRequest(), ds).has(Permission.EditDataset)){ + dsv = null; } } - return null; + + return dsv; } + + + } From 7e30c4ae14d2b4af5d80e9722192ec0a2680bcd9 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Wed, 10 Jan 2024 14:32:34 -0500 Subject: [PATCH 182/689] fixes the API tests. #3437 --- .../iq/dataverse/api/HarvestingServerIT.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java index f076f819f6f..cca571efee0 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java @@ -106,9 +106,9 @@ private static void setupDatasets() { // So wait for all of this to finish. UtilIT.sleepForReexport(singleSetDatasetPersistentId, adminUserAPIKey, 10); - // ... And let's create 4 more datasets for a multi-dataset experiment: + // ... And let's create 5 more datasets for a multi-dataset experiment: - for (int i = 0; i < 4; i++) { + for (int i = 0; i < 5; i++) { // create dataset: createDatasetResponse = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, adminUserAPIKey); createDatasetResponse.prettyPrint(); @@ -653,9 +653,13 @@ public void testMultiRecordOaiSet() throws InterruptedException { // in the class init: String setName = UtilIT.getRandomString(6); - String setQuery = "(dsPersistentId:" + singleSetDatasetIdentifier; + String setQuery = ""; for (String persistentId : extraDatasetsIdentifiers) { - setQuery = setQuery.concat(" OR dsPersistentId:" + persistentId); + if (setQuery.equals("")) { + setQuery = "(dsPersistentId:" + persistentId; + } else { + setQuery = setQuery.concat(" OR dsPersistentId:" + persistentId); + } } setQuery = setQuery.concat(")"); @@ -796,7 +800,6 @@ public void testMultiRecordOaiSet() throws InterruptedException { boolean allDatasetsListed = true; - allDatasetsListed = persistentIdsInListIdentifiers.contains(singleSetDatasetIdentifier); for (String persistentId : extraDatasetsIdentifiers) { allDatasetsListed = allDatasetsListed && persistentIdsInListIdentifiers.contains(persistentId); } @@ -921,12 +924,11 @@ public void testMultiRecordOaiSet() throws InterruptedException { // Record the last identifier listed on this final page: persistentIdsInListRecords.add(ret.get(0).substring(ret.get(0).lastIndexOf('/') + 1)); - // Finally, let's confirm that the expected 5 datasets have been listed + // Finally, let's confirm again that the expected 5 datasets have been listed // as part of this Set: allDatasetsListed = true; - allDatasetsListed = persistentIdsInListRecords.contains(singleSetDatasetIdentifier); for (String persistentId : extraDatasetsIdentifiers) { allDatasetsListed = allDatasetsListed && persistentIdsInListRecords.contains(persistentId); } From 9d18da511af71dd4daeb1f76c330c5a25dbcca23 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 11 Jan 2024 11:01:08 +0000 Subject: [PATCH 183/689] Added: displayOrder and isRequired fields to DatasetFieldType payload --- .../harvard/iq/dataverse/util/json/JsonPrinter.java | 2 ++ .../edu/harvard/iq/dataverse/api/MetadataBlocksIT.java | 10 ++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) 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 cfc266f2ba7..a97ef9c12d1 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 @@ -570,6 +570,8 @@ public static JsonObjectBuilder json(DatasetFieldType fld) { fieldsBld.add("multiple", fld.isAllowMultiples()); fieldsBld.add("isControlledVocabulary", fld.isControlledVocabulary()); fieldsBld.add("displayFormat", fld.getDisplayFormat()); + fieldsBld.add("isRequired", fld.isRequired()); + fieldsBld.add("displayOrder", fld.getDisplayOrder()); if (fld.isControlledVocabulary()) { // If the field has a controlled vocabulary, // add all values to the resulting JSON diff --git a/src/test/java/edu/harvard/iq/dataverse/api/MetadataBlocksIT.java b/src/test/java/edu/harvard/iq/dataverse/api/MetadataBlocksIT.java index c301e158b4e..f1c3a9815f1 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/MetadataBlocksIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/MetadataBlocksIT.java @@ -25,7 +25,9 @@ void testGetCitationBlock() { getCitationBlock.prettyPrint(); getCitationBlock.then().assertThat() .statusCode(OK.getStatusCode()) - .body("data.fields.subject.controlledVocabularyValues[0]", CoreMatchers.is("Agricultural Sciences")); + .body("data.fields.subject.controlledVocabularyValues[0]", CoreMatchers.is("Agricultural Sciences")) + .body("data.fields.title.displayOrder", CoreMatchers.is(0)) + .body("data.fields.title.isRequired", CoreMatchers.is(true)); } @Test @@ -37,18 +39,18 @@ void testDatasetWithAllDefaultMetadata() { ", response=" + createUser.prettyPrint()); String apiToken = UtilIT.getApiTokenFromResponse(createUser); assumeFalse(apiToken == null || apiToken.isBlank()); - + Response createCollection = UtilIT.createRandomDataverse(apiToken); assumeTrue(createCollection.statusCode() < 300, "code=" + createCollection.statusCode() + ", response=" + createCollection.prettyPrint()); String dataverseAlias = UtilIT.getAliasFromResponse(createCollection); assumeFalse(dataverseAlias == null || dataverseAlias.isBlank()); - + // when String pathToJsonFile = "scripts/api/data/dataset-create-new-all-default-fields.json"; Response createDataset = UtilIT.createDatasetViaNativeApi(dataverseAlias, pathToJsonFile, apiToken); - + // then assertEquals(CREATED.getStatusCode(), createDataset.statusCode(), "code=" + createDataset.statusCode() + From e8054138219ffc499c756ee9d77bdb77d7450a23 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 11 Jan 2024 11:06:16 +0000 Subject: [PATCH 184/689] Added: release notes for #10216 --- doc/release-notes/10216-metadatablocks.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 doc/release-notes/10216-metadatablocks.md diff --git a/doc/release-notes/10216-metadatablocks.md b/doc/release-notes/10216-metadatablocks.md new file mode 100644 index 00000000000..8fbd4f37e14 --- /dev/null +++ b/doc/release-notes/10216-metadatablocks.md @@ -0,0 +1,4 @@ +The API endpoint `/api/metadatablocks/{block_id}` has been extended to include the following fields: + +- `isRequired` - Wether or not this field is required +- `displayOrder`: The display order of the field in create/edit forms From a833e168d8d4df499feca9796f38fbb186581c8b Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Thu, 11 Jan 2024 09:46:24 -0500 Subject: [PATCH 185/689] minor doc changes #3437 --- doc/release-notes/3437-new-index-api-added.md | 4 ++++ doc/sphinx-guides/source/admin/solr-search-index.rst | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 doc/release-notes/3437-new-index-api-added.md diff --git a/doc/release-notes/3437-new-index-api-added.md b/doc/release-notes/3437-new-index-api-added.md new file mode 100644 index 00000000000..2f40c65073f --- /dev/null +++ b/doc/release-notes/3437-new-index-api-added.md @@ -0,0 +1,4 @@ +(this API was added as a side feature of the pr #10222. the main point of the pr was an improvement in the OAI set housekeeping logic, I believe it's too obscure part of the system to warrant a relase note by itself. but the new API below needs to be announced). + +A new Index API endpoint has been added allowing an admin to clear an individual dataset from Solr. + diff --git a/doc/sphinx-guides/source/admin/solr-search-index.rst b/doc/sphinx-guides/source/admin/solr-search-index.rst index 4c71ef9d4a8..3f7b9d5b547 100644 --- a/doc/sphinx-guides/source/admin/solr-search-index.rst +++ b/doc/sphinx-guides/source/admin/solr-search-index.rst @@ -89,7 +89,7 @@ To re-index a dataset by its database ID: Clearing a Dataset from Solr ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This API will clear the Solr entry for the dataset specified. It can be useful if you have reasons to stop showing a published dataset in search results and/or on Collection pages, but don't want to destroy and purge it from the database just yet. +This API will clear the Solr entry for the dataset specified. It can be useful if you have reasons to want to hide a published dataset from showing in search results and/or on Collection pages, but don't want to destroy and purge it from the database just yet. ``curl -X DELETE http://localhost:8080/api/admin/index/datasets/`` From 462d8f743ba96beb39a2d30ec49eb0ee3ae9d210 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Thu, 11 Jan 2024 10:17:18 -0500 Subject: [PATCH 186/689] #10216 typo in release note --- doc/release-notes/10216-metadatablocks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/10216-metadatablocks.md b/doc/release-notes/10216-metadatablocks.md index 8fbd4f37e14..b3be7e76abc 100644 --- a/doc/release-notes/10216-metadatablocks.md +++ b/doc/release-notes/10216-metadatablocks.md @@ -1,4 +1,4 @@ The API endpoint `/api/metadatablocks/{block_id}` has been extended to include the following fields: -- `isRequired` - Wether or not this field is required +- `isRequired` - Whether or not this field is required - `displayOrder`: The display order of the field in create/edit forms From b1bb6a047cc347a6d6c97ba9f56060d3805ec545 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 11 Jan 2024 11:35:34 -0500 Subject: [PATCH 187/689] minor doc tweaks #10200 --- doc/sphinx-guides/source/developers/globus-api.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/sphinx-guides/source/developers/globus-api.rst b/doc/sphinx-guides/source/developers/globus-api.rst index b5d420467aa..96475f33230 100644 --- a/doc/sphinx-guides/source/developers/globus-api.rst +++ b/doc/sphinx-guides/source/developers/globus-api.rst @@ -72,7 +72,7 @@ The response includes the id for the Globus endpoint to use along with several s The getDatasetMetadata and getFileListing URLs are just signed versions of the standard Dataset metadata and file listing API calls. The other two are Globus specific. -If called for a dataset using a store that is configured with a remote Globus endpoint(s), the return response is similar but the response includes a +If called for, a dataset using a store that is configured with a remote Globus endpoint(s), the return response is similar but the response includes a the "managed" parameter will be false, the "endpoint" parameter is replaced with a JSON array of "referenceEndpointsWithPaths" and the requestGlobusTransferPaths and addGlobusFiles URLs are replaced with ones for requestGlobusReferencePaths and addFiles. All of these calls are described further below. @@ -91,7 +91,7 @@ The returned response includes the same getDatasetMetadata and getFileListing UR Performing an Upload/Transfer In -------------------------------- -The information from the API call above can be used to provide a user with information about the dataset and to prepare to transfer or to reference files (based on the "managed" parameter). +The information from the API call above can be used to provide a user with information about the dataset and to prepare to transfer (managed=true) or to reference files (managed=false). Once the user identifies which files are to be added, the requestGlobusTransferPaths or requestGlobusReferencePaths URLs can be called. These both reference the same API call but must be used with different entries in the JSON body sent: From 1c3162f01cb921b21a72042ea03b1e9ca94c6da9 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 11 Jan 2024 11:49:01 -0500 Subject: [PATCH 188/689] typo #10200 --- doc/sphinx-guides/source/developers/globus-api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/developers/globus-api.rst b/doc/sphinx-guides/source/developers/globus-api.rst index 96475f33230..57748d0afc9 100644 --- a/doc/sphinx-guides/source/developers/globus-api.rst +++ b/doc/sphinx-guides/source/developers/globus-api.rst @@ -170,7 +170,7 @@ In the managed case, once a Globus transfer has been initiated a final API call curl -H "X-Dataverse-key:$API_TOKEN" -H "Content-type:multipart/form-data" -X POST "$SERVER_URL/api/datasets/:persistentId/addGlobusFiles -F "jsonData=$JSON_DATA" -Note that the mimetype is multipart/form-data, matching the /addFiles API call. ALso note that the API_TOKEN is not needed when using a signed URL. +Note that the mimetype is multipart/form-data, matching the /addFiles API call. Also note that the API_TOKEN is not needed when using a signed URL. With this information, Dataverse will begin to monitor the transfer and when it completes, will add all files for which the transfer succeeded. As the transfer can take significant time and the API call is asynchronous, the only way to determine if the transfer succeeded via API is to use the standard calls to check the dataset lock state and contents. From 8cc2e7c0e5ba16b2f380f8fd31531e1f90271c12 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 11 Jan 2024 11:56:50 -0500 Subject: [PATCH 189/689] fix path in globus endpoint docs #10200 --- doc/sphinx-guides/source/developers/globus-api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/developers/globus-api.rst b/doc/sphinx-guides/source/developers/globus-api.rst index 57748d0afc9..a9cfe5aedff 100644 --- a/doc/sphinx-guides/source/developers/globus-api.rst +++ b/doc/sphinx-guides/source/developers/globus-api.rst @@ -102,7 +102,7 @@ Once the user identifies which files are to be added, the requestGlobusTransferP export PERSISTENT_IDENTIFIER=doi:10.5072/FK27U7YBV export LOCALE=en-US - curl -H "X-Dataverse-key:$API_TOKEN" -H "Content-type:application/json" -X POST "$SERVER_URL/api/datasets/:persistentId/requestGlobusUpload" + curl -H "X-Dataverse-key:$API_TOKEN" -H "Content-type:application/json" -X POST "$SERVER_URL/api/datasets/:persistentId/requestGlobusUploadPaths" Note that when using the dataverse-globus app or the return from the previous call, the URL for this call will be signed and no API_TOKEN is needed. From c3556e012a03b1e131146821faabb183b1a62a87 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 11 Jan 2024 12:14:24 -0500 Subject: [PATCH 190/689] add missing trailing double quote #10200 --- doc/sphinx-guides/source/developers/globus-api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/developers/globus-api.rst b/doc/sphinx-guides/source/developers/globus-api.rst index a9cfe5aedff..5a90243bd93 100644 --- a/doc/sphinx-guides/source/developers/globus-api.rst +++ b/doc/sphinx-guides/source/developers/globus-api.rst @@ -168,7 +168,7 @@ In the managed case, once a Globus transfer has been initiated a final API call "files": [{"description":"My description.","directoryLabel":"data/subdir1","categories":["Data"], "restrict":"false", "storageIdentifier":"globusm://18b3972213f-f6b5c2221423", "fileName":"file1.txt", "mimeType":"text/plain", "checksum": {"@type": "MD5", "@value": "1234"}}, \ {"description":"My description.","directoryLabel":"data/subdir1","categories":["Data"], "restrict":"false", "storageIdentifier":"globusm://18b39722140-50eb7d3c5ece", "fileName":"file2.txt", "mimeType":"text/plain", "checksum": {"@type": "MD5", "@value": "2345"}}]}' - curl -H "X-Dataverse-key:$API_TOKEN" -H "Content-type:multipart/form-data" -X POST "$SERVER_URL/api/datasets/:persistentId/addGlobusFiles -F "jsonData=$JSON_DATA" + curl -H "X-Dataverse-key:$API_TOKEN" -H "Content-type:multipart/form-data" -X POST "$SERVER_URL/api/datasets/:persistentId/addGlobusFiles" -F "jsonData=$JSON_DATA" Note that the mimetype is multipart/form-data, matching the /addFiles API call. Also note that the API_TOKEN is not needed when using a signed URL. From 50425d3f6e063b7f54d5a49b7bcb758f0ffde3b6 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Thu, 11 Jan 2024 14:20:03 -0500 Subject: [PATCH 191/689] only list the OAI sets that have associated records #3322 --- .../harvest/server/OAISetServiceBean.java | 20 +++++++++++++++++++ .../xoai/DataverseXoaiSetRepository.java | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAISetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAISetServiceBean.java index 2bd666401c7..d5c78c36b98 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAISetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAISetServiceBean.java @@ -25,6 +25,7 @@ import jakarta.inject.Named; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.BaseHttpSolrClient.RemoteSolrException; @@ -121,6 +122,25 @@ public List findAllNamedSets() { } } + /** + * "Active" sets are the ones that have been successfully exported, and contain + * a non-zero number of records. (Although a set that contains a number of + * records that are all marked as "deleted" is still an active set!) + * @return list of OAISets + */ + public List findAllActiveNamedSets() { + String jpaQueryString = "select object(o) " + + "from OAISet as o, OAIRecord as r " + + "where r.setName = o.spec " + + "and o.spec != '' " + + "group by o order by o.spec"; + + Query query = em.createQuery(jpaQueryString); + List queryResults = query.getResultList(); + + return queryResults; + } + @Asynchronous public void remove(Long setId) { OAISet oaiSet = find(setId); diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiSetRepository.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiSetRepository.java index b4e275b6059..1e713b08adb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiSetRepository.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiSetRepository.java @@ -35,7 +35,7 @@ public void setSetService(OAISetServiceBean setService) { @Override public boolean supportSets() { - List dataverseOAISets = setService.findAllNamedSets(); + List dataverseOAISets = setService.findAllActiveNamedSets(); if (dataverseOAISets == null || dataverseOAISets.isEmpty()) { return false; @@ -46,7 +46,7 @@ public boolean supportSets() { @Override public List getSets() { logger.fine("calling retrieveSets()"); - List dataverseOAISets = setService.findAllNamedSets(); + List dataverseOAISets = setService.findAllActiveNamedSets(); List XOAISets = new ArrayList(); if (dataverseOAISets != null) { From 15ad04ee96164806036a974dbe5bf41ea2a7f0fa Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Thu, 11 Jan 2024 14:52:24 -0500 Subject: [PATCH 192/689] A test for the new "don't list until exported" OAI set feature (#3322) --- .../iq/dataverse/api/HarvestingServerIT.java | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java index e02964ef28f..e0f121305e0 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java @@ -288,7 +288,7 @@ public void testNativeSetAPI() { } @Test - public void testSetEditAPIandOAIlistSets() { + public void testSetEditAPIandOAIlistSets() throws InterruptedException { // This test focuses on testing the Edit functionality of the Dataverse // OAI Set API and the ListSets method of the Dataverse OAI server. @@ -299,7 +299,8 @@ public void testSetEditAPIandOAIlistSets() { // expected HTTP result codes. String setName = UtilIT.getRandomString(6); - String setDef = "*"; + String persistentId = extraDatasetsIdentifiers.get(0); + String setDef = "dsPersistentId:"+persistentId; // Make sure the set does not exist String setPath = String.format("/api/harvest/server/oaisets/%s", setName); @@ -369,16 +370,35 @@ public void testSetEditAPIandOAIlistSets() { XmlPath responseXmlPath = validateOaiVerbResponse(listSetsResponse, "ListSets"); - // 2. Validate the payload of the response, by confirming that the set + // 2. The set hasn't been exported yet, so it shouldn't be listed in + // ListSets (#3322). Let's confirm that: + + List listSets = responseXmlPath.getList("OAI-PMH.ListSets.set.list().findAll{it.setName=='"+setName+"'}", Node.class); + // 2a. Confirm that our set is listed: + assertNotNull(listSets, "Unexpected response from ListSets"); + assertEquals(0, listSets.size(), "An unexported OAI set is listed in ListSets"); + + // export the set: + + Response exportSetResponse = UtilIT.exportOaiSet(setName); + assertEquals(200, exportSetResponse.getStatusCode()); + Thread.sleep(1000L); // sleep for a sec to be sure + + // ... try again: + + listSetsResponse = UtilIT.getOaiListSets(); + responseXmlPath = validateOaiVerbResponse(listSetsResponse, "ListSets"); + + // 3. Validate the payload of the response, by confirming that the set // we created and modified, above, is being listed by the OAI server // and its xml record is properly formatted - List listSets = responseXmlPath.getList("OAI-PMH.ListSets.set.list().findAll{it.setName=='"+setName+"'}", Node.class); + listSets = responseXmlPath.getList("OAI-PMH.ListSets.set.list().findAll{it.setName=='"+setName+"'}", Node.class); - // 2a. Confirm that our set is listed: + // 3a. Confirm that our set is listed: assertNotNull(listSets, "Unexpected response from ListSets"); assertEquals(1, listSets.size(), "Newly-created set isn't properly listed by the OAI server"); - // 2b. Confirm that the set entry contains the updated description: + // 3b. Confirm that the set entry contains the updated description: assertEquals(newDescription, listSets.get(0).getPath("setDescription.metadata.element.field", String.class), "Incorrect description in the ListSets entry"); // ok, the xml record looks good! From 3a81926980edc7c8228dddf18a8f1305b32fc2c8 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 11 Jan 2024 15:40:14 -0500 Subject: [PATCH 193/689] add requestGlobusUploadPaths to UtilIT #10200 --- src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java | 8 ++++++++ 1 file changed, 8 insertions(+) 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 e29677c2252..33dda05b4d7 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -3718,4 +3718,12 @@ static Response requestGlobusDownload(Integer datasetId, JsonObject body, String .post("/api/datasets/" + datasetId + "/requestGlobusDownload"); } + static Response requestGlobusUploadPaths(Integer datasetId, JsonObject body, String apiToken) { + return given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .body(body.toString()) + .contentType("application/json") + .post("/api/datasets/" + datasetId + "/requestGlobusUploadPaths"); + } + } From 83120012480ce12ef8db3d33d3a1c93c4605945a Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 11 Jan 2024 15:47:17 -0500 Subject: [PATCH 194/689] clarify where taskIdentifier comes from #10200 --- doc/sphinx-guides/source/developers/globus-api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/developers/globus-api.rst b/doc/sphinx-guides/source/developers/globus-api.rst index 5a90243bd93..834db8161f0 100644 --- a/doc/sphinx-guides/source/developers/globus-api.rst +++ b/doc/sphinx-guides/source/developers/globus-api.rst @@ -157,7 +157,7 @@ In the remote/reference case, the map is from the initially supplied endpoint/pa Adding Files to the Dataset --------------------------- -In the managed case, once a Globus transfer has been initiated a final API call is made to Dataverse to provide it with the task identifier of the transfer and information about the files being transferred: +In the managed case, you must initiate a Globus transfer and take note of its task identifier. As in the JSON example below, you will pass it as ``taskIdentifier`` along with details about the files you are transferring: .. code-block:: bash From 2f571e23c7b1b98ce530d5a87ed20c8797810175 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Thu, 11 Jan 2024 16:38:18 -0500 Subject: [PATCH 195/689] Got rid of some unnecessary database lookups that were made when rendering the harvesting server page. #3322 --- .../iq/dataverse/HarvestingSetsPage.java | 60 +++++++++++++++++-- src/main/java/propertyFiles/Bundle.properties | 2 +- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/HarvestingSetsPage.java b/src/main/java/edu/harvard/iq/dataverse/HarvestingSetsPage.java index 6dbba34920b..0b66b652e0c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/HarvestingSetsPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/HarvestingSetsPage.java @@ -30,6 +30,8 @@ import jakarta.faces.view.ViewScoped; import jakarta.inject.Inject; import jakarta.inject.Named; +import java.util.HashMap; +import java.util.Map; import org.apache.commons.lang3.StringUtils; /** @@ -430,44 +432,92 @@ public boolean isSessionUserAuthenticated() { return false; } + // The numbers of datasets and deleted/exported records below are used + // in rendering rules on the page. They absolutely need to be cached + // on the first lookup. + + Map cachedSetInfoNumDatasets = new HashMap<>(); + public int getSetInfoNumOfDatasets(OAISet oaiSet) { if (oaiSet.isDefaultSet()) { return getSetInfoNumOfExported(oaiSet); } + if (cachedSetInfoNumDatasets.get(oaiSet.getSpec()) != null) { + return cachedSetInfoNumDatasets.get(oaiSet.getSpec()); + } + String query = oaiSet.getDefinition(); try { int num = oaiSetService.validateDefinitionQuery(query); if (num > -1) { + cachedSetInfoNumDatasets.put(oaiSet.getSpec(), num); return num; } } catch (OaiSetException ose) { - // do notghin - will return zero. + // do nothing - will return zero. } + cachedSetInfoNumDatasets.put(oaiSet.getSpec(), 0); return 0; } + Map cachedSetInfoNumExported = new HashMap<>(); + Integer defaultSetNumExported = null; + public int getSetInfoNumOfExported(OAISet oaiSet) { + if (oaiSet.isDefaultSet() && defaultSetNumExported != null) { + return defaultSetNumExported; + } else if (cachedSetInfoNumExported.get(oaiSet.getSpec()) != null) { + return cachedSetInfoNumExported.get(oaiSet.getSpec()); + } + List records = oaiRecordService.findActiveOaiRecordsBySetName(oaiSet.getSpec()); + int num; + if (records == null || records.isEmpty()) { - return 0; + num = 0; + } else { + num = records.size(); } - return records.size(); + if (oaiSet.isDefaultSet()) { + defaultSetNumExported = num; + } else { + cachedSetInfoNumExported.put(oaiSet.getSpec(), num); + } + return num; } + Map cachedSetInfoNumDeleted = new HashMap<>(); + Integer defaultSetNumDeleted = null; + public int getSetInfoNumOfDeleted(OAISet oaiSet) { + if (oaiSet.isDefaultSet() && defaultSetNumDeleted != null) { + return defaultSetNumDeleted; + } else if (cachedSetInfoNumDeleted.get(oaiSet.getSpec()) != null) { + return cachedSetInfoNumDeleted.get(oaiSet.getSpec()); + } + List records = oaiRecordService.findDeletedOaiRecordsBySetName(oaiSet.getSpec()); + int num; + if (records == null || records.isEmpty()) { - return 0; + num = 0; + } else { + num = records.size(); } - return records.size(); + if (oaiSet.isDefaultSet()) { + defaultSetNumDeleted = num; + } else { + cachedSetInfoNumDeleted.put(oaiSet.getSpec(), num); + } + return num; } public void validateSetQuery() { diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index ece3f070cdd..157f2ecaf54 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -631,7 +631,7 @@ harvestserver.tab.header.description=Description harvestserver.tab.header.definition=Definition Query harvestserver.tab.col.definition.default=All Published Local Datasets harvestserver.tab.header.stats=Datasets -harvestserver.tab.col.stats.empty=No records (empty set) +harvestserver.tab.col.stats.empty=No active records ({2} {2, choice, 0#records|1#record|2#records} marked as deleted) harvestserver.tab.col.stats.results={0} {0, choice, 0#datasets|1#dataset|2#datasets} ({1} {1, choice, 0#records|1#record|2#records} exported, {2} marked as deleted) harvestserver.tab.header.action=Actions harvestserver.tab.header.action.btn.export=Run Export From d86ab1587cb5088330c2df6565744769cc859119 Mon Sep 17 00:00:00 2001 From: Vera Clemens Date: Fri, 12 Jan 2024 11:36:30 +0100 Subject: [PATCH 196/689] test: use curator role in testListRoleAssignments --- scripts/api/data/role-contributor-plus.json | 12 ---------- .../harvard/iq/dataverse/api/DatasetsIT.java | 22 ++++--------------- 2 files changed, 4 insertions(+), 30 deletions(-) delete mode 100644 scripts/api/data/role-contributor-plus.json diff --git a/scripts/api/data/role-contributor-plus.json b/scripts/api/data/role-contributor-plus.json deleted file mode 100644 index ef9ba3aaff6..00000000000 --- a/scripts/api/data/role-contributor-plus.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "alias":"contributorPlus", - "name":"ContributorPlus", - "description":"For datasets, a person who can edit License + Terms, then submit them for review, and add collaborators.", - "permissions":[ - "ViewUnpublishedDataset", - "EditDataset", - "DownloadFile", - "DeleteDatasetDraft", - "ManageDatasetPermissions" - ] -} diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index b51d400d2d4..787b9b018a9 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -1349,17 +1349,11 @@ public void testListRoleAssignments() { Response notPermittedToListRoleAssignmentOnDataset = UtilIT.getRoleAssignmentsOnDataset(datasetId.toString(), null, contributorApiToken); assertEquals(UNAUTHORIZED.getStatusCode(), notPermittedToListRoleAssignmentOnDataset.getStatusCode()); - // We create a new role that includes "ManageDatasetPermissions" which are required for listing role assignments - // of a dataset and assign it to the contributor user + // We assign the curator role to the contributor user + // (includes "ManageDatasetPermissions" which are required for listing role assignments of a dataset, but not + // "ManageDataversePermissions") - String pathToJsonFile = "scripts/api/data/role-contributor-plus.json"; - Response addDataverseRoleResponse = UtilIT.addDataverseRole(pathToJsonFile, dataverseAlias, adminApiToken); - addDataverseRoleResponse.prettyPrint(); - String body = addDataverseRoleResponse.getBody().asString(); - String status = JsonPath.from(body).getString("status"); - assertEquals("OK", status); - - Response giveRandoPermission = UtilIT.grantRoleOnDataset(datasetPersistentId, "contributorPlus", "@" + contributorUsername, adminApiToken); + Response giveRandoPermission = UtilIT.grantRoleOnDataset(datasetPersistentId, "curator", "@" + contributorUsername, adminApiToken); giveRandoPermission.prettyPrint(); assertEquals(200, giveRandoPermission.getStatusCode()); @@ -1373,14 +1367,6 @@ public void testListRoleAssignments() { notPermittedToListRoleAssignmentOnDataverse = UtilIT.getRoleAssignmentsOnDataverse(dataverseAlias, contributorApiToken); assertEquals(UNAUTHORIZED.getStatusCode(), notPermittedToListRoleAssignmentOnDataverse.getStatusCode()); - - // Finally, we clean up and delete the role we created - - Response deleteDataverseRoleResponse = UtilIT.deleteDataverseRole("contributorPlus", adminApiToken); - deleteDataverseRoleResponse.prettyPrint(); - body = deleteDataverseRoleResponse.getBody().asString(); - status = JsonPath.from(body).getString("status"); - assertEquals("OK", status); } @Test From 5e9cc2ff4764915324ffc3c990f02e09738101c0 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Fri, 12 Jan 2024 13:57:59 -0500 Subject: [PATCH 197/689] fix bad SQL query in guestbook #10232 --- .../edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java index b0cc41eb448..01e6ecf7ff2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java @@ -928,7 +928,7 @@ public Long getDownloadCountByDatasetId(Long datasetId, LocalDate date) { if(date != null) { query = em.createNativeQuery("select count(o.id) from GuestbookResponse o where o.dataset_id = " + datasetId + " and responsetime < '" + date.toString() + "' and eventtype != '" + GuestbookResponse.ACCESS_REQUEST +"'"); }else { - query = em.createNativeQuery("select count(o.id) from GuestbookResponse o where o.dataset_id = " + datasetId+ "and eventtype != '" + GuestbookResponse.ACCESS_REQUEST +"'"); + query = em.createNativeQuery("select count(o.id) from GuestbookResponse o where o.dataset_id = " + datasetId+ " and eventtype != '" + GuestbookResponse.ACCESS_REQUEST +"'"); } return (Long) query.getSingleResult(); } From d3f3eb9219fa101db8ebfea34ee62ccd3111194a Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Fri, 12 Jan 2024 14:18:25 -0500 Subject: [PATCH 198/689] Update docker-compose-dev.yml better explain presence of settings #9275 --- docker-compose-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index ce9f39a418a..10fe62ff6df 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -19,7 +19,7 @@ services: DATAVERSE_AUTH_OIDC_CLIENT_SECRET: 94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8 DATAVERSE_AUTH_OIDC_AUTH_SERVER_URL: http://keycloak.mydomain.com:8090/realms/test DATAVERSE_JSF_REFRESH_PERIOD: "1" - # to get HarvestingServerIT to pass + # These two oai settings are here to get HarvestingServerIT to pass dataverse_oai_server_maxidentifiers: "2" dataverse_oai_server_maxrecords: "2" JVM_ARGS: -Ddataverse.files.storage-driver-id=file1 From 74b45e1d7d24b621a7368c517e687df0b21f199c Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 16 Jan 2024 10:21:42 -0500 Subject: [PATCH 199/689] QA Guide general update --- doc/sphinx-guides/source/qa/index.md | 6 +-- doc/sphinx-guides/source/qa/overview.md | 22 ++++++---- .../source/qa/performance-tests.md | 8 ++++ .../{other-approaches.md => qa-workflow.md} | 41 ++++--------------- ...{manual-testing.md => testing-approach.md} | 9 +++- 5 files changed, 42 insertions(+), 44 deletions(-) rename doc/sphinx-guides/source/qa/{other-approaches.md => qa-workflow.md} (58%) rename doc/sphinx-guides/source/qa/{manual-testing.md => testing-approach.md} (84%) diff --git a/doc/sphinx-guides/source/qa/index.md b/doc/sphinx-guides/source/qa/index.md index 6027f07574f..c7582a2169f 100644 --- a/doc/sphinx-guides/source/qa/index.md +++ b/doc/sphinx-guides/source/qa/index.md @@ -3,9 +3,9 @@ ```{toctree} overview.md testing-infrastructure.md -performance-tests.md -manual-testing.md +qa-workflow.md +testing-approach.md test-automation.md -other-approaches.md jenkins.md +performance-tests.md ``` diff --git a/doc/sphinx-guides/source/qa/overview.md b/doc/sphinx-guides/source/qa/overview.md index c4f66446ca3..08740e9345d 100644 --- a/doc/sphinx-guides/source/qa/overview.md +++ b/doc/sphinx-guides/source/qa/overview.md @@ -11,19 +11,27 @@ This guide describes the testing process used by QA at IQSS and provides a refer ## Workflow -The basic workflow is as follows. Bugs or feature requests are submitted to GitHub by the community or by team members as issues. These issues are prioritized and added to a two-week sprint that is reflected on the GitHub {ref}`kanban-board`. As developers work on these issues, a GitHub branch is produced, code is contributed, and a pull request is made to merge these new changes back into the common {ref}`develop branch ` and ultimately released as part of the product. Before a pull request is moved to QA, it must be reviewed by a member of the development team from a coding perspective, and it must pass automated tests. There it is tested manually, exercising the UI (using three common browsers) and any business logic it implements. Depending on whether the code modifies existing code or is completely new, a smoke test of core functionality is performed and some basic regression testing of modified or related code is performed. Any documentation provided is used to understand the feature and any assertions made in that documentation are tested. Once this passes and any bugs that are found are corrected, and the automated tests are confirmed to be passing, the PR is merged into the develop, the PR is closed, and the branch is deleted (if it is local). At this point, the PR moves from the QA column automatically into the Done column and the process repeats with the next PR until it is decided to {doc}`make a release `. +The basic workflow is as follows. Bugs or feature requests are submitted to GitHub by the community or by team members as [issues](https://github.com/IQSS/dataverse/issues). These issues are prioritized and added to a two-week sprint that is reflected on the GitHub {ref}`kanban-board`. As developers work on these issues, a GitHub branch is produced, code is contributed, and a pull request is made to merge these new changes back into the common {ref}`develop branch ` and ultimately released as part of the product. -## Release Cadence and Sprints +Before a pull request is moved to QA, it must be reviewed by a member of the development team from a coding perspective, and it must pass automated tests. There it is tested manually, exercising the UI (using three common browsers) and any business logic it implements. -A release likely spans multiple two-week sprints. Each sprint represents the priorities for that time and is sized so that the team can reasonably complete most of the work on time. This is a goal to help with planning, it is not a strict requirement. Some issues from the previous sprint may remain and likely be included in the next sprint but occasionally may be deprioritized and deferred to another time. +Depending on whether the code modifies existing code or is completely new, a smoke test of core functionality is performed and some basic regression testing of modified or related code is performed. Any documentation provided is used to understand the feature and any assertions made in that documentation are tested. Once this passes and any bugs that are found are corrected, and the automated tests are confirmed to be passing, the PR is merged into the develop, the PR is closed, and the branch is deleted (if it is local). At this point, the PR moves from the QA column automatically into the Done column and the process repeats with the next PR until it is decided to {doc}`make a release `. -The decision to make a release can be based on the time since the last release, some important feature needed by the community or contractual deadline, or some other logical reason to package the work completed into a named release and posted to the releases section on GitHub. +## Tips and Tricks -## Performance Testing and Deployment +- Start testing simply, with the most obvious test. You don’t need to know all your tests upfront. As you gain comfort and understanding of how it works, try more tests until you are done. If it is a complex feature, jot down your tests in an outline format, some beforehand as a guide, and some after as things occur to you. Save the doc in a testing folder (on Google Drive). This potentially will help with future testing. +- When in doubt, ask someone. If you are confused about how something is working, it may be something you have missed, or it could be a documentation issue, or it could be a bug! Talk to the code reviewer and the contributor/developer for their opinion and advice. +- Always tail the server.log file while testing. Open a terminal window to the test instance and `tail -F server.log`. This helps you get a real-time sense of what the server is doing when you act and makes it easier to identify any stack trace on failure. +- When overloaded, do the simple pull requests first to reduce the queue. It gives you a mental boost to complete something and reduces the perception of the amount of work still to be done. +- When testing a bug fix, try reproducing the bug on the demo before testing the fix, that way you know you are taking the correct steps to verify that the fix worked. +- When testing an optional feature that requires configuration, do a smoke test without the feature configured and then with it configured. That way you know that folks using the standard config are unaffected by the option if they choose not to configure it. +- Back up your DB before applying an irreversible DB update and you are using a persistent/reusable platform. Just in case it fails, and you need to carry on testing something else you can use the backup. -The final testing activity before producing a release is performance testing. This could be done throughout the release cycle but since it is time-consuming it is done once near the end. Using a load-generating tool named {ref}`Locust `, it loads the statistically most loaded pages, according to Google Analytics, that is 50% homepage and 50% some type of dataset page. Since dataset page weight also varies by the number of files, a selection of about 10 datasets with varying file counts is used. The pages are called randomly as a guest user with increasing levels of user load, from 1 user to 250 users. Typical daily loads in production are around the 50-user level. Though the simulated user level does have a modest amount of random think time before repeated calls, from 5-20 seconds, it is not a real-world load so direct comparisons to production are not reliable. Instead, we compare performance to prior versions of the product, and based on how that performed in production we have some idea whether this might be similar in performance or whether there is some undetected issue that appears under load, such as inefficient or too many DB queries per page. +## Release Cadence and Sprints -Once the performance has been tested and recorded in a [Google spreadsheet](https://docs.google.com/spreadsheets/d/1lwPlifvgu3-X_6xLwq6Zr6sCOervr1mV_InHIWjh5KA/edit?usp=sharing) for this proposed version, the release will be prepared and posted. +A release likely spans multiple two-week sprints. Each sprint represents the priorities for that time and is sized so that the team can reasonably complete most of the work on time. This is a goal to help with planning, it is not a strict requirement. Some issues from the previous sprint may remain and likely be included in the next sprint but occasionally may be deprioritized and deferred to another time. + +The decision to make a release can be based on the time since the last release, some important feature needed by the community or contractual deadline, or some other logical reason to package the work completed into a named release and posted to the releases section on GitHub. ## Making a Release diff --git a/doc/sphinx-guides/source/qa/performance-tests.md b/doc/sphinx-guides/source/qa/performance-tests.md index ad7972bd75e..3fab0386eb0 100644 --- a/doc/sphinx-guides/source/qa/performance-tests.md +++ b/doc/sphinx-guides/source/qa/performance-tests.md @@ -7,8 +7,16 @@ ## Introduction +The final testing activity before producing a release is performance testing. This could be done throughout the release cycle but since it is time-consuming it is done once near the end. Using a load-generating tool named {ref}`Locust `, it loads the statistically most loaded pages, according to Google Analytics, that is 50% homepage and 50% some type of dataset page. + +Since dataset page weight also varies by the number of files, a selection of about 10 datasets with varying file counts is used. The pages are called randomly as a guest user with increasing levels of user load, from 1 user to 250 users. Typical daily loads in production are around the 50-user level. Though the simulated user level does have a modest amount of random think time before repeated calls, from 5-20 seconds, it is not a real-world load so direct comparisons to production are not reliable. Instead, we compare performance to prior versions of the product, and based on how that performed in production we have some idea whether this might be similar in performance or whether there is some undetected issue that appears under load, such as inefficient or too many DB queries per page. + +## Testing Environment + To run performance tests, we have a performance test cluster on AWS that employs web, database, and Solr. The database contains a copy of production that is updated weekly on Sundays. To ensure the homepage content is consistent between test runs across releases, two scripts set the datasets that will appear on the homepage. There is a script on the web server in the default CentOS user dir and one on the database server in the default CentOS user dir. Run these scripts before conducting the tests. +Once the performance has been tested and recorded in a [Google spreadsheet](https://docs.google.com/spreadsheets/d/1lwPlifvgu3-X_6xLwq6Zr6sCOervr1mV_InHIWjh5KA/edit?usp=sharing) for this proposed version, the release will be prepared and posted. + ## Access Access to performance cluster instances requires ssh keys. The cluster itself is normally not running to reduce costs. To turn on the cluster, log on to the demo server and run the perfenv scripts from the centos default user dir. Access to the demo requires an ssh key, see Leonid. diff --git a/doc/sphinx-guides/source/qa/other-approaches.md b/doc/sphinx-guides/source/qa/qa-workflow.md similarity index 58% rename from doc/sphinx-guides/source/qa/other-approaches.md rename to doc/sphinx-guides/source/qa/qa-workflow.md index 2e2ef906191..78dcd1b6322 100644 --- a/doc/sphinx-guides/source/qa/other-approaches.md +++ b/doc/sphinx-guides/source/qa/qa-workflow.md @@ -1,24 +1,10 @@ -# Other Approaches to Deploying and Testing +# QA workflow for Pull Requests ```{contents} Contents: :local: :depth: 3 ``` -This workflow is fine for a single person testing a PR, one at a time. It would be awkward or impossible if there were multiple people wanting to test different PRs at the same time. If a developer is testing, they would likely just deploy to their dev environment. That might be ok, but is the env is fully configured enough to offer a real-world testing scenario? An alternative might be to spin an EC2 branch on AWS, potentially using sample data. This can take some time so another option might be to spin up a few, persistent AWS instances with sample data this way, one per tester, and just deploy new builds there when you want to test. You could even configure Jenkins projects for each if desired to maintain consistency in how they’re built. - -## Tips and Tricks - -- Start testing simply, with the most obvious test. You don’t need to know all your tests upfront. As you gain comfort and understanding of how it works, try more tests until you are done. If it is a complex feature, jot down your tests in an outline format, some beforehand as a guide, and some after as things occur to you. Save the doc in a testing folder (on Google Drive). This potentially will help with future testing. -- When in doubt, ask someone. If you are confused about how something is working, it may be something you have missed, or it could be a documentation issue, or it could be a bug! Talk to the code reviewer and the contributor/developer for their opinion and advice. -- Always tail the server.log file while testing. Open a terminal window to the test instance and `tail -F server.log`. This helps you get a real-time sense of what the server is doing when you act and makes it easier to identify any stack trace on failure. -- When overloaded, do the simple pull requests first to reduce the queue. It gives you a mental boost to complete something and reduces the perception of the amount of work still to be done. -- When testing a bug fix, try reproducing the bug on the demo before testing the fix, that way you know you are taking the correct steps to verify that the fix worked. -- When testing an optional feature that requires configuration, do a smoke test without the feature configured and then with it configured. That way you know that folks using the standard config are unaffected by the option if they choose not to configure it. -- Back up your DB before applying an irreversible DB update and you are using a persistent/reusable platform. Just in case it fails, and you need to carry on testing something else you can use the backup. - -## Workflow for Completing QA on a PR - 1. Assign the PR you are working on to yourself. 1. What does it do? @@ -98,24 +84,13 @@ This workflow is fine for a single person testing a PR, one at a time. It would 1. Merge PR - Click merge to include this PR into the common develop branch. + Click the "Merge pull request" button and be sure to use the "Create a merge commit" option to include this PR into the common develop branch. + + Some of the reasons why we encourage using option over Rebase or Squash are: + -Preserving commit hitory + -Clearer context and treaceability + -Easier collaboration, bug tracking and reverting 1. Delete merged branch - Just a housekeeping move if the PR is from IQSS. Click the delete branch button where the merge button had been. There is no deletion for outside contributions. - - -## Checklist for Completing QA on a PR - -1. Build the docs -1. Smoke test the pr -1. Test the new functionality -1. Regression test -1. Test any upgrade instructions - -## Checklist for QA on Release - -1. Review Consolidated Release Notes, in particular upgrade instructions. -1. Conduct performance testing and compare with the previous release. -1. Perform clean install and smoke test. -1. Potentially follow upgrade instructions. Though they have been performed incrementally for each PR, the sequence may need checking + Just a housekeeping move if the PR is from IQSS. Click the delete branch button where the merge button had been. There is no deletion for outside contributions. \ No newline at end of file diff --git a/doc/sphinx-guides/source/qa/manual-testing.md b/doc/sphinx-guides/source/qa/testing-approach.md similarity index 84% rename from doc/sphinx-guides/source/qa/manual-testing.md rename to doc/sphinx-guides/source/qa/testing-approach.md index 580e5153394..21039c10b1f 100644 --- a/doc/sphinx-guides/source/qa/manual-testing.md +++ b/doc/sphinx-guides/source/qa/testing-approach.md @@ -1,4 +1,4 @@ -# Manual Testing Approach +# Testing Approach ```{contents} Contents: :local: @@ -41,3 +41,10 @@ Think about risk. Is the feature or function part of a critical area such as per 1. Upload 3 different types of files: You can use a tabular file, 50by1000.dta, an image file, and a text file. 1. Publish the dataset. 1. Download a file. + + +## Alternative deployment and testing + +This workflow is fine for a single person testing a PR, one at a time. It would be awkward or impossible if there were multiple people wanting to test different PRs at the same time. If a developer is testing, they would likely just deploy to their dev environment. That might be ok, but is the env is fully configured enough to offer a real-world testing scenario? + +An alternative might be to spin an EC2 branch on AWS, potentially using sample data. This can take some time so another option might be to spin up a few, persistent AWS instances with sample data this way, one per tester, and just deploy new builds there when you want to test. You could even configure Jenkins projects for each if desired to maintain consistency in how they’re built. \ No newline at end of file From ff044632aff9c2b98aea01da934cfbf63476dc40 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 16 Jan 2024 11:32:17 -0500 Subject: [PATCH 200/689] add release note #9926 --- doc/release-notes/9926-list-role-assignments-permissions.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/release-notes/9926-list-role-assignments-permissions.md diff --git a/doc/release-notes/9926-list-role-assignments-permissions.md b/doc/release-notes/9926-list-role-assignments-permissions.md new file mode 100644 index 00000000000..43cd83dc5c9 --- /dev/null +++ b/doc/release-notes/9926-list-role-assignments-permissions.md @@ -0,0 +1 @@ +Listing collction/dataverse role assignments via API still requires ManageDataversePermissions, but listing dataset role assignments via API now requires only ManageDatasetPermissions. From 30e357bcfba66a2c7c2044beb4f03d88e532b96a Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 16 Jan 2024 12:37:10 -0500 Subject: [PATCH 201/689] expect noSetHierarchy rather than noRecordsMatch #9275 --- .../java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java index 45dd0c08226..ac28e7a3605 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java @@ -888,7 +888,7 @@ public void testNoSuchSetError() { noSuchSet.prettyPrint(); noSuchSet.then().assertThat() .statusCode(OK.getStatusCode()) - .body("oai.error.@code", equalTo("noRecordsMatch")) + .body("oai.error.@code", equalTo("noSetHierarchy")) .body("oai.error", equalTo("Requested set 'census' does not exist")); } From dc08219cc6f7a2b1152c0acfe67b26844daa5abe Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 16 Jan 2024 12:46:32 -0500 Subject: [PATCH 202/689] Changes after talking to Phil at 12:00 on Jan 16 --- doc/sphinx-guides/source/qa/index.md | 1 - doc/sphinx-guides/source/qa/jenkins.md | 59 ------------------- doc/sphinx-guides/source/qa/overview.md | 8 ++- doc/sphinx-guides/source/qa/qa-workflow.md | 5 +- .../source/qa/test-automation.md | 58 ++++++++++++++++-- .../source/qa/testing-approach.md | 2 +- 6 files changed, 65 insertions(+), 68 deletions(-) delete mode 100644 doc/sphinx-guides/source/qa/jenkins.md diff --git a/doc/sphinx-guides/source/qa/index.md b/doc/sphinx-guides/source/qa/index.md index c7582a2169f..937b352bccb 100644 --- a/doc/sphinx-guides/source/qa/index.md +++ b/doc/sphinx-guides/source/qa/index.md @@ -6,6 +6,5 @@ testing-infrastructure.md qa-workflow.md testing-approach.md test-automation.md -jenkins.md performance-tests.md ``` diff --git a/doc/sphinx-guides/source/qa/jenkins.md b/doc/sphinx-guides/source/qa/jenkins.md deleted file mode 100644 index 9259284beb9..00000000000 --- a/doc/sphinx-guides/source/qa/jenkins.md +++ /dev/null @@ -1,59 +0,0 @@ -# Jenkins - -```{contents} Contents: -:local: -:depth: 3 -``` - -## Introduction - -Jenkins is our primary tool for knowing if our API tests are passing. (Unit tests are executed locally by developers.) - -You can find our Jenkins installation at . - -Please note that while it has been open to the public in the past, it is currently firewalled off. We can poke a hole in the firewall for your IP address if necessary. Please get in touch. (You might also be interested in which is about restoring the ability of contributors to see if their pull requests are passing API tests or not.) - -## Jobs - -Jenkins is organized into jobs. We'll highlight a few. - -### IQSS-dataverse-develop - -, which we will refer to as the "develop" job runs after pull requests are merged. It is crucial that this job stays green (passing) because we always want to stay in a "release ready" state. If you notice that this job is failing, make noise about it! - -You can get to this job from the README at . - -### IQSS-Dataverse-Develop-PR - - can be thought of as "PR jobs". It's a collection of jobs run on pull requests. Typically, you will navigate directly into the job (and it's particular build number) from a pull request. For example, from , look for a check called "continuous-integration/jenkins/pr-merge". Clicking it will bring you to a particular build like (build #10). - -### guides.dataverse.org - - is what we use to build guides. See {doc}`/developers/making-releases` in the Developer Guide. - -## Checking if API Tests are Passing - -If API tests are failing, you should not merge the pull request. - -How can you know if API tests are passing? Here are the steps, by way of example. - -- From the pull request, navigate to the build. For example from , look for a check called "continuous-integration/jenkins/pr-merge". Clicking it will bring you to a particular build like (build #10). -- You are now on the new "blue" interface for Jenkins. Click the button with an arrow on the right side of the header called "go to classic" which should take you to (for example) . -- Click "Test Result". -- Under "All Tests", look at the duration for "edu.harvard.iq.dataverse.api". It should be ten minutes or higher. If it was only a few seconds, tests did not run. -- Assuming tests ran, if there were failures, they should appear at the top under "All Failed Tests". Inform the author of the pull request about the error. - -## Diagnosing Failures - -API test failures can have multiple causes. As described above, from the "Test Result" page, you might see the failure under "All Failed Tests". However, the test could have failed because of some underlying system issue. - -If you have determined that the API tests have not run at all, your next step should be to click on "Console Output". For example, . Click "Full log" to see the full log in the browser or navigate to (for example) to get a plain text version. - -Go to the end of the log and then scroll up, looking for the failure. A failed Ansible task can look like this: - -``` -TASK [dataverse : download payara zip] ***************************************** -fatal: [localhost]: FAILED! => {"changed": false, "dest": "/tmp/payara.zip", "elapsed": 10, "msg": "Request failed: ", "url": "https://nexus.payara.fish/repository/payara-community/fish/payara/distributions/payara/6.2023.8/payara-6.2023.8.zip"} -``` - -In the example above, if Payara can't be downloaded, we're obviously going to have problems deploying Dataverse to it! diff --git a/doc/sphinx-guides/source/qa/overview.md b/doc/sphinx-guides/source/qa/overview.md index 08740e9345d..01ab629db8c 100644 --- a/doc/sphinx-guides/source/qa/overview.md +++ b/doc/sphinx-guides/source/qa/overview.md @@ -33,6 +33,12 @@ A release likely spans multiple two-week sprints. Each sprint represents the pri The decision to make a release can be based on the time since the last release, some important feature needed by the community or contractual deadline, or some other logical reason to package the work completed into a named release and posted to the releases section on GitHub. +## Test API + +The API test suite is added to and maintained by development. (See {doc}`/developers/testing` in the Developer Guide.) It is generally advisable for code contributors to add API tests when adding new functionality. The approach here is one of code coverage: exercise as much of the code base's code paths as possible, every time to catch bugs. + +This type of approach is often used to give contributing developers confidence that their code didn’t introduce any obvious, major issues and is run on each commit. Since it is a broad set of tests, it is not clear whether any specific, conceivable test is run but it does add a lot of confidence that the code base is functioning due to its reach and consistency. (See {doc}`/qa/test-automation` in the Developer Guide.) + ## Making a Release -See {doc}`/developers/making-releases` in the Developer Guide. +See {doc}`/developers/making-releases` in the Developer Guide. \ No newline at end of file diff --git a/doc/sphinx-guides/source/qa/qa-workflow.md b/doc/sphinx-guides/source/qa/qa-workflow.md index 78dcd1b6322..df274d2405d 100644 --- a/doc/sphinx-guides/source/qa/qa-workflow.md +++ b/doc/sphinx-guides/source/qa/qa-workflow.md @@ -1,4 +1,4 @@ -# QA workflow for Pull Requests +# QA Workflow for Pull Requests ```{contents} Contents: :local: @@ -87,7 +87,8 @@ Click the "Merge pull request" button and be sure to use the "Create a merge commit" option to include this PR into the common develop branch. Some of the reasons why we encourage using option over Rebase or Squash are: - -Preserving commit hitory + + -Preserving commit history -Clearer context and treaceability -Easier collaboration, bug tracking and reverting diff --git a/doc/sphinx-guides/source/qa/test-automation.md b/doc/sphinx-guides/source/qa/test-automation.md index c2b649df498..c996b4cea8f 100644 --- a/doc/sphinx-guides/source/qa/test-automation.md +++ b/doc/sphinx-guides/source/qa/test-automation.md @@ -1,15 +1,36 @@ # Test Automation - ```{contents} Contents: :local: :depth: 3 ``` -The API test suite is added to and maintained by development. (See {doc}`/developers/testing` in the Developer Guide.) It is generally advisable for code contributors to add API tests when adding new functionality. The approach here is one of code coverage: exercise as much of the code base's code paths as possible, every time to catch bugs. +## Introduction + +Jenkins is our primary tool for knowing if our API tests are passing. (Unit tests are executed locally by developers.) + +You can find our Jenkins installation at . + +Please note that while it has been open to the public in the past, it is currently firewalled off. We can poke a hole in the firewall for your IP address if necessary. Please get in touch. (You might also be interested in which is about restoring the ability of contributors to see if their pull requests are passing API tests or not.) + +## Jobs + +Jenkins is organized into jobs. We'll highlight a few. + +### IQSS-dataverse-develop -This type of approach is often used to give contributing developers confidence that their code didn’t introduce any obvious, major issues and is run on each commit. Since it is a broad set of tests, it is not clear whether any specific, conceivable test is run but it does add a lot of confidence that the code base is functioning due to its reach and consistency. +, which we will refer to as the "develop" job runs after pull requests are merged. It is crucial that this job stays green (passing) because we always want to stay in a "release ready" state. If you notice that this job is failing, make noise about it! -## Building and Deploying a Pull Request from Jenkins to Dataverse-Internal +You can get to this job from the README at . + +### IQSS-Dataverse-Develop-PR + + can be thought of as "PR jobs". It's a collection of jobs run on pull requests. Typically, you will navigate directly into the job (and it's particular build number) from a pull request. For example, from , look for a check called "continuous-integration/jenkins/pr-merge". Clicking it will bring you to a particular build like (build #10). + +### guides.dataverse.org + + is what we use to build guides. See {doc}`/developers/making-releases` in the Developer Guide. + +### Building and Deploying a Pull Request from Jenkins to Dataverse-Internal 1. Log on to GitHub, go to projects, dataverse to see Kanban board, select a pull request to test from the QA queue. @@ -34,3 +55,32 @@ This type of approach is often used to give contributing developers confidence t 1. If that didn't work, you may have run into a Flyway DB script collision error but that should be indicated by the server.log. See {doc}`/developers/sql-upgrade-scripts` in the Developer Guide. 1. Assuming the above steps worked, and they should 99% of the time, test away! Note: be sure to `tail -F server.log` in a terminal window while you are doing any testing. This way you can spot problems that may not appear in the UI and have easier access to any stack traces for easier reporting. + + + +## Checking if API Tests are Passing + +If API tests are failing, you should not merge the pull request. + +How can you know if API tests are passing? Here are the steps, by way of example. + +- From the pull request, navigate to the build. For example from , look for a check called "continuous-integration/jenkins/pr-merge". Clicking it will bring you to a particular build like (build #10). +- You are now on the new "blue" interface for Jenkins. Click the button with an arrow on the right side of the header called "go to classic" which should take you to (for example) . +- Click "Test Result". +- Under "All Tests", look at the duration for "edu.harvard.iq.dataverse.api". It should be ten minutes or higher. If it was only a few seconds, tests did not run. +- Assuming tests ran, if there were failures, they should appear at the top under "All Failed Tests". Inform the author of the pull request about the error. + +## Diagnosing Failures + +API test failures can have multiple causes. As described above, from the "Test Result" page, you might see the failure under "All Failed Tests". However, the test could have failed because of some underlying system issue. + +If you have determined that the API tests have not run at all, your next step should be to click on "Console Output". For example, . Click "Full log" to see the full log in the browser or navigate to (for example) to get a plain text version. + +Go to the end of the log and then scroll up, looking for the failure. A failed Ansible task can look like this: + +``` +TASK [dataverse : download payara zip] ***************************************** +fatal: [localhost]: FAILED! => {"changed": false, "dest": "/tmp/payara.zip", "elapsed": 10, "msg": "Request failed: ", "url": "https://nexus.payara.fish/repository/payara-community/fish/payara/distributions/payara/6.2023.8/payara-6.2023.8.zip"} +``` + +In the example above, if Payara can't be downloaded, we're obviously going to have problems deploying Dataverse to it! diff --git a/doc/sphinx-guides/source/qa/testing-approach.md b/doc/sphinx-guides/source/qa/testing-approach.md index 21039c10b1f..2c7241999a8 100644 --- a/doc/sphinx-guides/source/qa/testing-approach.md +++ b/doc/sphinx-guides/source/qa/testing-approach.md @@ -43,7 +43,7 @@ Think about risk. Is the feature or function part of a critical area such as per 1. Download a file. -## Alternative deployment and testing +## Alternative Deployment and Testing This workflow is fine for a single person testing a PR, one at a time. It would be awkward or impossible if there were multiple people wanting to test different PRs at the same time. If a developer is testing, they would likely just deploy to their dev environment. That might be ok, but is the env is fully configured enough to offer a real-world testing scenario? From 95cc8cbffb79f8f91ba2e9137c2b3106e4c1f6b5 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 16 Jan 2024 14:57:15 -0500 Subject: [PATCH 203/689] remove assertion about census not existing (doesn't appear) #9275 --- .../java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java index ac28e7a3605..60e4f623992 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java @@ -888,8 +888,7 @@ public void testNoSuchSetError() { noSuchSet.prettyPrint(); noSuchSet.then().assertThat() .statusCode(OK.getStatusCode()) - .body("oai.error.@code", equalTo("noSetHierarchy")) - .body("oai.error", equalTo("Requested set 'census' does not exist")); + .body("oai.error.@code", equalTo("noSetHierarchy")); } // TODO: From edd6fc861f899b7ddb07c51fb5d900dbd0096a6c Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 16 Jan 2024 16:15:42 -0500 Subject: [PATCH 204/689] drop "no such set test" #9275 --- .../edu/harvard/iq/dataverse/api/HarvestingServerIT.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java index 60e4f623992..e77853d6495 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java @@ -882,15 +882,6 @@ public void testInvalidQueryParams() { } - @Test - public void testNoSuchSetError() { - Response noSuchSet = given().get("/oai?verb=ListIdentifiers&set=census&metadataPrefix=dc"); - noSuchSet.prettyPrint(); - noSuchSet.then().assertThat() - .statusCode(OK.getStatusCode()) - .body("oai.error.@code", equalTo("noSetHierarchy")); - } - // TODO: // What else can we test? // Some ideas: From 2adbabb31e9206eb1518048a66f98e5853502707 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 17 Jan 2024 12:24:04 +0000 Subject: [PATCH 205/689] Added: typeClass field to DatasetFieldType payload --- doc/release-notes/10216-metadatablocks.md | 5 +++-- .../java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java | 1 + .../java/edu/harvard/iq/dataverse/api/MetadataBlocksIT.java | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/release-notes/10216-metadatablocks.md b/doc/release-notes/10216-metadatablocks.md index b3be7e76abc..59d9c1640a5 100644 --- a/doc/release-notes/10216-metadatablocks.md +++ b/doc/release-notes/10216-metadatablocks.md @@ -1,4 +1,5 @@ The API endpoint `/api/metadatablocks/{block_id}` has been extended to include the following fields: -- `isRequired` - Whether or not this field is required -- `displayOrder`: The display order of the field in create/edit forms +- `isRequired`: Whether or not this field is required +- `displayOrder`: The display order of the field in create/edit forms +- `typeClass`: The type class of this field ("controlledVocabulary", "compound", or "primitive") 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 a97ef9c12d1..2eaf6b64579 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 @@ -565,6 +565,7 @@ public static JsonObjectBuilder json(DatasetFieldType fld) { fieldsBld.add("displayName", fld.getDisplayName()); fieldsBld.add("title", fld.getTitle()); fieldsBld.add("type", fld.getFieldType().toString()); + fieldsBld.add("typeClass", typeClassString(fld)); fieldsBld.add("watermark", fld.getWatermark()); fieldsBld.add("description", fld.getDescription()); fieldsBld.add("multiple", fld.isAllowMultiples()); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/MetadataBlocksIT.java b/src/test/java/edu/harvard/iq/dataverse/api/MetadataBlocksIT.java index f1c3a9815f1..39152bccad8 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/MetadataBlocksIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/MetadataBlocksIT.java @@ -27,6 +27,7 @@ void testGetCitationBlock() { .statusCode(OK.getStatusCode()) .body("data.fields.subject.controlledVocabularyValues[0]", CoreMatchers.is("Agricultural Sciences")) .body("data.fields.title.displayOrder", CoreMatchers.is(0)) + .body("data.fields.title.typeClass", CoreMatchers.is("primitive")) .body("data.fields.title.isRequired", CoreMatchers.is(true)); } From ebe95fdb2d81321e9de2d9e3fd3c41aacb474447 Mon Sep 17 00:00:00 2001 From: Katie Mika Date: Wed, 17 Jan 2024 11:35:33 -0500 Subject: [PATCH 206/689] Update native-api.rst Added clarification to what is affected in Set Citation Data Field Type for a Dataset --- doc/sphinx-guides/source/api/native-api.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 09fc3c69693..dbe769e2fd1 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1572,8 +1572,8 @@ The fully expanded example above (without environment variables) looks like this Set Citation Date Field Type for a Dataset ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Sets the dataset citation date field type for a given dataset. ``:publicationDate`` is the default. -Note that the dataset citation date field type must be a date field. +Sets the dataset citation date field type for a given dataset. ``:publicationDate`` is the default. +Note that the dataset citation date field type must be a date field. This change applies to all versions of the dataset that have an entry for the new date field. It also applies to all file citations in the dataset. .. code-block:: bash From 598c40b8e5ccb2bb3db7a839e4549ac4d00ff8e1 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 17 Jan 2024 16:10:03 -0500 Subject: [PATCH 207/689] replace project 2 with 34 #9157 --- CONTRIBUTING.md | 2 +- doc/sphinx-guides/source/admin/integrations.rst | 2 +- doc/sphinx-guides/source/developers/documentation.rst | 2 +- doc/sphinx-guides/source/developers/version-control.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b2be8f531c4..44f8ae65135 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,7 +56,7 @@ If you are interested in working on the main Dataverse code, great! Before you s Please read http://guides.dataverse.org/en/latest/developers/version-control.html to understand how we use the "git flow" model of development and how we will encourage you to create a GitHub issue (if it doesn't exist already) to associate with your pull request. That page also includes tips on making a pull request. -After making your pull request, your goal should be to help it advance through our kanban board at https://github.com/orgs/IQSS/projects/2 . If no one has moved your pull request to the code review column in a timely manner, please reach out. Note that once a pull request is created for an issue, we'll remove the issue from the board so that we only track one card (the pull request). +After making your pull request, your goal should be to help it advance through our kanban board at https://github.com/orgs/IQSS/projects/34 . If no one has moved your pull request to the code review column in a timely manner, please reach out. Note that once a pull request is created for an issue, we'll remove the issue from the board so that we only track one card (the pull request). Thanks for your contribution! diff --git a/doc/sphinx-guides/source/admin/integrations.rst b/doc/sphinx-guides/source/admin/integrations.rst index db566106b49..cae44d42dbf 100644 --- a/doc/sphinx-guides/source/admin/integrations.rst +++ b/doc/sphinx-guides/source/admin/integrations.rst @@ -245,7 +245,7 @@ Future Integrations The `Dataverse Project Roadmap `_ is a good place to see integrations that the core Dataverse Project team is working on. -The `Community Dev `_ column of our project board is a good way to track integrations that are being worked on by the Dataverse Community but many are not listed and if you have an idea for an integration, please ask on the `dataverse-community `_ mailing list if someone is already working on it. +If you have an idea for an integration, please ask on the `dataverse-community `_ mailing list if someone is already working on it. Many integrations take the form of "external tools". See the :doc:`external-tools` section for details. External tool makers should check out the :doc:`/api/external-tools` section of the API Guide. diff --git a/doc/sphinx-guides/source/developers/documentation.rst b/doc/sphinx-guides/source/developers/documentation.rst index d07b5b63f72..4ec011f2b24 100755 --- a/doc/sphinx-guides/source/developers/documentation.rst +++ b/doc/sphinx-guides/source/developers/documentation.rst @@ -18,7 +18,7 @@ If you find a typo or a small error in the documentation you can fix it using Gi - Under the **Write** tab, delete the long welcome message and write a few words about what you fixed. - Click **Create Pull Request**. -That's it! Thank you for your contribution! Your pull request will be added manually to the main Dataverse Project board at https://github.com/orgs/IQSS/projects/2 and will go through code review and QA before it is merged into the "develop" branch. Along the way, developers might suggest changes or make them on your behalf. Once your pull request has been merged you will be listed as a contributor at https://github.com/IQSS/dataverse/graphs/contributors +That's it! Thank you for your contribution! Your pull request will be added manually to the main Dataverse Project board at https://github.com/orgs/IQSS/projects/34 and will go through code review and QA before it is merged into the "develop" branch. Along the way, developers might suggest changes or make them on your behalf. Once your pull request has been merged you will be listed as a contributor at https://github.com/IQSS/dataverse/graphs/contributors Please see https://github.com/IQSS/dataverse/pull/5857 for an example of a quick fix that was merged (the "Files changed" tab shows how a typo was fixed). diff --git a/doc/sphinx-guides/source/developers/version-control.rst b/doc/sphinx-guides/source/developers/version-control.rst index 12f3d5b81fd..c36c7d1e963 100644 --- a/doc/sphinx-guides/source/developers/version-control.rst +++ b/doc/sphinx-guides/source/developers/version-control.rst @@ -142,7 +142,7 @@ Feedback on the pull request template we use is welcome! Here's an example of a Make Sure Your Pull Request Has Been Advanced to Code Review ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Now that you've made your pull request, your goal is to make sure it appears in the "Code Review" column at https://github.com/orgs/IQSS/projects/2. +Now that you've made your pull request, your goal is to make sure it appears in the "Code Review" column at https://github.com/orgs/IQSS/projects/34. Look at https://github.com/IQSS/dataverse/blob/master/CONTRIBUTING.md for various ways to reach out to developers who have enough access to the GitHub repo to move your issue and pull request to the "Code Review" column. From 2593310b4746fa7022d62c6955db3e69b4d03471 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 17 Jan 2024 16:13:50 -0500 Subject: [PATCH 208/689] use "Community Backlog" as "dev efforts" #9157 --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 44f8ae65135..1430ba951a6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,4 +64,4 @@ Thanks for your contribution! [Community Call]: https://dataverse.org/community-calls [dataverse-dev Google Group]: https://groups.google.com/group/dataverse-dev [community contributors]: https://docs.google.com/spreadsheets/d/1o9DD-MQ0WkrYaEFTD5rF_NtyL8aUISgURsAXSL7Budk/edit?usp=sharing -[dev efforts]: https://github.com/orgs/IQSS/projects/2#column-5298405 +[dev efforts]: https://github.com/orgs/IQSS/projects/34/views/6 From 4f3a6ac3c038d920b7eb687a1eae6b7871e6eba8 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 18 Jan 2024 12:43:43 -0500 Subject: [PATCH 209/689] Add fix for SQL on guestbook service bean --- .../edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java index 01e6ecf7ff2..04f1ebf4bd0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java @@ -914,7 +914,7 @@ public void save(GuestbookResponse guestbookResponse) { public Long getDownloadCountByDataFileId(Long dataFileId) { // datafile id is null, will return 0 - Query query = em.createNativeQuery("select count(o.id) from GuestbookResponse o where o.datafile_id = " + dataFileId + "and eventtype != '" + GuestbookResponse.ACCESS_REQUEST +"'"); + Query query = em.createNativeQuery("select count(o.id) from GuestbookResponse o where o.datafile_id = " + dataFileId + " and eventtype != '" + GuestbookResponse.ACCESS_REQUEST +"'"); return (Long) query.getSingleResult(); } From eb6da705e1c2dcf4e657326a09646a47bec8cb88 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 18 Jan 2024 14:11:37 -0500 Subject: [PATCH 210/689] Add fix for same issue on another query reported by Jim Myers --- .../edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java index 04f1ebf4bd0..6c043b78941 100644 --- a/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/GuestbookResponseServiceBean.java @@ -432,7 +432,7 @@ public Long findCountByGuestbookId(Long guestbookId, Long dataverseId) { Query query = em.createNativeQuery(queryString); return (Long) query.getSingleResult(); } else { - String queryString = "select count(o) from GuestbookResponse as o, Dataset d, DvObject obj where o.dataset_id = d.id and d.id = obj.id and obj.owner_id = " + dataverseId + "and o.guestbook_id = " + guestbookId; + String queryString = "select count(o) from GuestbookResponse as o, Dataset d, DvObject obj where o.dataset_id = d.id and d.id = obj.id and obj.owner_id = " + dataverseId + " and o.guestbook_id = " + guestbookId; Query query = em.createNativeQuery(queryString); return (Long) query.getSingleResult(); } From 867b7dcc8244e0ea4396ef1ef0dcadec40ce6b2c Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Thu, 18 Jan 2024 14:58:14 -0500 Subject: [PATCH 211/689] a better test setup (#3322) --- .../harvard/iq/dataverse/api/HarvestingServerIT.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java index e0f121305e0..ed9cbdaaed0 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingServerIT.java @@ -299,8 +299,7 @@ public void testSetEditAPIandOAIlistSets() throws InterruptedException { // expected HTTP result codes. String setName = UtilIT.getRandomString(6); - String persistentId = extraDatasetsIdentifiers.get(0); - String setDef = "dsPersistentId:"+persistentId; + String setDefinition = "title:Sample"; // Make sure the set does not exist String setPath = String.format("/api/harvest/server/oaisets/%s", setName); @@ -313,20 +312,21 @@ public void testSetEditAPIandOAIlistSets() throws InterruptedException { // Create the set as admin user Response createSetResponse = given() .header(UtilIT.API_TOKEN_HTTP_HEADER, adminUserAPIKey) - .body(jsonForTestSpec(setName, setDef)) + .body(jsonForTestSpec(setName, setDefinition)) .post(createPath); assertEquals(201, createSetResponse.getStatusCode()); // I. Test the Modify/Edit (POST method) functionality of the // Dataverse OAI Sets API - String newDefinition = "title:New"; + String persistentId = extraDatasetsIdentifiers.get(0); + String newDefinition = "dsPersistentId:"+persistentId; String newDescription = "updated"; // API Test 1. Try to modify the set as normal user, should fail Response editSetResponse = given() .header(UtilIT.API_TOKEN_HTTP_HEADER, normalUserAPIKey) - .body(jsonForEditSpec(setName, setDef, "")) + .body(jsonForEditSpec(setName, newDefinition, "")) .put(setPath); logger.info("non-admin user editSetResponse.getStatusCode(): " + editSetResponse.getStatusCode()); assertEquals(400, editSetResponse.getStatusCode()); From 091629a6b9db2a3d1b879817a162b4309c040d15 Mon Sep 17 00:00:00 2001 From: "Balazs E. Pataki" Date: Fri, 19 Jan 2024 12:28:41 +0100 Subject: [PATCH 212/689] Add configuration for automatic XHTML/CSS/etc. reloading in IDEA in docker When running Dataverse in Docker we still want to be able to just edit things under src/main/webapp and then just reload the web page to see the changes. To do this: 1. Mapped Payara /opt/payara/appserver/glassfish/domains/domain1/applications folder to ./docker-dev-volumes/glassfish/applications 2. Added watchers.xml File watcher configuration, which can be imported into IDEA to ... 3. ... run cpwebapp.sh to copy changed files under src/main/webapp to ./docker-dev-volumes/glassfish/applications/dataverse-{current version} --- docker-compose-dev.yml | 2 ++ scripts/intellij/cpwebapp.sh | 33 +++++++++++++++++++++++++++++++++ scripts/intellij/watchers.xml | 22 ++++++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100755 scripts/intellij/cpwebapp.sh create mode 100644 scripts/intellij/watchers.xml diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 10fe62ff6df..76a4c8a745d 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -60,6 +60,8 @@ services: volumes: - ./docker-dev-volumes/app/data:/dv - ./docker-dev-volumes/app/secrets:/secrets + # Map the glassfish applications folder so that we can update webapp resources using scripts/intellij/cpwebapp.sh + - ./docker-dev-volumes/glassfish/applications:/opt/payara/appserver/glassfish/domains/domain1/applications # Uncomment for changes to xhtml to be deployed immediately (if supported your IDE or toolchain). # Replace 6.0 with the current version. # - ./target/dataverse-6.0:/opt/payara/deployments/dataverse diff --git a/scripts/intellij/cpwebapp.sh b/scripts/intellij/cpwebapp.sh new file mode 100755 index 00000000000..6ecad367048 --- /dev/null +++ b/scripts/intellij/cpwebapp.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# +# cpwebapp +# +# Usage: +# +# Add a File watcher by importing watchers.xml into IntelliJ IDEA, and let it do the copying whenever you save a +# file under webapp. +# +# https://www.jetbrains.com/help/idea/settings-tools-file-watchers.html +# +# Alternatively, you can add an External tool and trigger via menu or shortcut to do the copying manually: +# +# https://www.jetbrains.com/help/idea/configuring-third-party-tools.html +# + +PROJECT_DIR=$1 +FILE_TO_COPY=$2 +RELATIVE_PATH="${FILE_TO_COPY#$PROJECT_DIR/}" + +# Check if RELATIVE_PATH starts with 'src/main/webapp', otherwise ignore +if [[ $RELATIVE_PATH == src/main/webapp* ]]; then + # Get current version. Any other way to do this? A simple VERSION file would help. + VERSION=`perl -ne 'print $1 if /(.*?)<\/revision>/' ./modules/dataverse-parent/pom.xml` + RELATIVE_PATH_WITHOUT_WEBAPP="${RELATIVE_PATH#src/main/webapp/}" + TARGET_DIR=./docker-dev-volumes/glassfish/applications/dataverse-$VERSION + TARGET_PATH="${TARGET_DIR}/${RELATIVE_PATH_WITHOUT_WEBAPP}" + + mkdir -p "$(dirname "$TARGET_PATH")" + cp "$FILE_TO_COPY" "$TARGET_PATH" + + echo "File $FILE_TO_COPY copied to $TARGET_PATH" +fi diff --git a/scripts/intellij/watchers.xml b/scripts/intellij/watchers.xml new file mode 100644 index 00000000000..e118fea558f --- /dev/null +++ b/scripts/intellij/watchers.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file From cb08667a77a2ea2a51093c81e6048ee9b5b1ef30 Mon Sep 17 00:00:00 2001 From: Don Sizemore Date: Fri, 19 Jan 2024 15:10:17 -0500 Subject: [PATCH 213/689] #10249 correct typo in search API documentation --- doc/sphinx-guides/source/api/search.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/api/search.rst b/doc/sphinx-guides/source/api/search.rst index b941064f173..e8d0a0b3ea7 100755 --- a/doc/sphinx-guides/source/api/search.rst +++ b/doc/sphinx-guides/source/api/search.rst @@ -25,7 +25,7 @@ Parameters Name Type Description =============== ======= =========== q string The search term or terms. Using "title:data" will search only the "title" field. "*" can be used as a wildcard either alone or adjacent to a term (i.e. "bird*"). For example, https://demo.dataverse.org/api/search?q=title:data . For a list of fields to search, please see https://github.com/IQSS/dataverse/issues/2558 (for now). -type string Can be either "Dataverse", "dataset", or "file". Multiple "type" parameters can be used to include multiple types (i.e. ``type=dataset&type=file``). If omitted, all types will be returned. For example, https://demo.dataverse.org/api/search?q=*&type=dataset +type string Can be either "dataverse", "dataset", or "file". Multiple "type" parameters can be used to include multiple types (i.e. ``type=dataset&type=file``). If omitted, all types will be returned. For example, https://demo.dataverse.org/api/search?q=*&type=dataset subtree string The identifier of the Dataverse collection to which the search should be narrowed. The subtree of this Dataverse collection and all its children will be searched. Multiple "subtree" parameters can be used to include multiple Dataverse collections. For example, https://demo.dataverse.org/api/search?q=data&subtree=birds&subtree=cats . sort string The sort field. Supported values include "name" and "date". See example under "order". order string The order in which to sort. Can either be "asc" or "desc". For example, https://demo.dataverse.org/api/search?q=data&sort=name&order=asc From fc28b37a9bdc847f04f1988f922a1414b1c70527 Mon Sep 17 00:00:00 2001 From: Don Sizemore Date: Mon, 22 Jan 2024 13:17:38 -0500 Subject: [PATCH 214/689] bump google.library.version to 26.30.0 per Jim --- modules/dataverse-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/dataverse-parent/pom.xml b/modules/dataverse-parent/pom.xml index e2d1ceec539..386d4934cb1 100644 --- a/modules/dataverse-parent/pom.xml +++ b/modules/dataverse-parent/pom.xml @@ -152,7 +152,7 @@ 42.6.0 9.3.0 1.12.290 - 26.29.0 + 26.30.0 8.0.0 From f902a3ec75a6ca1d23d81f02585902b5873c1fbd Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 18 Jan 2024 15:55:32 -0500 Subject: [PATCH 215/689] add API endpoint to return file citation #10240 --- .../edu/harvard/iq/dataverse/api/Files.java | 18 +++++++++ .../edu/harvard/iq/dataverse/api/FilesIT.java | 40 +++++++++++++++++++ .../edu/harvard/iq/dataverse/api/UtilIT.java | 7 ++++ 3 files changed, 65 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index 5d400ee1438..f4282b794b1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -2,6 +2,7 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; +import edu.harvard.iq.dataverse.DataCitation; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.DataFileServiceBean; import edu.harvard.iq.dataverse.DataFileTag; @@ -931,4 +932,21 @@ public Response getHasBeenDeleted(@Context ContainerRequestContext crc, @PathPar return ok(dataFileServiceBean.hasBeenDeleted(dataFile)); }, getRequestUser(crc)); } + + @GET + @AuthRequired + @Path("{id}/citation") + public Response getFileCitation(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId) { + try { + DataverseRequest req = createDataverseRequest(getRequestUser(crc)); + final DataFile df = execCommand(new GetDataFileCommand(req, findDataFileOrDie(fileIdOrPersistentId))); + FileMetadata fm = df.getLatestFileMetadata(); + boolean direct = false; + DataCitation citation = new DataCitation(fm, direct); + return ok(citation.toString(true)); + } catch (WrappedResponse ex) { + return ex.getResponse(); + } + } + } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java index 915f82a6de2..853d92aac0e 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java @@ -33,6 +33,7 @@ import jakarta.json.JsonObjectBuilder; import static jakarta.ws.rs.core.Response.Status.*; +import java.time.Year; import org.hamcrest.CoreMatchers; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterAll; @@ -2483,4 +2484,43 @@ public void testCollectionStorageQuotas() { UtilIT.deleteSetting(SettingsServiceBean.Key.UseStorageQuotas); } + + @Test + public void getFileCitation() throws IOException { + Response createUser = UtilIT.createRandomUser(); + createUser.then().assertThat().statusCode(OK.getStatusCode()); + String apiToken = UtilIT.getApiTokenFromResponse(createUser); + + Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); + createDataverseResponse.then().assertThat().statusCode(CREATED.getStatusCode()); + String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + + Response createDatasetResponse = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, apiToken); + createDatasetResponse.prettyPrint(); + createDatasetResponse.then().assertThat().statusCode(CREATED.getStatusCode()); + Integer datasetId = JsonPath.from(createDatasetResponse.body().asString()).getInt("data.id"); + String datasetPid = JsonPath.from(createDatasetResponse.body().asString()).getString("data.persistentId"); + + Path pathToTxt = Paths.get(java.nio.file.Files.createTempDirectory(null) + File.separator + "file.txt"); + String contentOfTxt = "foobar"; + java.nio.file.Files.write(pathToTxt, contentOfTxt.getBytes()); + + Response uploadFileTxt = UtilIT.uploadFileViaNative(datasetId.toString(), pathToTxt.toString(), apiToken); + uploadFileTxt.prettyPrint(); + uploadFileTxt.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.files[0].label", equalTo("file.txt")); + + Integer fileId = JsonPath.from(uploadFileTxt.body().asString()).getInt("data.files[0].dataFile.id"); + + String pidAsUrl = "https://doi.org/" + datasetPid.split("doi:")[1]; + int currentYear = Year.now().getValue(); + + Response getFileCitation = UtilIT.getFileCitation(fileId, true, apiToken); + getFileCitation.prettyPrint(); + getFileCitation.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.message", equalTo("Finch, Fiona, " + currentYear + ", \"Darwin's Finches\",
" + pidAsUrl + ", Root, DRAFT VERSION; file.txt [fileName]")); + } + } 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 6af3f8a0a09..9b9f8ddff47 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -3459,6 +3459,13 @@ static Response getDatasetVersionCitation(Integer datasetId, String version, boo return response; } + static Response getFileCitation(Integer fileId, boolean getDraft, String apiToken) { + Response response = given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .get("/api/files/" + fileId + "/citation"); + return response; + } + static Response getVersionFiles(Integer datasetId, String version, Integer limit, From 85018f5182fbdd8f59dad75e9e9612ac7c657c54 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Mon, 22 Jan 2024 10:42:28 -0500 Subject: [PATCH 216/689] make assertings on draft vs published #10240 --- .../edu/harvard/iq/dataverse/api/FilesIT.java | 23 +++++++++++++++++++ .../edu/harvard/iq/dataverse/api/UtilIT.java | 9 ++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java index 853d92aac0e..49e6c5c4f22 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java @@ -2516,11 +2516,34 @@ public void getFileCitation() throws IOException { String pidAsUrl = "https://doi.org/" + datasetPid.split("doi:")[1]; int currentYear = Year.now().getValue(); + Response draftUnauthNoApitoken = UtilIT.getFileCitation(fileId, true, null); + draftUnauthNoApitoken.then().assertThat().statusCode(UNAUTHORIZED.getStatusCode()); + + Response createNoPermsUser = UtilIT.createRandomUser(); + createNoPermsUser.then().assertThat().statusCode(OK.getStatusCode()); + String noPermsApiToken = UtilIT.getApiTokenFromResponse(createNoPermsUser); + + Response draftUnauthNoPermsApiToken = UtilIT.getFileCitation(fileId, true, noPermsApiToken); + draftUnauthNoPermsApiToken.then().assertThat().statusCode(UNAUTHORIZED.getStatusCode()); + Response getFileCitation = UtilIT.getFileCitation(fileId, true, apiToken); getFileCitation.prettyPrint(); getFileCitation.then().assertThat() .statusCode(OK.getStatusCode()) .body("data.message", equalTo("Finch, Fiona, " + currentYear + ", \"Darwin's Finches\", " + pidAsUrl + ", Root, DRAFT VERSION; file.txt [fileName]")); + + Response publishDataverseResponse = UtilIT.publishDataverseViaNativeApi(dataverseAlias, apiToken); + publishDataverseResponse.then().assertThat().statusCode(OK.getStatusCode()); + + Response publishDatasetResponse = UtilIT.publishDatasetViaNativeApi(datasetId, "major", apiToken); + publishDatasetResponse.then().assertThat().statusCode(OK.getStatusCode()); + + Response publishedNoApiTokenNeeded = UtilIT.getFileCitation(fileId, true, null); + publishedNoApiTokenNeeded.then().assertThat().statusCode(OK.getStatusCode()); + + Response publishedNoPermsApiTokenAllowed = UtilIT.getFileCitation(fileId, true, noPermsApiToken); + publishedNoPermsApiTokenAllowed.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 9b9f8ddff47..946bc6d5c83 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -3460,10 +3460,11 @@ static Response getDatasetVersionCitation(Integer datasetId, String version, boo } static Response getFileCitation(Integer fileId, boolean getDraft, String apiToken) { - Response response = given() - .header(API_TOKEN_HTTP_HEADER, apiToken) - .get("/api/files/" + fileId + "/citation"); - return response; + var spec = given(); + if (apiToken != null) { + spec.header(API_TOKEN_HTTP_HEADER, apiToken); + } + return spec.get("/api/files/" + fileId + "/citation"); } static Response getVersionFiles(Integer datasetId, From f34f82be7281e05cf80cee461ed948187f0a537b Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Mon, 22 Jan 2024 16:20:43 -0500 Subject: [PATCH 217/689] handle versions for "get data file citation" API #10240 --- .../edu/harvard/iq/dataverse/api/Files.java | 48 +++++++++++++++++-- .../edu/harvard/iq/dataverse/api/FilesIT.java | 45 ++++++++++++++--- .../edu/harvard/iq/dataverse/api/UtilIT.java | 4 +- 3 files changed, 85 insertions(+), 12 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index f4282b794b1..c30503199e0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -19,6 +19,7 @@ import edu.harvard.iq.dataverse.GuestbookResponseServiceBean; import edu.harvard.iq.dataverse.TermsOfUseAndAccessValidator; import edu.harvard.iq.dataverse.UserNotificationServiceBean; +import static edu.harvard.iq.dataverse.api.Datasets.handleVersion; import edu.harvard.iq.dataverse.api.auth.AuthRequired; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.ApiToken; @@ -28,11 +29,16 @@ import edu.harvard.iq.dataverse.datasetutility.DataFileTagException; import edu.harvard.iq.dataverse.datasetutility.NoFilesException; import edu.harvard.iq.dataverse.datasetutility.OptionalFileParams; +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.impl.GetDataFileCommand; +import edu.harvard.iq.dataverse.engine.command.impl.GetDraftDatasetVersionCommand; import edu.harvard.iq.dataverse.engine.command.impl.GetDraftFileMetadataIfAvailableCommand; +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.RedetectFileTypeCommand; import edu.harvard.iq.dataverse.engine.command.impl.RestrictFileCommand; import edu.harvard.iq.dataverse.engine.command.impl.UningestFileCommand; @@ -933,14 +939,50 @@ public Response getHasBeenDeleted(@Context ContainerRequestContext crc, @PathPar }, getRequestUser(crc)); } + /** + * @param fileIdOrPersistentId Database ID or PID of the data file. + * @param dsVersionString The version of the dataset, such as 1.0, :draft, + * :latest-published, etc. + */ @GET @AuthRequired - @Path("{id}/citation") - public Response getFileCitation(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId) { + @Path("{id}/versions/{dsVersionString}/citation") + public Response getFileCitationByVersion(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @PathParam("dsVersionString") String dsVersionString) { try { DataverseRequest req = createDataverseRequest(getRequestUser(crc)); final DataFile df = execCommand(new GetDataFileCommand(req, findDataFileOrDie(fileIdOrPersistentId))); - FileMetadata fm = df.getLatestFileMetadata(); + Dataset ds = df.getOwner(); + // Adapted from getDatasetVersionOrDie + // includeDeaccessioned and checkPermsWhenDeaccessioned were removed + // because they aren't needed. + DatasetVersion dsv = execCommand(handleVersion(dsVersionString, new Datasets.DsVersionHandler>() { + + @Override + public Command handleLatest() { + return new GetLatestAccessibleDatasetVersionCommand(req, ds); + } + + @Override + public Command handleDraft() { + return new GetDraftDatasetVersionCommand(req, ds); + } + + @Override + public Command handleSpecific(long major, long minor) { + return new GetSpecificPublishedDatasetVersionCommand(req, ds, major, minor); + } + + @Override + public Command handleLatestPublished() { + return new GetLatestPublishedDatasetVersionCommand(req, ds); + } + })); + + Long getDatasetVersionID = dsv.getId(); + FileMetadata fm = dataFileServiceBean.findFileMetadataByDatasetVersionIdAndDataFileId(getDatasetVersionID, df.getId()); + if (fm == null) { + return notFound("File could not be found."); + } boolean direct = false; DataCitation citation = new DataCitation(fm, direct); return ok(citation.toString(true)); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java index 49e6c5c4f22..4bc7456e7e7 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java @@ -17,6 +17,7 @@ import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.SystemConfig; +import edu.harvard.iq.dataverse.util.json.JsonUtil; import java.io.File; import java.io.IOException; @@ -2501,6 +2502,13 @@ public void getFileCitation() throws IOException { Integer datasetId = JsonPath.from(createDatasetResponse.body().asString()).getInt("data.id"); String datasetPid = JsonPath.from(createDatasetResponse.body().asString()).getString("data.persistentId"); + Response getDatasetVersionCitationResponse = UtilIT.getDatasetVersionCitation(datasetId, DS_VERSION_DRAFT, false, apiToken); + getDatasetVersionCitationResponse.prettyPrint(); + getDatasetVersionCitationResponse.then().assertThat() + .statusCode(OK.getStatusCode()) + // We check that the returned message contains information expected for the citation string + .body("data.message", containsString("DRAFT VERSION")); + Path pathToTxt = Paths.get(java.nio.file.Files.createTempDirectory(null) + File.separator + "file.txt"); String contentOfTxt = "foobar"; java.nio.file.Files.write(pathToTxt, contentOfTxt.getBytes()); @@ -2516,19 +2524,21 @@ public void getFileCitation() throws IOException { String pidAsUrl = "https://doi.org/" + datasetPid.split("doi:")[1]; int currentYear = Year.now().getValue(); - Response draftUnauthNoApitoken = UtilIT.getFileCitation(fileId, true, null); + Response draftUnauthNoApitoken = UtilIT.getFileCitation(fileId, DS_VERSION_DRAFT, null); + draftUnauthNoApitoken.prettyPrint(); draftUnauthNoApitoken.then().assertThat().statusCode(UNAUTHORIZED.getStatusCode()); Response createNoPermsUser = UtilIT.createRandomUser(); createNoPermsUser.then().assertThat().statusCode(OK.getStatusCode()); String noPermsApiToken = UtilIT.getApiTokenFromResponse(createNoPermsUser); - Response draftUnauthNoPermsApiToken = UtilIT.getFileCitation(fileId, true, noPermsApiToken); + Response draftUnauthNoPermsApiToken = UtilIT.getFileCitation(fileId, DS_VERSION_DRAFT, noPermsApiToken); + draftUnauthNoPermsApiToken.prettyPrint(); draftUnauthNoPermsApiToken.then().assertThat().statusCode(UNAUTHORIZED.getStatusCode()); - Response getFileCitation = UtilIT.getFileCitation(fileId, true, apiToken); - getFileCitation.prettyPrint(); - getFileCitation.then().assertThat() + Response getFileCitationDraft = UtilIT.getFileCitation(fileId, DS_VERSION_DRAFT, apiToken); + getFileCitationDraft.prettyPrint(); + getFileCitationDraft.then().assertThat() .statusCode(OK.getStatusCode()) .body("data.message", equalTo("Finch, Fiona, " + currentYear + ", \"Darwin's Finches\", " + pidAsUrl + ", Root, DRAFT VERSION; file.txt [fileName]")); @@ -2538,12 +2548,33 @@ public void getFileCitation() throws IOException { Response publishDatasetResponse = UtilIT.publishDatasetViaNativeApi(datasetId, "major", apiToken); publishDatasetResponse.then().assertThat().statusCode(OK.getStatusCode()); - Response publishedNoApiTokenNeeded = UtilIT.getFileCitation(fileId, true, null); + Response publishedNoApiTokenNeeded = UtilIT.getFileCitation(fileId, "1.0", null); publishedNoApiTokenNeeded.then().assertThat().statusCode(OK.getStatusCode()); - Response publishedNoPermsApiTokenAllowed = UtilIT.getFileCitation(fileId, true, noPermsApiToken); + Response publishedNoPermsApiTokenAllowed = UtilIT.getFileCitation(fileId, "1.0", noPermsApiToken); publishedNoPermsApiTokenAllowed.then().assertThat().statusCode(OK.getStatusCode()); + String updateJsonString = """ +{ + "label": "foo.txt" +} +"""; + + Response updateMetadataResponse = UtilIT.updateFileMetadata(fileId.toString(), updateJsonString, apiToken); + updateMetadataResponse.prettyPrint(); + assertEquals(OK.getStatusCode(), updateMetadataResponse.getStatusCode()); + + Response getFileCitationPostV1Draft = UtilIT.getFileCitation(fileId, DS_VERSION_DRAFT, apiToken); + getFileCitationPostV1Draft.prettyPrint(); + getFileCitationPostV1Draft.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.message", equalTo("Finch, Fiona, " + currentYear + ", \"Darwin's Finches\", " + pidAsUrl + ", Root, DRAFT VERSION; foo.txt [fileName]")); + + Response getFileCitationV1Filename = UtilIT.getFileCitation(fileId, "1.0", apiToken); + getFileCitationV1Filename.prettyPrint(); + getFileCitationV1Filename.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.message", equalTo("Finch, Fiona, " + currentYear + ", \"Darwin's Finches\", " + pidAsUrl + ", Root, V1; file.txt [fileName]")); } } 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 946bc6d5c83..520d68428a3 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -3459,12 +3459,12 @@ static Response getDatasetVersionCitation(Integer datasetId, String version, boo return response; } - static Response getFileCitation(Integer fileId, boolean getDraft, String apiToken) { + static Response getFileCitation(Integer fileId, String datasetVersion, String apiToken) { var spec = given(); if (apiToken != null) { spec.header(API_TOKEN_HTTP_HEADER, apiToken); } - return spec.get("/api/files/" + fileId + "/citation"); + return spec.get("/api/files/" + fileId + "/versions/" + datasetVersion + "/citation"); } static Response getVersionFiles(Integer datasetId, From a28e15a9316cb1f4d726ddd0afee6cd817324c3b Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Tue, 23 Jan 2024 10:22:55 -0500 Subject: [PATCH 218/689] #9686 display harvesting client info on cards of harvested objects --- .../iq/dataverse/DatasetServiceBean.java | 48 ------------------- .../iq/dataverse/DvObjectServiceBean.java | 48 +++++++++++++++++++ .../search/SearchIncludeFragment.java | 41 ++++++++++------ .../harvard/iq/dataverse/api/DatasetsIT.java | 2 + 4 files changed, 76 insertions(+), 63 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index c6df2a2e1ab..4c4aafdd1ec 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -583,54 +583,6 @@ public Long getDatasetVersionCardImage(Long versionId, User user) { return null; } - /** - * Used to identify and properly display Harvested objects on the dataverse page. - * - * @param datasetIds - * @return - */ - public Map getArchiveDescriptionsForHarvestedDatasets(Set datasetIds){ - if (datasetIds == null || datasetIds.size() < 1) { - return null; - } - - String datasetIdStr = StringUtils.join(datasetIds, ", "); - - String qstr = "SELECT d.id, h.archiveDescription FROM harvestingClient h, dataset d WHERE d.harvestingClient_id = h.id AND d.id IN (" + datasetIdStr + ")"; - List searchResults; - - try { - searchResults = em.createNativeQuery(qstr).getResultList(); - } catch (Exception ex) { - searchResults = null; - } - - if (searchResults == null) { - return null; - } - - Map ret = new HashMap<>(); - - for (Object[] result : searchResults) { - Long dsId; - if (result[0] != null) { - try { - dsId = (Long)result[0]; - } catch (Exception ex) { - dsId = null; - } - if (dsId == null) { - continue; - } - - ret.put(dsId, (String)result[1]); - } - } - - return ret; - } - - public boolean isDatasetCardImageAvailable(DatasetVersion datasetVersion, User user) { if (datasetVersion == null) { diff --git a/src/main/java/edu/harvard/iq/dataverse/DvObjectServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DvObjectServiceBean.java index d4219c36149..58a246b364a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DvObjectServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DvObjectServiceBean.java @@ -383,6 +383,54 @@ public Map getObjectPathsByIds(Set objectIds){ return ret; } + /** + * Used to identify and properly display Harvested objects on the dataverse page. + * + * @param dvObjectIds + * @return + */ + public Map getArchiveDescriptionsForHarvestedDvObjects(Set dvObjectIds){ + + if (dvObjectIds == null || dvObjectIds.size() < 1) { + return null; + } + + String dvObjectIsString = StringUtils.join(dvObjectIds, ", "); + String qstr = "SELECT d.id, h.archiveDescription FROM harvestingClient h, DvObject d WHERE d.harvestingClient_id = h.id AND d.id IN (" + dvObjectIsString + ")"; + List searchResults; + + try { + searchResults = em.createNativeQuery(qstr).getResultList(); + } catch (Exception ex) { + searchResults = null; + } + + if (searchResults == null) { + return null; + } + + Map ret = new HashMap<>(); + + for (Object[] result : searchResults) { + Long dvObjId; + if (result[0] != null) { + try { + Integer castResult = (Integer) result[0]; + dvObjId = Long.valueOf(castResult); + } catch (Exception ex) { + dvObjId = null; + } + if (dvObjId == null) { + continue; + } + ret.put(dvObjId, (String)result[1]); + } + } + + return ret; + } + + public String generateNewIdentifierByStoredProcedure() { StoredProcedureQuery query = this.em.createNamedStoredProcedureQuery("Dataset.generateIdentifierFromStoredProcedure"); query.execute(); diff --git a/src/main/java/edu/harvard/iq/dataverse/search/SearchIncludeFragment.java b/src/main/java/edu/harvard/iq/dataverse/search/SearchIncludeFragment.java index 5a5d8781726..939b39b94ef 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/SearchIncludeFragment.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/SearchIncludeFragment.java @@ -1367,6 +1367,7 @@ public boolean canPublishDataset(Long datasetId){ public void setDisplayCardValues() { Set harvestedDatasetIds = null; + Set harvestedFileIds = null; for (SolrSearchResult result : searchResultsList) { //logger.info("checking DisplayImage for the search result " + i++); if (result.getType().equals("dataverses")) { @@ -1392,10 +1393,10 @@ public void setDisplayCardValues() { } else if (result.getType().equals("files")) { result.setImageUrl(thumbnailServiceWrapper.getFileCardImageAsBase64Url(result)); if (result.isHarvested()) { - if (harvestedDatasetIds == null) { - harvestedDatasetIds = new HashSet<>(); + if (harvestedFileIds == null) { + harvestedFileIds = new HashSet<>(); } - harvestedDatasetIds.add(result.getParentIdAsLong()); + harvestedFileIds.add(result.getEntityId()); } } } @@ -1407,25 +1408,35 @@ public void setDisplayCardValues() { // SQL query: if (harvestedDatasetIds != null) { - Map descriptionsForHarvestedDatasets = datasetService.getArchiveDescriptionsForHarvestedDatasets(harvestedDatasetIds); - if (descriptionsForHarvestedDatasets != null && descriptionsForHarvestedDatasets.size() > 0) { + Map descriptionsForHarvestedDatasets = dvObjectService.getArchiveDescriptionsForHarvestedDvObjects(harvestedDatasetIds); + if (descriptionsForHarvestedDatasets != null && !descriptionsForHarvestedDatasets.isEmpty()) { for (SolrSearchResult result : searchResultsList) { - if (result.isHarvested()) { - if (result.getType().equals("files")) { - if (descriptionsForHarvestedDatasets.containsKey(result.getParentIdAsLong())) { - result.setHarvestingDescription(descriptionsForHarvestedDatasets.get(result.getParentIdAsLong())); - } - } else if (result.getType().equals("datasets")) { - if (descriptionsForHarvestedDatasets.containsKey(result.getEntityId())) { - result.setHarvestingDescription(descriptionsForHarvestedDatasets.get(result.getEntityId())); - } - } + if (result.isHarvested() && result.getType().equals("datasets") && descriptionsForHarvestedDatasets.containsKey(result.getEntityId())) { + result.setHarvestingDescription(descriptionsForHarvestedDatasets.get(result.getEntityId())); } } } descriptionsForHarvestedDatasets = null; harvestedDatasetIds = null; } + + if (harvestedFileIds != null) { + + Map descriptionsForHarvestedFiles = dvObjectService.getArchiveDescriptionsForHarvestedDvObjects(harvestedFileIds); + if (descriptionsForHarvestedFiles != null && !descriptionsForHarvestedFiles.isEmpty()) { + for (SolrSearchResult result : searchResultsList) { + if (result.isHarvested() && result.getType().equals("files") && descriptionsForHarvestedFiles.containsKey(result.getEntityId())) { + + result.setHarvestingDescription(descriptionsForHarvestedFiles.get(result.getEntityId())); + + } + } + } + descriptionsForHarvestedFiles = null; + harvestedDatasetIds = null; + + } + // determine which of the objects are linked: diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index 9b51be4b365..087db4858b2 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -2548,6 +2548,8 @@ public void testLinkingDatasets() { EntityManager entityManager = entityManagerFactory.createEntityManager(); entityManager.getTransaction().begin(); // Do stuff... + //SEK 01/22/2024 - as of 6.2 harvestingclient_id will be on the dv object table + // so if this is ever implemented change will probably need to happen in the updatequery below entityManager.createNativeQuery("UPDATE dataset SET harvestingclient_id=1 WHERE id="+datasetId2).executeUpdate(); entityManager.getTransaction().commit(); entityManager.close(); From 88bae3bb295c26e7eda57d1ad5fbb34b67788542 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Tue, 23 Jan 2024 10:59:46 -0500 Subject: [PATCH 219/689] #9686 fix script names --- ...emetadata.sql => V6.1.0.1__9728-universe-variablemetadata.sql} | 0 ...gclient-id.sql => V6.1.0.2__9686-move-harvestingclient-id.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V5.13.0.3__9728-universe-variablemetadata.sql => V6.1.0.1__9728-universe-variablemetadata.sql} (100%) rename src/main/resources/db/migration/{V6.1.0.1__9686-move-harvestingclient-id.sql => V6.1.0.2__9686-move-harvestingclient-id.sql} (100%) diff --git a/src/main/resources/db/migration/V5.13.0.3__9728-universe-variablemetadata.sql b/src/main/resources/db/migration/V6.1.0.1__9728-universe-variablemetadata.sql similarity index 100% rename from src/main/resources/db/migration/V5.13.0.3__9728-universe-variablemetadata.sql rename to src/main/resources/db/migration/V6.1.0.1__9728-universe-variablemetadata.sql diff --git a/src/main/resources/db/migration/V6.1.0.1__9686-move-harvestingclient-id.sql b/src/main/resources/db/migration/V6.1.0.2__9686-move-harvestingclient-id.sql similarity index 100% rename from src/main/resources/db/migration/V6.1.0.1__9686-move-harvestingclient-id.sql rename to src/main/resources/db/migration/V6.1.0.2__9686-move-harvestingclient-id.sql From 7d27a9b64736780314ed3a203990d701db2ab399 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Tue, 23 Jan 2024 11:17:50 -0500 Subject: [PATCH 220/689] #10255 fix script name --- ...emetadata.sql => V6.1.0.1__9728-universe-variablemetadata.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V5.13.0.3__9728-universe-variablemetadata.sql => V6.1.0.1__9728-universe-variablemetadata.sql} (100%) diff --git a/src/main/resources/db/migration/V5.13.0.3__9728-universe-variablemetadata.sql b/src/main/resources/db/migration/V6.1.0.1__9728-universe-variablemetadata.sql similarity index 100% rename from src/main/resources/db/migration/V5.13.0.3__9728-universe-variablemetadata.sql rename to src/main/resources/db/migration/V6.1.0.1__9728-universe-variablemetadata.sql From c999ac7721e4202a36790c23ab8acadf95b6ba8c Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 23 Jan 2024 11:28:04 -0500 Subject: [PATCH 221/689] handle deaccessioned versions #10240 --- .../edu/harvard/iq/dataverse/api/Files.java | 11 ++++-- .../edu/harvard/iq/dataverse/api/FilesIT.java | 34 ++++++++++++++++--- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index c30503199e0..ed331e6835d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -953,10 +953,11 @@ public Response getFileCitationByVersion(@Context ContainerRequestContext crc, @ final DataFile df = execCommand(new GetDataFileCommand(req, findDataFileOrDie(fileIdOrPersistentId))); Dataset ds = df.getOwner(); // Adapted from getDatasetVersionOrDie - // includeDeaccessioned and checkPermsWhenDeaccessioned were removed - // because they aren't needed. DatasetVersion dsv = execCommand(handleVersion(dsVersionString, new Datasets.DsVersionHandler>() { + boolean includeDeaccessioned = true; + boolean checkPermsWhenDeaccessioned = true; + @Override public Command handleLatest() { return new GetLatestAccessibleDatasetVersionCommand(req, ds); @@ -969,7 +970,7 @@ public Command handleDraft() { @Override public Command handleSpecific(long major, long minor) { - return new GetSpecificPublishedDatasetVersionCommand(req, ds, major, minor); + return new GetSpecificPublishedDatasetVersionCommand(req, ds, major, minor, includeDeaccessioned, checkPermsWhenDeaccessioned); } @Override @@ -978,6 +979,10 @@ public Command handleLatestPublished() { } })); + if (dsv == null) { + return unauthorized("Dataset version cannot be found or unauthorized."); + } + Long getDatasetVersionID = dsv.getId(); FileMetadata fm = dataFileServiceBean.findFileMetadataByDatasetVersionIdAndDataFileId(getDatasetVersionID, df.getId()); if (fm == null) { diff --git a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java index 4bc7456e7e7..1e8a806faa2 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java @@ -2487,7 +2487,7 @@ public void testCollectionStorageQuotas() { } @Test - public void getFileCitation() throws IOException { + public void testFileCitation() throws IOException { Response createUser = UtilIT.createRandomUser(); createUser.then().assertThat().statusCode(OK.getStatusCode()); String apiToken = UtilIT.getApiTokenFromResponse(createUser); @@ -2570,11 +2570,37 @@ public void getFileCitation() throws IOException { .statusCode(OK.getStatusCode()) .body("data.message", equalTo("Finch, Fiona, " + currentYear + ", \"Darwin's Finches\", " + pidAsUrl + ", Root, DRAFT VERSION; foo.txt [fileName]")); - Response getFileCitationV1Filename = UtilIT.getFileCitation(fileId, "1.0", apiToken); - getFileCitationV1Filename.prettyPrint(); - getFileCitationV1Filename.then().assertThat() + Response getFileCitationV1OldFilename = UtilIT.getFileCitation(fileId, "1.0", apiToken); + getFileCitationV1OldFilename.prettyPrint(); + getFileCitationV1OldFilename.then().assertThat() .statusCode(OK.getStatusCode()) .body("data.message", equalTo("Finch, Fiona, " + currentYear + ", \"Darwin's Finches\", " + pidAsUrl + ", Root, V1; file.txt [fileName]")); + + UtilIT.publishDatasetViaNativeApi(datasetId, "major", apiToken) + .then().assertThat().statusCode(OK.getStatusCode()); + + Response deaccessionDataset = UtilIT.deaccessionDataset(datasetId, "1.0", "just because", "http://example.com", apiToken); + deaccessionDataset.prettyPrint(); + deaccessionDataset.then().assertThat().statusCode(OK.getStatusCode()); + + Response getFileCitationV1PostDeaccessionAuthor = UtilIT.getFileCitation(fileId, "1.0", apiToken); + getFileCitationV1PostDeaccessionAuthor.prettyPrint(); + getFileCitationV1PostDeaccessionAuthor.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.message", equalTo("Finch, Fiona, " + currentYear + ", \"Darwin's Finches\", " + pidAsUrl + ", Root, V1, DEACCESSIONED VERSION; file.txt [fileName]")); + + Response getFileCitationV1PostDeaccessionNoApiToken = UtilIT.getFileCitation(fileId, "1.0", null); + getFileCitationV1PostDeaccessionNoApiToken.prettyPrint(); + getFileCitationV1PostDeaccessionNoApiToken.then().assertThat() + .statusCode(UNAUTHORIZED.getStatusCode()) + .body("message", equalTo("Dataset version cannot be found or unauthorized.")); + + Response getFileCitationV1PostDeaccessionNoPermsUser = UtilIT.getFileCitation(fileId, "1.0", noPermsApiToken); + getFileCitationV1PostDeaccessionNoPermsUser.prettyPrint(); + getFileCitationV1PostDeaccessionNoPermsUser.then().assertThat() + .statusCode(UNAUTHORIZED.getStatusCode()) + .body("message", equalTo("Dataset version cannot be found or unauthorized.")); + } } From 89b7f277ccddfc849611d7e08c16fcd3b2af3dcc Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 23 Jan 2024 13:46:16 -0500 Subject: [PATCH 222/689] Fix the issue with the thumbnail size --- src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java | 2 +- src/main/webapp/resources/css/structure.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java index ccf861ebdc8..03a0044a987 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java @@ -464,7 +464,7 @@ public static InputStream getLogoAsInputStream(Dataset dataset) { try { in = ImageThumbConverter.getImageThumbnailAsInputStream(thumbnailFile.getStorageIO(), - ImageThumbConverter.DEFAULT_CARDIMAGE_SIZE).getInputStream(); + ImageThumbConverter.DEFAULT_DATASETLOGO_SIZE).getInputStream(); } catch (IOException ioex) { logger.warning("getLogo(): Failed to get logo from DataFile for " + dataset.getStorageIdentifier() + " (" + ioex.getMessage() + ")"); diff --git a/src/main/webapp/resources/css/structure.css b/src/main/webapp/resources/css/structure.css index 470c07d4534..b81cf2a2c47 100644 --- a/src/main/webapp/resources/css/structure.css +++ b/src/main/webapp/resources/css/structure.css @@ -483,7 +483,7 @@ span.search-term-match {font-weight: bold;} [id$='resultsTable'] div.card-title-icon-block span.label {vertical-align:15%} [id$='resultsTable'] div.card-preview-icon-block {width:48px; float:left; margin:4px 12px 6px 0;} [id$='resultsTable'] div.card-preview-icon-block a {display:block; height:48px; line-height:48px;} -[id$='resultsTable'] div.card-preview-icon-block img {vertical-align:middle;} +[id$='resultsTable'] div.card-preview-icon-block img {vertical-align:middle; max-width: 64px; max-height: 48px; padding-right: 10px;} [id$='resultsTable'] div.card-preview-icon-block span[class^='icon'], [id$='resultsTable'] div.card-preview-icon-block span[class^='glyphicon'] {font-size:2.8em;} From d40ecfd420bc34df884a6b4e820946f0f457c6be Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 23 Jan 2024 13:57:09 -0500 Subject: [PATCH 223/689] add docs for "get file citation" API #10240 --- doc/sphinx-guides/source/api/native-api.rst | 53 ++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index dbe769e2fd1..f161dd67ca9 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -845,7 +845,12 @@ Datasets **Note** Creation of new datasets is done with a ``POST`` onto a Dataverse collection. See the Dataverse Collections section above. -**Note** In all commands below, dataset versions can be referred to as: +.. _dataset-version-specifiers: + +Dataset Version Specifiers +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In all commands below, dataset versions can be referred to as: * ``:draft`` the draft version, if any * ``:latest`` either a draft (if exists) or the latest published version. @@ -2712,6 +2717,8 @@ The fully expanded example above (without environment variables) looks like this Files ----- +.. _get-json-rep-of-file: + Get JSON Representation of a File ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -3499,6 +3506,50 @@ The fully expanded example above (without environment variables) looks like this You can download :download:`dct.xml <../../../../src/test/resources/xml/dct.xml>` from the example above to see what the XML looks like. +Get File Citation as JSON +~~~~~~~~~~~~~~~~~~~~~~~~~ + +This API is for getting the file citation as it appears on the file landing page. It is formatted in HTML and encoded in JSON. + +To specify the version, you can use ``:latest-published`` or ``:draft`` or ``1.0`` or any other style listed under :ref:`dataset-version-specifiers`. + +When the dataset version is published, authentication is not required: + +.. code-block:: bash + + export SERVER_URL=https://demo.dataverse.org + export FILE_ID=42 + export DATASET_VERSION=":latest-published" + + curl "$SERVER_URL/api/files/$FILE_ID/versions/$DATASET_VERSION/citation" + +The fully expanded example above (without environment variables) looks like this: + +.. code-block:: bash + + curl "https://demo.dataverse.org/api/files/42/versions/:latest-published/citation" + +When the dataset version is a draft or deaccessioned, authentication is required: + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export SERVER_URL=https://demo.dataverse.org + export FILE_ID=42 + export DATASET_VERSION=":draft" + + curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/files/$FILE_ID/versions/$DATASET_VERSION/citation" + +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/files/42/versions/:draft/citation + +If your file has a persistent identifier (PID, such as a DOI), you can pass it using the technique described under :ref:`get-json-rep-of-file`. + +This API is not for downloading various citation formats such as EndNote XML, RIS, or BibTeX. This functionality has been requested in https://github.com/IQSS/dataverse/issues/3140 and https://github.com/IQSS/dataverse/issues/9994. + Provenance ~~~~~~~~~~ From 521afc50d807aacb39a74166c303d61fe5f64b2d Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 23 Jan 2024 13:59:32 -0500 Subject: [PATCH 224/689] add release note for "get file citation" API #10240 --- doc/release-notes/10240-file-citation.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 doc/release-notes/10240-file-citation.md diff --git a/doc/release-notes/10240-file-citation.md b/doc/release-notes/10240-file-citation.md new file mode 100644 index 00000000000..fb747527669 --- /dev/null +++ b/doc/release-notes/10240-file-citation.md @@ -0,0 +1,5 @@ +## Get file citation as JSON + +It is now possible to retrieve via API the file citation as it appears on the file landing page. It is formatted in HTML and encoded in JSON. + +This API is not for downloading various citation formats such as EndNote XML, RIS, or BibTeX. This functionality has been requested in https://github.com/IQSS/dataverse/issues/3140 and https://github.com/IQSS/dataverse/issues/9994 From 59690d4c9a2b5686e3b38f07c634fb32323400ff Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 24 Jan 2024 09:55:46 -0500 Subject: [PATCH 225/689] emphasize need to check flyway number before merging #10101 --- .../source/developers/sql-upgrade-scripts.rst | 2 ++ doc/sphinx-guides/source/qa/qa-workflow.md | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/sphinx-guides/source/developers/sql-upgrade-scripts.rst b/doc/sphinx-guides/source/developers/sql-upgrade-scripts.rst index bace682b1b8..4689aeec0f2 100644 --- a/doc/sphinx-guides/source/developers/sql-upgrade-scripts.rst +++ b/doc/sphinx-guides/source/developers/sql-upgrade-scripts.rst @@ -21,6 +21,8 @@ If you are creating a new database table (which maps to an ``@Entity`` in JPA), If you are doing anything other than creating a new database table such as adding a column to an existing table, you must create or update a SQL upgrade script. +.. _create-sql-script: + How to Create a SQL Upgrade Script ---------------------------------- diff --git a/doc/sphinx-guides/source/qa/qa-workflow.md b/doc/sphinx-guides/source/qa/qa-workflow.md index df274d2405d..cb047a3086a 100644 --- a/doc/sphinx-guides/source/qa/qa-workflow.md +++ b/doc/sphinx-guides/source/qa/qa-workflow.md @@ -27,9 +27,11 @@ Same as for doc, just a heads up to an admin for something of note or especially upgrade instructions as needed. -1. Does it use a DB, Flyway script? +1. Does it include a database migration script (Flyway)? - Good to know since it may collide with another existing one by version or it could be a one way transform of your DB so back up your test DB before. Also, happens during deployment so be on the lookout for any issues. + First, check the numbering in the filename of the script. It must be in line with the rules defined at {ref}`create-sql-script`. If the number is out of date (very common for older pull requests), do not merge and ask the developer to rename the script. Otherwise, deployment will fail. + + Once you're sure the numbering is ok (the next available number, basically), back up your database and proceeed with testing. 1. Validate the documentation. @@ -94,4 +96,4 @@ 1. Delete merged branch - Just a housekeeping move if the PR is from IQSS. Click the delete branch button where the merge button had been. There is no deletion for outside contributions. \ No newline at end of file + Just a housekeeping move if the PR is from IQSS. Click the delete branch button where the merge button had been. There is no deletion for outside contributions. From 5292682d6724e1b24cb4001768ce82d97d8dc771 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 24 Jan 2024 12:05:09 -0500 Subject: [PATCH 226/689] fix for #10251 - sync terms popup required code --- .../harvard/iq/dataverse/util/FileUtil.java | 30 +++---------------- 1 file changed, 4 insertions(+), 26 deletions(-) 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 776d04e98cc..8decf74fe13 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java @@ -1199,34 +1199,12 @@ public static boolean isGuestbookPopupRequired(DatasetVersion datasetVersion) { } public static boolean isTermsPopupRequired(DatasetVersion datasetVersion) { - - if (datasetVersion == null) { - logger.fine("TermsPopup not required because datasetVersion is null."); - return false; - } - //0. if version is draft then Popup "not required" - if (!datasetVersion.isReleased()) { - logger.fine("TermsPopup not required because datasetVersion has not been released."); + Boolean answer = popupDueToStateOrTerms(datasetVersion); + if(answer == null) { + logger.fine("TermsPopup is not required."); return false; } - // 1. License and Terms of Use: - if (datasetVersion.getTermsOfUseAndAccess() != null) { - if (!License.CC0.equals(datasetVersion.getTermsOfUseAndAccess().getLicense()) - && !(datasetVersion.getTermsOfUseAndAccess().getTermsOfUse() == null - || datasetVersion.getTermsOfUseAndAccess().getTermsOfUse().equals(""))) { - logger.fine("TermsPopup required because of license or terms of use."); - return true; - } - - // 2. Terms of Access: - if (!(datasetVersion.getTermsOfUseAndAccess().getTermsOfAccess() == null) && !datasetVersion.getTermsOfUseAndAccess().getTermsOfAccess().equals("")) { - logger.fine("TermsPopup required because of terms of access."); - return true; - } - } - - logger.fine("TermsPopup is not required."); - return false; + return answer; } /** From 51984163525453b7360dd0b89db8746b8d55c031 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 24 Jan 2024 13:04:33 -0500 Subject: [PATCH 227/689] fix null issue found in #10251 --- .../java/edu/harvard/iq/dataverse/FileDownloadServiceBean.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/FileDownloadServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/FileDownloadServiceBean.java index ca3f5b4bded..de3f4d2ab56 100644 --- a/src/main/java/edu/harvard/iq/dataverse/FileDownloadServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/FileDownloadServiceBean.java @@ -316,7 +316,7 @@ private void redirectToDownloadAPI(String downloadType, Long fileId, boolean gue Long fileMetadataId) { String fileDownloadUrl = FileUtil.getFileDownloadUrlPath(downloadType, fileId, guestBookRecordAlreadyWritten, fileMetadataId); - if (downloadType.equals("GlobusTransfer")) { + if ("GlobusTransfer".equals(downloadType)) { PrimeFaces.current().executeScript(URLTokenUtil.getScriptForUrl(fileDownloadUrl)); } else { logger.fine("Redirecting to file download url: " + fileDownloadUrl); From 96f2c95a26f6bf9d153a0b95f6cea7bdac7bd4ea Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 24 Jan 2024 14:40:12 -0500 Subject: [PATCH 228/689] minor tweaks #10101 --- .../source/developers/making-releases.rst | 2 ++ doc/sphinx-guides/source/qa/overview.md | 12 +++---- .../source/qa/performance-tests.md | 6 ++-- doc/sphinx-guides/source/qa/qa-workflow.md | 14 ++++---- .../source/qa/test-automation.md | 35 ++++++++++--------- .../source/qa/testing-approach.md | 14 ++++---- .../source/qa/testing-infrastructure.md | 4 +-- 7 files changed, 45 insertions(+), 42 deletions(-) diff --git a/doc/sphinx-guides/source/developers/making-releases.rst b/doc/sphinx-guides/source/developers/making-releases.rst index 6b94282d55e..18ae34ee656 100755 --- a/doc/sphinx-guides/source/developers/making-releases.rst +++ b/doc/sphinx-guides/source/developers/making-releases.rst @@ -83,6 +83,8 @@ To test these images against our API test suite, go to the "alpha" workflow at h If there are failures, additional dependencies or settings may have been added to the "develop" workflow. Copy them over and try again. +.. _build-guides: + Build the Guides for the Release -------------------------------- diff --git a/doc/sphinx-guides/source/qa/overview.md b/doc/sphinx-guides/source/qa/overview.md index 01ab629db8c..f8eb7b19297 100644 --- a/doc/sphinx-guides/source/qa/overview.md +++ b/doc/sphinx-guides/source/qa/overview.md @@ -15,17 +15,17 @@ The basic workflow is as follows. Bugs or feature requests are submitted to GitH Before a pull request is moved to QA, it must be reviewed by a member of the development team from a coding perspective, and it must pass automated tests. There it is tested manually, exercising the UI (using three common browsers) and any business logic it implements. -Depending on whether the code modifies existing code or is completely new, a smoke test of core functionality is performed and some basic regression testing of modified or related code is performed. Any documentation provided is used to understand the feature and any assertions made in that documentation are tested. Once this passes and any bugs that are found are corrected, and the automated tests are confirmed to be passing, the PR is merged into the develop, the PR is closed, and the branch is deleted (if it is local). At this point, the PR moves from the QA column automatically into the Done column and the process repeats with the next PR until it is decided to {doc}`make a release `. +Depending on whether the code modifies existing code or is completely new, a smoke test of core functionality is performed and some basic regression testing of modified or related code is performed. Any documentation provided is used to understand the feature and any assertions made in that documentation are tested. Once this passes and any bugs that are found are corrected, and the automated tests are confirmed to be passing, the PR is merged into the develop branch, the PR is closed, and the branch is deleted (if it is local). At this point, the PR moves from the QA column automatically into the Merged column (where it might be discussed at the next standup) and the process repeats with the next PR until it is decided to {doc}`make a release `. ## Tips and Tricks - Start testing simply, with the most obvious test. You don’t need to know all your tests upfront. As you gain comfort and understanding of how it works, try more tests until you are done. If it is a complex feature, jot down your tests in an outline format, some beforehand as a guide, and some after as things occur to you. Save the doc in a testing folder (on Google Drive). This potentially will help with future testing. - When in doubt, ask someone. If you are confused about how something is working, it may be something you have missed, or it could be a documentation issue, or it could be a bug! Talk to the code reviewer and the contributor/developer for their opinion and advice. -- Always tail the server.log file while testing. Open a terminal window to the test instance and `tail -F server.log`. This helps you get a real-time sense of what the server is doing when you act and makes it easier to identify any stack trace on failure. -- When overloaded, do the simple pull requests first to reduce the queue. It gives you a mental boost to complete something and reduces the perception of the amount of work still to be done. -- When testing a bug fix, try reproducing the bug on the demo before testing the fix, that way you know you are taking the correct steps to verify that the fix worked. +- Always tail the server.log file while testing. Open a terminal window to the test instance and `tail -F server.log`. This helps you get a real-time sense of what the server is doing when you interact with the application and makes it easier to identify any stack trace on failure. +- When overloaded, QA the simple pull requests first to reduce the queue. It gives you a mental boost to complete something and reduces the perception of the amount of work still to be done. +- When testing a bug fix, try reproducing the bug on the demo server before testing the fix. That way you know you are taking the correct steps to verify that the fix worked. - When testing an optional feature that requires configuration, do a smoke test without the feature configured and then with it configured. That way you know that folks using the standard config are unaffected by the option if they choose not to configure it. -- Back up your DB before applying an irreversible DB update and you are using a persistent/reusable platform. Just in case it fails, and you need to carry on testing something else you can use the backup. +- Back up your DB before applying an irreversible DB update when you are using a persistent/reusable platform. Just in case it fails, and you need to carry on testing something else you can use the backup. ## Release Cadence and Sprints @@ -41,4 +41,4 @@ This type of approach is often used to give contributing developers confidence t ## Making a Release -See {doc}`/developers/making-releases` in the Developer Guide. \ No newline at end of file +See {doc}`/developers/making-releases` in the Developer Guide. diff --git a/doc/sphinx-guides/source/qa/performance-tests.md b/doc/sphinx-guides/source/qa/performance-tests.md index 3fab0386eb0..404188735a2 100644 --- a/doc/sphinx-guides/source/qa/performance-tests.md +++ b/doc/sphinx-guides/source/qa/performance-tests.md @@ -7,7 +7,7 @@ ## Introduction -The final testing activity before producing a release is performance testing. This could be done throughout the release cycle but since it is time-consuming it is done once near the end. Using a load-generating tool named {ref}`Locust `, it loads the statistically most loaded pages, according to Google Analytics, that is 50% homepage and 50% some type of dataset page. +The final testing activity before producing a release is performance testing. This could be done throughout the release cycle but since it is time-consuming, it is done once near the end. Using a load-generating tool named {ref}`Locust `, our scripts load the statistically most-loaded pages (according to Google Analytics): 50% homepage and 50% some type of dataset page. Since dataset page weight also varies by the number of files, a selection of about 10 datasets with varying file counts is used. The pages are called randomly as a guest user with increasing levels of user load, from 1 user to 250 users. Typical daily loads in production are around the 50-user level. Though the simulated user level does have a modest amount of random think time before repeated calls, from 5-20 seconds, it is not a real-world load so direct comparisons to production are not reliable. Instead, we compare performance to prior versions of the product, and based on how that performed in production we have some idea whether this might be similar in performance or whether there is some undetected issue that appears under load, such as inefficient or too many DB queries per page. @@ -19,11 +19,11 @@ Once the performance has been tested and recorded in a [Google spreadsheet](http ## Access -Access to performance cluster instances requires ssh keys. The cluster itself is normally not running to reduce costs. To turn on the cluster, log on to the demo server and run the perfenv scripts from the centos default user dir. Access to the demo requires an ssh key, see Leonid. +Access to performance cluster instances requires ssh keys. The cluster itself is normally not running to reduce costs. To turn on the cluster, log on to the demo server and run the perfenv scripts from the centos default user dir. ## Special Notes ⚠️ -Please note the performance database is also used occasionally by Julian and the Curation team to generate prod reports so a courtesy check with Julian would be good before taking over the env. +Please note the performance database is also used occasionally by members of the Curation team to generate prod reports so a courtesy check with them would be good before taking over the env. Executing the Performance Script diff --git a/doc/sphinx-guides/source/qa/qa-workflow.md b/doc/sphinx-guides/source/qa/qa-workflow.md index cb047a3086a..3db17ecb8a4 100644 --- a/doc/sphinx-guides/source/qa/qa-workflow.md +++ b/doc/sphinx-guides/source/qa/qa-workflow.md @@ -23,9 +23,9 @@ Small changes or fixes usually don’t have docs but new features or extensions of a feature or new configuration options should have documentation. -1. Does it have or need release notes? +1. Does it have or need a release note snippet? - Same as for doc, just a heads up to an admin for something of note or especially upgrade instructions as needed. + Same as for doc, just a heads up to an admin for something of note or especially upgrade instructions as needed. See also {ref}`writing-release-note-snippets` for what to expect in a release note snippet. 1. Does it include a database migration script (Flyway)? @@ -35,7 +35,7 @@ 1. Validate the documentation. - Build the doc using Jenkins, does it build without errors? + Build the doc using Jenkins or read the automated Read the Docs preview. Does it build without errors? Read it through for sense. Use it for test cases and to understand the feature. @@ -88,11 +88,11 @@ Click the "Merge pull request" button and be sure to use the "Create a merge commit" option to include this PR into the common develop branch. - Some of the reasons why we encourage using option over Rebase or Squash are: + Some of the reasons why we encourage using this option over Rebase or Squash are: - -Preserving commit history - -Clearer context and treaceability - -Easier collaboration, bug tracking and reverting + - Preservation of commit history + - Clearer context and treaceability + - Easier collaboration, bug tracking and reverting 1. Delete merged branch diff --git a/doc/sphinx-guides/source/qa/test-automation.md b/doc/sphinx-guides/source/qa/test-automation.md index c996b4cea8f..e4b3b12ec43 100644 --- a/doc/sphinx-guides/source/qa/test-automation.md +++ b/doc/sphinx-guides/source/qa/test-automation.md @@ -4,7 +4,7 @@ :depth: 3 ``` -## Introduction +## Jenkins Jenkins is our primary tool for knowing if our API tests are passing. (Unit tests are executed locally by developers.) @@ -12,28 +12,27 @@ You can find our Jenkins installation at . Please note that while it has been open to the public in the past, it is currently firewalled off. We can poke a hole in the firewall for your IP address if necessary. Please get in touch. (You might also be interested in which is about restoring the ability of contributors to see if their pull requests are passing API tests or not.) -## Jobs +### Jenkins Jobs Jenkins is organized into jobs. We'll highlight a few. -### IQSS-dataverse-develop +#### IQSS-dataverse-develop -, which we will refer to as the "develop" job runs after pull requests are merged. It is crucial that this job stays green (passing) because we always want to stay in a "release ready" state. If you notice that this job is failing, make noise about it! +, which we will refer to as the "develop" job, runs after pull requests are merged. It is crucial that this job stays green (passing) because we always want to stay in a "release ready" state. If you notice that this job is failing, make noise about it! -You can get to this job from the README at . +You can access this job from the README at . -### IQSS-Dataverse-Develop-PR +#### IQSS-Dataverse-Develop-PR can be thought of as "PR jobs". It's a collection of jobs run on pull requests. Typically, you will navigate directly into the job (and it's particular build number) from a pull request. For example, from , look for a check called "continuous-integration/jenkins/pr-merge". Clicking it will bring you to a particular build like (build #10). -### guides.dataverse.org +#### guides.dataverse.org - is what we use to build guides. See {doc}`/developers/making-releases` in the Developer Guide. + is what we use to build guides. See {ref}`build-guides` in the Developer Guide for how this job is used at release time. -### Building and Deploying a Pull Request from Jenkins to Dataverse-Internal +#### Building and Deploying a Pull Request from Jenkins to Dataverse-Internal - -1. Log on to GitHub, go to projects, dataverse to see Kanban board, select a pull request to test from the QA queue. +1. Go to the QA column on our [project board](https://github.com/orgs/IQSS/projects/34), and select a pull request to test. 1. From the pull request page, click the copy icon next to the pull request branch name. @@ -50,15 +49,13 @@ You can get to this job from the README at . 1. Once complete, go to and check that the deployment succeeded, and that the homepage displays the latest build number. -1. If for some reason it didn’t deploy, check the server.log file. It may just be a caching issue so try un-deploying, deleting cache, restarting, and re-deploying on the server (`su - dataverse` then `/usr/local/payara5/bin/asadmin list-applications; /usr/local/payara5/bin/asadmin undeploy dataverse-5.11.1; /usr/local/payara5/bin/asadmin deploy /tmp/dataverse-5.11.1.war`) +1. If for some reason it didn't deploy, check the server.log file. It may just be a caching issue so try un-deploying, deleting cache, restarting, and re-deploying on the server (`su - dataverse` then `/usr/local/payara6/bin/asadmin list-applications; /usr/local/payara6/bin/asadmin undeploy dataverse-6.1; /usr/local/payara6/bin/asadmin deploy /tmp/dataverse-6.1.war`) -1. If that didn't work, you may have run into a Flyway DB script collision error but that should be indicated by the server.log. See {doc}`/developers/sql-upgrade-scripts` in the Developer Guide. +1. If that didn't work, you may have run into a Flyway DB script collision error but that should be indicated by the server.log. See {doc}`/developers/sql-upgrade-scripts` in the Developer Guide. In the case of a collision, ask the developer to rename the script. 1. Assuming the above steps worked, and they should 99% of the time, test away! Note: be sure to `tail -F server.log` in a terminal window while you are doing any testing. This way you can spot problems that may not appear in the UI and have easier access to any stack traces for easier reporting. - - -## Checking if API Tests are Passing +### Checking if API Tests are Passing on Jenkins If API tests are failing, you should not merge the pull request. @@ -70,7 +67,7 @@ How can you know if API tests are passing? Here are the steps, by way of example - Under "All Tests", look at the duration for "edu.harvard.iq.dataverse.api". It should be ten minutes or higher. If it was only a few seconds, tests did not run. - Assuming tests ran, if there were failures, they should appear at the top under "All Failed Tests". Inform the author of the pull request about the error. -## Diagnosing Failures +### Diagnosing Failures on Jenkins API test failures can have multiple causes. As described above, from the "Test Result" page, you might see the failure under "All Failed Tests". However, the test could have failed because of some underlying system issue. @@ -84,3 +81,7 @@ fatal: [localhost]: FAILED! => {"changed": false, "dest": "/tmp/payara.zip", "el ``` In the example above, if Payara can't be downloaded, we're obviously going to have problems deploying Dataverse to it! + +## GitHub Actions + +We also use GitHub Actions. See for a list of actions. diff --git a/doc/sphinx-guides/source/qa/testing-approach.md b/doc/sphinx-guides/source/qa/testing-approach.md index 2c7241999a8..817161d02a0 100644 --- a/doc/sphinx-guides/source/qa/testing-approach.md +++ b/doc/sphinx-guides/source/qa/testing-approach.md @@ -8,25 +8,25 @@ We use a risk-based, manual testing approach to achieve the most benefit with limited resources. This means we want to catch bugs where they are likely to exist, ensure core functions work, and failures do not have catastrophic results. In practice this means we do a brief positive check of core functions on each build called a smoke test, we test the most likely place for new bugs to exist, the area where things have changed, and attempt to prevent catastrophic failure by asking about the scope and reach of the code and how failures may occur. -If it seems possible through user error or some other occurrence that such a serious failure will occur, we try to make it happen in the test environment. If the code has a UI component, we also do a limited amount of browser compatibility testing using Chrome, Firefox, and Safari browsers. We do not currently do UX or accessibility testing on a regular basis, though both have been done product-wide by the Design group and by the community. +If it seems possible through user error or some other occurrence that such a serious failure will occur, we try to make it happen in the test environment. If the code has a UI component, we also do a limited amount of browser compatibility testing using Chrome, Firefox, and Safari browsers. We do not currently do UX or accessibility testing on a regular basis, though both have been done product-wide by a Design group (in the past) and by the community. ## Examining a Pull Request for Test Cases ### What Problem Does It Solve? -Read the top part of the pull request for a description, notes for reviewers, and usually a "how to test" section. Does it make sense? If not, read the underlying issue it closes, and any release notes or documentation. Knowing in general what it does helps you to think about how to approach it. +Read the top part of the pull request for a description, notes for reviewers, and usually a "how to test" section. Does it make sense? If not, read the underlying issue it closes and any release notes or documentation. Knowing in general what it does helps you to think about how to approach it. ### How is It Configured? -Most pull requests do not have any special configuration and are enabled on deployment, but some do. Configuration is part of testing. A sysadmin or superuser will need to follow these instructions so try them out. Plus, that is the only way you will get it working to test it! +Most pull requests do not have any special configuration and are enabled on deployment, but some do. Configuration is part of testing. A sysadmin or superuser will need to follow these instructions so make sure they are in the release note snippet and try them out. Plus, that is the only way you will get it working to test it! -Identify test cases by examining the problem report or feature description and any documentation of functionality. Look for statements or assertions about functions, what it does, as well as conditions or conditional behavior. These become your test cases. Think about how someone might make a mistake using it and try it. Does it fail gracefully or in a confusing or worse, damaging manner? Also, consider whether this pull request may interact with other functionality and try some spot checks there. For instance, if new metadata fields are added, try the export feature. Of course, try the suggestions under "how to test." Those may be sufficient, but you should always think about the pull request based on what it does. +Identify test cases by examining the problem report or feature description and any documentation of functionality. Look for statements or assertions about functions, what it does, as well as conditions or conditional behavior. These become your test cases. Think about how someone might make a mistake using it and try it. Does it fail gracefully or in a confusing, or worse, damaging manner? Also, consider whether this pull request may interact with other functionality and try some spot checks there. For instance, if new metadata fields have been added, try the export feature. Of course, try the suggestions under "how to test." Those may be sufficient, but you should always think about the pull request based on what it does. -Try adding, modifying, and deleting any objects involved. This is probably covered by using the feature but a good basic approach to keep in mind. +Try adding, modifying, and deleting any objects involved. This is probably covered by using the feature, but this is a good basic approach to keep in mind. Make sure any server logging is appropriate. You should tail the server log while running your tests. Watch for unreported errors or stack traces especially chatty logging. If you do find a bug you will need to report the stack trace from the server.log. Err on the side of providing the developer too much of server.log rather than too little. -Exercise the UI if there is one. We tend to use Chrome for most of my basic testing as it's used twice as much as the next most commonly used browser, according to our site's Google Analytics. First go through all the options in the UI. Then, if all works, spot-check using Firefox and Safari. +Exercise the UI if there is one. We tend to use Chrome for most of our basic testing as it's used twice as much as the next most commonly-used browser, according to our site's Google Analytics. First go through all the options in the UI. Then, if all works, spot-check using Firefox and Safari. Check permissions. Is this feature limited to a specific set of users? Can it be accessed by a guest or by a non-privileged user? How about pasting a privileged page URL into a non-privileged user’s browser? @@ -47,4 +47,4 @@ Think about risk. Is the feature or function part of a critical area such as per This workflow is fine for a single person testing a PR, one at a time. It would be awkward or impossible if there were multiple people wanting to test different PRs at the same time. If a developer is testing, they would likely just deploy to their dev environment. That might be ok, but is the env is fully configured enough to offer a real-world testing scenario? -An alternative might be to spin an EC2 branch on AWS, potentially using sample data. This can take some time so another option might be to spin up a few, persistent AWS instances with sample data this way, one per tester, and just deploy new builds there when you want to test. You could even configure Jenkins projects for each if desired to maintain consistency in how they’re built. \ No newline at end of file +An alternative might be to spin an EC2 branch on AWS, potentially using sample data. This can take some time so another option might be to spin up a few, persistent AWS instances with sample data this way, one per tester, and just deploy new builds there when you want to test. You could even configure Jenkins projects for each if desired to maintain consistency in how they’re built. diff --git a/doc/sphinx-guides/source/qa/testing-infrastructure.md b/doc/sphinx-guides/source/qa/testing-infrastructure.md index 7a4bda626fc..c099076c458 100644 --- a/doc/sphinx-guides/source/qa/testing-infrastructure.md +++ b/doc/sphinx-guides/source/qa/testing-infrastructure.md @@ -7,11 +7,11 @@ ## Dataverse Internal -To build and test a PR, we use a build named `IQSS_Dataverse_Internal` on , which deploys the .war file to an AWS instance named . +To build and test a PR, we use a job called `IQSS_Dataverse_Internal` on (see {doc}`test-automation`), which deploys the .war file to an AWS instance named . ## Guides Server -There is also a guides build project named `guides.dataverse.org`. Any test builds of guides are deployed to a named directory on guides.dataverse.org and can be found and tested by going to the existing guides, removing the part of the URL that contains the version, and browsing the resulting directory listing for the latest change. +There is also a guides job called `guides.dataverse.org` (see {doc}`test-automation`). Any test builds of guides are deployed to a named directory on guides.dataverse.org and can be found and tested by going to the existing guides, removing the part of the URL that contains the version, and browsing the resulting directory listing for the latest change. Note that changes to guides can also be previewed on Read the Docs. In the pull request, look for a link like . This Read the Docs preview is also mentioned under also {doc}`/developers/documentation`. From d06ded15c9da2024f75250bcc8a25c363ae1cdc9 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 24 Jan 2024 14:51:57 -0500 Subject: [PATCH 229/689] move "deploy to internal" out of "test automation" #10101 --- doc/sphinx-guides/source/qa/qa-workflow.md | 2 +- .../source/qa/test-automation.md | 25 ------------------ .../source/qa/testing-infrastructure.md | 26 +++++++++++++++++++ 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/doc/sphinx-guides/source/qa/qa-workflow.md b/doc/sphinx-guides/source/qa/qa-workflow.md index 3db17ecb8a4..4654a7456d2 100644 --- a/doc/sphinx-guides/source/qa/qa-workflow.md +++ b/doc/sphinx-guides/source/qa/qa-workflow.md @@ -41,7 +41,7 @@ 1. Build and deploy the pull request. - Normally this is done using Jenkins and automatically deployed to the QA test machine. + Normally this is done using Jenkins and automatically deployed to the QA test machine. See {ref}`deploy-to-internal`. 1. Configure if required diff --git a/doc/sphinx-guides/source/qa/test-automation.md b/doc/sphinx-guides/source/qa/test-automation.md index e4b3b12ec43..708d0f88e23 100644 --- a/doc/sphinx-guides/source/qa/test-automation.md +++ b/doc/sphinx-guides/source/qa/test-automation.md @@ -30,31 +30,6 @@ You can access this job from the README at . is what we use to build guides. See {ref}`build-guides` in the Developer Guide for how this job is used at release time. -#### Building and Deploying a Pull Request from Jenkins to Dataverse-Internal - -1. Go to the QA column on our [project board](https://github.com/orgs/IQSS/projects/34), and select a pull request to test. - -1. From the pull request page, click the copy icon next to the pull request branch name. - -1. Log on to , select the `IQSS_Dataverse_Internal` project, and configure the repository URL and branch specifier to match the ones from the pull request. For example: - - * 8372-gdcc-xoai-library has IQSS implied - - **Repository URL:** https://github.com/IQSS/dataverse.git - - **Branch specifier:** */8372-gdcc-xoai-library - * GlobalDataverseCommunityConsortium:GDCC/DC-3B - - **Repository URL:** https://github.com/GlobalDataverseCommunityConsortium/dataverse.git - - **Branch specifier:** */GDCC/DC-3B. - -1. Click "Build Now" and note the build number in progress. - -1. Once complete, go to and check that the deployment succeeded, and that the homepage displays the latest build number. - -1. If for some reason it didn't deploy, check the server.log file. It may just be a caching issue so try un-deploying, deleting cache, restarting, and re-deploying on the server (`su - dataverse` then `/usr/local/payara6/bin/asadmin list-applications; /usr/local/payara6/bin/asadmin undeploy dataverse-6.1; /usr/local/payara6/bin/asadmin deploy /tmp/dataverse-6.1.war`) - -1. If that didn't work, you may have run into a Flyway DB script collision error but that should be indicated by the server.log. See {doc}`/developers/sql-upgrade-scripts` in the Developer Guide. In the case of a collision, ask the developer to rename the script. - -1. Assuming the above steps worked, and they should 99% of the time, test away! Note: be sure to `tail -F server.log` in a terminal window while you are doing any testing. This way you can spot problems that may not appear in the UI and have easier access to any stack traces for easier reporting. - ### Checking if API Tests are Passing on Jenkins If API tests are failing, you should not merge the pull request. diff --git a/doc/sphinx-guides/source/qa/testing-infrastructure.md b/doc/sphinx-guides/source/qa/testing-infrastructure.md index c099076c458..804e4c0afe6 100644 --- a/doc/sphinx-guides/source/qa/testing-infrastructure.md +++ b/doc/sphinx-guides/source/qa/testing-infrastructure.md @@ -9,6 +9,32 @@ To build and test a PR, we use a job called `IQSS_Dataverse_Internal` on (see {doc}`test-automation`), which deploys the .war file to an AWS instance named . +(deploy-to-internal)= +### Building and Deploying a Pull Request from Jenkins to Dataverse-Internal + +1. Go to the QA column on our [project board](https://github.com/orgs/IQSS/projects/34), and select a pull request to test. + +1. From the pull request page, click the copy icon next to the pull request branch name. + +1. Log on to , select the `IQSS_Dataverse_Internal` project, and configure the repository URL and branch specifier to match the ones from the pull request. For example: + + * 8372-gdcc-xoai-library has IQSS implied + - **Repository URL:** https://github.com/IQSS/dataverse.git + - **Branch specifier:** */8372-gdcc-xoai-library + * GlobalDataverseCommunityConsortium:GDCC/DC-3B + - **Repository URL:** https://github.com/GlobalDataverseCommunityConsortium/dataverse.git + - **Branch specifier:** */GDCC/DC-3B. + +1. Click "Build Now" and note the build number in progress. + +1. Once complete, go to and check that the deployment succeeded, and that the homepage displays the latest build number. + +1. If for some reason it didn't deploy, check the server.log file. It may just be a caching issue so try un-deploying, deleting cache, restarting, and re-deploying on the server (`su - dataverse` then `/usr/local/payara6/bin/asadmin list-applications; /usr/local/payara6/bin/asadmin undeploy dataverse-6.1; /usr/local/payara6/bin/asadmin deploy /tmp/dataverse-6.1.war`) + +1. If that didn't work, you may have run into a Flyway DB script collision error but that should be indicated by the server.log. See {doc}`/developers/sql-upgrade-scripts` in the Developer Guide. In the case of a collision, ask the developer to rename the script. + +1. Assuming the above steps worked, and they should 99% of the time, test away! Note: be sure to `tail -F server.log` in a terminal window while you are doing any testing. This way you can spot problems that may not appear in the UI and have easier access to any stack traces for easier reporting. + ## Guides Server There is also a guides job called `guides.dataverse.org` (see {doc}`test-automation`). Any test builds of guides are deployed to a named directory on guides.dataverse.org and can be found and tested by going to the existing guides, removing the part of the URL that contains the version, and browsing the resulting directory listing for the latest change. From 5ffc0589c75fe2fcf2584050ae5a477ddce27e06 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 24 Jan 2024 15:06:42 -0500 Subject: [PATCH 230/689] move testing approaches just below overview #10101 --- doc/sphinx-guides/source/qa/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/qa/index.md b/doc/sphinx-guides/source/qa/index.md index 937b352bccb..f16cd1d38fc 100644 --- a/doc/sphinx-guides/source/qa/index.md +++ b/doc/sphinx-guides/source/qa/index.md @@ -2,9 +2,9 @@ ```{toctree} overview.md +testing-approach.md testing-infrastructure.md qa-workflow.md -testing-approach.md test-automation.md performance-tests.md ``` From 61abe519a429be60616cd61a56df4ad4f4aa52dd Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 24 Jan 2024 15:12:01 -0500 Subject: [PATCH 231/689] minor edits #10101 --- doc/sphinx-guides/source/qa/overview.md | 2 ++ doc/sphinx-guides/source/qa/qa-workflow.md | 1 + 2 files changed, 3 insertions(+) diff --git a/doc/sphinx-guides/source/qa/overview.md b/doc/sphinx-guides/source/qa/overview.md index f8eb7b19297..64796357831 100644 --- a/doc/sphinx-guides/source/qa/overview.md +++ b/doc/sphinx-guides/source/qa/overview.md @@ -17,6 +17,8 @@ Before a pull request is moved to QA, it must be reviewed by a member of the dev Depending on whether the code modifies existing code or is completely new, a smoke test of core functionality is performed and some basic regression testing of modified or related code is performed. Any documentation provided is used to understand the feature and any assertions made in that documentation are tested. Once this passes and any bugs that are found are corrected, and the automated tests are confirmed to be passing, the PR is merged into the develop branch, the PR is closed, and the branch is deleted (if it is local). At this point, the PR moves from the QA column automatically into the Merged column (where it might be discussed at the next standup) and the process repeats with the next PR until it is decided to {doc}`make a release `. +The complete suggested workflow can be found at {doc}`qa-workflow`. + ## Tips and Tricks - Start testing simply, with the most obvious test. You don’t need to know all your tests upfront. As you gain comfort and understanding of how it works, try more tests until you are done. If it is a complex feature, jot down your tests in an outline format, some beforehand as a guide, and some after as things occur to you. Save the doc in a testing folder (on Google Drive). This potentially will help with future testing. diff --git a/doc/sphinx-guides/source/qa/qa-workflow.md b/doc/sphinx-guides/source/qa/qa-workflow.md index 4654a7456d2..9915fe97d98 100644 --- a/doc/sphinx-guides/source/qa/qa-workflow.md +++ b/doc/sphinx-guides/source/qa/qa-workflow.md @@ -4,6 +4,7 @@ :local: :depth: 3 ``` +## Checklist 1. Assign the PR you are working on to yourself. From cad9e583732a568ff083999aba16941505a207f4 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 24 Jan 2024 15:20:17 -0500 Subject: [PATCH 232/689] add release note #10101 --- doc/release-notes/10101-qa-guide.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/release-notes/10101-qa-guide.md diff --git a/doc/release-notes/10101-qa-guide.md b/doc/release-notes/10101-qa-guide.md new file mode 100644 index 00000000000..11fbd7df2c4 --- /dev/null +++ b/doc/release-notes/10101-qa-guide.md @@ -0,0 +1 @@ +A new QA Guide is intended mostly for the core development team but may be of interest to contributors. From 93de747a3f7c31cbbacd9e4d3895c00f44c0b522 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 24 Jan 2024 16:49:31 -0500 Subject: [PATCH 233/689] Updating flyway name --- ...straints.sql => V6.1.0.4__9983-missing-unique-constraints.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V6.0.0.2__9983-missing-unique-constraints.sql => V6.1.0.4__9983-missing-unique-constraints.sql} (100%) diff --git a/src/main/resources/db/migration/V6.0.0.2__9983-missing-unique-constraints.sql b/src/main/resources/db/migration/V6.1.0.4__9983-missing-unique-constraints.sql similarity index 100% rename from src/main/resources/db/migration/V6.0.0.2__9983-missing-unique-constraints.sql rename to src/main/resources/db/migration/V6.1.0.4__9983-missing-unique-constraints.sql From 743dbbc6655fd9e8bcab9db7b9df71a2fa4758db Mon Sep 17 00:00:00 2001 From: beep Date: Thu, 25 Jan 2024 08:37:24 +0100 Subject: [PATCH 234/689] Update docker-compose-dev.yml Co-authored-by: Philip Durbin --- docker-compose-dev.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 76a4c8a745d..6eab84092ed 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -60,8 +60,8 @@ services: volumes: - ./docker-dev-volumes/app/data:/dv - ./docker-dev-volumes/app/secrets:/secrets - # Map the glassfish applications folder so that we can update webapp resources using scripts/intellij/cpwebapp.sh - - ./docker-dev-volumes/glassfish/applications:/opt/payara/appserver/glassfish/domains/domain1/applications + # Uncomment to map the glassfish applications folder so that we can update webapp resources using scripts/intellij/cpwebapp.sh + # - ./docker-dev-volumes/glassfish/applications:/opt/payara/appserver/glassfish/domains/domain1/applications # Uncomment for changes to xhtml to be deployed immediately (if supported your IDE or toolchain). # Replace 6.0 with the current version. # - ./target/dataverse-6.0:/opt/payara/deployments/dataverse From 9d124e760bba83b7baa46bb1f88ec453a6bf6e6a Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 25 Jan 2024 11:51:12 +0000 Subject: [PATCH 235/689] Refactor: GetLatestPublishedDatasetVersionCommand --- ...tLatestPublishedDatasetVersionCommand.java | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedDatasetVersionCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedDatasetVersionCommand.java index dd9a8112afe..9ba02ef750b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedDatasetVersionCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedDatasetVersionCommand.java @@ -17,7 +17,7 @@ public class GetLatestPublishedDatasetVersionCommand extends AbstractCommand { private final Dataset ds; private final boolean includeDeaccessioned; - private boolean checkPermsWhenDeaccessioned; + private final boolean checkPermsWhenDeaccessioned; public GetLatestPublishedDatasetVersionCommand(DataverseRequest aRequest, Dataset anAffectedDataset) { this(aRequest, anAffectedDataset, false, false); @@ -31,37 +31,35 @@ public GetLatestPublishedDatasetVersionCommand(DataverseRequest aRequest, Datase } /* - * This command depending on the requested parameters will return: - * - * If the user requested to include a deaccessioned dataset with the files, the command will return the deaccessioned version if the user has permissions to view the files. Otherwise, it will return null. - * If the user requested to include a deaccessioned dataset but did not request the files, the command will return the deaccessioned version. - * If the user did not request to include a deaccessioned dataset, the command will return the latest published version. - * - */ + * This command depending on the requested parameters will return: + * + * If the user requested to include a deaccessioned dataset with the files, the command will return the deaccessioned version if the user has permissions to view the files. Otherwise, it will return null. + * If the user requested to include a deaccessioned dataset but did not request the files, the command will return the deaccessioned version. + * If the user did not request to include a deaccessioned dataset, the command will return the latest published version. + * + */ @Override public DatasetVersion execute(CommandContext ctxt) throws CommandException { - - DatasetVersion dsv = null; - - //We search of a released or deaccessioned version if it is requested. - for (DatasetVersion next : ds.getVersions()) { - if (next.isReleased() || (includeDeaccessioned && next.isDeaccessioned())){ - dsv = next; - break; - } + DatasetVersion dsVersionResult = getReleaseOrDeaccessionedVersion(); + if (dsVersionResult != null && userHasPermissionsOnDatasetVersion(dsVersionResult, checkPermsWhenDeaccessioned, ctxt, ds)) { + return dsVersionResult; } + return null; + } - //Checking permissions if the deaccessionedVersion was found and we are checking permissions because files were requested. - if(dsv != null && (dsv.isDeaccessioned() && checkPermsWhenDeaccessioned)){ - //If the user has no permissions we return null - if(!ctxt.permissions().requestOn(getRequest(), ds).has(Permission.EditDataset)){ - dsv = null; + private DatasetVersion getReleaseOrDeaccessionedVersion() { + for (DatasetVersion dsVersion : ds.getVersions()) { + if (dsVersion.isReleased() || (includeDeaccessioned && dsVersion.isDeaccessioned())) { + return dsVersion; } } - - return dsv; + return null; } - - + private boolean userHasPermissionsOnDatasetVersion(DatasetVersion dsVersionResult, boolean checkPermsWhenDeaccessioned, CommandContext ctxt, Dataset ds) { + if (dsVersionResult.isDeaccessioned() && checkPermsWhenDeaccessioned) { + return ctxt.permissions().requestOn(getRequest(), ds).has(Permission.EditDataset); + } + return true; + } } From e59907bf76553701c8d7ff16428a9cea9f132d96 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 25 Jan 2024 11:55:13 +0000 Subject: [PATCH 236/689] Refactor: method name --- .../command/impl/GetLatestPublishedDatasetVersionCommand.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedDatasetVersionCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedDatasetVersionCommand.java index 9ba02ef750b..0afcbe2d0bb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedDatasetVersionCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedDatasetVersionCommand.java @@ -40,14 +40,14 @@ public GetLatestPublishedDatasetVersionCommand(DataverseRequest aRequest, Datase */ @Override public DatasetVersion execute(CommandContext ctxt) throws CommandException { - DatasetVersion dsVersionResult = getReleaseOrDeaccessionedVersion(); + DatasetVersion dsVersionResult = getReleaseOrDeaccessionedDatasetVersion(); if (dsVersionResult != null && userHasPermissionsOnDatasetVersion(dsVersionResult, checkPermsWhenDeaccessioned, ctxt, ds)) { return dsVersionResult; } return null; } - private DatasetVersion getReleaseOrDeaccessionedVersion() { + private DatasetVersion getReleaseOrDeaccessionedDatasetVersion() { for (DatasetVersion dsVersion : ds.getVersions()) { if (dsVersion.isReleased() || (includeDeaccessioned && dsVersion.isDeaccessioned())) { return dsVersion; From 252672ab68a52cd9b9d8e84b80ddb3f23df769b3 Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Wed, 24 Jan 2024 14:44:52 -0500 Subject: [PATCH 237/689] Proposed fix in #10220 comments --- .../iq/dataverse/ThumbnailServiceWrapper.java | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/ThumbnailServiceWrapper.java b/src/main/java/edu/harvard/iq/dataverse/ThumbnailServiceWrapper.java index ae81a9326c4..7f56ce0cb27 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ThumbnailServiceWrapper.java +++ b/src/main/java/edu/harvard/iq/dataverse/ThumbnailServiceWrapper.java @@ -5,11 +5,14 @@ */ package edu.harvard.iq.dataverse; +import edu.harvard.iq.dataverse.dataaccess.DataAccess; import edu.harvard.iq.dataverse.dataaccess.ImageThumbConverter; - +import edu.harvard.iq.dataverse.dataaccess.StorageIO; +import edu.harvard.iq.dataverse.dataset.DatasetUtil; import edu.harvard.iq.dataverse.search.SolrSearchResult; import edu.harvard.iq.dataverse.util.SystemConfig; +import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.logging.Logger; @@ -170,17 +173,30 @@ public String getDatasetCardImageAsUrl(Dataset dataset, Long versionId, boolean if (thumbnailFile == null) { - // We attempt to auto-select via the optimized, native query-based method + boolean hasDatasetLogo = false; + StorageIO storageIO = null; + try { + storageIO = DataAccess.getStorageIO(dataset); + if (!storageIO.isAuxObjectCached(DatasetUtil.datasetLogoFilenameFinal)) { + // If not, return null/use the default, otherwise pass the logo URL + hasDatasetLogo = true; + } + } catch (IOException ioex) { + logger.warning("getDatasetCardImageAsUrl(): Failed to initialize dataset StorageIO for " + + dataset.getStorageIdentifier() + " (" + ioex.getMessage() + ")"); + } + // If no other logo we attempt to auto-select via the optimized, native + // query-based method // from the DatasetVersionService: - if (datasetVersionService.getThumbnailByVersionId(versionId) == null) { + if (!hasDatasetLogo && datasetVersionService.getThumbnailByVersionId(versionId) == null) { return null; } } - String url = SystemConfig.getDataverseSiteUrlStatic() + "/api/datasets/" + dataset.getId() + "/logo"; logger.fine("getDatasetCardImageAsUrl: " + url); this.dvobjectThumbnailsMap.put(datasetId,url); return url; + } // it's the responsibility of the user - to make sure the search result From 2c989923fba155ef0fe56f46489c3eec77abb213 Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Wed, 24 Jan 2024 17:10:43 -0500 Subject: [PATCH 238/689] reverse logic --- .../java/edu/harvard/iq/dataverse/ThumbnailServiceWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/ThumbnailServiceWrapper.java b/src/main/java/edu/harvard/iq/dataverse/ThumbnailServiceWrapper.java index 7f56ce0cb27..b6ab23848e2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ThumbnailServiceWrapper.java +++ b/src/main/java/edu/harvard/iq/dataverse/ThumbnailServiceWrapper.java @@ -177,7 +177,7 @@ public String getDatasetCardImageAsUrl(Dataset dataset, Long versionId, boolean StorageIO storageIO = null; try { storageIO = DataAccess.getStorageIO(dataset); - if (!storageIO.isAuxObjectCached(DatasetUtil.datasetLogoFilenameFinal)) { + if (storageIO.isAuxObjectCached(DatasetUtil.datasetLogoFilenameFinal)) { // If not, return null/use the default, otherwise pass the logo URL hasDatasetLogo = true; } From 77ba2932551c4a1015745ef2f911fbb5ff7c730d Mon Sep 17 00:00:00 2001 From: landreev Date: Thu, 25 Jan 2024 11:23:19 -0500 Subject: [PATCH 239/689] Revert "9686 move harvesting client" --- .../9686-move-harvesting-client-id.md | 1 - .../edu/harvard/iq/dataverse/Dataset.java | 14 ++++- .../iq/dataverse/DatasetServiceBean.java | 48 +++++++++++++++++ .../edu/harvard/iq/dataverse/DvObject.java | 17 ------ .../iq/dataverse/DvObjectServiceBean.java | 48 ----------------- .../api/imports/ImportServiceBean.java | 5 -- .../client/HarvestingClientServiceBean.java | 4 +- .../dataverse/metrics/MetricsServiceBean.java | 52 +++++++++---------- .../search/SearchIncludeFragment.java | 41 ++++++--------- ...6.1.0.2__9686-move-harvestingclient-id.sql | 14 ----- .../harvard/iq/dataverse/api/DatasetsIT.java | 2 - .../harvard/iq/dataverse/api/MetricsIT.java | 17 +++--- 12 files changed, 112 insertions(+), 151 deletions(-) delete mode 100644 doc/release-notes/9686-move-harvesting-client-id.md delete mode 100644 src/main/resources/db/migration/V6.1.0.2__9686-move-harvestingclient-id.sql diff --git a/doc/release-notes/9686-move-harvesting-client-id.md b/doc/release-notes/9686-move-harvesting-client-id.md deleted file mode 100644 index 110fcc6ca6e..00000000000 --- a/doc/release-notes/9686-move-harvesting-client-id.md +++ /dev/null @@ -1 +0,0 @@ -With this release the harvesting client id will be available for harvested files. A database update will copy the id to previously harvested files./ diff --git a/src/main/java/edu/harvard/iq/dataverse/Dataset.java b/src/main/java/edu/harvard/iq/dataverse/Dataset.java index e2788e6acc6..a2f560bc959 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Dataset.java +++ b/src/main/java/edu/harvard/iq/dataverse/Dataset.java @@ -752,9 +752,21 @@ public void setDatasetExternalCitations(List datasetEx this.datasetExternalCitations = datasetExternalCitations; } + @ManyToOne + @JoinColumn(name="harvestingClient_id") + private HarvestingClient harvestedFrom; - + public HarvestingClient getHarvestedFrom() { + return this.harvestedFrom; + } + public void setHarvestedFrom(HarvestingClient harvestingClientConfig) { + this.harvestedFrom = harvestingClientConfig; + } + + public boolean isHarvested() { + return this.harvestedFrom != null; + } private String harvestIdentifier; diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index 4c4aafdd1ec..c6df2a2e1ab 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -583,6 +583,54 @@ public Long getDatasetVersionCardImage(Long versionId, User user) { return null; } + /** + * Used to identify and properly display Harvested objects on the dataverse page. + * + * @param datasetIds + * @return + */ + public Map getArchiveDescriptionsForHarvestedDatasets(Set datasetIds){ + if (datasetIds == null || datasetIds.size() < 1) { + return null; + } + + String datasetIdStr = StringUtils.join(datasetIds, ", "); + + String qstr = "SELECT d.id, h.archiveDescription FROM harvestingClient h, dataset d WHERE d.harvestingClient_id = h.id AND d.id IN (" + datasetIdStr + ")"; + List searchResults; + + try { + searchResults = em.createNativeQuery(qstr).getResultList(); + } catch (Exception ex) { + searchResults = null; + } + + if (searchResults == null) { + return null; + } + + Map ret = new HashMap<>(); + + for (Object[] result : searchResults) { + Long dsId; + if (result[0] != null) { + try { + dsId = (Long)result[0]; + } catch (Exception ex) { + dsId = null; + } + if (dsId == null) { + continue; + } + + ret.put(dsId, (String)result[1]); + } + } + + return ret; + } + + public boolean isDatasetCardImageAvailable(DatasetVersion datasetVersion, User user) { if (datasetVersion == null) { diff --git a/src/main/java/edu/harvard/iq/dataverse/DvObject.java b/src/main/java/edu/harvard/iq/dataverse/DvObject.java index 46955f52878..cc5d7620969 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DvObject.java +++ b/src/main/java/edu/harvard/iq/dataverse/DvObject.java @@ -1,7 +1,6 @@ package edu.harvard.iq.dataverse; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; -import edu.harvard.iq.dataverse.harvest.client.HarvestingClient; import edu.harvard.iq.dataverse.pidproviders.PidUtil; import edu.harvard.iq.dataverse.storageuse.StorageQuota; @@ -372,22 +371,6 @@ public GlobalId getGlobalId() { return globalId; } - @ManyToOne - @JoinColumn(name="harvestingClient_id") - private HarvestingClient harvestedFrom; - - public HarvestingClient getHarvestedFrom() { - return this.harvestedFrom; - } - - public void setHarvestedFrom(HarvestingClient harvestingClientConfig) { - this.harvestedFrom = harvestingClientConfig; - } - - public boolean isHarvested() { - return this.harvestedFrom != null; - } - public abstract T accept(Visitor v); @Override diff --git a/src/main/java/edu/harvard/iq/dataverse/DvObjectServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DvObjectServiceBean.java index 58a246b364a..d4219c36149 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DvObjectServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DvObjectServiceBean.java @@ -383,54 +383,6 @@ public Map getObjectPathsByIds(Set objectIds){ return ret; } - /** - * Used to identify and properly display Harvested objects on the dataverse page. - * - * @param dvObjectIds - * @return - */ - public Map getArchiveDescriptionsForHarvestedDvObjects(Set dvObjectIds){ - - if (dvObjectIds == null || dvObjectIds.size() < 1) { - return null; - } - - String dvObjectIsString = StringUtils.join(dvObjectIds, ", "); - String qstr = "SELECT d.id, h.archiveDescription FROM harvestingClient h, DvObject d WHERE d.harvestingClient_id = h.id AND d.id IN (" + dvObjectIsString + ")"; - List searchResults; - - try { - searchResults = em.createNativeQuery(qstr).getResultList(); - } catch (Exception ex) { - searchResults = null; - } - - if (searchResults == null) { - return null; - } - - Map ret = new HashMap<>(); - - for (Object[] result : searchResults) { - Long dvObjId; - if (result[0] != null) { - try { - Integer castResult = (Integer) result[0]; - dvObjId = Long.valueOf(castResult); - } catch (Exception ex) { - dvObjId = null; - } - if (dvObjId == null) { - continue; - } - ret.put(dvObjId, (String)result[1]); - } - } - - return ret; - } - - public String generateNewIdentifierByStoredProcedure() { StoredProcedureQuery query = this.em.createNamedStoredProcedureQuery("Dataset.generateIdentifierFromStoredProcedure"); query.execute(); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportServiceBean.java index c5812403f31..c17ba909230 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportServiceBean.java @@ -332,11 +332,6 @@ public Dataset doImportHarvestedDataset(DataverseRequest dataverseRequest, Harve Dataset existingDs = datasetService.findByGlobalId(ds.getGlobalId().asString()); - //adding the harvesting client id to harvested files #9686 - for (DataFile df : ds.getFiles()){ - df.setHarvestedFrom(harvestingClient); - } - if (existingDs != null) { // If this dataset already exists IN ANOTHER DATAVERSE // we are just going to skip it! diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvestingClientServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvestingClientServiceBean.java index 5747c64d217..7ec6d75a41c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvestingClientServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvestingClientServiceBean.java @@ -199,8 +199,8 @@ public void recordHarvestJobStatus(Long hcId, Date finishTime, int harvestedCoun public Long getNumberOfHarvestedDatasetsByAllClients() { try { - return (Long) em.createNativeQuery("SELECT count(d.id) FROM dvobject d " - + " WHERE d.harvestingclient_id IS NOT NULL and d.dtype = 'Dataset'").getSingleResult(); + return (Long) em.createNativeQuery("SELECT count(d.id) FROM dataset d " + + " WHERE d.harvestingclient_id IS NOT NULL").getSingleResult(); } catch (Exception ex) { logger.info("Warning: exception looking up the total number of harvested datasets: " + ex.getMessage()); diff --git a/src/main/java/edu/harvard/iq/dataverse/metrics/MetricsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/metrics/MetricsServiceBean.java index 9ae0c7cbb8f..1b5619c53e0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/metrics/MetricsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/metrics/MetricsServiceBean.java @@ -138,8 +138,8 @@ public JsonArray getDatasetsTimeSeries(UriInfo uriInfo, String dataLocation, Dat + "from datasetversion\n" + "where versionstate='RELEASED' \n" + (((d == null)&&(DATA_LOCATION_ALL.equals(dataLocation))) ? "" : "and dataset_id in (select dataset.id from dataset, dvobject where dataset.id=dvobject.id\n") - + ((DATA_LOCATION_LOCAL.equals(dataLocation)) ? "and dvobject.harvestingclient_id IS NULL and publicationdate is not null\n " : "") - + ((DATA_LOCATION_REMOTE.equals(dataLocation)) ? "and dvobject.harvestingclient_id IS NOT NULL\n " : "") + + ((DATA_LOCATION_LOCAL.equals(dataLocation)) ? "and dataset.harvestingclient_id IS NULL and publicationdate is not null\n " : "") + + ((DATA_LOCATION_REMOTE.equals(dataLocation)) ? "and dataset.harvestingclient_id IS NOT NULL\n " : "") + ((d == null) ? "" : "and dvobject.owner_id in (" + getCommaSeparatedIdStringForSubtree(d, "Dataverse") + ")\n ") + (((d == null)&&(DATA_LOCATION_ALL.equals(dataLocation))) ? "" : ")\n") + "group by dataset_id) as subq group by subq.date order by date;" @@ -156,11 +156,11 @@ public JsonArray getDatasetsTimeSeries(UriInfo uriInfo, String dataLocation, Dat * @param d */ public long datasetsToMonth(String yyyymm, String dataLocation, Dataverse d) { - String dataLocationLine = "(date_trunc('month', releasetime) <= to_date('" + yyyymm + "','YYYY-MM') and dvobject.harvestingclient_id IS NULL)\n"; + String dataLocationLine = "(date_trunc('month', releasetime) <= to_date('" + yyyymm + "','YYYY-MM') and dataset.harvestingclient_id IS NULL)\n"; if (!DATA_LOCATION_LOCAL.equals(dataLocation)) { // Default api state is DATA_LOCATION_LOCAL //we have to use createtime for harvest as post dvn3 harvests do not have releasetime populated - String harvestBaseLine = "(date_trunc('month', createtime) <= to_date('" + yyyymm + "','YYYY-MM') and dvobject.harvestingclient_id IS NOT NULL)\n"; + String harvestBaseLine = "(date_trunc('month', createtime) <= to_date('" + yyyymm + "','YYYY-MM') and dataset.harvestingclient_id IS NOT NULL)\n"; if (DATA_LOCATION_REMOTE.equals(dataLocation)) { dataLocationLine = harvestBaseLine; // replace } else if (DATA_LOCATION_ALL.equals(dataLocation)) { @@ -189,7 +189,7 @@ public long datasetsToMonth(String yyyymm, String dataLocation, Dataverse d) { + "select datasetversion.dataset_id || ':' || max(datasetversion.versionnumber + (.1 * datasetversion.minorversionnumber))\n" + "from datasetversion\n" + "join dataset on dataset.id = datasetversion.dataset_id\n" - + "join dvobject on dvobject.id = dataset.id\n" + + ((d == null) ? "" : "join dvobject on dvobject.id = dataset.id\n") + "where versionstate='RELEASED' \n" + ((d == null) ? "" : "and dvobject.owner_id in (" + getCommaSeparatedIdStringForSubtree(d, "Dataverse") + ")\n ") + "and \n" @@ -198,6 +198,7 @@ public long datasetsToMonth(String yyyymm, String dataLocation, Dataverse d) { +") sub_temp" ); logger.log(Level.FINE, "Metric query: {0}", query); + return (long) query.getSingleResult(); } @@ -206,17 +207,16 @@ public List datasetsBySubjectToMonth(String yyyymm, String dataLocatio // A published local datasets may have more than one released version! // So that's why we have to jump through some extra hoops below // in order to select the latest one: - String originClause = "(datasetversion.dataset_id || ':' || datasetversion.versionnumber + (.1 * datasetversion.minorversionnumber) in\n" - + "(\n" - + "select datasetversion.dataset_id || ':' || max(datasetversion.versionnumber + (.1 * datasetversion.minorversionnumber))\n" - + " from datasetversion\n" - + " join dataset on dataset.id = datasetversion.dataset_id\n" - + " join dvobject on dataset.id = dvobject.id\n" - + " where versionstate='RELEASED'\n" - + " and dvobject.harvestingclient_id is null" - + " and date_trunc('month', releasetime) <= to_date('" + yyyymm + "','YYYY-MM')\n" - + " group by dataset_id\n" - + "))\n"; + String originClause = "(datasetversion.dataset_id || ':' || datasetversion.versionnumber + (.1 * datasetversion.minorversionnumber) in\n" + + "(\n" + + "select datasetversion.dataset_id || ':' || max(datasetversion.versionnumber + (.1 * datasetversion.minorversionnumber))\n" + + " from datasetversion\n" + + " join dataset on dataset.id = datasetversion.dataset_id\n" + + " where versionstate='RELEASED'\n" + + " and dataset.harvestingclient_id is null\n" + + " and date_trunc('month', releasetime) <= to_date('" + yyyymm + "','YYYY-MM')\n" + + " group by dataset_id\n" + + "))\n"; if (!DATA_LOCATION_LOCAL.equals(dataLocation)) { // Default api state is DATA_LOCATION_LOCAL //we have to use createtime for harvest as post dvn3 harvests do not have releasetime populated @@ -225,7 +225,7 @@ public List datasetsBySubjectToMonth(String yyyymm, String dataLocatio // so the query is simpler: String harvestOriginClause = "(\n" + " datasetversion.dataset_id = dataset.id\n" + - " AND dvobject.harvestingclient_id IS NOT null \n" + + " AND dataset.harvestingclient_id IS NOT null \n" + " AND date_trunc('month', datasetversion.createtime) <= to_date('" + yyyymm + "','YYYY-MM')\n" + ")\n"; @@ -244,7 +244,7 @@ public List datasetsBySubjectToMonth(String yyyymm, String dataLocatio + "JOIN datasetfieldtype ON datasetfieldtype.id = controlledvocabularyvalue.datasetfieldtype_id\n" + "JOIN datasetversion ON datasetversion.id = datasetfield.datasetversion_id\n" + "JOIN dataset ON dataset.id = datasetversion.dataset_id\n" - + "JOIN dvobject ON dvobject.id = dataset.id\n" + + ((d == null) ? "" : "JOIN dvobject ON dvobject.id = dataset.id\n") + "WHERE\n" + originClause + "AND datasetfieldtype.name = 'subject'\n" @@ -258,11 +258,11 @@ public List datasetsBySubjectToMonth(String yyyymm, String dataLocatio } public long datasetsPastDays(int days, String dataLocation, Dataverse d) { - String dataLocationLine = "(releasetime > current_date - interval '" + days + "' day and dvobject.harvestingclient_id IS NULL)\n"; + String dataLocationLine = "(releasetime > current_date - interval '" + days + "' day and dataset.harvestingclient_id IS NULL)\n"; if (!DATA_LOCATION_LOCAL.equals(dataLocation)) { // Default api state is DATA_LOCATION_LOCAL //we have to use createtime for harvest as post dvn3 harvests do not have releasetime populated - String harvestBaseLine = "(createtime > current_date - interval '" + days + "' day and dvobject.harvestingclient_id IS NOT NULL)\n"; + String harvestBaseLine = "(createtime > current_date - interval '" + days + "' day and dataset.harvestingclient_id IS NOT NULL)\n"; if (DATA_LOCATION_REMOTE.equals(dataLocation)) { dataLocationLine = harvestBaseLine; // replace } else if (DATA_LOCATION_ALL.equals(dataLocation)) { @@ -276,7 +276,7 @@ public long datasetsPastDays(int days, String dataLocation, Dataverse d) { + "select datasetversion.dataset_id || ':' || max(datasetversion.versionnumber + (.1 * datasetversion.minorversionnumber)) as max\n" + "from datasetversion\n" + "join dataset on dataset.id = datasetversion.dataset_id\n" - + "join dvobject on dvobject.id = dataset.id\n" + + ((d == null) ? "" : "join dvobject on dvobject.id = dataset.id\n") + "where versionstate='RELEASED' \n" + ((d == null) ? "" : "and dvobject.owner_id in (" + getCommaSeparatedIdStringForSubtree(d, "Dataverse") + ")\n") + "and \n" @@ -304,7 +304,7 @@ public JsonArray filesTimeSeries(Dataverse d) { + "where datasetversion.id=filemetadata.datasetversion_id\n" + "and versionstate='RELEASED' \n" + "and dataset_id in (select dataset.id from dataset, dvobject where dataset.id=dvobject.id\n" - + "and dvobject.harvestingclient_id IS NULL and publicationdate is not null\n " + + "and dataset.harvestingclient_id IS NULL and publicationdate is not null\n " + ((d == null) ? ")" : "and dvobject.owner_id in (" + getCommaSeparatedIdStringForSubtree(d, "Dataverse") + "))\n ") + "group by filemetadata.id) as subq group by subq.date order by date;"); logger.log(Level.FINE, "Metric query: {0}", query); @@ -327,11 +327,11 @@ public long filesToMonth(String yyyymm, Dataverse d) { + "select datasetversion.dataset_id || ':' || max(datasetversion.versionnumber + (.1 * datasetversion.minorversionnumber)) as max \n" + "from datasetversion\n" + "join dataset on dataset.id = datasetversion.dataset_id\n" - + "join dvobject on dvobject.id = dataset.id\n" + + ((d == null) ? "" : "join dvobject on dvobject.id = dataset.id\n") + "where versionstate='RELEASED'\n" + ((d == null) ? "" : "and dvobject.owner_id in (" + getCommaSeparatedIdStringForSubtree(d, "Dataverse") + ")\n") + "and date_trunc('month', releasetime) <= to_date('" + yyyymm + "','YYYY-MM')\n" - + "and dvobject.harvestingclient_id is null\n" + + "and dataset.harvestingclient_id is null\n" + "group by dataset_id \n" + ");" ); @@ -350,11 +350,11 @@ public long filesPastDays(int days, Dataverse d) { + "select datasetversion.dataset_id || ':' || max(datasetversion.versionnumber + (.1 * datasetversion.minorversionnumber)) as max \n" + "from datasetversion\n" + "join dataset on dataset.id = datasetversion.dataset_id\n" - + "join dvobject on dvobject.id = dataset.id\n" + + ((d == null) ? "" : "join dvobject on dvobject.id = dataset.id\n") + "where versionstate='RELEASED'\n" + "and releasetime > current_date - interval '" + days + "' day\n" + ((d == null) ? "" : "AND dvobject.owner_id in (" + getCommaSeparatedIdStringForSubtree(d, "Dataverse") + ")\n") - + "and dvobject.harvestingclient_id is null\n" + + "and dataset.harvestingclient_id is null\n" + "group by dataset_id \n" + ");" ); diff --git a/src/main/java/edu/harvard/iq/dataverse/search/SearchIncludeFragment.java b/src/main/java/edu/harvard/iq/dataverse/search/SearchIncludeFragment.java index 939b39b94ef..5a5d8781726 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/SearchIncludeFragment.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/SearchIncludeFragment.java @@ -1367,7 +1367,6 @@ public boolean canPublishDataset(Long datasetId){ public void setDisplayCardValues() { Set harvestedDatasetIds = null; - Set harvestedFileIds = null; for (SolrSearchResult result : searchResultsList) { //logger.info("checking DisplayImage for the search result " + i++); if (result.getType().equals("dataverses")) { @@ -1393,10 +1392,10 @@ public void setDisplayCardValues() { } else if (result.getType().equals("files")) { result.setImageUrl(thumbnailServiceWrapper.getFileCardImageAsBase64Url(result)); if (result.isHarvested()) { - if (harvestedFileIds == null) { - harvestedFileIds = new HashSet<>(); + if (harvestedDatasetIds == null) { + harvestedDatasetIds = new HashSet<>(); } - harvestedFileIds.add(result.getEntityId()); + harvestedDatasetIds.add(result.getParentIdAsLong()); } } } @@ -1408,35 +1407,25 @@ public void setDisplayCardValues() { // SQL query: if (harvestedDatasetIds != null) { - Map descriptionsForHarvestedDatasets = dvObjectService.getArchiveDescriptionsForHarvestedDvObjects(harvestedDatasetIds); - if (descriptionsForHarvestedDatasets != null && !descriptionsForHarvestedDatasets.isEmpty()) { + Map descriptionsForHarvestedDatasets = datasetService.getArchiveDescriptionsForHarvestedDatasets(harvestedDatasetIds); + if (descriptionsForHarvestedDatasets != null && descriptionsForHarvestedDatasets.size() > 0) { for (SolrSearchResult result : searchResultsList) { - if (result.isHarvested() && result.getType().equals("datasets") && descriptionsForHarvestedDatasets.containsKey(result.getEntityId())) { - result.setHarvestingDescription(descriptionsForHarvestedDatasets.get(result.getEntityId())); + if (result.isHarvested()) { + if (result.getType().equals("files")) { + if (descriptionsForHarvestedDatasets.containsKey(result.getParentIdAsLong())) { + result.setHarvestingDescription(descriptionsForHarvestedDatasets.get(result.getParentIdAsLong())); + } + } else if (result.getType().equals("datasets")) { + if (descriptionsForHarvestedDatasets.containsKey(result.getEntityId())) { + result.setHarvestingDescription(descriptionsForHarvestedDatasets.get(result.getEntityId())); + } + } } } } descriptionsForHarvestedDatasets = null; harvestedDatasetIds = null; } - - if (harvestedFileIds != null) { - - Map descriptionsForHarvestedFiles = dvObjectService.getArchiveDescriptionsForHarvestedDvObjects(harvestedFileIds); - if (descriptionsForHarvestedFiles != null && !descriptionsForHarvestedFiles.isEmpty()) { - for (SolrSearchResult result : searchResultsList) { - if (result.isHarvested() && result.getType().equals("files") && descriptionsForHarvestedFiles.containsKey(result.getEntityId())) { - - result.setHarvestingDescription(descriptionsForHarvestedFiles.get(result.getEntityId())); - - } - } - } - descriptionsForHarvestedFiles = null; - harvestedDatasetIds = null; - - } - // determine which of the objects are linked: diff --git a/src/main/resources/db/migration/V6.1.0.2__9686-move-harvestingclient-id.sql b/src/main/resources/db/migration/V6.1.0.2__9686-move-harvestingclient-id.sql deleted file mode 100644 index 67ba026745f..00000000000 --- a/src/main/resources/db/migration/V6.1.0.2__9686-move-harvestingclient-id.sql +++ /dev/null @@ -1,14 +0,0 @@ -ALTER TABLE dvobject ADD COLUMN IF NOT EXISTS harvestingclient_id BIGINT; - ---add harvesting client id to dvobject records of harvested datasets -update dvobject dvo set harvestingclient_id = s.harvestingclient_id from -(select id, harvestingclient_id from dataset d where d.harvestingclient_id is not null) s -where s.id = dvo.id; - ---add harvesting client id to dvobject records of harvested files -update dvobject dvo set harvestingclient_id = s.harvestingclient_id from -(select id, harvestingclient_id from dataset d where d.harvestingclient_id is not null) s -where s.id = dvo.owner_id; - -ALTER TABLE dataset drop COLUMN IF EXISTS harvestingclient_id; - diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index 087db4858b2..9b51be4b365 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -2548,8 +2548,6 @@ public void testLinkingDatasets() { EntityManager entityManager = entityManagerFactory.createEntityManager(); entityManager.getTransaction().begin(); // Do stuff... - //SEK 01/22/2024 - as of 6.2 harvestingclient_id will be on the dv object table - // so if this is ever implemented change will probably need to happen in the updatequery below entityManager.createNativeQuery("UPDATE dataset SET harvestingclient_id=1 WHERE id="+datasetId2).executeUpdate(); entityManager.getTransaction().commit(); entityManager.close(); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/MetricsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/MetricsIT.java index 1425b7bc5d9..e3328eefb4a 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/MetricsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/MetricsIT.java @@ -5,8 +5,6 @@ import edu.harvard.iq.dataverse.metrics.MetricsUtil; import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; import static jakarta.ws.rs.core.Response.Status.OK; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -18,13 +16,10 @@ //To improve these tests we should try adding data and see if the number DOESN'T //go up to show that the caching worked public class MetricsIT { - - private static String yyyymm; @BeforeAll public static void setUpClass() { RestAssured.baseURI = UtilIT.getRestAssuredBaseUri(); - yyyymm = LocalDate.now().format(DateTimeFormatter.ofPattern(MetricsUtil.YEAR_AND_MONTH_PATTERN)); UtilIT.clearMetricCache(); } @@ -35,7 +30,8 @@ public static void cleanUpClass() { @Test public void testGetDataversesToMonth() { - + String yyyymm = "2018-04"; +// yyyymm = null; Response response = UtilIT.metricsDataversesToMonth(yyyymm, null); String precache = response.prettyPrint(); response.then().assertThat() @@ -58,7 +54,8 @@ public void testGetDataversesToMonth() { @Test public void testGetDatasetsToMonth() { - + String yyyymm = "2018-04"; +// yyyymm = null; Response response = UtilIT.metricsDatasetsToMonth(yyyymm, null); String precache = response.prettyPrint(); response.then().assertThat() @@ -80,7 +77,8 @@ public void testGetDatasetsToMonth() { @Test public void testGetFilesToMonth() { - + String yyyymm = "2018-04"; +// yyyymm = null; Response response = UtilIT.metricsFilesToMonth(yyyymm, null); String precache = response.prettyPrint(); response.then().assertThat() @@ -102,7 +100,8 @@ public void testGetFilesToMonth() { @Test public void testGetDownloadsToMonth() { - + String yyyymm = "2018-04"; +// yyyymm = null; Response response = UtilIT.metricsDownloadsToMonth(yyyymm, null); String precache = response.prettyPrint(); response.then().assertThat() From 994cf18e5c91245404830ef7e03d682c68a43538 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 25 Jan 2024 16:34:16 -0500 Subject: [PATCH 240/689] add "running Dataverse in docker", other cleanup #10238 --- doc/sphinx-guides/source/container/index.rst | 20 ++------------ doc/sphinx-guides/source/container/intro.rst | 26 ++++++++++++++++++ .../source/container/running/backend-dev.rst | 7 +++++ .../source/container/running/demo.rst | 27 +++++++++++++++++++ .../source/container/running/frontend-dev.rst | 7 +++++ .../source/container/running/index.rst | 12 +++++++++ .../container/running/metadata-blocks.rst | 9 +++++++ .../source/container/running/production.rst | 11 ++++++++ docker/compose/demo/compose.yml | 0 9 files changed, 101 insertions(+), 18 deletions(-) create mode 100644 doc/sphinx-guides/source/container/intro.rst create mode 100644 doc/sphinx-guides/source/container/running/backend-dev.rst create mode 100644 doc/sphinx-guides/source/container/running/demo.rst create mode 100644 doc/sphinx-guides/source/container/running/frontend-dev.rst create mode 100755 doc/sphinx-guides/source/container/running/index.rst create mode 100644 doc/sphinx-guides/source/container/running/metadata-blocks.rst create mode 100644 doc/sphinx-guides/source/container/running/production.rst create mode 100644 docker/compose/demo/compose.yml diff --git a/doc/sphinx-guides/source/container/index.rst b/doc/sphinx-guides/source/container/index.rst index 4bbc87a4845..abf871dd340 100644 --- a/doc/sphinx-guides/source/container/index.rst +++ b/doc/sphinx-guides/source/container/index.rst @@ -1,28 +1,12 @@ Container Guide =============== -Running the Dataverse software in containers is quite different than in a :doc:`standard installation <../installation/prep>`. - -Both approaches have pros and cons. These days, containers are very often used for development and testing, -but there is an ever rising move toward running applications in the cloud using container technology. - -**NOTE:** -**As the Institute for Quantitative Social Sciences (IQSS) at Harvard is running a standard, non-containerized installation, -container support described in this guide is mostly created and maintained by the Dataverse community on a best-effort -basis.** - -This guide is *not* about installation on technology like Docker Swarm, Kubernetes, Rancher or other -solutions to run containers in production. There is the `Dataverse on K8s project `_ for this -purpose, as mentioned in the :doc:`/developers/containers` section of the Developer Guide. - -This guide focuses on describing the container images managed from the main Dataverse repository (again: by the -community, not IQSS), their features and limitations. Instructions on how to build the images yourself and how to -develop and extend them further are provided. - **Contents:** .. toctree:: + intro + running/index dev-usage base-image app-image diff --git a/doc/sphinx-guides/source/container/intro.rst b/doc/sphinx-guides/source/container/intro.rst new file mode 100644 index 00000000000..94b2c99f0d1 --- /dev/null +++ b/doc/sphinx-guides/source/container/intro.rst @@ -0,0 +1,26 @@ +Introduction +============ + +Dataverse in containers! + +.. contents:: |toctitle| + :local: + +Intended Audience +----------------- + +This guide is intended for anyone who wants to run Dataverse in containers. This is potentially a wide audience, from sysadmins interested in running Dataverse in production in containers (not recommended yet) to contributors working on a bug fix (encouraged!). + +.. _getting-help-containers: + +Getting Help +------------ + +Please ask in #containers at https://chat.dataverse.org + +.. _helping-containers: + +Helping with the Containerization Effort +---------------------------------------- + +In 2023 the Containerization Working Group started meeting regularly. All are welcome to join! We talk in #containers at https://chat.dataverse.org and have a regular video call. For details, please visit https://ct.gdcc.io diff --git a/doc/sphinx-guides/source/container/running/backend-dev.rst b/doc/sphinx-guides/source/container/running/backend-dev.rst new file mode 100644 index 00000000000..45aa4450bfb --- /dev/null +++ b/doc/sphinx-guides/source/container/running/backend-dev.rst @@ -0,0 +1,7 @@ +Backend Development +=================== + +.. contents:: |toctitle| + :local: + +See :doc:`../dev-usage`. diff --git a/doc/sphinx-guides/source/container/running/demo.rst b/doc/sphinx-guides/source/container/running/demo.rst new file mode 100644 index 00000000000..71e45f5028e --- /dev/null +++ b/doc/sphinx-guides/source/container/running/demo.rst @@ -0,0 +1,27 @@ +Demo or Evaluation +================== + +If you would like to demo or evaluate Dataverse running in containers, you're in the right place. + +.. contents:: |toctitle| + :local: + +Hardware Requirements +--------------------- + +- 8 GB RAM + +Software Requirements +--------------------- + +- Mac, Linux, or Windows (experimental) +- Docker + +Windows support is experimental but we are very interested in supporting Windows better. Please report bugs and see :ref:`helping-containers`. + +Steps +----- + +- Download :download:`compose.yml <../../../../../docker/compose/demo/compose.yml>` +- Run ``docker compose up`` in the directory where you put ``compose.yml`` + diff --git a/doc/sphinx-guides/source/container/running/frontend-dev.rst b/doc/sphinx-guides/source/container/running/frontend-dev.rst new file mode 100644 index 00000000000..1f57d4531ba --- /dev/null +++ b/doc/sphinx-guides/source/container/running/frontend-dev.rst @@ -0,0 +1,7 @@ +Frontend Development +==================== + +.. contents:: |toctitle| + :local: + +https://github.com/IQSS/dataverse-frontend includes docs and scripts for running Dataverse in Docker for frontend development. diff --git a/doc/sphinx-guides/source/container/running/index.rst b/doc/sphinx-guides/source/container/running/index.rst new file mode 100755 index 00000000000..8d17b105eb4 --- /dev/null +++ b/doc/sphinx-guides/source/container/running/index.rst @@ -0,0 +1,12 @@ +Running Dataverse in Docker +=========================== + +Contents: + +.. toctree:: + + production + demo + metadata-blocks + frontend-dev + backend-dev diff --git a/doc/sphinx-guides/source/container/running/metadata-blocks.rst b/doc/sphinx-guides/source/container/running/metadata-blocks.rst new file mode 100644 index 00000000000..4794f29ab42 --- /dev/null +++ b/doc/sphinx-guides/source/container/running/metadata-blocks.rst @@ -0,0 +1,9 @@ +Editing Metadata Blocks +======================= + +.. contents:: |toctitle| + :local: + +The Admin Guide has a section on :doc:`/admin/metadatacustomization` and suggests running Dataverse in containers (Docker) for this purpose. + +This is certainly possible but the specifics have not yet been written. Until then, please see :doc:`demo`, which should also provide a suitable environment. diff --git a/doc/sphinx-guides/source/container/running/production.rst b/doc/sphinx-guides/source/container/running/production.rst new file mode 100644 index 00000000000..89e63ff5ab1 --- /dev/null +++ b/doc/sphinx-guides/source/container/running/production.rst @@ -0,0 +1,11 @@ +Production (Future) +=================== + +.. contents:: |toctitle| + :local: + +The images described in this guide not yet recommended for production usage. + +You can help the effort to support these images in production by trying them out and giving feedback (see :ref:`helping-containers`). + +For now, please follow :doc:`demo`. diff --git a/docker/compose/demo/compose.yml b/docker/compose/demo/compose.yml new file mode 100644 index 00000000000..e69de29bb2d From fb58d895edac32744cae7b164d7ae9f1121dba94 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Fri, 26 Jan 2024 10:58:07 -0500 Subject: [PATCH 241/689] tweaks and more use cases #10238 --- doc/sphinx-guides/source/container/intro.rst | 2 +- .../source/container/running/backend-dev.rst | 3 +++ .../source/container/running/demo.rst | 4 ++-- .../source/container/running/frontend-dev.rst | 5 ++++- .../source/container/running/github-action.rst | 18 ++++++++++++++++++ .../source/container/running/index.rst | 1 + .../container/running/metadata-blocks.rst | 8 +++++++- .../source/container/running/production.rst | 15 ++++++++++++--- 8 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 doc/sphinx-guides/source/container/running/github-action.rst diff --git a/doc/sphinx-guides/source/container/intro.rst b/doc/sphinx-guides/source/container/intro.rst index 94b2c99f0d1..42b095f3158 100644 --- a/doc/sphinx-guides/source/container/intro.rst +++ b/doc/sphinx-guides/source/container/intro.rst @@ -9,7 +9,7 @@ Dataverse in containers! Intended Audience ----------------- -This guide is intended for anyone who wants to run Dataverse in containers. This is potentially a wide audience, from sysadmins interested in running Dataverse in production in containers (not recommended yet) to contributors working on a bug fix (encouraged!). +This guide is intended for anyone who wants to run Dataverse in containers. This is potentially a wide audience, from sysadmins interested in running Dataverse in production in containers (not recommended yet) to contributors working on a bug fix (encouraged!). See :doc:`running/index` for various scenarios and please let us know if your use case is not covered. .. _getting-help-containers: diff --git a/doc/sphinx-guides/source/container/running/backend-dev.rst b/doc/sphinx-guides/source/container/running/backend-dev.rst index 45aa4450bfb..8b2dab956ad 100644 --- a/doc/sphinx-guides/source/container/running/backend-dev.rst +++ b/doc/sphinx-guides/source/container/running/backend-dev.rst @@ -4,4 +4,7 @@ Backend Development .. contents:: |toctitle| :local: +Intro +----- + See :doc:`../dev-usage`. diff --git a/doc/sphinx-guides/source/container/running/demo.rst b/doc/sphinx-guides/source/container/running/demo.rst index 71e45f5028e..8db8cfb2a9c 100644 --- a/doc/sphinx-guides/source/container/running/demo.rst +++ b/doc/sphinx-guides/source/container/running/demo.rst @@ -1,7 +1,7 @@ Demo or Evaluation ================== -If you would like to demo or evaluate Dataverse running in containers, you're in the right place. +If you would like to demo or evaluate Dataverse running in containers, you're in the right place. Your feedback is extremely valuable to us! To let us know what you think, pease see :ref:`helping-containers`. .. contents:: |toctitle| :local: @@ -17,7 +17,7 @@ Software Requirements - Mac, Linux, or Windows (experimental) - Docker -Windows support is experimental but we are very interested in supporting Windows better. Please report bugs and see :ref:`helping-containers`. +Windows support is experimental but we are very interested in supporting Windows better. Please report bugs (see :ref:`helping-containers`). Steps ----- diff --git a/doc/sphinx-guides/source/container/running/frontend-dev.rst b/doc/sphinx-guides/source/container/running/frontend-dev.rst index 1f57d4531ba..88d40c12053 100644 --- a/doc/sphinx-guides/source/container/running/frontend-dev.rst +++ b/doc/sphinx-guides/source/container/running/frontend-dev.rst @@ -4,4 +4,7 @@ Frontend Development .. contents:: |toctitle| :local: -https://github.com/IQSS/dataverse-frontend includes docs and scripts for running Dataverse in Docker for frontend development. +Intro +----- + +The frontend (web interface) of Dataverse is being decoupled from the backend. This evolving codebase has its own repo at https://github.com/IQSS/dataverse-frontend which includes docs and scripts for running the backend of Dataverse in Docker. diff --git a/doc/sphinx-guides/source/container/running/github-action.rst b/doc/sphinx-guides/source/container/running/github-action.rst new file mode 100644 index 00000000000..ae42dd494d1 --- /dev/null +++ b/doc/sphinx-guides/source/container/running/github-action.rst @@ -0,0 +1,18 @@ +GitHub Action +============= + +.. contents:: |toctitle| + :local: + +Intro +----- + +A GitHub Action is under development that will spin up a Dataverse instance within the context of GitHub CI workflows: https://github.com/gdcc/dataverse-action + +Use Cases +--------- + +Use cases for the GitHub Action include: + +- Testing :doc:`/api/client-libraries` that interact with Dataverse APIs +- Testing :doc:`/admin/integrations` of third party software with Dataverse diff --git a/doc/sphinx-guides/source/container/running/index.rst b/doc/sphinx-guides/source/container/running/index.rst index 8d17b105eb4..a02266f7cba 100755 --- a/doc/sphinx-guides/source/container/running/index.rst +++ b/doc/sphinx-guides/source/container/running/index.rst @@ -8,5 +8,6 @@ Contents: production demo metadata-blocks + github-action frontend-dev backend-dev diff --git a/doc/sphinx-guides/source/container/running/metadata-blocks.rst b/doc/sphinx-guides/source/container/running/metadata-blocks.rst index 4794f29ab42..fcc80ce1909 100644 --- a/doc/sphinx-guides/source/container/running/metadata-blocks.rst +++ b/doc/sphinx-guides/source/container/running/metadata-blocks.rst @@ -4,6 +4,12 @@ Editing Metadata Blocks .. contents:: |toctitle| :local: +Intro +----- + The Admin Guide has a section on :doc:`/admin/metadatacustomization` and suggests running Dataverse in containers (Docker) for this purpose. -This is certainly possible but the specifics have not yet been written. Until then, please see :doc:`demo`, which should also provide a suitable environment. +Status +------ + +For now, please see :doc:`demo`, which should also provide a suitable Dockerized Dataverse environment. diff --git a/doc/sphinx-guides/source/container/running/production.rst b/doc/sphinx-guides/source/container/running/production.rst index 89e63ff5ab1..0a628dc57b9 100644 --- a/doc/sphinx-guides/source/container/running/production.rst +++ b/doc/sphinx-guides/source/container/running/production.rst @@ -4,8 +4,17 @@ Production (Future) .. contents:: |toctitle| :local: -The images described in this guide not yet recommended for production usage. +Status +------ -You can help the effort to support these images in production by trying them out and giving feedback (see :ref:`helping-containers`). +The images described in this guide are not yet recommended for production usage. -For now, please follow :doc:`demo`. +How to Help +----------- + +You can help the effort to support these images in production by trying them out (see :doc:`demo`) and giving feedback (see :ref:`helping-containers`). + +Alternatives +------------ + +Until the images are ready for production, please use the traditional installation method described in the :doc:`/installation/index`. From b7ec6465b09e41929f985089c2a5c566e95308e4 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Fri, 26 Jan 2024 11:12:50 -0500 Subject: [PATCH 242/689] #9748 delete tools only added by tests --- .../iq/dataverse/api/ExternalToolsIT.java | 102 +++++++----------- 1 file changed, 39 insertions(+), 63 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java index 022747a3cdc..664c07d598c 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java @@ -40,21 +40,6 @@ public void testGetExternalTools() { @Test public void testFileLevelTool1() { - // Delete all external tools before testing. - Response getTools = UtilIT.getExternalTools(); - getTools.prettyPrint(); - getTools.then().assertThat() - .statusCode(OK.getStatusCode()); - String body = getTools.getBody().asString(); - JsonReader bodyObject = Json.createReader(new StringReader(body)); - JsonArray tools = bodyObject.readObject().getJsonArray("data"); - for (int i = 0; i < tools.size(); i++) { - JsonObject tool = tools.getJsonObject(i); - int id = tool.getInt("id"); - Response deleteExternalTool = UtilIT.deleteExternalTool(id); - deleteExternalTool.prettyPrint(); - } - Response createUser = UtilIT.createRandomUser(); createUser.prettyPrint(); createUser.then().assertThat() @@ -145,26 +130,14 @@ public void testFileLevelTool1() { .statusCode(OK.getStatusCode()) // No tools for this file type. .body("data", Matchers.hasSize(0)); + + //Delete the tool added by this test... + Response deleteExternalTool = UtilIT.deleteExternalTool(toolId); } @Test public void testDatasetLevelTool1() { - // Delete all external tools before testing. - Response getTools = UtilIT.getExternalTools(); - getTools.prettyPrint(); - getTools.then().assertThat() - .statusCode(OK.getStatusCode()); - String body = getTools.getBody().asString(); - JsonReader bodyObject = Json.createReader(new StringReader(body)); - JsonArray tools = bodyObject.readObject().getJsonArray("data"); - for (int i = 0; i < tools.size(); i++) { - JsonObject tool = tools.getJsonObject(i); - int id = tool.getInt("id"); - Response deleteExternalTool = UtilIT.deleteExternalTool(id); - deleteExternalTool.prettyPrint(); - } - Response createUser = UtilIT.createRandomUser(); createUser.prettyPrint(); createUser.then().assertThat() @@ -184,7 +157,6 @@ public void testDatasetLevelTool1() { createDataset.then().assertThat() .statusCode(CREATED.getStatusCode()); -// Integer datasetId = UtilIT.getDatasetIdFromResponse(createDataset); Integer datasetId = JsonPath.from(createDataset.getBody().asString()).getInt("data.id"); String datasetPid = JsonPath.from(createDataset.getBody().asString()).getString("data.persistentId"); @@ -219,6 +191,8 @@ public void testDatasetLevelTool1() { addExternalTool.then().assertThat() .statusCode(OK.getStatusCode()) .body("data.displayName", CoreMatchers.equalTo("DatasetTool1")); + + long toolId = JsonPath.from(addExternalTool.getBody().asString()).getLong("data.id"); Response getExternalToolsByDatasetIdInvalidType = UtilIT.getExternalToolsForDataset(datasetId.toString(), "invalidType", apiToken); getExternalToolsByDatasetIdInvalidType.prettyPrint(); @@ -233,27 +207,16 @@ public void testDatasetLevelTool1() { .body("data[0].scope", CoreMatchers.equalTo("dataset")) .body("data[0].toolUrlWithQueryParams", CoreMatchers.equalTo("http://datasettool1.com?datasetPid=" + datasetPid + "&key=" + apiToken)) .statusCode(OK.getStatusCode()); - + + //Delete the tool added by this test... + Response deleteExternalTool = UtilIT.deleteExternalTool(toolId); + deleteExternalTool.then().assertThat() + .statusCode(OK.getStatusCode()); } @Test public void testDatasetLevelToolConfigure() { - // Delete all external tools before testing. - Response getTools = UtilIT.getExternalTools(); - getTools.prettyPrint(); - getTools.then().assertThat() - .statusCode(OK.getStatusCode()); - String body = getTools.getBody().asString(); - JsonReader bodyObject = Json.createReader(new StringReader(body)); - JsonArray tools = bodyObject.readObject().getJsonArray("data"); - for (int i = 0; i < tools.size(); i++) { - JsonObject tool = tools.getJsonObject(i); - int id = tool.getInt("id"); - Response deleteExternalTool = UtilIT.deleteExternalTool(id); - deleteExternalTool.prettyPrint(); - } - Response createUser = UtilIT.createRandomUser(); createUser.prettyPrint(); createUser.then().assertThat() @@ -302,6 +265,8 @@ public void testDatasetLevelToolConfigure() { addExternalTool.then().assertThat() .statusCode(OK.getStatusCode()) .body("data.displayName", CoreMatchers.equalTo("Dataset Configurator")); + + long toolId = JsonPath.from(addExternalTool.getBody().asString()).getLong("data.id"); Response getExternalToolsByDatasetId = UtilIT.getExternalToolsForDataset(datasetId.toString(), "configure", apiToken); getExternalToolsByDatasetId.prettyPrint(); @@ -311,6 +276,11 @@ public void testDatasetLevelToolConfigure() { .body("data[0].types[0]", CoreMatchers.equalTo("configure")) .body("data[0].toolUrlWithQueryParams", CoreMatchers.equalTo("https://datasetconfigurator.com?datasetPid=" + datasetPid)) .statusCode(OK.getStatusCode()); + + //Delete the tool added by this test... + Response deleteExternalTool = UtilIT.deleteExternalTool(toolId); + deleteExternalTool.then().assertThat() + .statusCode(OK.getStatusCode()); } @@ -400,12 +370,13 @@ public void deleteTools() { String body = getTools.getBody().asString(); JsonReader bodyObject = Json.createReader(new StringReader(body)); JsonArray tools = bodyObject.readObject().getJsonArray("data"); + /* for (int i = 0; i < tools.size(); i++) { JsonObject tool = tools.getJsonObject(i); int id = tool.getInt("id"); Response deleteExternalTool = UtilIT.deleteExternalTool(id); deleteExternalTool.prettyPrint(); - } + }*/ } // preview only @@ -446,6 +417,13 @@ public void createToolShellScript() { addExternalTool.prettyPrint(); addExternalTool.then().assertThat() .statusCode(OK.getStatusCode()); + + long toolId = JsonPath.from(addExternalTool.getBody().asString()).getLong("data.id"); + + //Delete the tool added by this test... + Response deleteExternalTool = UtilIT.deleteExternalTool(toolId); + deleteExternalTool.then().assertThat() + .statusCode(OK.getStatusCode()); } // explore only @@ -479,6 +457,13 @@ public void createToolDataExplorer() { addExternalTool.prettyPrint(); addExternalTool.then().assertThat() .statusCode(OK.getStatusCode()); + + long toolId = JsonPath.from(addExternalTool.getBody().asString()).getLong("data.id"); + + //Delete the tool added by this test... + Response deleteExternalTool = UtilIT.deleteExternalTool(toolId); + deleteExternalTool.then().assertThat() + .statusCode(OK.getStatusCode()); } // both preview and explore @@ -527,21 +512,6 @@ public void createToolSpreadsheetViewer() { @Test public void testFileLevelToolWithAuxFileReq() throws IOException { - // Delete all external tools before testing. - Response getTools = UtilIT.getExternalTools(); - getTools.prettyPrint(); - getTools.then().assertThat() - .statusCode(OK.getStatusCode()); - String body = getTools.getBody().asString(); - JsonReader bodyObject = Json.createReader(new StringReader(body)); - JsonArray tools = bodyObject.readObject().getJsonArray("data"); - for (int i = 0; i < tools.size(); i++) { - JsonObject tool = tools.getJsonObject(i); - int id = tool.getInt("id"); - Response deleteExternalTool = UtilIT.deleteExternalTool(id); - deleteExternalTool.prettyPrint(); - } - Response createUser = UtilIT.createRandomUser(); createUser.prettyPrint(); createUser.then().assertThat() @@ -640,6 +610,12 @@ public void testFileLevelToolWithAuxFileReq() throws IOException { .body("data[0].displayName", CoreMatchers.equalTo("HDF5 Tool")) .body("data[0].scope", CoreMatchers.equalTo("file")) .body("data[0].contentType", CoreMatchers.equalTo("application/x-hdf5")); + + //Delete the tool added by this test... + Response deleteExternalTool = UtilIT.deleteExternalTool(toolId); + deleteExternalTool.then().assertThat() + .statusCode(OK.getStatusCode()); + } } From cc29efecd2748ad005760610c6be65ba073b35c6 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Fri, 26 Jan 2024 11:30:19 -0500 Subject: [PATCH 243/689] stub out demo/eval compose.yml based on dev compose #10238 Differences from dev version: - localstack and minio removed - env vars filled in based on current .env The goal is to have a single file to download, rather than a compose file and an .env file. --- docker/compose/demo/compose.yml | 170 ++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/docker/compose/demo/compose.yml b/docker/compose/demo/compose.yml index e69de29bb2d..aea99040acd 100644 --- a/docker/compose/demo/compose.yml +++ b/docker/compose/demo/compose.yml @@ -0,0 +1,170 @@ +version: "2.4" + +services: + + dev_dataverse: + container_name: "dev_dataverse" + hostname: dataverse + image: gdcc/dataverse:unstable + restart: on-failure + user: payara + environment: + DATAVERSE_DB_HOST: postgres + DATAVERSE_DB_PASSWORD: secret + DATAVERSE_DB_USER: dataverse + ENABLE_JDWP: "1" + DATAVERSE_FEATURE_API_BEARER_AUTH: "1" + DATAVERSE_AUTH_OIDC_ENABLED: "1" + DATAVERSE_AUTH_OIDC_CLIENT_ID: test + DATAVERSE_AUTH_OIDC_CLIENT_SECRET: 94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8 + DATAVERSE_AUTH_OIDC_AUTH_SERVER_URL: http://keycloak.mydomain.com:8090/realms/test + DATAVERSE_JSF_REFRESH_PERIOD: "1" + # These two oai settings are here to get HarvestingServerIT to pass + dataverse_oai_server_maxidentifiers: "2" + dataverse_oai_server_maxrecords: "2" + JVM_ARGS: -Ddataverse.files.storage-driver-id=file1 + -Ddataverse.files.file1.type=file + -Ddataverse.files.file1.label=Filesystem + -Ddataverse.files.file1.directory=${STORAGE_DIR}/store + ports: + - "8080:8080" # HTTP (Dataverse Application) + - "4848:4848" # HTTP (Payara Admin Console) + - "9009:9009" # JDWP + - "8686:8686" # JMX + networks: + - dataverse + depends_on: + - dev_postgres + - dev_solr + - dev_dv_initializer + volumes: + - ./docker-dev-volumes/app/data:/dv + - ./docker-dev-volumes/app/secrets:/secrets + # Uncomment to map the glassfish applications folder so that we can update webapp resources using scripts/intellij/cpwebapp.sh + # - ./docker-dev-volumes/glassfish/applications:/opt/payara/appserver/glassfish/domains/domain1/applications + # Uncomment for changes to xhtml to be deployed immediately (if supported your IDE or toolchain). + # Replace 6.0 with the current version. + # - ./target/dataverse-6.0:/opt/payara/deployments/dataverse + tmpfs: + - /dumps:mode=770,size=2052M,uid=1000,gid=1000 + - /tmp:mode=770,size=2052M,uid=1000,gid=1000 + mem_limit: 2147483648 # 2 GiB + mem_reservation: 1024m + privileged: false + + dev_bootstrap: + container_name: "dev_bootstrap" + image: gdcc/configbaker:unstable + restart: "no" + command: + - bootstrap.sh + - dev + networks: + - dataverse + + dev_dv_initializer: + container_name: "dev_dv_initializer" + image: gdcc/configbaker:unstable + restart: "no" + command: + - sh + - -c + - "fix-fs-perms.sh dv" + volumes: + - ./docker-dev-volumes/app/data:/dv + + dev_postgres: + container_name: "dev_postgres" + hostname: postgres + image: postgres:13 + restart: on-failure + environment: + - POSTGRES_USER=dataverse + - POSTGRES_PASSWORD=secret + ports: + - "5432:5432" + networks: + - dataverse + volumes: + - ./docker-dev-volumes/postgresql/data:/var/lib/postgresql/data + + dev_solr_initializer: + container_name: "dev_solr_initializer" + image: gdcc/configbaker:unstable + restart: "no" + command: + - sh + - -c + - "fix-fs-perms.sh solr && cp -a /template/* /solr-template" + volumes: + - ./docker-dev-volumes/solr/data:/var/solr + - ./docker-dev-volumes/solr/conf:/solr-template + + dev_solr: + container_name: "dev_solr" + hostname: "solr" + image: solr:9.3.0 + depends_on: + - dev_solr_initializer + restart: on-failure + ports: + - "8983:8983" + networks: + - dataverse + command: + - "solr-precreate" + - "collection1" + - "/template" + volumes: + - ./docker-dev-volumes/solr/data:/var/solr + - ./docker-dev-volumes/solr/conf:/template + + dev_smtp: + container_name: "dev_smtp" + hostname: "smtp" + image: maildev/maildev:2.0.5 + restart: on-failure + ports: + - "25:25" # smtp server + - "1080:1080" # web ui + environment: + - MAILDEV_SMTP_PORT=25 + - MAILDEV_MAIL_DIRECTORY=/mail + networks: + - dataverse + #volumes: + # - ./docker-dev-volumes/smtp/data:/mail + tmpfs: + - /mail:mode=770,size=128M,uid=1000,gid=1000 + + dev_keycloak: + container_name: "dev_keycloak" + image: 'quay.io/keycloak/keycloak:21.0' + hostname: keycloak + environment: + - KEYCLOAK_ADMIN=kcadmin + - KEYCLOAK_ADMIN_PASSWORD=kcpassword + - KEYCLOAK_LOGLEVEL=DEBUG + - KC_HOSTNAME_STRICT=false + networks: + dataverse: + aliases: + - keycloak.mydomain.com #create a DNS alias within the network (add the same alias to your /etc/hosts to get a working OIDC flow) + command: start-dev --import-realm --http-port=8090 # change port to 8090, so within the network and external the same port is used + ports: + - "8090:8090" + volumes: + - './conf/keycloak/test-realm.json:/opt/keycloak/data/import/test-realm.json' + + dev_nginx: + container_name: dev_nginx + image: gdcc/dev_nginx:unstable + ports: + - "4849:4849" + restart: always + networks: + - dataverse + +networks: + dataverse: + driver: bridge From 0c736cc698a3fef25fa8d5f25e76d4a85a6ec088 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Fri, 26 Jan 2024 12:47:38 -0500 Subject: [PATCH 244/689] switch from unstable to alpha images #10238 --- docker/compose/demo/compose.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker/compose/demo/compose.yml b/docker/compose/demo/compose.yml index aea99040acd..403143130ac 100644 --- a/docker/compose/demo/compose.yml +++ b/docker/compose/demo/compose.yml @@ -5,7 +5,7 @@ services: dev_dataverse: container_name: "dev_dataverse" hostname: dataverse - image: gdcc/dataverse:unstable + image: gdcc/dataverse:alpha restart: on-failure user: payara environment: @@ -54,7 +54,7 @@ services: dev_bootstrap: container_name: "dev_bootstrap" - image: gdcc/configbaker:unstable + image: gdcc/configbaker:alpha restart: "no" command: - bootstrap.sh @@ -64,7 +64,7 @@ services: dev_dv_initializer: container_name: "dev_dv_initializer" - image: gdcc/configbaker:unstable + image: gdcc/configbaker:alpha restart: "no" command: - sh @@ -90,7 +90,7 @@ services: dev_solr_initializer: container_name: "dev_solr_initializer" - image: gdcc/configbaker:unstable + image: gdcc/configbaker:alpha restart: "no" command: - sh From 91287b35960afd0d351d1b07942333763ce84555 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Fri, 26 Jan 2024 15:55:12 -0500 Subject: [PATCH 245/689] #9748 one more assert --- src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java index 664c07d598c..6f0aa499dd1 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java @@ -133,6 +133,8 @@ public void testFileLevelTool1() { //Delete the tool added by this test... Response deleteExternalTool = UtilIT.deleteExternalTool(toolId); + deleteExternalTool.then().assertThat() + .statusCode(OK.getStatusCode()); } @Test From 69d3bb9172ad134c32299a326ef76efda2420458 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Fri, 26 Jan 2024 16:21:58 -0500 Subject: [PATCH 246/689] more content for demo/eval #10238 Also update tags section under "app image" (now live). --- .../source/container/app-image.rst | 18 +-- doc/sphinx-guides/source/container/intro.rst | 2 + .../source/container/running/demo.rst | 125 ++++++++++++++++-- 3 files changed, 126 insertions(+), 19 deletions(-) diff --git a/doc/sphinx-guides/source/container/app-image.rst b/doc/sphinx-guides/source/container/app-image.rst index 29f6d6ac1d4..caf4aadbf7e 100644 --- a/doc/sphinx-guides/source/container/app-image.rst +++ b/doc/sphinx-guides/source/container/app-image.rst @@ -22,20 +22,20 @@ IQSS will not offer you support how to deploy or run it, please reach out to the You might be interested in taking a look at :doc:`../developers/containers`, linking you to some (community-based) efforts. - +.. _supported-image-tags-app: Supported Image Tags ++++++++++++++++++++ This image is sourced from the main upstream code `repository of the Dataverse software `_. -Development and maintenance of the `image's code `_ happens there -(again, by the community). - -.. note:: - Please note that this image is not (yet) available from Docker Hub. You need to build local to use - (see below). Follow https://github.com/IQSS/dataverse/issues/9444 for new developments. - - +Development and maintenance of the `image's code `_ +happens there (again, by the community). Community-supported image tags are based on the two most important +upstream branches: + +- The ``unstable`` tag corresponds to the ``develop`` branch, where pull requests are merged. + (`Dockerfile `__) +- The ``alpha`` tag corresponds to the ``master`` branch, where releases are cut from. + (`Dockerfile `__) Image Contents ++++++++++++++ diff --git a/doc/sphinx-guides/source/container/intro.rst b/doc/sphinx-guides/source/container/intro.rst index 42b095f3158..5099531dcc9 100644 --- a/doc/sphinx-guides/source/container/intro.rst +++ b/doc/sphinx-guides/source/container/intro.rst @@ -18,6 +18,8 @@ Getting Help Please ask in #containers at https://chat.dataverse.org +Alternatively, you can try one or more of the channels under :ref:`support`. + .. _helping-containers: Helping with the Containerization Effort diff --git a/doc/sphinx-guides/source/container/running/demo.rst b/doc/sphinx-guides/source/container/running/demo.rst index 8db8cfb2a9c..0ad1e50442f 100644 --- a/doc/sphinx-guides/source/container/running/demo.rst +++ b/doc/sphinx-guides/source/container/running/demo.rst @@ -1,27 +1,132 @@ Demo or Evaluation ================== -If you would like to demo or evaluate Dataverse running in containers, you're in the right place. Your feedback is extremely valuable to us! To let us know what you think, pease see :ref:`helping-containers`. +If you would like to demo or evaluate Dataverse running in containers, you're in the right place. Your feedback is extremely valuable to us! To let us know what you think, please see :ref:`helping-containers`. .. contents:: |toctitle| :local: -Hardware Requirements ---------------------- +Quickstart +---------- -- 8 GB RAM +- Download :download:`compose.yml <../../../../../docker/compose/demo/compose.yml>` +- Run ``docker compose up`` in the directory where you put ``compose.yml`` +- Visit http://localhost:8080 and try logging in: + + - username: dataverseAdmin + - password: admin1 -Software Requirements ---------------------- +Hardware and Software Requirements +----------------------------------- +- 8 GB RAM (if not much else is running) - Mac, Linux, or Windows (experimental) - Docker Windows support is experimental but we are very interested in supporting Windows better. Please report bugs (see :ref:`helping-containers`). -Steps ------ +Tags and Versions +----------------- -- Download :download:`compose.yml <../../../../../docker/compose/demo/compose.yml>` -- Run ``docker compose up`` in the directory where you put ``compose.yml`` +The compose file references a tag called "alpha", which corresponds to the latest released version of Dataverse. This means that if a release of Dataverse comes out while you are demo'ing or evaluating, the version of Dataverse you are using could change. We are aware that there is a desire for tags that correspond to versions to ensure consistency. You are welcome to join `the discussion `_ and otherwise get in touch (see :ref:`helping-containers`). For more on tags, see :ref:`supported-image-tags-app`. + +Once Dataverse is running, you can check which version you have through the normal methods: + +- Check the bottom right in a web browser. +- Check http://localhost:8080/api/info/version via API. + +About the Containers +-------------------- + +If you run ``docker ps``, you'll see that multiple containers are spun up in a demo or evaluation. Here are the most important ones: + +- dataverse +- postgres +- solr +- smtp +- bootstrap + +Most are self-explanatory, and correspond to components listed under :doc:`/installation/prerequisites` in the (traditional) Installation Guide, but "bootstrap" refers to :doc:`../configbaker-image`. + +Additional containers are used in development (see :doc:`../dev-usage`), but for the purposes of a demo or evaluation, fewer moving (sometimes pointy) parts are included. + +Security +-------- + +Please be aware that for now, the "dev" persona is used to bootstrap Dataverse, which means that admin APIs are wide open (to allow developers to test them; see :ref:`securing-your-installation` for more on API blocking), the "create user" key is set to a default value, etc. You can inspect the dev person `on GitHub `_ (look for ``--insecure``). + +We plan to ship a "demo" persona but it is not ready yet. See also :ref:`configbaker-personas`. + +Common Operations +----------------- + +Starting the Containers ++++++++++++++++++++++++ + +First, download :download:`compose.yml <../../../../../docker/compose/demo/compose.yml>` and place it somewhere you'll remember. + +Then, run ``docker compose up`` in the directory where you put ``compose.yml`` + +Starting the containers for the first time involves a bootstrap process. You should see "have a nice day" output at the end. + +Stopping the Containers ++++++++++++++++++++++++ + +You might want to stop the containers if you aren't using them. Hit ``Ctrl-c`` (hold down the ``Ctrl`` key and then hit the ``c`` key). + +You data is still intact and you can start the containers again with ``docker compose up``. + +Deleting the Containers ++++++++++++++++++++++++ + +If you no longer need the containers because your demo or evaluation is finished and you want to reclaim disk space, run ``docker compose down`` in the directory where you put ``compose.yml``. + +Deleting the Data Directory ++++++++++++++++++++++++++++ + +Data related to the Dataverse containers is placed in a directory called ``docker-dev-volumes`` next to the ``compose.yml`` file. If you are finished with your demo or evaluation or you want to start fresh, simply delete this directory. + +Configuration +------------- + +Configuration is described in greater detail under :doc:`/installation/config` in the Installation Guide, but there are some specifics to running in containers you should know about. + +.. _configbaker-personas: + +Personas +++++++++ + +When the containers are bootstrapped, the "dev" persona is used. In the future we plan to add a "demo" persona that is more suited to demo and evaluation use cases. + +Database Settings ++++++++++++++++++ + +Updating database settings is the same as described under :ref:`database-settings` in the Installation Guide. + +MPCONFIG Options +++++++++++++++++ + +The compose file contains an ``environment`` section with various MicroProfile Config (MPCONFIG) options. You can experiment with this by adding ``DATAVERSE_VERSION: foobar`` to change the (displayed) version of Dataverse to "foobar". + +JVM Options ++++++++++++ + +JVM options are not especially easy to change in the container. The general process is to get a shell on the "dataverse" container, change the settings, and then stop and start the containers. See :ref:`jvm-options` for more. + +Troubleshooting +--------------- + +Bootstrapping Did Not Complete +++++++++++++++++++++++++++++++ + +In the compose file, try increasing the timeout in the bootstrap container by adding something like this: + +.. code-block:: bash + + environment: + - TIMEOUT=10m + +Getting Help +------------ +Please do not be shy about reaching out for help. We very much want you to have a pleasant demo or evaluation experience. For ways to contact us, please see See :ref:`getting-help-containers`. From d3a378de0815a8d9af94fe8972f61d95841f89f2 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Fri, 26 Jan 2024 16:23:20 -0500 Subject: [PATCH 247/689] remove limits used for harvesting tests #10238 --- docker/compose/demo/compose.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/docker/compose/demo/compose.yml b/docker/compose/demo/compose.yml index 403143130ac..4cfd8cd9345 100644 --- a/docker/compose/demo/compose.yml +++ b/docker/compose/demo/compose.yml @@ -19,9 +19,6 @@ services: DATAVERSE_AUTH_OIDC_CLIENT_SECRET: 94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8 DATAVERSE_AUTH_OIDC_AUTH_SERVER_URL: http://keycloak.mydomain.com:8090/realms/test DATAVERSE_JSF_REFRESH_PERIOD: "1" - # These two oai settings are here to get HarvestingServerIT to pass - dataverse_oai_server_maxidentifiers: "2" - dataverse_oai_server_maxrecords: "2" JVM_ARGS: -Ddataverse.files.storage-driver-id=file1 -Ddataverse.files.file1.type=file -Ddataverse.files.file1.label=Filesystem From 4555ae3f9dae12fd83c369b846c4aff114fecbf0 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Fri, 26 Jan 2024 16:25:59 -0500 Subject: [PATCH 248/689] remove keycloak container and OIDC config #10238 --- docker/compose/demo/compose.yml | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/docker/compose/demo/compose.yml b/docker/compose/demo/compose.yml index 4cfd8cd9345..e0839eb1023 100644 --- a/docker/compose/demo/compose.yml +++ b/docker/compose/demo/compose.yml @@ -14,10 +14,6 @@ services: DATAVERSE_DB_USER: dataverse ENABLE_JDWP: "1" DATAVERSE_FEATURE_API_BEARER_AUTH: "1" - DATAVERSE_AUTH_OIDC_ENABLED: "1" - DATAVERSE_AUTH_OIDC_CLIENT_ID: test - DATAVERSE_AUTH_OIDC_CLIENT_SECRET: 94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8 - DATAVERSE_AUTH_OIDC_AUTH_SERVER_URL: http://keycloak.mydomain.com:8090/realms/test DATAVERSE_JSF_REFRESH_PERIOD: "1" JVM_ARGS: -Ddataverse.files.storage-driver-id=file1 -Ddataverse.files.file1.type=file @@ -134,25 +130,6 @@ services: tmpfs: - /mail:mode=770,size=128M,uid=1000,gid=1000 - dev_keycloak: - container_name: "dev_keycloak" - image: 'quay.io/keycloak/keycloak:21.0' - hostname: keycloak - environment: - - KEYCLOAK_ADMIN=kcadmin - - KEYCLOAK_ADMIN_PASSWORD=kcpassword - - KEYCLOAK_LOGLEVEL=DEBUG - - KC_HOSTNAME_STRICT=false - networks: - dataverse: - aliases: - - keycloak.mydomain.com #create a DNS alias within the network (add the same alias to your /etc/hosts to get a working OIDC flow) - command: start-dev --import-realm --http-port=8090 # change port to 8090, so within the network and external the same port is used - ports: - - "8090:8090" - volumes: - - './conf/keycloak/test-realm.json:/opt/keycloak/data/import/test-realm.json' - dev_nginx: container_name: dev_nginx image: gdcc/dev_nginx:unstable From bb4d78649338ced4f66ec4ba4167c6a94efcd23f Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Fri, 26 Jan 2024 16:29:33 -0500 Subject: [PATCH 249/689] remove various dev stuff not needed for a demo #10238 --- docker/compose/demo/compose.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/docker/compose/demo/compose.yml b/docker/compose/demo/compose.yml index e0839eb1023..b72d06951e8 100644 --- a/docker/compose/demo/compose.yml +++ b/docker/compose/demo/compose.yml @@ -12,9 +12,7 @@ services: DATAVERSE_DB_HOST: postgres DATAVERSE_DB_PASSWORD: secret DATAVERSE_DB_USER: dataverse - ENABLE_JDWP: "1" DATAVERSE_FEATURE_API_BEARER_AUTH: "1" - DATAVERSE_JSF_REFRESH_PERIOD: "1" JVM_ARGS: -Ddataverse.files.storage-driver-id=file1 -Ddataverse.files.file1.type=file -Ddataverse.files.file1.label=Filesystem @@ -33,11 +31,6 @@ services: volumes: - ./docker-dev-volumes/app/data:/dv - ./docker-dev-volumes/app/secrets:/secrets - # Uncomment to map the glassfish applications folder so that we can update webapp resources using scripts/intellij/cpwebapp.sh - # - ./docker-dev-volumes/glassfish/applications:/opt/payara/appserver/glassfish/domains/domain1/applications - # Uncomment for changes to xhtml to be deployed immediately (if supported your IDE or toolchain). - # Replace 6.0 with the current version. - # - ./target/dataverse-6.0:/opt/payara/deployments/dataverse tmpfs: - /dumps:mode=770,size=2052M,uid=1000,gid=1000 - /tmp:mode=770,size=2052M,uid=1000,gid=1000 @@ -130,15 +123,6 @@ services: tmpfs: - /mail:mode=770,size=128M,uid=1000,gid=1000 - dev_nginx: - container_name: dev_nginx - image: gdcc/dev_nginx:unstable - ports: - - "4849:4849" - restart: always - networks: - - dataverse - networks: dataverse: driver: bridge From a5b07964dbaca8dc4f436d4ae9405548d4a01374 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Sun, 28 Jan 2024 10:53:46 -0500 Subject: [PATCH 250/689] Straightforward fixes for the broken redirects for harvested records from "generic OAI" archives. #10254 --- src/main/java/edu/harvard/iq/dataverse/Dataset.java | 9 +++++++++ src/main/java/edu/harvard/iq/dataverse/DatasetPage.java | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/Dataset.java b/src/main/java/edu/harvard/iq/dataverse/Dataset.java index a2f560bc959..f75c3d92881 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Dataset.java +++ b/src/main/java/edu/harvard/iq/dataverse/Dataset.java @@ -852,6 +852,15 @@ public String getRemoteArchiveURL() { if (StringUtil.nonEmpty(this.getProtocol()) && StringUtil.nonEmpty(this.getAuthority()) && StringUtil.nonEmpty(this.getIdentifier())) { + + // If there is a custom archival url for this Harvesting + // Source, we'll use that + String harvestingUrl = this.getHarvestedFrom().getHarvestingUrl(); + String archivalUrl = this.getHarvestedFrom().getArchiveUrl(); + if (!harvestingUrl.contains(archivalUrl)) { + return archivalUrl + this.getAuthority() + "/" + this.getIdentifier(); + } + // ... if not, we'll redirect to the resolver for the global id: return this.getPersistentURL(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index b79f387f20b..1f2e603d984 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -2026,7 +2026,7 @@ private String init(boolean initFull) { // to the local 404 page, below. logger.warning("failed to issue a redirect to "+originalSourceURL); } - return originalSourceURL; + return null; } return permissionsWrapper.notFound(); From c5f4ca46b6d384965c80926bce199f64f80d1af3 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Mon, 29 Jan 2024 10:33:34 -0500 Subject: [PATCH 251/689] remove "dev_" from container names #10238 --- docker/compose/demo/compose.yml | 36 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/docker/compose/demo/compose.yml b/docker/compose/demo/compose.yml index b72d06951e8..09dde63d5f4 100644 --- a/docker/compose/demo/compose.yml +++ b/docker/compose/demo/compose.yml @@ -2,8 +2,8 @@ version: "2.4" services: - dev_dataverse: - container_name: "dev_dataverse" + dataverse: + container_name: "dataverse" hostname: dataverse image: gdcc/dataverse:alpha restart: on-failure @@ -25,9 +25,9 @@ services: networks: - dataverse depends_on: - - dev_postgres - - dev_solr - - dev_dv_initializer + - postgres + - solr + - dv_initializer volumes: - ./docker-dev-volumes/app/data:/dv - ./docker-dev-volumes/app/secrets:/secrets @@ -38,8 +38,8 @@ services: mem_reservation: 1024m privileged: false - dev_bootstrap: - container_name: "dev_bootstrap" + bootstrap: + container_name: "bootstrap" image: gdcc/configbaker:alpha restart: "no" command: @@ -48,8 +48,8 @@ services: networks: - dataverse - dev_dv_initializer: - container_name: "dev_dv_initializer" + dv_initializer: + container_name: "dv_initializer" image: gdcc/configbaker:alpha restart: "no" command: @@ -59,8 +59,8 @@ services: volumes: - ./docker-dev-volumes/app/data:/dv - dev_postgres: - container_name: "dev_postgres" + postgres: + container_name: "postgres" hostname: postgres image: postgres:13 restart: on-failure @@ -74,8 +74,8 @@ services: volumes: - ./docker-dev-volumes/postgresql/data:/var/lib/postgresql/data - dev_solr_initializer: - container_name: "dev_solr_initializer" + solr_initializer: + container_name: "solr_initializer" image: gdcc/configbaker:alpha restart: "no" command: @@ -86,12 +86,12 @@ services: - ./docker-dev-volumes/solr/data:/var/solr - ./docker-dev-volumes/solr/conf:/solr-template - dev_solr: - container_name: "dev_solr" + solr: + container_name: "solr" hostname: "solr" image: solr:9.3.0 depends_on: - - dev_solr_initializer + - solr_initializer restart: on-failure ports: - "8983:8983" @@ -105,8 +105,8 @@ services: - ./docker-dev-volumes/solr/data:/var/solr - ./docker-dev-volumes/solr/conf:/template - dev_smtp: - container_name: "dev_smtp" + smtp: + container_name: "smtp" hostname: "smtp" image: maildev/maildev:2.0.5 restart: on-failure From c0cda028c3ce0922f51c670917d94ef22cab61c5 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Mon, 29 Jan 2024 10:39:14 -0500 Subject: [PATCH 252/689] rename docker-dev-volumes to data #10238 --- .../source/container/running/demo.rst | 2 +- docker/compose/demo/.gitignore | 1 + docker/compose/demo/compose.yml | 18 +++++++++--------- 3 files changed, 11 insertions(+), 10 deletions(-) create mode 100644 docker/compose/demo/.gitignore diff --git a/doc/sphinx-guides/source/container/running/demo.rst b/doc/sphinx-guides/source/container/running/demo.rst index 0ad1e50442f..5eda108c842 100644 --- a/doc/sphinx-guides/source/container/running/demo.rst +++ b/doc/sphinx-guides/source/container/running/demo.rst @@ -84,7 +84,7 @@ If you no longer need the containers because your demo or evaluation is finished Deleting the Data Directory +++++++++++++++++++++++++++ -Data related to the Dataverse containers is placed in a directory called ``docker-dev-volumes`` next to the ``compose.yml`` file. If you are finished with your demo or evaluation or you want to start fresh, simply delete this directory. +Data related to the Dataverse containers is placed in a directory called ``data`` next to the ``compose.yml`` file. If you are finished with your demo or evaluation or you want to start fresh, simply delete this directory. Configuration ------------- diff --git a/docker/compose/demo/.gitignore b/docker/compose/demo/.gitignore new file mode 100644 index 00000000000..1269488f7fb --- /dev/null +++ b/docker/compose/demo/.gitignore @@ -0,0 +1 @@ +data diff --git a/docker/compose/demo/compose.yml b/docker/compose/demo/compose.yml index 09dde63d5f4..3817921f10a 100644 --- a/docker/compose/demo/compose.yml +++ b/docker/compose/demo/compose.yml @@ -29,8 +29,8 @@ services: - solr - dv_initializer volumes: - - ./docker-dev-volumes/app/data:/dv - - ./docker-dev-volumes/app/secrets:/secrets + - ./data/app/data:/dv + - ./data/app/secrets:/secrets tmpfs: - /dumps:mode=770,size=2052M,uid=1000,gid=1000 - /tmp:mode=770,size=2052M,uid=1000,gid=1000 @@ -57,7 +57,7 @@ services: - -c - "fix-fs-perms.sh dv" volumes: - - ./docker-dev-volumes/app/data:/dv + - ./data/app/data:/dv postgres: container_name: "postgres" @@ -72,7 +72,7 @@ services: networks: - dataverse volumes: - - ./docker-dev-volumes/postgresql/data:/var/lib/postgresql/data + - ./data/postgresql/data:/var/lib/postgresql/data solr_initializer: container_name: "solr_initializer" @@ -83,8 +83,8 @@ services: - -c - "fix-fs-perms.sh solr && cp -a /template/* /solr-template" volumes: - - ./docker-dev-volumes/solr/data:/var/solr - - ./docker-dev-volumes/solr/conf:/solr-template + - ./data/solr/data:/var/solr + - ./data/solr/conf:/solr-template solr: container_name: "solr" @@ -102,8 +102,8 @@ services: - "collection1" - "/template" volumes: - - ./docker-dev-volumes/solr/data:/var/solr - - ./docker-dev-volumes/solr/conf:/template + - ./data/solr/data:/var/solr + - ./data/solr/conf:/template smtp: container_name: "smtp" @@ -119,7 +119,7 @@ services: networks: - dataverse #volumes: - # - ./docker-dev-volumes/smtp/data:/mail + # - ./data/smtp/data:/mail tmpfs: - /mail:mode=770,size=128M,uid=1000,gid=1000 From d275a6343c0b7d0b296e8dc2d3c158afdd980058 Mon Sep 17 00:00:00 2001 From: raravumich <48064835+raravumich@users.noreply.github.com> Date: Mon, 29 Jan 2024 10:42:23 -0500 Subject: [PATCH 253/689] Add TurboCurator to External Tools list --- .../source/_static/admin/dataverse-external-tools.tsv | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv b/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv index 4f4c29d0670..a20ab864d2a 100644 --- a/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv +++ b/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv @@ -5,3 +5,4 @@ Binder explore dataset Binder allows you to spin up custom computing environment File Previewers explore file "A set of tools that display the content of files - including audio, html, `Hypothes.is `_ annotations, images, PDF, Markdown, text, video, tabular data, spreadsheets, GeoJSON, zip, and NcML files - allowing them to be viewed without downloading the file. The previewers can be run directly from github.io, so the only required step is using the Dataverse API to register the ones you want to use. Documentation, including how to optionally brand the previewers, and an invitation to contribute through github are in the README.md file. Initial development was led by the Qualitative Data Repository and the spreasdheet previewer was added by the Social Sciences and Humanities Open Cloud (SSHOC) project. https://github.com/gdcc/dataverse-previewers" Data Curation Tool configure file "A GUI for curating data by adding labels, groups, weights and other details to assist with informed reuse. See the README.md file at https://github.com/scholarsportal/Dataverse-Data-Curation-Tool for the installation instructions." Ask the Data query file Ask the Data is an experimental tool that allows you ask natural language questions about the data contained in Dataverse tables (tabular data). See the README.md file at https://github.com/IQSS/askdataverse/tree/main/askthedata for the instructions on adding Ask the Data to your Dataverse installation. +TurboCurator by ICPSR configure dataset "TurboCurator generates metadata improvements for title, description, and keywords. It relies on open AI’s ChatGPT & ICPSR best practices. See the `TurboCurator Dataverse Administrator `_ page for more details on how it works and adding TurboCurator to your Dataverse installation." From 1ea4db3f3c011dc8ea28d9eb656e423fdccfccd9 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Tue, 30 Jan 2024 13:02:34 -0500 Subject: [PATCH 254/689] a checklist for making a core field allowMultiples for the dev. guide #9634 --- doc/sphinx-guides/source/developers/index.rst | 1 + .../source/developers/metadatablocksdev.rst | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 doc/sphinx-guides/source/developers/metadatablocksdev.rst diff --git a/doc/sphinx-guides/source/developers/index.rst b/doc/sphinx-guides/source/developers/index.rst index 25fea138736..25007baf589 100755 --- a/doc/sphinx-guides/source/developers/index.rst +++ b/doc/sphinx-guides/source/developers/index.rst @@ -31,6 +31,7 @@ Developer Guide making-releases making-library-releases metadataexport + metadatablocksdev tools unf/index make-data-count diff --git a/doc/sphinx-guides/source/developers/metadatablocksdev.rst b/doc/sphinx-guides/source/developers/metadatablocksdev.rst new file mode 100644 index 00000000000..17093471467 --- /dev/null +++ b/doc/sphinx-guides/source/developers/metadatablocksdev.rst @@ -0,0 +1,26 @@ +=========================== +Metadata Blocks Development +=========================== + +.. contents:: |toctitle| + :local: + +Introduction +------------ + +The idea behind Metadata Blocks in Dataverse is to have everything about the supported metadata fields configurable and customizable. Ideally, this should be accomplished by simply re-importing the updated tsv for the block via the API. In practice, when it comes to the core blocks that are distributed with Dataverse - such as the Citation and Social Science blocks - unfortunately, many dependencies exist in various parts of Dataverse, primarily import and export subsystems, on many specific fields being configured a certain way. This means that code changes may be required whenever a field from one of these core blocks is modified. + +Making a Field Multiple +----------------------- + +Back in 2023, in order to accommodate specific needs of some community member institutions a few fields from Citation and Social Science were changed to support multiple values. (For example, the ``alternativeTitle`` field from the Citation block.) A number of code changes had to be made to accommodate this, plus a number of changes in the sample metadata files that are maintained in the Dataverse code tree. The checklist below is to help another developer should a similar change become necessary in the future. Note that some of the steps below may not apply 1:1 to a different metadata field, depending on how it is exported and imported in various formats by Dataverse. It may help to consult the PR `#9440 `_ as a specific example of the changes that had to be made for the ``alternativeTitle`` field. + +- Change the value from ``FALSE`` to ``TRUE`` in the ``alowmultiples`` column of the .tsv file for the block (obviously). +- Change the value of the ``multiValued`` attribute for the search field in the Solr schema (``conf/solr/9.3.0/schema.xml`` as of writing this). +- Modify the DDI import code (``ImportDDIServiceBean.java``) to support multiple values. (you may be able to use the change in the PR above as a model.) +- Modify the DDI export utility (``DdiExportUtil.java``). +- Modify the OpenAire export utility (``OpenAireExportUtil.java``). +- Modify the following JSON source files in the Dataverse code tree to actually include multiple values for the field (two should be quite enough!): ``scripts/api/data/dataset-create-new-all-default-fields.json``, ``src/test/java/edu/harvard/iq/dataverse/export/dataset-all-defaults.txt``, ``src/test/java/edu/harvard/iq/dataverse/export/ddi/dataset-finch1.json`` and ``src/test/java/edu/harvard/iq/dataverse/export/ddi/dataset-create-new-all-ddi-fields.json``. (These are used as examples for populating datasets via the import API and by the automated import and export code tests). +- Similarly modify the following XML files that are used by the DDI export code tests: ``src/test/java/edu/harvard/iq/dataverse/export/ddi/dataset-finch1.xml`` and ``src/test/java/edu/harvard/iq/dataverse/export/ddi/exportfull.xml``. +- Make sure all the automated Unit and Integration tests are passing. +- Write a short release note to announce the change in the upcoming release. From 2eeda3d910ed128176c75b290c651252722dd919 Mon Sep 17 00:00:00 2001 From: Don Sizemore Date: Tue, 30 Jan 2024 13:08:58 -0500 Subject: [PATCH 255/689] add sleep to SwordIT per qqmyers --- src/test/java/edu/harvard/iq/dataverse/api/SwordIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/SwordIT.java b/src/test/java/edu/harvard/iq/dataverse/api/SwordIT.java index 39156f1c59b..4df6c89411d 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/SwordIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/SwordIT.java @@ -855,7 +855,7 @@ public void testDeleteFiles() { List oneFileLeftInV2Draft = statement3.getBody().xmlPath().getList("feed.entry.id"); logger.info("Number of files remaining in this post version 1 draft:" + oneFileLeftInV2Draft.size()); assertEquals(1, oneFileLeftInV2Draft.size()); - + UtilIT.sleepForLock(datasetPersistentId, "EditInProgress", apiToken, UtilIT.MAXIMUM_PUBLISH_LOCK_DURATION); Response deleteIndex1b = UtilIT.deleteFile(Integer.parseInt(index1b), apiToken); deleteIndex1b.then().assertThat() .statusCode(NO_CONTENT.getStatusCode()); From e4776101e8507a4b470b58ec70e90046516e4fa4 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Tue, 30 Jan 2024 13:16:11 -0500 Subject: [PATCH 256/689] linked the dev. checklist in the metadata customization section of the admin guide. #9634 --- doc/sphinx-guides/source/admin/metadatacustomization.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/sphinx-guides/source/admin/metadatacustomization.rst b/doc/sphinx-guides/source/admin/metadatacustomization.rst index 4f737bd730b..36956567a7d 100644 --- a/doc/sphinx-guides/source/admin/metadatacustomization.rst +++ b/doc/sphinx-guides/source/admin/metadatacustomization.rst @@ -648,6 +648,11 @@ Alternatively, you are welcome to request "edit" access to this "Tips for Datave The thinking is that the tips can become issues and the issues can eventually be worked on as features to improve the Dataverse Software metadata system. +Development Tasks Specific to Changing Fields in Core Metadata Blocks +--------------------------------------------------------------------- + +When it comes to the fields from the core blocks that are distributed with Dataverse (such as Citation and Social Science blocks), code dependencies may exist in Dataverse, primarily in the Import and Export subsystems, on these fields being configured a certain way. So, if it becomes necessary to modify one of such core fields (a real life example is making a single value-only field support multiple values), code changes may be necessary to accompany the change in the block tsv, plus some sample and test files maintained in the Dataverse source tree will need to be adjusted accordingly. An example of a checklist of such tasks is provided in the Development Guide, please see the :doc:`/developers/metadatablocksdev` section. + Footnotes --------- From d960b980f926ba3e1d8ed0336ef3d541ddc6fb50 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Tue, 30 Jan 2024 16:01:55 -0500 Subject: [PATCH 257/689] #9748 comment out disabled test --- src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java index 6f0aa499dd1..2c96ce96dea 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java @@ -432,6 +432,7 @@ public void createToolShellScript() { @Disabled @Test public void createToolDataExplorer() { + /* JsonObjectBuilder job = Json.createObjectBuilder(); job.add("displayName", "Data Explorer"); job.add("description", ""); @@ -466,6 +467,7 @@ public void createToolDataExplorer() { Response deleteExternalTool = UtilIT.deleteExternalTool(toolId); deleteExternalTool.then().assertThat() .statusCode(OK.getStatusCode()); + */ } // both preview and explore From 9b0a3cf2f0c5a6337aaed925ff640651fecf6116 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 30 Jan 2024 16:50:07 -0500 Subject: [PATCH 258/689] rewrite demo page as a tutorial #10238 Also, explain how to create a persona and some basic config. --- .../source/container/running/demo.rst | 169 +++++++++++------- docker/compose/demo/compose.yml | 4 + .../scripts/bootstrap/demo/init.sh | 13 ++ 3 files changed, 126 insertions(+), 60 deletions(-) create mode 100644 modules/container-configbaker/scripts/bootstrap/demo/init.sh diff --git a/doc/sphinx-guides/source/container/running/demo.rst b/doc/sphinx-guides/source/container/running/demo.rst index 5eda108c842..4e2a9db3f48 100644 --- a/doc/sphinx-guides/source/container/running/demo.rst +++ b/doc/sphinx-guides/source/container/running/demo.rst @@ -1,7 +1,7 @@ Demo or Evaluation ================== -If you would like to demo or evaluate Dataverse running in containers, you're in the right place. Your feedback is extremely valuable to us! To let us know what you think, please see :ref:`helping-containers`. +In the following tutorial we'll walk through spinning up Dataverse in containers for demo or evaluation purposes. .. contents:: |toctitle| :local: @@ -9,6 +9,8 @@ If you would like to demo or evaluate Dataverse running in containers, you're in Quickstart ---------- +First, let's confirm that we can get Dataverse running on your system. + - Download :download:`compose.yml <../../../../../docker/compose/demo/compose.yml>` - Run ``docker compose up`` in the directory where you put ``compose.yml`` - Visit http://localhost:8080 and try logging in: @@ -16,106 +18,138 @@ Quickstart - username: dataverseAdmin - password: admin1 -Hardware and Software Requirements ------------------------------------ +If you can log in, great! Please continue through the tutorial. If you have any trouble, please consult the sections below on troubleshooting and getting help. -- 8 GB RAM (if not much else is running) -- Mac, Linux, or Windows (experimental) -- Docker +Stopping and Starting the Containers +------------------------------------ -Windows support is experimental but we are very interested in supporting Windows better. Please report bugs (see :ref:`helping-containers`). +Let's practice stopping the containers and starting them up again. Your data, stored in a directory called ``data``, will remain intact -Tags and Versions ------------------ +To stop the containers hit ``Ctrl-c`` (hold down the ``Ctrl`` key and then hit the ``c`` key). -The compose file references a tag called "alpha", which corresponds to the latest released version of Dataverse. This means that if a release of Dataverse comes out while you are demo'ing or evaluating, the version of Dataverse you are using could change. We are aware that there is a desire for tags that correspond to versions to ensure consistency. You are welcome to join `the discussion `_ and otherwise get in touch (see :ref:`helping-containers`). For more on tags, see :ref:`supported-image-tags-app`. +To start the containers, run ``docker compose up``. -Once Dataverse is running, you can check which version you have through the normal methods: +Deleting Data and Starting Over +------------------------------- -- Check the bottom right in a web browser. -- Check http://localhost:8080/api/info/version via API. +Again, data related to your Dataverse installation such as the database is stored in a directory called ``data`` that gets created in the directory where you ran ``docker compose`` commands. -About the Containers --------------------- +You may reach a point during your demo or evaluation that you'd like to start over with a fresh database. Simply make sure the containers are not running and then remove the ``data`` directory. Now, as before, you can run ``docker compose up`` to spin up the containers. -If you run ``docker ps``, you'll see that multiple containers are spun up in a demo or evaluation. Here are the most important ones: +Configuring Dataverse +--------------------- -- dataverse -- postgres -- solr -- smtp -- bootstrap +Now that you are familiar with the basics of running Dataverse in containers, let's move on to configuration. -Most are self-explanatory, and correspond to components listed under :doc:`/installation/prerequisites` in the (traditional) Installation Guide, but "bootstrap" refers to :doc:`../configbaker-image`. +Start Fresh ++++++++++++ -Additional containers are used in development (see :doc:`../dev-usage`), but for the purposes of a demo or evaluation, fewer moving (sometimes pointy) parts are included. +For this configuration exercise, please start fresh by stopping all containers and removing the ``data`` directory. -Security --------- +Change the Site URL ++++++++++++++++++++ -Please be aware that for now, the "dev" persona is used to bootstrap Dataverse, which means that admin APIs are wide open (to allow developers to test them; see :ref:`securing-your-installation` for more on API blocking), the "create user" key is set to a default value, etc. You can inspect the dev person `on GitHub `_ (look for ``--insecure``). +Edit ``compose.yml`` and change ``_CT_DATAVERSE_SITEURL`` to the URL you plan to use for your installation. -We plan to ship a "demo" persona but it is not ready yet. See also :ref:`configbaker-personas`. +(You can read more about this setting at :ref:`dataverse.siteUrl`.) -Common Operations ------------------ +This is an example of setting an environment variable to configure Dataverse. -Starting the Containers -+++++++++++++++++++++++ +Create and Run a Demo Persona ++++++++++++++++++++++++++++++ -First, download :download:`compose.yml <../../../../../docker/compose/demo/compose.yml>` and place it somewhere you'll remember. +Previously we used the "dev" persona to bootstrap Dataverse, but for security reasons, we should create a persona more suited to demos and evaluations. -Then, run ``docker compose up`` in the directory where you put ``compose.yml`` +Edit the ``compose.yml`` file and look for the following section. -Starting the containers for the first time involves a bootstrap process. You should see "have a nice day" output at the end. +.. code-block:: bash -Stopping the Containers -+++++++++++++++++++++++ + bootstrap: + container_name: "bootstrap" + image: gdcc/configbaker:alpha + restart: "no" + command: + - bootstrap.sh + - dev + #- demo + #volumes: + # - ./demo:/scripts/bootstrap/demo + networks: + - dataverse -You might want to stop the containers if you aren't using them. Hit ``Ctrl-c`` (hold down the ``Ctrl`` key and then hit the ``c`` key). +Comment out "dev" and uncomment "demo". -You data is still intact and you can start the containers again with ``docker compose up``. +Uncomment the "volumes" section. -Deleting the Containers -+++++++++++++++++++++++ +Create a directory called "demo" and copy :download:`init.sh <../../../../../modules/container-configbaker/scripts/bootstrap/demo/init.sh>` into it. You are welcome to edit this demo init script, customizing the final message, for example. -If you no longer need the containers because your demo or evaluation is finished and you want to reclaim disk space, run ``docker compose down`` in the directory where you put ``compose.yml``. +Now run ``docker compose up``. The "bootstrap" container should exit with the message from the init script and Dataverse should be running on http://localhost:8080 as before during the quickstart exercise. -Deleting the Data Directory -+++++++++++++++++++++++++++ +One of the main differences between the "dev" persona and our new "demo" persona is that we are now running the setup-all script without the ``--insecure`` flag. This makes our installation more secure, though it does block "admin" APIs that are useful for configuration. -Data related to the Dataverse containers is placed in a directory called ``data`` next to the ``compose.yml`` file. If you are finished with your demo or evaluation or you want to start fresh, simply delete this directory. +Set DOI Provider to FAKE +++++++++++++++++++++++++ -Configuration -------------- +For the purposes of a demo, we'll use the "FAKE" DOI provider. (For more on this and related settings, see :ref:`pids-configuration` in the Installation Guide.) Without this step, you won't be able to create or publish datasets. -Configuration is described in greater detail under :doc:`/installation/config` in the Installation Guide, but there are some specifics to running in containers you should know about. +Run the following command. (In this context, "dataverse" is the name of the running container.) -.. _configbaker-personas: +``docker exec -it dataverse curl http://localhost:8080/api/admin/settings/:DoiProvider -X PUT -d FAKE`` -Personas -++++++++ +This is an example of configuring a database setting, which you can read more about at :ref:`database-settings` in the Installation Guide. -When the containers are bootstrapped, the "dev" persona is used. In the future we plan to add a "demo" persona that is more suited to demo and evaluation use cases. +Smoke Test +---------- -Database Settings -+++++++++++++++++ +At this point, please try some basic operations within your installation, such as: -Updating database settings is the same as described under :ref:`database-settings` in the Installation Guide. +- logging in as dataverseAdmin +- publishing the "root" collection (dataverse) +- creating a collection +- creating a dataset +- uploading a data file +- publishing the dataset -MPCONFIG Options -++++++++++++++++ +About the Containers +-------------------- -The compose file contains an ``environment`` section with various MicroProfile Config (MPCONFIG) options. You can experiment with this by adding ``DATAVERSE_VERSION: foobar`` to change the (displayed) version of Dataverse to "foobar". +Container List +++++++++++++++ -JVM Options -+++++++++++ +If you run ``docker ps``, you'll see that multiple containers are spun up in a demo or evaluation. Here are the most important ones: -JVM options are not especially easy to change in the container. The general process is to get a shell on the "dataverse" container, change the settings, and then stop and start the containers. See :ref:`jvm-options` for more. +- dataverse +- postgres +- solr +- smtp +- bootstrap + +Most are self-explanatory, and correspond to components listed under :doc:`/installation/prerequisites` in the (traditional) Installation Guide, but "bootstrap" refers to :doc:`../configbaker-image`. + +Additional containers are used in development (see :doc:`../dev-usage`), but for the purposes of a demo or evaluation, fewer moving (sometimes pointy) parts are included. + +Tags and Versions ++++++++++++++++++ + +The compose file references a tag called "alpha", which corresponds to the latest released version of Dataverse. This means that if a release of Dataverse comes out while you are demo'ing or evaluating, the version of Dataverse you are using could change if you do a ``docker pull``. We are aware that there is a desire for tags that correspond to versions to ensure consistency. You are welcome to join `the discussion `_ and otherwise get in touch (see :ref:`helping-containers`). For more on tags, see :ref:`supported-image-tags-app`. + +Once Dataverse is running, you can check which version you have through the normal methods: + +- Check the bottom right in a web browser. +- Check http://localhost:8080/api/info/version via API. Troubleshooting --------------- +Hardware and Software Requirements +++++++++++++++++++++++++++++++++++ + +- 8 GB RAM (if not much else is running) +- Mac, Linux, or Windows (experimental) +- Docker + +Windows support is experimental but we are very interested in supporting Windows better. Please report bugs (see :ref:`helping-containers`). + Bootstrapping Did Not Complete ++++++++++++++++++++++++++++++ @@ -126,6 +160,21 @@ In the compose file, try increasing the timeout in the bootstrap container by ad environment: - TIMEOUT=10m +Wrapping Up +----------- + +Deleting the Containers and Data +++++++++++++++++++++++++++++++++ + +If you no longer need the containers because your demo or evaluation is finished and you want to reclaim disk space, run ``docker compose down`` in the directory where you put ``compose.yml``. + +You might also want to delete the ``data`` directory, as described above. + +Giving Feedback +--------------- + +Your feedback is extremely valuable to us! To let us know what you think, please see :ref:`helping-containers`. + Getting Help ------------ diff --git a/docker/compose/demo/compose.yml b/docker/compose/demo/compose.yml index 3817921f10a..a262f43006a 100644 --- a/docker/compose/demo/compose.yml +++ b/docker/compose/demo/compose.yml @@ -9,6 +9,7 @@ services: restart: on-failure user: payara environment: + _CT_DATAVERSE_SITEURL: "https://demo.example.org" DATAVERSE_DB_HOST: postgres DATAVERSE_DB_PASSWORD: secret DATAVERSE_DB_USER: dataverse @@ -45,6 +46,9 @@ services: command: - bootstrap.sh - dev + #- demo + #volumes: + # - ./demo:/scripts/bootstrap/demo networks: - dataverse diff --git a/modules/container-configbaker/scripts/bootstrap/demo/init.sh b/modules/container-configbaker/scripts/bootstrap/demo/init.sh new file mode 100644 index 00000000000..0e9be7ffef5 --- /dev/null +++ b/modules/container-configbaker/scripts/bootstrap/demo/init.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -euo pipefail + +# Set some defaults as documented +DATAVERSE_URL=${DATAVERSE_URL:-"http://dataverse:8080"} +export DATAVERSE_URL + +echo "Running base setup-all.sh..." +"${BOOTSTRAP_DIR}"/base/setup-all.sh -p=admin1 | tee /tmp/setup-all.sh.out + +echo "" +echo "Done, your instance has been configured for demo or eval. Have a nice day!" From bdc2c8e980ac9878ef472f874098e4f25431592b Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Wed, 31 Jan 2024 10:05:04 -0500 Subject: [PATCH 259/689] #9748 avoid issue with existing tools --- .../edu/harvard/iq/dataverse/api/TestApi.java | 26 +++++++++++++++++++ .../iq/dataverse/api/ExternalToolsIT.java | 15 ++++++----- .../edu/harvard/iq/dataverse/api/UtilIT.java | 15 +++++++++++ 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java b/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java index 87be1f14e05..10510013495 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java @@ -71,5 +71,31 @@ public Response getExternalToolsForFile(@PathParam("id") String idSupplied, @Que return wr.getResponse(); } } + + @Path("files/{id}/externalTool/{toolId}") + @GET + public Response getExternalToolForFileById(@PathParam("id") String idSupplied, @QueryParam("type") String typeSupplied, @PathParam("toolId") String toolId) { + ExternalTool.Type type; + try { + type = ExternalTool.Type.fromString(typeSupplied); + } catch (IllegalArgumentException ex) { + return error(BAD_REQUEST, ex.getLocalizedMessage()); + } + try { + DataFile dataFile = findDataFileOrDie(idSupplied); + List datasetTools = externalToolService.findFileToolsByTypeAndContentType(type, dataFile.getContentType()); + for (ExternalTool tool : datasetTools) { + ApiToken apiToken = externalToolService.getApiToken(getRequestApiKey()); + ExternalToolHandler externalToolHandler = new ExternalToolHandler(tool, dataFile, apiToken, dataFile.getFileMetadata(), null); + JsonObjectBuilder toolToJson = externalToolService.getToolAsJsonWithQueryParameters(externalToolHandler); + if (externalToolService.meetsRequirements(tool, dataFile) && tool.getId().toString().equals(toolId)) { + return ok(toolToJson); + } + } + return error(BAD_REQUEST, "Could not find external tool with id of " + toolId); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + } } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java index 2c96ce96dea..9a280f475a1 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java @@ -101,7 +101,7 @@ public void testFileLevelTool1() { .statusCode(OK.getStatusCode()) .body("data.displayName", CoreMatchers.equalTo("AwesomeTool")); - long toolId = JsonPath.from(addExternalTool.getBody().asString()).getLong("data.id"); + Long toolId = JsonPath.from(addExternalTool.getBody().asString()).getLong("data.id"); Response getTool = UtilIT.getExternalTool(toolId); getTool.prettyPrint(); @@ -115,14 +115,17 @@ public void testFileLevelTool1() { .statusCode(BAD_REQUEST.getStatusCode()) .body("message", CoreMatchers.equalTo("Type must be one of these values: [explore, configure, preview, query].")); - Response getExternalToolsForTabularFiles = UtilIT.getExternalToolsForFile(tabularFileId.toString(), "explore", apiToken); + // Getting tool by tool Id to avoid issue where there are existing tools + String toolIdString = toolId.toString(); + Response getExternalToolsForTabularFiles = UtilIT.getExternalToolForFileById(tabularFileId.toString(), "explore", apiToken, toolIdString); getExternalToolsForTabularFiles.prettyPrint(); + getExternalToolsForTabularFiles.then().assertThat() .statusCode(OK.getStatusCode()) - .body("data[0].displayName", CoreMatchers.equalTo("AwesomeTool")) - .body("data[0].scope", CoreMatchers.equalTo("file")) - .body("data[0].contentType", CoreMatchers.equalTo("text/tab-separated-values")) - .body("data[0].toolUrlWithQueryParams", CoreMatchers.equalTo("http://awesometool.com?fileid=" + tabularFileId + "&key=" + apiToken)); + .body("data.displayName", CoreMatchers.equalTo("AwesomeTool")) + .body("data.scope", CoreMatchers.equalTo("file")) + .body("data.contentType", CoreMatchers.equalTo("text/tab-separated-values")) + .body("data.toolUrlWithQueryParams", CoreMatchers.equalTo("http://awesometool.com?fileid=" + tabularFileId + "&key=" + apiToken)); Response getExternalToolsForJuptyerNotebooks = UtilIT.getExternalToolsForFile(jupyterNotebookFileId.toString(), "explore", apiToken); getExternalToolsForJuptyerNotebooks.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 6af3f8a0a09..ec41248a65f 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -2354,6 +2354,21 @@ static Response getExternalToolsForFile(String idOrPersistentIdOfFile, String ty } return requestSpecification.get("/api/admin/test/files/" + idInPath + "/externalTools?type=" + type + optionalQueryParam); } + + static Response getExternalToolForFileById(String idOrPersistentIdOfFile, String type, String apiToken, String toolId) { + String idInPath = idOrPersistentIdOfFile; // Assume it's a number. + String optionalQueryParam = ""; // If idOrPersistentId is a number we'll just put it in the path. + if (!NumberUtils.isCreatable(idOrPersistentIdOfFile)) { + idInPath = ":persistentId"; + optionalQueryParam = "&persistentId=" + idOrPersistentIdOfFile; + } + RequestSpecification requestSpecification = given(); + if (apiToken != null) { + requestSpecification = given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, apiToken); + } + return requestSpecification.get("/api/admin/test/files/" + idInPath + "/externalTool/" + toolId + "?type=" + type + optionalQueryParam); + } static Response submitFeedback(JsonObjectBuilder job) { return given() From 7d537aa394c447562820cf0343fd6ec2d8a760ca Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Wed, 31 Jan 2024 17:45:01 -0500 Subject: [PATCH 260/689] simplified/reorganized the new dev. checklist for making a core field multiple #9634 --- .../source/admin/metadatacustomization.rst | 19 +++++++++++++- doc/sphinx-guides/source/developers/index.rst | 1 - .../source/developers/metadatablocksdev.rst | 26 ------------------- 3 files changed, 18 insertions(+), 28 deletions(-) delete mode 100644 doc/sphinx-guides/source/developers/metadatablocksdev.rst diff --git a/doc/sphinx-guides/source/admin/metadatacustomization.rst b/doc/sphinx-guides/source/admin/metadatacustomization.rst index 36956567a7d..f97b222b51f 100644 --- a/doc/sphinx-guides/source/admin/metadatacustomization.rst +++ b/doc/sphinx-guides/source/admin/metadatacustomization.rst @@ -651,7 +651,24 @@ The thinking is that the tips can become issues and the issues can eventually be Development Tasks Specific to Changing Fields in Core Metadata Blocks --------------------------------------------------------------------- -When it comes to the fields from the core blocks that are distributed with Dataverse (such as Citation and Social Science blocks), code dependencies may exist in Dataverse, primarily in the Import and Export subsystems, on these fields being configured a certain way. So, if it becomes necessary to modify one of such core fields (a real life example is making a single value-only field support multiple values), code changes may be necessary to accompany the change in the block tsv, plus some sample and test files maintained in the Dataverse source tree will need to be adjusted accordingly. An example of a checklist of such tasks is provided in the Development Guide, please see the :doc:`/developers/metadatablocksdev` section. +When it comes to the fields from the core blocks that are distributed with Dataverse (such as Citation, Social Science and Geospatial blocks), code dependencies may exist in Dataverse, primarily in the Import and Export subsystems, on these fields being configured a certain way. So, if it becomes necessary to modify one of such core fields, code changes may be necessary to accompany the change in the block tsv, plus some sample and test files maintained in the Dataverse source tree will need to be adjusted accordingly. + +Making a Field Multi-Valued +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As a recent real life example, a few fields from the Citation and Social Science block were changed to support multiple values, in order to accommodate specific needs of some community member institutions. A PR for one of these fields, ``alternativeTitle`` from the Citation block is linked below. Each time a number of code changes, plus some changes in the sample metadata files in the Dataverse code tree had to be made. The checklist below is to help another developer in the event that a similar change becomes necessary in the future. Note that some of the steps below may not apply 1:1 to a different metadata field, depending on how it is exported and imported in various formats by Dataverse. It may help to consult the PR `#9440 `_ as a specific example of the changes that had to be made for the ``alternativeTitle`` field. + +- Change the value from ``FALSE`` to ``TRUE`` in the ``alowmultiples`` column of the .tsv file for the block. +- Change the value of the ``multiValued`` attribute for the search field in the Solr schema (``conf/solr/9.3.0/schema.xml`` as of writing this). +- Modify the DDI import code (``ImportDDIServiceBean.java``) to support multiple values. (you may be able to use the change in the PR above as a model.) +- Modify the DDI export utility (``DdiExportUtil.java``). +- Modify the OpenAire export utility (``OpenAireExportUtil.java``). +- Modify the following JSON source files in the Dataverse code tree to actually include multiple values for the field (two should be quite enough!): ``scripts/api/data/dataset-create-new-all-default-fields.json``, ``src/test/java/edu/harvard/iq/dataverse/export/dataset-all-defaults.txt``, ``src/test/java/edu/harvard/iq/dataverse/export/ddi/dataset-finch1.json`` and ``src/test/java/edu/harvard/iq/dataverse/export/ddi/dataset-create-new-all-ddi-fields.json``. (These are used as examples for populating datasets via the import API and by the automated import and export code tests). +- Similarly modify the following XML files that are used by the DDI export code tests: ``src/test/java/edu/harvard/iq/dataverse/export/ddi/dataset-finch1.xml`` and ``src/test/java/edu/harvard/iq/dataverse/export/ddi/exportfull.xml``. +- Make sure all the automated Unit and Integration tests are passing. +- Write a short release note to announce the change in the upcoming release. +- Make a Pull Request. + Footnotes --------- diff --git a/doc/sphinx-guides/source/developers/index.rst b/doc/sphinx-guides/source/developers/index.rst index 25007baf589..25fea138736 100755 --- a/doc/sphinx-guides/source/developers/index.rst +++ b/doc/sphinx-guides/source/developers/index.rst @@ -31,7 +31,6 @@ Developer Guide making-releases making-library-releases metadataexport - metadatablocksdev tools unf/index make-data-count diff --git a/doc/sphinx-guides/source/developers/metadatablocksdev.rst b/doc/sphinx-guides/source/developers/metadatablocksdev.rst deleted file mode 100644 index 17093471467..00000000000 --- a/doc/sphinx-guides/source/developers/metadatablocksdev.rst +++ /dev/null @@ -1,26 +0,0 @@ -=========================== -Metadata Blocks Development -=========================== - -.. contents:: |toctitle| - :local: - -Introduction ------------- - -The idea behind Metadata Blocks in Dataverse is to have everything about the supported metadata fields configurable and customizable. Ideally, this should be accomplished by simply re-importing the updated tsv for the block via the API. In practice, when it comes to the core blocks that are distributed with Dataverse - such as the Citation and Social Science blocks - unfortunately, many dependencies exist in various parts of Dataverse, primarily import and export subsystems, on many specific fields being configured a certain way. This means that code changes may be required whenever a field from one of these core blocks is modified. - -Making a Field Multiple ------------------------ - -Back in 2023, in order to accommodate specific needs of some community member institutions a few fields from Citation and Social Science were changed to support multiple values. (For example, the ``alternativeTitle`` field from the Citation block.) A number of code changes had to be made to accommodate this, plus a number of changes in the sample metadata files that are maintained in the Dataverse code tree. The checklist below is to help another developer should a similar change become necessary in the future. Note that some of the steps below may not apply 1:1 to a different metadata field, depending on how it is exported and imported in various formats by Dataverse. It may help to consult the PR `#9440 `_ as a specific example of the changes that had to be made for the ``alternativeTitle`` field. - -- Change the value from ``FALSE`` to ``TRUE`` in the ``alowmultiples`` column of the .tsv file for the block (obviously). -- Change the value of the ``multiValued`` attribute for the search field in the Solr schema (``conf/solr/9.3.0/schema.xml`` as of writing this). -- Modify the DDI import code (``ImportDDIServiceBean.java``) to support multiple values. (you may be able to use the change in the PR above as a model.) -- Modify the DDI export utility (``DdiExportUtil.java``). -- Modify the OpenAire export utility (``OpenAireExportUtil.java``). -- Modify the following JSON source files in the Dataverse code tree to actually include multiple values for the field (two should be quite enough!): ``scripts/api/data/dataset-create-new-all-default-fields.json``, ``src/test/java/edu/harvard/iq/dataverse/export/dataset-all-defaults.txt``, ``src/test/java/edu/harvard/iq/dataverse/export/ddi/dataset-finch1.json`` and ``src/test/java/edu/harvard/iq/dataverse/export/ddi/dataset-create-new-all-ddi-fields.json``. (These are used as examples for populating datasets via the import API and by the automated import and export code tests). -- Similarly modify the following XML files that are used by the DDI export code tests: ``src/test/java/edu/harvard/iq/dataverse/export/ddi/dataset-finch1.xml`` and ``src/test/java/edu/harvard/iq/dataverse/export/ddi/exportfull.xml``. -- Make sure all the automated Unit and Integration tests are passing. -- Write a short release note to announce the change in the upcoming release. From ad12c7f2ddaf4f6fb1ec5023845d98092df0da47 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 1 Feb 2024 12:28:06 -0500 Subject: [PATCH 261/689] Apply suggestions from code review --- .../source/admin/metadatacustomization.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/sphinx-guides/source/admin/metadatacustomization.rst b/doc/sphinx-guides/source/admin/metadatacustomization.rst index f97b222b51f..841dfd8b3cd 100644 --- a/doc/sphinx-guides/source/admin/metadatacustomization.rst +++ b/doc/sphinx-guides/source/admin/metadatacustomization.rst @@ -658,16 +658,16 @@ Making a Field Multi-Valued As a recent real life example, a few fields from the Citation and Social Science block were changed to support multiple values, in order to accommodate specific needs of some community member institutions. A PR for one of these fields, ``alternativeTitle`` from the Citation block is linked below. Each time a number of code changes, plus some changes in the sample metadata files in the Dataverse code tree had to be made. The checklist below is to help another developer in the event that a similar change becomes necessary in the future. Note that some of the steps below may not apply 1:1 to a different metadata field, depending on how it is exported and imported in various formats by Dataverse. It may help to consult the PR `#9440 `_ as a specific example of the changes that had to be made for the ``alternativeTitle`` field. -- Change the value from ``FALSE`` to ``TRUE`` in the ``alowmultiples`` column of the .tsv file for the block. -- Change the value of the ``multiValued`` attribute for the search field in the Solr schema (``conf/solr/9.3.0/schema.xml`` as of writing this). -- Modify the DDI import code (``ImportDDIServiceBean.java``) to support multiple values. (you may be able to use the change in the PR above as a model.) +- Change the value from ``FALSE`` to ``TRUE`` in the ``allowmultiples`` column of the .tsv file for the block. +- Change the value of the ``multiValued`` attribute for the search field in the Solr schema (``conf/solr/x.x.x/schema.xml``). +- Modify the DDI import code (``ImportDDIServiceBean.java``) to support multiple values. (You may be able to use the change in the PR above as a model.) - Modify the DDI export utility (``DdiExportUtil.java``). - Modify the OpenAire export utility (``OpenAireExportUtil.java``). - Modify the following JSON source files in the Dataverse code tree to actually include multiple values for the field (two should be quite enough!): ``scripts/api/data/dataset-create-new-all-default-fields.json``, ``src/test/java/edu/harvard/iq/dataverse/export/dataset-all-defaults.txt``, ``src/test/java/edu/harvard/iq/dataverse/export/ddi/dataset-finch1.json`` and ``src/test/java/edu/harvard/iq/dataverse/export/ddi/dataset-create-new-all-ddi-fields.json``. (These are used as examples for populating datasets via the import API and by the automated import and export code tests). - Similarly modify the following XML files that are used by the DDI export code tests: ``src/test/java/edu/harvard/iq/dataverse/export/ddi/dataset-finch1.xml`` and ``src/test/java/edu/harvard/iq/dataverse/export/ddi/exportfull.xml``. -- Make sure all the automated Unit and Integration tests are passing. +- Make sure all the automated unit and integration tests are passing. - Write a short release note to announce the change in the upcoming release. -- Make a Pull Request. +- Make a pull request. Footnotes From e064313c4c11fbec2bf875d0f8dbe98b99013fca Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 1 Feb 2024 12:31:01 -0500 Subject: [PATCH 262/689] add refs to dev guide #9634 --- doc/sphinx-guides/source/admin/metadatacustomization.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/sphinx-guides/source/admin/metadatacustomization.rst b/doc/sphinx-guides/source/admin/metadatacustomization.rst index 841dfd8b3cd..5bd28bfa103 100644 --- a/doc/sphinx-guides/source/admin/metadatacustomization.rst +++ b/doc/sphinx-guides/source/admin/metadatacustomization.rst @@ -665,8 +665,8 @@ As a recent real life example, a few fields from the Citation and Social Science - Modify the OpenAire export utility (``OpenAireExportUtil.java``). - Modify the following JSON source files in the Dataverse code tree to actually include multiple values for the field (two should be quite enough!): ``scripts/api/data/dataset-create-new-all-default-fields.json``, ``src/test/java/edu/harvard/iq/dataverse/export/dataset-all-defaults.txt``, ``src/test/java/edu/harvard/iq/dataverse/export/ddi/dataset-finch1.json`` and ``src/test/java/edu/harvard/iq/dataverse/export/ddi/dataset-create-new-all-ddi-fields.json``. (These are used as examples for populating datasets via the import API and by the automated import and export code tests). - Similarly modify the following XML files that are used by the DDI export code tests: ``src/test/java/edu/harvard/iq/dataverse/export/ddi/dataset-finch1.xml`` and ``src/test/java/edu/harvard/iq/dataverse/export/ddi/exportfull.xml``. -- Make sure all the automated unit and integration tests are passing. -- Write a short release note to announce the change in the upcoming release. +- Make sure all the automated unit and integration tests are passing. See :doc:`/developers/testing` in the Developer Guide. +- Write a short release note to announce the change in the upcoming release. See :ref:`writing-release-note-snippets` in the Developer Guide. - Make a pull request. From 89739bc39542930546c807c2236033b7da790688 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 1 Feb 2024 16:37:58 -0500 Subject: [PATCH 263/689] use --insecure and secure later #10238 Using --insecure at first and then doing securing APIs, etc later (like non --insecure does) seems like the best option for now. It allows us to simplify the tutorial and set up an unblock key for later use. --- .../source/container/running/demo.rst | 96 +++++++++++++------ .../scripts/bootstrap/demo/init.sh | 30 +++++- 2 files changed, 94 insertions(+), 32 deletions(-) diff --git a/doc/sphinx-guides/source/container/running/demo.rst b/doc/sphinx-guides/source/container/running/demo.rst index 4e2a9db3f48..24027e677a1 100644 --- a/doc/sphinx-guides/source/container/running/demo.rst +++ b/doc/sphinx-guides/source/container/running/demo.rst @@ -36,27 +36,18 @@ Again, data related to your Dataverse installation such as the database is store You may reach a point during your demo or evaluation that you'd like to start over with a fresh database. Simply make sure the containers are not running and then remove the ``data`` directory. Now, as before, you can run ``docker compose up`` to spin up the containers. -Configuring Dataverse +Setting Up for a Demo --------------------- -Now that you are familiar with the basics of running Dataverse in containers, let's move on to configuration. +Now that you are familiar with the basics of running Dataverse in containers, let's move on to a better setup for a demo or evaluation. -Start Fresh -+++++++++++ - -For this configuration exercise, please start fresh by stopping all containers and removing the ``data`` directory. - -Change the Site URL -+++++++++++++++++++ - -Edit ``compose.yml`` and change ``_CT_DATAVERSE_SITEURL`` to the URL you plan to use for your installation. - -(You can read more about this setting at :ref:`dataverse.siteUrl`.) +Starting Fresh +++++++++++++++ -This is an example of setting an environment variable to configure Dataverse. +For this exercise, please start fresh by stopping all containers and removing the ``data`` directory. -Create and Run a Demo Persona -+++++++++++++++++++++++++++++ +Creating and Running a Demo Persona ++++++++++++++++++++++++++++++++++++ Previously we used the "dev" persona to bootstrap Dataverse, but for security reasons, we should create a persona more suited to demos and evaluations. @@ -83,36 +74,81 @@ Uncomment the "volumes" section. Create a directory called "demo" and copy :download:`init.sh <../../../../../modules/container-configbaker/scripts/bootstrap/demo/init.sh>` into it. You are welcome to edit this demo init script, customizing the final message, for example. +Note that the init script contains a key for using the admin API once it is blocked. You should change it in the script from "unblockme" to something only you know. + Now run ``docker compose up``. The "bootstrap" container should exit with the message from the init script and Dataverse should be running on http://localhost:8080 as before during the quickstart exercise. One of the main differences between the "dev" persona and our new "demo" persona is that we are now running the setup-all script without the ``--insecure`` flag. This makes our installation more secure, though it does block "admin" APIs that are useful for configuration. -Set DOI Provider to FAKE -++++++++++++++++++++++++ +Smoke Testing +------------- + +At this point, please try the following basic operations within your installation: + +- logging in as dataverseAdmin (password "admin1") +- publishing the "root" collection (dataverse) +- creating a collection +- creating a dataset +- uploading a data file +- publishing the dataset + +If anything isn't working, please see the sections below on troubleshooting, giving feedback, and getting help. + +Further Configuration +--------------------- + +Now that we've verified through a smoke test that basic operations are working, let's configure our installation of Dataverse. + +Please refer to the :doc:`/installation/config` section of the Installation Guide for various configuration options. -For the purposes of a demo, we'll use the "FAKE" DOI provider. (For more on this and related settings, see :ref:`pids-configuration` in the Installation Guide.) Without this step, you won't be able to create or publish datasets. +Below we'll explain some specifics for configuration in containers. -Run the following command. (In this context, "dataverse" is the name of the running container.) +JVM Options/MicroProfile Config ++++++++++++++++++++++++++++++++ -``docker exec -it dataverse curl http://localhost:8080/api/admin/settings/:DoiProvider -X PUT -d FAKE`` +:ref:`jvm-options` can be configured under ``JVM_ARGS`` in the ``compose.yml`` file. Here's an example: + +.. code-block:: bash -This is an example of configuring a database setting, which you can read more about at :ref:`database-settings` in the Installation Guide. + environment: + JVM_ARGS: -Ddataverse.files.storage-driver-id=file1 -Smoke Test +Some JVM options can be configured as environment variables. For example, you can configure the database host like this: + +.. code-block:: bash + + environment: + DATAVERSE_DB_HOST: postgres + +We are in the process of making more JVM options configurable as environment variables. Look for the term "MicroProfile Config" in under :doc:`/installation/config` in the Installation Guide to know if you can use them this way. + +Please note that for a few environment variables (the ones that start with ``%ct`` in :download:`microprofile-config.properties <../../../../../src/main/resources/META-INF/microprofile-config.properties>`), you have to prepend ``_CT_`` to make, for example, ``_CT_DATAVERSE_SITEURL``. We are working on a fix for this in https://github.com/IQSS/dataverse/issues/10285. + +There is a final way to configure JVM options that we plan to deprecate once all JVM options have been converted to MicroProfile Config. Look for "magic trick" under "tunables" at :doc:`../app-image` for more information. + +Database Settings ++++++++++++++++++ + +Generally, you should be able to look at the list of :ref:`database-settings` and configure them but the "demo" persona above secured your installation to the point that you'll need an "unblock key" to access the "admin" API and change database settings. + +In the example below of configuring :ref:`:FooterCopyright` we use the default unblock key of "unblockme" but you should use the key you set above. + +``curl -X PUT -d ", My Org" "http://localhost:8080/api/admin/settings/:FooterCopyright?unblock-key=unblockme"`` + +One you make this change it should be visible in the copyright in the bottom left of every page. + +Next Steps ---------- -At this point, please try some basic operations within your installation, such as: +From here, you are encouraged to continue poking around, configuring, and testing. You probably spend a lot of time reading the :doc:`/installation/config` section of the Installation Guide. -- logging in as dataverseAdmin -- publishing the "root" collection (dataverse) -- creating a collection -- creating a dataset -- uploading a data file -- publishing the dataset +Please consider giving feedback using the methods described below. Good luck with your demo! About the Containers -------------------- +Now that you've gone through the tutorial, you might be interested in the various containers you've spun up and what they do. + Container List ++++++++++++++ diff --git a/modules/container-configbaker/scripts/bootstrap/demo/init.sh b/modules/container-configbaker/scripts/bootstrap/demo/init.sh index 0e9be7ffef5..e8d1d07dd2d 100644 --- a/modules/container-configbaker/scripts/bootstrap/demo/init.sh +++ b/modules/container-configbaker/scripts/bootstrap/demo/init.sh @@ -2,12 +2,38 @@ set -euo pipefail -# Set some defaults as documented +# Set some defaults DATAVERSE_URL=${DATAVERSE_URL:-"http://dataverse:8080"} export DATAVERSE_URL +BLOCKED_API_KEY=${BLOCKED_API_KEY:-"unblockme"} +export BLOCKED_API_KEY + +# --insecure is used so we can configure a few things but +# later in this script we'll apply the changes as if we had +# run the script without --insecure. echo "Running base setup-all.sh..." -"${BOOTSTRAP_DIR}"/base/setup-all.sh -p=admin1 | tee /tmp/setup-all.sh.out +"${BOOTSTRAP_DIR}"/base/setup-all.sh --insecure -p=admin1 | tee /tmp/setup-all.sh.out + +echo "" +echo "Setting DOI provider to \"FAKE\"..." +curl -sS -X PUT -d FAKE "${DATAVERSE_URL}/api/admin/settings/:DoiProvider" + +echo "" +echo "Revoke the key that allows for creation of builtin users..." +curl -sS -X DELETE "${DATAVERSE_URL}/api/admin/settings/BuiltinUsers.KEY" + +echo "" +echo "Set key for accessing blocked API endpoints..." +curl -sS -X PUT -d "$BLOCKED_API_KEY" "${DATAVERSE_URL}/api/admin/settings/:BlockedApiKey" + +echo "" +echo "Set policy to only allow access to admin APIs with with a key..." +curl -sS -X PUT -d unblock-key "${DATAVERSE_URL}/api/admin/settings/:BlockedApiPolicy" + +echo "" +echo "Block admin and other sensitive API endpoints..." +curl -sS -X PUT -d 'admin,builtin-users' "${DATAVERSE_URL}/api/admin/settings/:BlockedApiEndpoints" echo "" echo "Done, your instance has been configured for demo or eval. Have a nice day!" From c8f71f16d41c83586bd4572fd2e4bcf9f8b3962b Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Fri, 2 Feb 2024 16:15:17 -0500 Subject: [PATCH 264/689] Update metadatacustomization.rst The /tree seems to be just a reference for the GitHub URL but the project doesn't have a "tree" directory so probably would be better or less confusing to reference the root of the project. Also the property files are in a different location than the one specified on the Documentation. --- doc/sphinx-guides/source/admin/metadatacustomization.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/sphinx-guides/source/admin/metadatacustomization.rst b/doc/sphinx-guides/source/admin/metadatacustomization.rst index 5bd28bfa103..c9cb3c47f85 100644 --- a/doc/sphinx-guides/source/admin/metadatacustomization.rst +++ b/doc/sphinx-guides/source/admin/metadatacustomization.rst @@ -37,8 +37,8 @@ tab-separated value (TSV). [1]_\ :sup:`,`\ [2]_ While it is technically possible to define more than one metadata block in a TSV file, it is good organizational practice to define only one in each file. -The metadata block TSVs shipped with the Dataverse Software are in `/tree/develop/scripts/api/data/metadatablocks -`__ and the corresponding ResourceBundle property files `/tree/develop/src/main/java `__ of the Dataverse Software GitHub repo. Human-readable copies are available in `this Google Sheets +The metadata block TSVs shipped with the Dataverse Software are in `/src/scripts/api/data/metadatablocks +`__ and the corresponding ResourceBundle property files `/src/main/java/propertyFiles `__ of the Dataverse Software GitHub repo. Human-readable copies are available in `this Google Sheets document `__ but they tend to get out of sync with the TSV files, which should be considered authoritative. The Dataverse Software installation process operates on the TSVs, not the Google spreadsheet. About the metadata block TSV From 2978080e5299d91d340ff926ec2a3a33a81b40df Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 2 Feb 2024 16:50:20 -0500 Subject: [PATCH 265/689] Update metadatacustomization.rst --- doc/sphinx-guides/source/admin/metadatacustomization.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/sphinx-guides/source/admin/metadatacustomization.rst b/doc/sphinx-guides/source/admin/metadatacustomization.rst index c9cb3c47f85..78eadd9b2ce 100644 --- a/doc/sphinx-guides/source/admin/metadatacustomization.rst +++ b/doc/sphinx-guides/source/admin/metadatacustomization.rst @@ -37,8 +37,8 @@ tab-separated value (TSV). [1]_\ :sup:`,`\ [2]_ While it is technically possible to define more than one metadata block in a TSV file, it is good organizational practice to define only one in each file. -The metadata block TSVs shipped with the Dataverse Software are in `/src/scripts/api/data/metadatablocks -`__ and the corresponding ResourceBundle property files `/src/main/java/propertyFiles `__ of the Dataverse Software GitHub repo. Human-readable copies are available in `this Google Sheets +The metadata block TSVs shipped with the Dataverse Software are in `/scripts/api/data/metadatablocks +`__ with the corresponding ResourceBundle property files in `/src/main/java/propertyFiles `__ of the Dataverse Software GitHub repo. Human-readable copies are available in `this Google Sheets document `__ but they tend to get out of sync with the TSV files, which should be considered authoritative. The Dataverse Software installation process operates on the TSVs, not the Google spreadsheet. About the metadata block TSV From 24daf553ecdbc7811737da58d6a41b6294a98434 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Fri, 2 Feb 2024 16:53:24 -0500 Subject: [PATCH 266/689] Update metadatacustomization.rst As @qqmyers pointed these are not on /src --- doc/sphinx-guides/source/admin/metadatacustomization.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/admin/metadatacustomization.rst b/doc/sphinx-guides/source/admin/metadatacustomization.rst index c9cb3c47f85..4920859d716 100644 --- a/doc/sphinx-guides/source/admin/metadatacustomization.rst +++ b/doc/sphinx-guides/source/admin/metadatacustomization.rst @@ -37,7 +37,7 @@ tab-separated value (TSV). [1]_\ :sup:`,`\ [2]_ While it is technically possible to define more than one metadata block in a TSV file, it is good organizational practice to define only one in each file. -The metadata block TSVs shipped with the Dataverse Software are in `/src/scripts/api/data/metadatablocks +The metadata block TSVs shipped with the Dataverse Software are in `/scripts/api/data/metadatablocks `__ and the corresponding ResourceBundle property files `/src/main/java/propertyFiles `__ of the Dataverse Software GitHub repo. Human-readable copies are available in `this Google Sheets document `__ but they tend to get out of sync with the TSV files, which should be considered authoritative. The Dataverse Software installation process operates on the TSVs, not the Google spreadsheet. From 7c248239c260e56c2c7e162b0ddfafda1af7d9f6 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Fri, 2 Feb 2024 19:12:59 -0500 Subject: [PATCH 267/689] Fix line break --- doc/sphinx-guides/source/admin/metadatacustomization.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/sphinx-guides/source/admin/metadatacustomization.rst b/doc/sphinx-guides/source/admin/metadatacustomization.rst index 7d6e0c4c5c1..f518c7eb802 100644 --- a/doc/sphinx-guides/source/admin/metadatacustomization.rst +++ b/doc/sphinx-guides/source/admin/metadatacustomization.rst @@ -40,7 +40,6 @@ good organizational practice to define only one in each file. The metadata block TSVs shipped with the Dataverse Software are in `/scripts/api/data/metadatablocks `__ with the corresponding ResourceBundle property files in `/src/main/java/propertyFiles `__ of the Dataverse Software GitHub repo. Human-readable copies are available in `this Google Sheets - document `__ but they tend to get out of sync with the TSV files, which should be considered authoritative. The Dataverse Software installation process operates on the TSVs, not the Google spreadsheet. About the metadata block TSV From 59f1560daa77404c602029e2112546b00f9f19f2 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Fri, 2 Feb 2024 19:16:02 -0500 Subject: [PATCH 268/689] Fix incorrect line break that cause build fail --- doc/sphinx-guides/source/admin/metadatacustomization.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/sphinx-guides/source/admin/metadatacustomization.rst b/doc/sphinx-guides/source/admin/metadatacustomization.rst index f518c7eb802..78eadd9b2ce 100644 --- a/doc/sphinx-guides/source/admin/metadatacustomization.rst +++ b/doc/sphinx-guides/source/admin/metadatacustomization.rst @@ -38,7 +38,6 @@ possible to define more than one metadata block in a TSV file, it is good organizational practice to define only one in each file. The metadata block TSVs shipped with the Dataverse Software are in `/scripts/api/data/metadatablocks - `__ with the corresponding ResourceBundle property files in `/src/main/java/propertyFiles `__ of the Dataverse Software GitHub repo. Human-readable copies are available in `this Google Sheets document `__ but they tend to get out of sync with the TSV files, which should be considered authoritative. The Dataverse Software installation process operates on the TSVs, not the Google spreadsheet. From 77951683a2f495e04098125a81945dc076d80b4b Mon Sep 17 00:00:00 2001 From: raravumich <48064835+raravumich@users.noreply.github.com> Date: Mon, 5 Feb 2024 10:33:46 -0500 Subject: [PATCH 269/689] added tabs --- .../source/_static/admin/dataverse-external-tools.tsv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv b/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv index a20ab864d2a..05263498977 100644 --- a/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv +++ b/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv @@ -5,4 +5,4 @@ Binder explore dataset Binder allows you to spin up custom computing environment File Previewers explore file "A set of tools that display the content of files - including audio, html, `Hypothes.is `_ annotations, images, PDF, Markdown, text, video, tabular data, spreadsheets, GeoJSON, zip, and NcML files - allowing them to be viewed without downloading the file. The previewers can be run directly from github.io, so the only required step is using the Dataverse API to register the ones you want to use. Documentation, including how to optionally brand the previewers, and an invitation to contribute through github are in the README.md file. Initial development was led by the Qualitative Data Repository and the spreasdheet previewer was added by the Social Sciences and Humanities Open Cloud (SSHOC) project. https://github.com/gdcc/dataverse-previewers" Data Curation Tool configure file "A GUI for curating data by adding labels, groups, weights and other details to assist with informed reuse. See the README.md file at https://github.com/scholarsportal/Dataverse-Data-Curation-Tool for the installation instructions." Ask the Data query file Ask the Data is an experimental tool that allows you ask natural language questions about the data contained in Dataverse tables (tabular data). See the README.md file at https://github.com/IQSS/askdataverse/tree/main/askthedata for the instructions on adding Ask the Data to your Dataverse installation. -TurboCurator by ICPSR configure dataset "TurboCurator generates metadata improvements for title, description, and keywords. It relies on open AI’s ChatGPT & ICPSR best practices. See the `TurboCurator Dataverse Administrator `_ page for more details on how it works and adding TurboCurator to your Dataverse installation." +TurboCurator by ICPSR configure dataset "TurboCurator generates metadata improvements for title, description, and keywords. It relies on open AI’s ChatGPT & ICPSR best practices. See the `TurboCurator Dataverse Administrator `_ page for more details on how it works and adding TurboCurator to your Dataverse installation." From 905c8cf906857feb2e7231f31c1a2e224b33d26b Mon Sep 17 00:00:00 2001 From: raravumich <48064835+raravumich@users.noreply.github.com> Date: Mon, 5 Feb 2024 10:36:27 -0500 Subject: [PATCH 270/689] added correct tabs --- .../source/_static/admin/dataverse-external-tools.tsv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv b/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv index 05263498977..10f9a6a6062 100644 --- a/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv +++ b/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv @@ -5,4 +5,4 @@ Binder explore dataset Binder allows you to spin up custom computing environment File Previewers explore file "A set of tools that display the content of files - including audio, html, `Hypothes.is `_ annotations, images, PDF, Markdown, text, video, tabular data, spreadsheets, GeoJSON, zip, and NcML files - allowing them to be viewed without downloading the file. The previewers can be run directly from github.io, so the only required step is using the Dataverse API to register the ones you want to use. Documentation, including how to optionally brand the previewers, and an invitation to contribute through github are in the README.md file. Initial development was led by the Qualitative Data Repository and the spreasdheet previewer was added by the Social Sciences and Humanities Open Cloud (SSHOC) project. https://github.com/gdcc/dataverse-previewers" Data Curation Tool configure file "A GUI for curating data by adding labels, groups, weights and other details to assist with informed reuse. See the README.md file at https://github.com/scholarsportal/Dataverse-Data-Curation-Tool for the installation instructions." Ask the Data query file Ask the Data is an experimental tool that allows you ask natural language questions about the data contained in Dataverse tables (tabular data). See the README.md file at https://github.com/IQSS/askdataverse/tree/main/askthedata for the instructions on adding Ask the Data to your Dataverse installation. -TurboCurator by ICPSR configure dataset "TurboCurator generates metadata improvements for title, description, and keywords. It relies on open AI’s ChatGPT & ICPSR best practices. See the `TurboCurator Dataverse Administrator `_ page for more details on how it works and adding TurboCurator to your Dataverse installation." +TurboCurator by ICPSR configure dataset "TurboCurator generates metadata improvements for title, description, and keywords. It relies on open AI’s ChatGPT & ICPSR best practices. See the `TurboCurator Dataverse Administrator `_ page for more details on how it works and adding TurboCurator to your Dataverse installation." From 5760c259ae493ce3670eefcd850480e5106133ef Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Mon, 5 Feb 2024 15:11:55 -0500 Subject: [PATCH 271/689] fix formatting #10279 --- .../source/_static/admin/dataverse-external-tools.tsv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv b/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv index 10f9a6a6062..c22392a7c5e 100644 --- a/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv +++ b/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv @@ -5,4 +5,4 @@ Binder explore dataset Binder allows you to spin up custom computing environment File Previewers explore file "A set of tools that display the content of files - including audio, html, `Hypothes.is `_ annotations, images, PDF, Markdown, text, video, tabular data, spreadsheets, GeoJSON, zip, and NcML files - allowing them to be viewed without downloading the file. The previewers can be run directly from github.io, so the only required step is using the Dataverse API to register the ones you want to use. Documentation, including how to optionally brand the previewers, and an invitation to contribute through github are in the README.md file. Initial development was led by the Qualitative Data Repository and the spreasdheet previewer was added by the Social Sciences and Humanities Open Cloud (SSHOC) project. https://github.com/gdcc/dataverse-previewers" Data Curation Tool configure file "A GUI for curating data by adding labels, groups, weights and other details to assist with informed reuse. See the README.md file at https://github.com/scholarsportal/Dataverse-Data-Curation-Tool for the installation instructions." Ask the Data query file Ask the Data is an experimental tool that allows you ask natural language questions about the data contained in Dataverse tables (tabular data). See the README.md file at https://github.com/IQSS/askdataverse/tree/main/askthedata for the instructions on adding Ask the Data to your Dataverse installation. -TurboCurator by ICPSR configure dataset "TurboCurator generates metadata improvements for title, description, and keywords. It relies on open AI’s ChatGPT & ICPSR best practices. See the `TurboCurator Dataverse Administrator `_ page for more details on how it works and adding TurboCurator to your Dataverse installation." +TurboCurator by ICPSR configure dataset TurboCurator generates metadata improvements for title, description, and keywords. It relies on open AI's ChatGPT & ICPSR best practices. See the `TurboCurator Dataverse Administrator `_ page for more details on how it works and adding TurboCurator to your Dataverse installation. From a92560059ecd18a081a063a08f4c5a998fb1e3d4 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Mon, 5 Feb 2024 19:33:33 -0500 Subject: [PATCH 272/689] Fix to provide latest version metadata --- src/main/java/edu/harvard/iq/dataverse/api/Datasets.java | 2 +- src/test/java/edu/harvard/iq/dataverse/api/SearchIT.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index ea74368d110..e3505cbbb33 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -778,7 +778,7 @@ public Response getVersionJsonLDMetadata(@Context ContainerRequestContext crc, @ @Path("{id}/metadata") @Produces("application/ld+json, application/json-ld") public Response getVersionJsonLDMetadata(@Context ContainerRequestContext crc, @PathParam("id") String id, @Context UriInfo uriInfo, @Context HttpHeaders headers) { - return getVersionJsonLDMetadata(crc, id, DS_VERSION_DRAFT, uriInfo, headers); + return getVersionJsonLDMetadata(crc, id, DS_VERSION_LATEST, uriInfo, headers); } @PUT diff --git a/src/test/java/edu/harvard/iq/dataverse/api/SearchIT.java b/src/test/java/edu/harvard/iq/dataverse/api/SearchIT.java index 125753296a2..cd292a40a1e 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/SearchIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/SearchIT.java @@ -1202,7 +1202,7 @@ public void testGeospatialSearch() { .add("value", "42.33661") .add("typeClass", "primitive") .add("multiple", false) - .add("typeName", "southLongitude") + .add("typeName", "southLongitud e") ) .add("eastLongitude", Json.createObjectBuilder() From f4b94837a8dbfa1f657ab31ba66a83c5abd4d5e7 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 6 Feb 2024 12:32:57 +0000 Subject: [PATCH 273/689] Stash: getFileData by datasetVersionId param WIP --- .../edu/harvard/iq/dataverse/api/Files.java | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index 5d400ee1438..6a9b1803583 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -4,7 +4,6 @@ import com.google.gson.JsonObject; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.DataFileServiceBean; -import edu.harvard.iq.dataverse.DataFileTag; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetLock; import edu.harvard.iq.dataverse.DatasetServiceBean; @@ -13,7 +12,6 @@ import edu.harvard.iq.dataverse.DataverseRequestServiceBean; import edu.harvard.iq.dataverse.DataverseServiceBean; import edu.harvard.iq.dataverse.EjbDataverseEngine; -import edu.harvard.iq.dataverse.FileDownloadServiceBean; import edu.harvard.iq.dataverse.FileMetadata; import edu.harvard.iq.dataverse.GuestbookResponseServiceBean; import edu.harvard.iq.dataverse.TermsOfUseAndAccessValidator; @@ -51,6 +49,7 @@ import edu.harvard.iq.dataverse.util.SystemConfig; import edu.harvard.iq.dataverse.util.URLTokenUtil; +import static edu.harvard.iq.dataverse.api.ApiConstants.DS_VERSION_DRAFT; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; import edu.harvard.iq.dataverse.util.json.JsonUtil; import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; @@ -81,7 +80,6 @@ import static edu.harvard.iq.dataverse.util.json.JsonPrinter.jsonDT; import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; -import static jakarta.ws.rs.core.Response.Status.FORBIDDEN; import jakarta.ws.rs.core.UriInfo; import org.glassfish.jersey.media.multipart.FormDataBodyPart; @@ -500,22 +498,22 @@ public Response updateFileMetadata(@Context ContainerRequestContext crc, @FormDa .type(MediaType.TEXT_PLAIN) //Our plain text string is already json .build(); } - + @GET @AuthRequired - @Path("{id}/draft") - public Response getFileDataDraft(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) throws WrappedResponse, Exception { - return getFileDataResponse(getRequestUser(crc), fileIdOrPersistentId, uriInfo, headers, response, true); + @Path("{id}") + public Response getFileData(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @Context UriInfo uriInfo, @Context HttpHeaders headers) { + return getFileDataResponse(getRequestUser(crc), fileIdOrPersistentId, uriInfo, headers, null); } - + @GET @AuthRequired - @Path("{id}") - public Response getFileData(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) throws WrappedResponse, Exception { - return getFileDataResponse(getRequestUser(crc), fileIdOrPersistentId, uriInfo, headers, response, false); + @Path("{id}/{datasetVersionId}") + public Response getFileData(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @PathParam("datasetVersionId") String datasetVersionId, @Context UriInfo uriInfo, @Context HttpHeaders headers) { + return getFileDataResponse(getRequestUser(crc), fileIdOrPersistentId, uriInfo, headers, datasetVersionId); } - - private Response getFileDataResponse(User user, String fileIdOrPersistentId, UriInfo uriInfo, HttpHeaders headers, HttpServletResponse response, boolean draft ){ + + private Response getFileDataResponse(User user, String fileIdOrPersistentId, UriInfo uriInfo, HttpHeaders headers, String datasetVersionId){ DataverseRequest req; try { @@ -532,7 +530,7 @@ private Response getFileDataResponse(User user, String fileIdOrPersistentId, Uri FileMetadata fm; - if (draft) { + if (datasetVersionId.equals(DS_VERSION_DRAFT)) { try { fm = execCommand(new GetDraftFileMetadataIfAvailableCommand(req, df)); } catch (WrappedResponse w) { @@ -547,7 +545,7 @@ private Response getFileDataResponse(User user, String fileIdOrPersistentId, Uri try { fm = df.getLatestPublishedFileMetadata(); - + } catch (UnsupportedOperationException e) { try { fm = execCommand(new GetDraftFileMetadataIfAvailableCommand(req, df)); From ae9b74fd4592103e1c8135655d312bb7ef0c24d7 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Tue, 6 Feb 2024 09:27:09 -0500 Subject: [PATCH 274/689] #10229 fix popup list --- src/main/java/edu/harvard/iq/dataverse/DataversePage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataversePage.java b/src/main/java/edu/harvard/iq/dataverse/DataversePage.java index 943a74327d5..3dbc22902b0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataversePage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataversePage.java @@ -362,7 +362,7 @@ public void initFeaturedDataverses() { List featuredSource = new ArrayList<>(); List featuredTarget = new ArrayList<>(); featuredSource.addAll(dataverseService.findAllPublishedByOwnerId(dataverse.getId())); - featuredSource.addAll(linkingService.findLinkingDataverses(dataverse.getId())); + featuredSource.addAll(linkingService.findLinkedDataverses(dataverse.getId())); List featuredList = featuredDataverseService.findByDataverseId(dataverse.getId()); for (DataverseFeaturedDataverse dfd : featuredList) { Dataverse fd = dfd.getFeaturedDataverse(); From 4309ab06308f1be2333dcf40bc0bda3c11022437 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Tue, 6 Feb 2024 09:34:01 -0500 Subject: [PATCH 275/689] #10229 add to error message --- 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 157f2ecaf54..f1c8381816c 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -875,7 +875,7 @@ dataverse.option.deleteDataverse=Delete Dataverse dataverse.publish.btn=Publish dataverse.publish.header=Publish Dataverse dataverse.nopublished=No Published Dataverses -dataverse.nopublished.tip=In order to use this feature you must have at least one published dataverse. +dataverse.nopublished.tip=In order to use this feature you must have at least one published or linked dataverse. dataverse.contact=Email Dataverse Contact dataverse.link=Link Dataverse dataverse.link.btn.tip=Link to Your Dataverse From 2f7ce01fd67539a9213d87884dc229e689a055da Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 6 Feb 2024 10:38:44 -0500 Subject: [PATCH 276/689] Add to DatasetsIT testSemanticMetadataAPIs test cases for published and draft --- .../harvard/iq/dataverse/api/DatasetsIT.java | 60 +++++++++++++++++-- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index 6e6855306e4..e1c4b901116 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -3013,6 +3013,46 @@ public void testSemanticMetadataAPIs() { response = UtilIT.updateDatasetJsonLDMetadata(datasetId, apiToken, badTerms, false); response.then().assertThat().statusCode(BAD_REQUEST.getStatusCode()); + + //We publish the dataset and dataverse + UtilIT.publishDataverseViaNativeApi(dataverseAlias, apiToken).then().assertThat().statusCode(OK.getStatusCode()); + UtilIT.publishDatasetViaNativeApi(datasetId, "major", apiToken).then().assertThat().statusCode(OK.getStatusCode()); + + //We check the version is published + response = UtilIT.getDatasetJsonLDMetadata(datasetId, apiToken); + response.prettyPrint(); + jsonLDString = getData(response.getBody().asString()); + jsonLDObject = JSONLDUtil.decontextualizeJsonLD(jsonLDString); + String publishedVersion = jsonLDObject.getString("http://schema.org/version"); + assertNotEquals("DRAFT", publishedVersion); + + // Upload a file so a draft version is created + String pathToFile = "src/main/webapp/resources/images/cc0.png"; + Response uploadResponse = UtilIT.uploadFileViaNative(datasetId.toString(), pathToFile, apiToken); + uploadResponse.prettyPrint(); + uploadResponse.then().assertThat().statusCode(OK.getStatusCode()); + int fileID = uploadResponse.jsonPath().getInt("data.files[0].dataFile.id"); + + //We check the authenticated user gets DRAFT + response = UtilIT.getDatasetJsonLDMetadata(datasetId, apiToken); + response.prettyPrint(); + jsonLDString = getData(response.getBody().asString()); + jsonLDObject = JSONLDUtil.decontextualizeJsonLD(jsonLDString); + assertEquals("DRAFT", jsonLDObject.getString("http://schema.org/version")); + + // Create user with no permission and check they get published version + String apiTokenNoPerms = UtilIT.createRandomUserGetToken(); + response = UtilIT.getDatasetJsonLDMetadata(datasetId, apiTokenNoPerms); + response.prettyPrint(); + jsonLDString = getData(response.getBody().asString()); + jsonLDObject = JSONLDUtil.decontextualizeJsonLD(jsonLDString); + assertNotEquals("DRAFT", jsonLDObject.getString("http://schema.org/version")); + + // Delete the file + Response deleteFileResponse = UtilIT.deleteFileInDataset(fileID, apiToken); + deleteFileResponse.prettyPrint(); + deleteFileResponse.then().assertThat().statusCode(OK.getStatusCode()); + // Delete the terms of use response = UtilIT.deleteDatasetJsonLDMetadata(datasetId, apiToken, "{\"https://dataverse.org/schema/core#termsOfUse\": \"New terms\"}"); @@ -3026,15 +3066,27 @@ public void testSemanticMetadataAPIs() { jsonLDObject = JSONLDUtil.decontextualizeJsonLD(jsonLDString); assertTrue(!jsonLDObject.containsKey("https://dataverse.org/schema/core#termsOfUse")); - // Cleanup - delete dataset, dataverse, user... - Response deleteDatasetResponse = UtilIT.deleteDatasetViaNativeApi(datasetId, apiToken); - deleteDatasetResponse.prettyPrint(); - assertEquals(200, deleteDatasetResponse.getStatusCode()); + //Delete the DRAFT dataset + Response deleteDraftResponse = UtilIT.deleteDatasetVersionViaNativeApi(datasetId, DS_VERSION_DRAFT, apiToken); + deleteDraftResponse.prettyPrint(); + deleteDraftResponse.then().assertThat().statusCode(OK.getStatusCode()); + + //We set the user as superuser so we can delete the published dataset + Response superUserResponse = UtilIT.makeSuperUser(username); + superUserResponse.prettyPrint(); + deleteDraftResponse.then().assertThat().statusCode(OK.getStatusCode()); + + //Delete the published dataset + Response deletePublishedResponse = UtilIT.deleteDatasetViaNativeApi(datasetId, apiToken); + deletePublishedResponse.prettyPrint(); + deleteDraftResponse.then().assertThat().statusCode(OK.getStatusCode()); + //Delete the dataverse Response deleteDataverseResponse = UtilIT.deleteDataverse(dataverseAlias, apiToken); deleteDataverseResponse.prettyPrint(); assertEquals(200, deleteDataverseResponse.getStatusCode()); + //Delete the user Response deleteUserResponse = UtilIT.deleteUser(username); deleteUserResponse.prettyPrint(); assertEquals(200, deleteUserResponse.getStatusCode()); From 9568c20359234bbe87b17656c91926ab11329a57 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 6 Feb 2024 10:53:24 -0500 Subject: [PATCH 277/689] Add release notes --- doc/release-notes/10297-metadata-api-fix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/release-notes/10297-metadata-api-fix.md diff --git a/doc/release-notes/10297-metadata-api-fix.md b/doc/release-notes/10297-metadata-api-fix.md new file mode 100644 index 00000000000..11ee086af04 --- /dev/null +++ b/doc/release-notes/10297-metadata-api-fix.md @@ -0,0 +1 @@ +The API endpoint `api/datasets/{id}/metadata` has been changed to default to the latest version of the dataset that the user has access. From 2f167cf57def265d719f52a7211ed6648b7e3df8 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 6 Feb 2024 10:56:03 -0500 Subject: [PATCH 278/689] Restore SearchIT --- src/test/java/edu/harvard/iq/dataverse/api/SearchIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/SearchIT.java b/src/test/java/edu/harvard/iq/dataverse/api/SearchIT.java index cd292a40a1e..125753296a2 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/SearchIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/SearchIT.java @@ -1202,7 +1202,7 @@ public void testGeospatialSearch() { .add("value", "42.33661") .add("typeClass", "primitive") .add("multiple", false) - .add("typeName", "southLongitud e") + .add("typeName", "southLongitude") ) .add("eastLongitude", Json.createObjectBuilder() From df4f49a1650070427a710046be32b7c5f6ad5312 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 6 Feb 2024 14:43:38 -0500 Subject: [PATCH 279/689] add release note #10238 --- doc/release-notes/10238-container-demo.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/release-notes/10238-container-demo.md diff --git a/doc/release-notes/10238-container-demo.md b/doc/release-notes/10238-container-demo.md new file mode 100644 index 00000000000..edc4db4b650 --- /dev/null +++ b/doc/release-notes/10238-container-demo.md @@ -0,0 +1 @@ +The Container Guide now containers a tutorial for running Dataverse in containers for demo or evaluation purposes: https://guides.dataverse.org/en/6.2/container From ce4b1e0418b31a9a4db9fa7ab1926f17459a046c Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 6 Feb 2024 14:50:35 -0500 Subject: [PATCH 280/689] Change the workflow section including feedback from @sekmiller --- doc/sphinx-guides/source/qa/overview.md | 27 ++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/doc/sphinx-guides/source/qa/overview.md b/doc/sphinx-guides/source/qa/overview.md index 64796357831..a5b613f6516 100644 --- a/doc/sphinx-guides/source/qa/overview.md +++ b/doc/sphinx-guides/source/qa/overview.md @@ -11,11 +11,28 @@ This guide describes the testing process used by QA at IQSS and provides a refer ## Workflow -The basic workflow is as follows. Bugs or feature requests are submitted to GitHub by the community or by team members as [issues](https://github.com/IQSS/dataverse/issues). These issues are prioritized and added to a two-week sprint that is reflected on the GitHub {ref}`kanban-board`. As developers work on these issues, a GitHub branch is produced, code is contributed, and a pull request is made to merge these new changes back into the common {ref}`develop branch ` and ultimately released as part of the product. - -Before a pull request is moved to QA, it must be reviewed by a member of the development team from a coding perspective, and it must pass automated tests. There it is tested manually, exercising the UI (using three common browsers) and any business logic it implements. - -Depending on whether the code modifies existing code or is completely new, a smoke test of core functionality is performed and some basic regression testing of modified or related code is performed. Any documentation provided is used to understand the feature and any assertions made in that documentation are tested. Once this passes and any bugs that are found are corrected, and the automated tests are confirmed to be passing, the PR is merged into the develop branch, the PR is closed, and the branch is deleted (if it is local). At this point, the PR moves from the QA column automatically into the Merged column (where it might be discussed at the next standup) and the process repeats with the next PR until it is decided to {doc}`make a release `. +Here is a brief description of our workflow: + +### Issue Submission and Prioritization: +- Members of the community or the development team submit bugs or request features through GitHub as [Issues](https://github.com/IQSS/dataverse/issues)sues. +- These Issues are prioritized and added to a two-week-long sprint that can be tracked on the {ref}`kanban-board`. + +### Development Process: +- Developers will work on a solution on a separate branch +- Once a developer completes their work, they submit a [Pull Request](https://github.com/IQSS/dataverse/pulls) (PR). +- The PR is reviewed by a developer from the team. +- During the review, the reviewer may suggest coding or documentation changes to the original developer. + +### Quality Assurance (QA) Testing: +- The QA tester performs a smoke test of core functionality and regression testing. +- Documentation is used to understand the feature and validate any assertions made. +- If no documentation is provided in the PR, the tester may refer to the original bug report to determine the desired outcome of the changes. +- Once the branch is assumed to be safe, it is merged into the develop branch. + +### Final Steps: +- The PR and the Issue are closed and assigned the “merged” status. +- It is good practice to delete the branch if it is local. +- The content from the PR becomes part of the codebase for {doc}`future releases `. The complete suggested workflow can be found at {doc}`qa-workflow`. From de3bad6e6ec000f182c9a50e019f155cb0c20fb9 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 6 Feb 2024 14:53:05 -0500 Subject: [PATCH 281/689] Typo correction --- doc/sphinx-guides/source/qa/overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/qa/overview.md b/doc/sphinx-guides/source/qa/overview.md index a5b613f6516..60e6a28ee9a 100644 --- a/doc/sphinx-guides/source/qa/overview.md +++ b/doc/sphinx-guides/source/qa/overview.md @@ -14,7 +14,7 @@ This guide describes the testing process used by QA at IQSS and provides a refer Here is a brief description of our workflow: ### Issue Submission and Prioritization: -- Members of the community or the development team submit bugs or request features through GitHub as [Issues](https://github.com/IQSS/dataverse/issues)sues. +- Members of the community or the development team submit bugs or request features through GitHub as [Issues](https://github.com/IQSS/dataverse/issues). - These Issues are prioritized and added to a two-week-long sprint that can be tracked on the {ref}`kanban-board`. ### Development Process: From bf3c2c7100b996e4a1a5d4e4616c99c880b9674a Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 7 Feb 2024 12:13:55 +0000 Subject: [PATCH 282/689] Fixed: if condition and endpoint path --- .../java/edu/harvard/iq/dataverse/api/Files.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index 6a9b1803583..95117162094 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -508,7 +508,7 @@ public Response getFileData(@Context ContainerRequestContext crc, @PathParam("id @GET @AuthRequired - @Path("{id}/{datasetVersionId}") + @Path("{id}/versions/{datasetVersionId}") public Response getFileData(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @PathParam("datasetVersionId") String datasetVersionId, @Context UriInfo uriInfo, @Context HttpHeaders headers) { return getFileDataResponse(getRequestUser(crc), fileIdOrPersistentId, uriInfo, headers, datasetVersionId); } @@ -530,7 +530,7 @@ private Response getFileDataResponse(User user, String fileIdOrPersistentId, Uri FileMetadata fm; - if (datasetVersionId.equals(DS_VERSION_DRAFT)) { + if (datasetVersionId != null && datasetVersionId.equals(DS_VERSION_DRAFT)) { try { fm = execCommand(new GetDraftFileMetadataIfAvailableCommand(req, df)); } catch (WrappedResponse w) { @@ -558,19 +558,19 @@ private Response getFileDataResponse(User user, String fileIdOrPersistentId, Uri } } - + if (fm.getDatasetVersion().isReleased()) { MakeDataCountLoggingServiceBean.MakeDataCountEntry entry = new MakeDataCountLoggingServiceBean.MakeDataCountEntry(uriInfo, headers, dvRequestService, df); mdcLogService.logEntry(entry); - } - + } + return Response.ok(Json.createObjectBuilder() .add("status", ApiConstants.STATUS_OK) .add("data", json(fm)).build()) .type(MediaType.APPLICATION_JSON) .build(); } - + @GET @AuthRequired @Path("{id}/metadata") From c8f8227ffa2efe79d6840fe7641a7c23690caea8 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 7 Feb 2024 13:11:00 +0000 Subject: [PATCH 283/689] Added: new commands for getting FileMetadata --- ...etDraftFileMetadataIfAvailableCommand.java | 19 ++++---- ...etLatestAccessibleFileMetadataCommand.java | 35 +++++++++++++++ ...GetLatestPublishedFileMetadataCommand.java | 28 ++++++++++++ ...edFileMetadataByDatasetVersionCommand.java | 43 +++++++++++++++++++ 4 files changed, 114 insertions(+), 11 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedFileMetadataCommand.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetSpecificPublishedFileMetadataByDatasetVersionCommand.java diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDraftFileMetadataIfAvailableCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDraftFileMetadataIfAvailableCommand.java index 14999548b34..4673f45412a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDraftFileMetadataIfAvailableCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDraftFileMetadataIfAvailableCommand.java @@ -1,7 +1,6 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.DataFile; -import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.FileMetadata; import edu.harvard.iq.dataverse.authorization.Permission; @@ -12,25 +11,23 @@ import edu.harvard.iq.dataverse.engine.command.exception.CommandException; /** - * * @author Matthew */ -@RequiredPermissions( Permission.ViewUnpublishedDataset ) -public class GetDraftFileMetadataIfAvailableCommand extends AbstractCommand{ - private final DataFile df; +@RequiredPermissions(Permission.ViewUnpublishedDataset) +public class GetDraftFileMetadataIfAvailableCommand extends AbstractCommand { + private final DataFile dataFile; public GetDraftFileMetadataIfAvailableCommand(DataverseRequest aRequest, DataFile dataFile) { super(aRequest, dataFile); - df = dataFile; + this.dataFile = dataFile; } @Override public FileMetadata execute(CommandContext ctxt) throws CommandException { - FileMetadata fm = df.getLatestFileMetadata(); - if(fm.getDatasetVersion().getVersionState().equals(DatasetVersion.VersionState.DRAFT)) { - return df.getLatestFileMetadata(); - } + FileMetadata latestFileMetadata = dataFile.getLatestFileMetadata(); + if (latestFileMetadata.getDatasetVersion().isDraft()) { + return latestFileMetadata; + } return null; } - } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java new file mode 100644 index 00000000000..306221ed86c --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java @@ -0,0 +1,35 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.FileMetadata; +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; + +@RequiredPermissions(Permission.ViewUnpublishedDataset) +public class GetLatestAccessibleFileMetadataCommand extends AbstractCommand { + private final DataFile dataFile; + + public GetLatestAccessibleFileMetadataCommand(DataverseRequest aRequest, DataFile dataFile) { + super(aRequest, dataFile); + this.dataFile = dataFile; + } + + @Override + public FileMetadata execute(CommandContext ctxt) throws CommandException { + FileMetadata fileMetadata = ctxt.engine().submit( + new GetLatestPublishedFileMetadataCommand(getRequest(), dataFile) + ); + + if (fileMetadata == null) { + fileMetadata = ctxt.engine().submit( + new GetDraftFileMetadataIfAvailableCommand(getRequest(), dataFile) + ); + } + + return fileMetadata; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedFileMetadataCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedFileMetadataCommand.java new file mode 100644 index 00000000000..147a0fdce76 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedFileMetadataCommand.java @@ -0,0 +1,28 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.FileMetadata; +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; + +@RequiredPermissions({}) +public class GetLatestPublishedFileMetadataCommand extends AbstractCommand { + private final DataFile dataFile; + + public GetLatestPublishedFileMetadataCommand(DataverseRequest aRequest, DataFile dataFile) { + super(aRequest, dataFile); + this.dataFile = dataFile; + } + + @Override + public FileMetadata execute(CommandContext ctxt) throws CommandException { + try { + return dataFile.getLatestPublishedFileMetadata(); + } catch (UnsupportedOperationException e) { + return null; + } + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetSpecificPublishedFileMetadataByDatasetVersionCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetSpecificPublishedFileMetadataByDatasetVersionCommand.java new file mode 100644 index 00000000000..564b81d62ac --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetSpecificPublishedFileMetadataByDatasetVersionCommand.java @@ -0,0 +1,43 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.DatasetVersion; +import edu.harvard.iq.dataverse.FileMetadata; +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 java.util.List; + +@RequiredPermissions({}) +public class GetSpecificPublishedFileMetadataByDatasetVersionCommand extends AbstractCommand { + private final long majorVersion; + private final long minorVersion; + private final DataFile dataFile; + + public GetSpecificPublishedFileMetadataByDatasetVersionCommand(DataverseRequest aRequest, DataFile dataFile, long majorVersionNum, long minorVersionNum) { + super(aRequest, dataFile); + this.dataFile = dataFile; + majorVersion = majorVersionNum; + minorVersion = minorVersionNum; + } + + @Override + public FileMetadata execute(CommandContext ctxt) throws CommandException { + List fileMetadatas = dataFile.getFileMetadatas(); + + for (FileMetadata fileMetadata : fileMetadatas) { + DatasetVersion datasetVersion = fileMetadata.getDatasetVersion(); + + if (datasetVersion.isPublished() && + datasetVersion.getVersionNumber().equals(majorVersion) && + datasetVersion.getMinorVersionNumber().equals(minorVersion)) { + return fileMetadata; + } + } + + return null; + } +} From 8cda7ce79278797767664291ec3d4175a11a14f9 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 7 Feb 2024 13:13:33 +0000 Subject: [PATCH 284/689] Added: readability minor change --- ...pecificPublishedFileMetadataByDatasetVersionCommand.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetSpecificPublishedFileMetadataByDatasetVersionCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetSpecificPublishedFileMetadataByDatasetVersionCommand.java index 564b81d62ac..84a51f6b31d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetSpecificPublishedFileMetadataByDatasetVersionCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetSpecificPublishedFileMetadataByDatasetVersionCommand.java @@ -17,11 +17,11 @@ public class GetSpecificPublishedFileMetadataByDatasetVersionCommand extends Abs private final long minorVersion; private final DataFile dataFile; - public GetSpecificPublishedFileMetadataByDatasetVersionCommand(DataverseRequest aRequest, DataFile dataFile, long majorVersionNum, long minorVersionNum) { + public GetSpecificPublishedFileMetadataByDatasetVersionCommand(DataverseRequest aRequest, DataFile dataFile, long majorVersion, long minorVersion) { super(aRequest, dataFile); this.dataFile = dataFile; - majorVersion = majorVersionNum; - minorVersion = minorVersionNum; + this.majorVersion = majorVersion; + this.minorVersion = minorVersion; } @Override From 99555017b3890c3d7c8c244ee5bae92f7ec962da Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 7 Feb 2024 10:16:32 -0500 Subject: [PATCH 285/689] remove superflous double quotes #10240 --- 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 f161dd67ca9..d6f88df3235 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -3536,7 +3536,7 @@ When the dataset version is a draft or deaccessioned, authentication is required export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx export SERVER_URL=https://demo.dataverse.org export FILE_ID=42 - export DATASET_VERSION=":draft" + export DATASET_VERSION=:draft curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/files/$FILE_ID/versions/$DATASET_VERSION/citation" From 316524a45e85ca359357b568774dd1493e913e2e Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 7 Feb 2024 10:26:48 -0500 Subject: [PATCH 286/689] move English to bundle #10240 --- src/main/java/edu/harvard/iq/dataverse/api/Files.java | 4 ++-- src/main/java/propertyFiles/Bundle.properties | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index ed331e6835d..f7cdf2df10b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -980,13 +980,13 @@ public Command handleLatestPublished() { })); if (dsv == null) { - return unauthorized("Dataset version cannot be found or unauthorized."); + return unauthorized(BundleUtil.getStringFromBundle("files.api.no.draftOrUnauth")); } Long getDatasetVersionID = dsv.getId(); FileMetadata fm = dataFileServiceBean.findFileMetadataByDatasetVersionIdAndDataFileId(getDatasetVersionID, df.getId()); if (fm == null) { - return notFound("File could not be found."); + return notFound(BundleUtil.getStringFromBundle("files.api.fileNotFound")); } boolean direct = false; DataCitation citation = new DataCitation(fm, direct); diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 157f2ecaf54..5ecab876e01 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -2633,7 +2633,9 @@ admin.api.deleteUser.success=Authenticated User {0} deleted. #Files.java files.api.metadata.update.duplicateFile=Filename already exists at {0} files.api.no.draft=No draft available for this file +files.api.no.draftOrUnauth=Dataset version cannot be found or unauthorized. files.api.only.tabular.supported=This operation is only available for tabular files. +files.api.fileNotFound=File could not be found. #Datasets.java datasets.api.updatePIDMetadata.failure.dataset.must.be.released=Modify Registration Metadata must be run on a published dataset. From e9ab40bdaadb9246a8d191ff6c76ba6dc16762e5 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 7 Feb 2024 10:44:16 -0500 Subject: [PATCH 287/689] simplify and rename tests #10240 --- .../edu/harvard/iq/dataverse/api/FilesIT.java | 33 ++++++------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java index 1e8a806faa2..53f8aa40e4a 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java @@ -2487,7 +2487,7 @@ public void testCollectionStorageQuotas() { } @Test - public void testFileCitation() throws IOException { + public void testFileCitationByVersion() throws IOException { Response createUser = UtilIT.createRandomUser(); createUser.then().assertThat().statusCode(OK.getStatusCode()); String apiToken = UtilIT.getApiTokenFromResponse(createUser); @@ -2502,24 +2502,11 @@ public void testFileCitation() throws IOException { Integer datasetId = JsonPath.from(createDatasetResponse.body().asString()).getInt("data.id"); String datasetPid = JsonPath.from(createDatasetResponse.body().asString()).getString("data.persistentId"); - Response getDatasetVersionCitationResponse = UtilIT.getDatasetVersionCitation(datasetId, DS_VERSION_DRAFT, false, apiToken); - getDatasetVersionCitationResponse.prettyPrint(); - getDatasetVersionCitationResponse.then().assertThat() - .statusCode(OK.getStatusCode()) - // We check that the returned message contains information expected for the citation string - .body("data.message", containsString("DRAFT VERSION")); - - Path pathToTxt = Paths.get(java.nio.file.Files.createTempDirectory(null) + File.separator + "file.txt"); - String contentOfTxt = "foobar"; - java.nio.file.Files.write(pathToTxt, contentOfTxt.getBytes()); - - Response uploadFileTxt = UtilIT.uploadFileViaNative(datasetId.toString(), pathToTxt.toString(), apiToken); - uploadFileTxt.prettyPrint(); - uploadFileTxt.then().assertThat() - .statusCode(OK.getStatusCode()) - .body("data.files[0].label", equalTo("file.txt")); + String pathToTestFile = "src/test/resources/images/coffeeshop.png"; + Response uploadFile = UtilIT.uploadFileViaNative(datasetId.toString(), pathToTestFile, Json.createObjectBuilder().build(), apiToken); + uploadFile.then().assertThat().statusCode(OK.getStatusCode()); - Integer fileId = JsonPath.from(uploadFileTxt.body().asString()).getInt("data.files[0].dataFile.id"); + Integer fileId = JsonPath.from(uploadFile.body().asString()).getInt("data.files[0].dataFile.id"); String pidAsUrl = "https://doi.org/" + datasetPid.split("doi:")[1]; int currentYear = Year.now().getValue(); @@ -2540,7 +2527,7 @@ public void testFileCitation() throws IOException { getFileCitationDraft.prettyPrint(); getFileCitationDraft.then().assertThat() .statusCode(OK.getStatusCode()) - .body("data.message", equalTo("Finch, Fiona, " + currentYear + ", \"Darwin's Finches\", " + pidAsUrl + ", Root, DRAFT VERSION; file.txt [fileName]")); + .body("data.message", equalTo("Finch, Fiona, " + currentYear + ", \"Darwin's Finches\", " + pidAsUrl + ", Root, DRAFT VERSION; coffeeshop.png [fileName]")); Response publishDataverseResponse = UtilIT.publishDataverseViaNativeApi(dataverseAlias, apiToken); publishDataverseResponse.then().assertThat().statusCode(OK.getStatusCode()); @@ -2556,7 +2543,7 @@ public void testFileCitation() throws IOException { String updateJsonString = """ { - "label": "foo.txt" + "label": "foo.png" } """; @@ -2568,13 +2555,13 @@ public void testFileCitation() throws IOException { getFileCitationPostV1Draft.prettyPrint(); getFileCitationPostV1Draft.then().assertThat() .statusCode(OK.getStatusCode()) - .body("data.message", equalTo("Finch, Fiona, " + currentYear + ", \"Darwin's Finches\", " + pidAsUrl + ", Root, DRAFT VERSION; foo.txt [fileName]")); + .body("data.message", equalTo("Finch, Fiona, " + currentYear + ", \"Darwin's Finches\", " + pidAsUrl + ", Root, DRAFT VERSION; foo.png [fileName]")); Response getFileCitationV1OldFilename = UtilIT.getFileCitation(fileId, "1.0", apiToken); getFileCitationV1OldFilename.prettyPrint(); getFileCitationV1OldFilename.then().assertThat() .statusCode(OK.getStatusCode()) - .body("data.message", equalTo("Finch, Fiona, " + currentYear + ", \"Darwin's Finches\", " + pidAsUrl + ", Root, V1; file.txt [fileName]")); + .body("data.message", equalTo("Finch, Fiona, " + currentYear + ", \"Darwin's Finches\", " + pidAsUrl + ", Root, V1; coffeeshop.png [fileName]")); UtilIT.publishDatasetViaNativeApi(datasetId, "major", apiToken) .then().assertThat().statusCode(OK.getStatusCode()); @@ -2587,7 +2574,7 @@ public void testFileCitation() throws IOException { getFileCitationV1PostDeaccessionAuthor.prettyPrint(); getFileCitationV1PostDeaccessionAuthor.then().assertThat() .statusCode(OK.getStatusCode()) - .body("data.message", equalTo("Finch, Fiona, " + currentYear + ", \"Darwin's Finches\", " + pidAsUrl + ", Root, V1, DEACCESSIONED VERSION; file.txt [fileName]")); + .body("data.message", equalTo("Finch, Fiona, " + currentYear + ", \"Darwin's Finches\", " + pidAsUrl + ", Root, V1, DEACCESSIONED VERSION; coffeeshop.png [fileName]")); Response getFileCitationV1PostDeaccessionNoApiToken = UtilIT.getFileCitation(fileId, "1.0", null); getFileCitationV1PostDeaccessionNoApiToken.prettyPrint(); From a2194d9ce6229072a19f1ccd5a13a70e6546f447 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 7 Feb 2024 10:48:37 -0500 Subject: [PATCH 288/689] stop using var; make consistent with older method #10240 --- src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 b5957a756d3..b51d6af75a9 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -3475,11 +3475,11 @@ static Response getDatasetVersionCitation(Integer datasetId, String version, boo } static Response getFileCitation(Integer fileId, String datasetVersion, String apiToken) { - var spec = given(); + RequestSpecification requestSpecification = given(); if (apiToken != null) { - spec.header(API_TOKEN_HTTP_HEADER, apiToken); + requestSpecification.header(API_TOKEN_HTTP_HEADER, apiToken); } - return spec.get("/api/files/" + fileId + "/versions/" + datasetVersion + "/citation"); + return requestSpecification.get("/api/files/" + fileId + "/versions/" + datasetVersion + "/citation"); } static Response getVersionFiles(Integer datasetId, From bec394519826529c02adedfdd601f04b45f859c2 Mon Sep 17 00:00:00 2001 From: landreev Date: Wed, 7 Feb 2024 11:50:52 -0500 Subject: [PATCH 289/689] 8524 adding mechanism for storing tab. files with variable headers (#10282) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * "stored with header" flag #8524 * more changes for the streaming and redirect code. #8524 * disabling dynamically-generated varheader in the remaining storage drivers. #8524 * Ingest plugins (work in progress) #8524 * R ingest plugin (#8524) * still some unaddressed @todo:s, but the branch should build and the unit tests should be passing. # 8524 * work-in-progress, on the subsetting code in the download instance writer. #8524 * more work-in-progress changes. removing all the unused code from TabularSubsetGenerator, for clarity etc. #8524 * more bits and pieces #8524 * 2 more ingest plugins. #8542 * Integration tests. #8524 * typo #8524 * documenting the new setting. #8524 * a release note for the pr. also, added the "storage quotas enabled" to the list of settings documented in the config guide while I was at it. #8524 * removed all the unused code from this class (lots of it) for clarity, etc. git history can be consulted if anyone is curious about what we used to do here. #8524 * removing @todo: that's no longer relevant #8524 * (cosmetic) defined the control constants used in the integration test. #8524 --- ...4-storing-tabular-files-with-varheaders.md | 6 + .../source/installation/config.rst | 22 + .../edu/harvard/iq/dataverse/DataTable.java | 18 + .../dataverse/api/DownloadInstanceWriter.java | 78 +- .../harvard/iq/dataverse/api/TestIngest.java | 2 +- .../iq/dataverse/dataaccess/FileAccessIO.java | 3 +- .../dataaccess/GlobusOverlayAccessIO.java | 8 +- .../dataaccess/RemoteOverlayAccessIO.java | 8 +- .../iq/dataverse/dataaccess/S3AccessIO.java | 3 +- .../dataverse/dataaccess/SwiftAccessIO.java | 3 +- .../dataaccess/TabularSubsetGenerator.java | 1150 +---------------- .../dataaccess/TabularSubsetInputStream.java | 114 -- .../export/DDIExportServiceBean.java | 11 + .../dataverse/ingest/IngestServiceBean.java | 64 +- .../tabulardata/TabularDataFileReader.java | 26 +- .../impl/plugins/csv/CSVFileReader.java | 24 +- .../impl/plugins/dta/DTAFileReader.java | 11 +- .../impl/plugins/dta/NewDTAFileReader.java | 19 +- .../impl/plugins/por/PORFileReader.java | 13 +- .../impl/plugins/rdata/RDATAFileReader.java | 4 +- .../impl/plugins/rdata/RTabFileParser.java | 28 +- .../impl/plugins/sav/SAVFileReader.java | 24 +- .../impl/plugins/xlsx/XLSXFileReader.java | 11 +- .../settings/SettingsServiceBean.java | 7 +- .../iq/dataverse/util/SystemConfig.java | 8 + ...24-store-tabular-files-with-varheaders.sql | 1 + .../edu/harvard/iq/dataverse/api/FilesIT.java | 128 ++ .../dataverse/ingest/IngestFrequencyTest.java | 2 +- .../impl/plugins/csv/CSVFileReaderTest.java | 24 +- .../impl/plugins/dta/DTAFileReaderTest.java | 2 +- .../plugins/dta/NewDTAFileReaderTest.java | 14 +- 31 files changed, 501 insertions(+), 1335 deletions(-) create mode 100644 doc/release-notes/8524-storing-tabular-files-with-varheaders.md delete mode 100644 src/main/java/edu/harvard/iq/dataverse/dataaccess/TabularSubsetInputStream.java create mode 100644 src/main/resources/db/migration/V6.1.0.2__8524-store-tabular-files-with-varheaders.sql diff --git a/doc/release-notes/8524-storing-tabular-files-with-varheaders.md b/doc/release-notes/8524-storing-tabular-files-with-varheaders.md new file mode 100644 index 00000000000..f7034c846f6 --- /dev/null +++ b/doc/release-notes/8524-storing-tabular-files-with-varheaders.md @@ -0,0 +1,6 @@ +Tabular Data Ingest can now save the generated archival files with the list of variable names added as the first tab-delimited line. As the most significant effect of this feature, +Access API will be able to take advantage of Direct Download for tab. files saved with these headers on S3 - since they no longer have to be generated and added to the streamed content on the fly. + +This behavior is controlled by the new setting `:StoreIngestedTabularFilesWithVarHeaders`. It is false by default, preserving the legacy behavior. When enabled, Dataverse will be able to handle both the newly ingested files, and any already-existing legacy files stored without these headers transparently to the user. E.g. the access API will continue delivering tab-delimited files **with** this header line, whether it needs to add it dynamically for the legacy files, or reading complete files directly from storage for the ones stored with it. + +An API for converting existing legacy tabular files will be added separately. [this line will need to be changed if we have time to add said API before 6.2 is released]. \ No newline at end of file diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index a7d7905ca4a..c233e594fa7 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -4151,3 +4151,25 @@ A true/false (default) option determining whether the dataset datafile table dis .. _supported MicroProfile Config API source: https://docs.payara.fish/community/docs/Technical%20Documentation/MicroProfile/Config/Overview.html + +.. _:UseStorageQuotas: + +:UseStorageQuotas ++++++++++++++++++ + +Enables storage use quotas in collections. See the :doc:`/api/native-api` for details. + + +.. _:StoreIngestedTabularFilesWithVarHeaders: + +:StoreIngestedTabularFilesWithVarHeaders +++++++++++++++++++++++++++++++++++++++++ + +With this setting enabled, tabular files produced during Ingest will +be stored with the list of variable names added as the first +tab-delimited line. As the most significant effect of this feature, +Access API will be able to take advantage of Direct Download for +tab. files saved with these headers on S3 - since they no longer have +to be generated and added to the streamed file on the fly. + +The setting is ``false`` by default, preserving the legacy behavior. diff --git a/src/main/java/edu/harvard/iq/dataverse/DataTable.java b/src/main/java/edu/harvard/iq/dataverse/DataTable.java index a17d8c65138..95f3aed0f40 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataTable.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataTable.java @@ -112,6 +112,16 @@ public DataTable() { @Column( nullable = true ) private String originalFileName; + + /** + * The physical tab-delimited file is in storage with the list of variable + * names saved as the 1st line. This means that we do not need to generate + * this line on the fly. (Also means that direct download mechanism can be + * used for this file!) + */ + @Column(nullable = false) + private boolean storedWithVariableHeader = false; + /* * Getter and Setter methods: */ @@ -206,6 +216,14 @@ public void setOriginalFileName(String originalFileName) { this.originalFileName = originalFileName; } + public boolean isStoredWithVariableHeader() { + return storedWithVariableHeader; + } + + public void setStoredWithVariableHeader(boolean storedWithVariableHeader) { + this.storedWithVariableHeader = storedWithVariableHeader; + } + /* * Custom overrides for hashCode(), equals() and toString() methods: */ diff --git a/src/main/java/edu/harvard/iq/dataverse/api/DownloadInstanceWriter.java b/src/main/java/edu/harvard/iq/dataverse/api/DownloadInstanceWriter.java index bcb8799ec9e..89b22b76a7d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/DownloadInstanceWriter.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/DownloadInstanceWriter.java @@ -22,7 +22,6 @@ import jakarta.ws.rs.ext.Provider; import edu.harvard.iq.dataverse.DataFile; -import edu.harvard.iq.dataverse.FileMetadata; import edu.harvard.iq.dataverse.dataaccess.*; import edu.harvard.iq.dataverse.datavariable.DataVariable; import edu.harvard.iq.dataverse.engine.command.Command; @@ -104,8 +103,10 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] String auxiliaryTag = null; String auxiliaryType = null; String auxiliaryFileName = null; + // Before we do anything else, check if this download can be handled // by a redirect to remote storage (only supported on S3, as of 5.4): + if (storageIO.downloadRedirectEnabled()) { // Even if the above is true, there are a few cases where a @@ -159,7 +160,7 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] } } else if (dataFile.isTabularData()) { - // Many separate special cases here. + // Many separate special cases here. if (di.getConversionParam() != null) { if (di.getConversionParam().equals("format")) { @@ -180,12 +181,26 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] redirectSupported = false; } } - } else if (!di.getConversionParam().equals("noVarHeader")) { - // This is a subset request - can't do. + } else if (di.getConversionParam().equals("noVarHeader")) { + // This will work just fine, if the tab. file is + // stored without the var. header. Throw "unavailable" + // exception otherwise. + // @todo: should we actually drop support for this "noVarHeader" flag? + if (dataFile.getDataTable().isStoredWithVariableHeader()) { + throw new ServiceUnavailableException(); + } + // ... defaults to redirectSupported = true + } else { + // This must be a subset request then - can't do. + redirectSupported = false; + } + } else { + // "straight" download of the full tab-delimited file. + // can redirect, but only if stored with the variable + // header already added: + if (!dataFile.getDataTable().isStoredWithVariableHeader()) { redirectSupported = false; } - } else { - redirectSupported = false; } } } @@ -247,11 +262,16 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] // finally, issue the redirect: Response response = Response.seeOther(redirect_uri).build(); logger.fine("Issuing redirect to the file location."); + // Yes, this throws an exception. It's not an exception + // as in, "bummer, something went wrong". This is how a + // redirect is produced here! throw new RedirectionException(response); } throw new ServiceUnavailableException(); } + // Past this point, this is a locally served/streamed download + if (di.getConversionParam() != null) { // Image Thumbnail and Tabular data conversion: // NOTE: only supported on local files, as of 4.0.2! @@ -285,9 +305,14 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] // request any tabular-specific services. if (di.getConversionParam().equals("noVarHeader")) { - logger.fine("tabular data with no var header requested"); - storageIO.setNoVarHeader(Boolean.TRUE); - storageIO.setVarHeader(null); + if (!dataFile.getDataTable().isStoredWithVariableHeader()) { + logger.fine("tabular data with no var header requested"); + storageIO.setNoVarHeader(Boolean.TRUE); + storageIO.setVarHeader(null); + } else { + logger.fine("can't serve request for tabular data without varheader, since stored with it"); + throw new ServiceUnavailableException(); + } } else if (di.getConversionParam().equals("format")) { // Conversions, and downloads of "stored originals" are // now supported on all DataFiles for which StorageIO @@ -329,11 +354,10 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] if (variable.getDataTable().getDataFile().getId().equals(dataFile.getId())) { logger.fine("adding variable id " + variable.getId() + " to the list."); variablePositionIndex.add(variable.getFileOrder()); - if (subsetVariableHeader == null) { - subsetVariableHeader = variable.getName(); - } else { - subsetVariableHeader = subsetVariableHeader.concat("\t"); - subsetVariableHeader = subsetVariableHeader.concat(variable.getName()); + if (!dataFile.getDataTable().isStoredWithVariableHeader()) { + subsetVariableHeader = subsetVariableHeader == null + ? variable.getName() + : subsetVariableHeader.concat("\t" + variable.getName()); } } else { logger.warning("variable does not belong to this data file."); @@ -346,7 +370,17 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] try { File tempSubsetFile = File.createTempFile("tempSubsetFile", ".tmp"); TabularSubsetGenerator tabularSubsetGenerator = new TabularSubsetGenerator(); - tabularSubsetGenerator.subsetFile(storageIO.getInputStream(), tempSubsetFile.getAbsolutePath(), variablePositionIndex, dataFile.getDataTable().getCaseQuantity(), "\t"); + + long numberOfLines = dataFile.getDataTable().getCaseQuantity(); + if (dataFile.getDataTable().isStoredWithVariableHeader()) { + numberOfLines++; + } + + tabularSubsetGenerator.subsetFile(storageIO.getInputStream(), + tempSubsetFile.getAbsolutePath(), + variablePositionIndex, + numberOfLines, + "\t"); if (tempSubsetFile.exists()) { FileInputStream subsetStream = new FileInputStream(tempSubsetFile); @@ -354,8 +388,11 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] InputStreamIO subsetStreamIO = new InputStreamIO(subsetStream, subsetSize); logger.fine("successfully created subset output stream."); - subsetVariableHeader = subsetVariableHeader.concat("\n"); - subsetStreamIO.setVarHeader(subsetVariableHeader); + + if (subsetVariableHeader != null) { + subsetVariableHeader = subsetVariableHeader.concat("\n"); + subsetStreamIO.setVarHeader(subsetVariableHeader); + } String tabularFileName = storageIO.getFileName(); @@ -380,8 +417,13 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] } else { logger.fine("empty list of extra arguments."); } + // end of tab. data subset case + } else if (dataFile.getDataTable().isStoredWithVariableHeader()) { + logger.fine("tabular file stored with the var header included, no need to generate it on the fly"); + storageIO.setNoVarHeader(Boolean.TRUE); + storageIO.setVarHeader(null); } - } + } // end of tab. data file case if (storageIO == null) { //throw new WebApplicationException(Response.Status.SERVICE_UNAVAILABLE); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/TestIngest.java b/src/main/java/edu/harvard/iq/dataverse/api/TestIngest.java index 05ba150df8e..add43ea2091 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/TestIngest.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/TestIngest.java @@ -100,7 +100,7 @@ public String datafile(@QueryParam("fileName") String fileName, @QueryParam("fil TabularDataIngest tabDataIngest = null; try { - tabDataIngest = ingestPlugin.read(fileInputStream, null); + tabDataIngest = ingestPlugin.read(fileInputStream, false, null); } catch (IOException ingestEx) { output = output.concat("Caught an exception trying to ingest file " + fileName + ": " + ingestEx.getLocalizedMessage()); return output; diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java index f2a1312a150..26637ec5742 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java @@ -120,7 +120,8 @@ public void open (DataAccessOption... options) throws IOException { && dataFile.getContentType().equals("text/tab-separated-values") && dataFile.isTabularData() && dataFile.getDataTable() != null - && (!this.noVarHeader())) { + && (!this.noVarHeader()) + && (!dataFile.getDataTable().isStoredWithVariableHeader())) { List datavariables = dataFile.getDataTable().getDataVariables(); String varHeaderLine = generateVariableHeader(datavariables); diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/GlobusOverlayAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/GlobusOverlayAccessIO.java index 7a6809cb2ff..733daaf1328 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/GlobusOverlayAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/GlobusOverlayAccessIO.java @@ -450,8 +450,12 @@ public void open(DataAccessOption... options) throws IOException { this.setSize(retrieveSizeFromMedia()); } // Only applies for the S3 Connector case (where we could have run an ingest) - if (dataFile.getContentType() != null && dataFile.getContentType().equals("text/tab-separated-values") - && dataFile.isTabularData() && dataFile.getDataTable() != null && (!this.noVarHeader())) { + if (dataFile.getContentType() != null + && dataFile.getContentType().equals("text/tab-separated-values") + && dataFile.isTabularData() + && dataFile.getDataTable() != null + && (!this.noVarHeader()) + && (!dataFile.getDataTable().isStoredWithVariableHeader())) { List datavariables = dataFile.getDataTable().getDataVariables(); String varHeaderLine = generateVariableHeader(datavariables); diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java index 1616bfabf96..bca70259cb7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java @@ -124,8 +124,12 @@ public void open(DataAccessOption... options) throws IOException { logger.fine("Setting size"); this.setSize(retrieveSizeFromMedia()); } - if (dataFile.getContentType() != null && dataFile.getContentType().equals("text/tab-separated-values") - && dataFile.isTabularData() && dataFile.getDataTable() != null && (!this.noVarHeader())) { + if (dataFile.getContentType() != null + && dataFile.getContentType().equals("text/tab-separated-values") + && dataFile.isTabularData() + && dataFile.getDataTable() != null + && (!this.noVarHeader()) + && (!dataFile.getDataTable().isStoredWithVariableHeader())) { List datavariables = dataFile.getDataTable().getDataVariables(); String varHeaderLine = generateVariableHeader(datavariables); diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index 8afc365417e..c2143bd4789 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -225,7 +225,8 @@ public void open(DataAccessOption... options) throws IOException { && dataFile.getContentType().equals("text/tab-separated-values") && dataFile.isTabularData() && dataFile.getDataTable() != null - && (!this.noVarHeader())) { + && (!this.noVarHeader()) + && (!dataFile.getDataTable().isStoredWithVariableHeader())) { List datavariables = dataFile.getDataTable().getDataVariables(); String varHeaderLine = generateVariableHeader(datavariables); diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java index 105a60ab418..717f46ffd60 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java @@ -142,7 +142,8 @@ public void open(DataAccessOption... options) throws IOException { && dataFile.getContentType().equals("text/tab-separated-values") && dataFile.isTabularData() && dataFile.getDataTable() != null - && (!this.noVarHeader())) { + && (!this.noVarHeader()) + && (!dataFile.getDataTable().isStoredWithVariableHeader())) { List datavariables = dataFile.getDataTable().getDataVariables(); String varHeaderLine = generateVariableHeader(datavariables); diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/TabularSubsetGenerator.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/TabularSubsetGenerator.java index 782f7f3a52d..c369010c8cd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/TabularSubsetGenerator.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/TabularSubsetGenerator.java @@ -60,305 +60,26 @@ public class TabularSubsetGenerator implements SubsetGenerator { - private static Logger dbgLog = Logger.getLogger(TabularSubsetGenerator.class.getPackage().getName()); + private static Logger logger = Logger.getLogger(TabularSubsetGenerator.class.getPackage().getName()); - private static int COLUMN_TYPE_STRING = 1; - private static int COLUMN_TYPE_LONG = 2; - private static int COLUMN_TYPE_DOUBLE = 3; - private static int COLUMN_TYPE_FLOAT = 4; - - private static int MAX_COLUMN_BUFFER = 8192; - - private FileChannel fileChannel = null; - - private int varcount; - private int casecount; - private int subsetcount; - - private byte[][] columnEntries = null; - - - private ByteBuffer[] columnByteBuffers; - private int[] columnBufferSizes; - private int[] columnBufferOffsets; - - private long[] columnStartOffsets; - private long[] columnTotalOffsets; - private long[] columnTotalLengths; - - public TabularSubsetGenerator() { - - } - - public TabularSubsetGenerator (DataFile datafile, List variables) throws IOException { - if (!datafile.isTabularData()) { - throw new IOException("DataFile is not tabular data."); - } - - setVarCount(datafile.getDataTable().getVarQuantity().intValue()); - setCaseCount(datafile.getDataTable().getCaseQuantity().intValue()); - - - - StorageIO dataAccess = datafile.getStorageIO(); - if (!dataAccess.isLocalFile()) { - throw new IOException("Subsetting is supported on local files only!"); - } - - //File tabfile = datafile.getFileSystemLocation().toFile(); - File tabfile = dataAccess.getFileSystemPath().toFile(); + //private static int MAX_COLUMN_BUFFER = 8192; - File rotatedImageFile = getRotatedImage(tabfile, getVarCount(), getCaseCount()); - long[] columnEndOffsets = extractColumnOffsets(rotatedImageFile, getVarCount(), getCaseCount()); - - fileChannel = (FileChannel.open(Paths.get(rotatedImageFile.getAbsolutePath()), StandardOpenOption.READ)); - - if (variables == null || variables.size() < 1 || variables.size() > getVarCount()) { - throw new IOException("Illegal number of variables in the subset request"); - } - - subsetcount = variables.size(); - columnTotalOffsets = new long[subsetcount]; - columnTotalLengths = new long[subsetcount]; - columnByteBuffers = new ByteBuffer[subsetcount]; - - + public TabularSubsetGenerator() { - if (subsetcount == 1) { - if (!datafile.getDataTable().getId().equals(variables.get(0).getDataTable().getId())) { - throw new IOException("Variable in the subset request does not belong to the datafile."); - } - dbgLog.fine("single variable subset; setting fileChannel position to "+extractColumnOffset(columnEndOffsets, variables.get(0).getFileOrder())); - fileChannel.position(extractColumnOffset(columnEndOffsets, variables.get(0).getFileOrder())); - columnTotalLengths[0] = extractColumnLength(columnEndOffsets, variables.get(0).getFileOrder()); - columnTotalOffsets[0] = 0; - } else { - columnEntries = new byte[subsetcount][]; - - columnBufferSizes = new int[subsetcount]; - columnBufferOffsets = new int[subsetcount]; - columnStartOffsets = new long[subsetcount]; - - int i = 0; - for (DataVariable var : variables) { - if (!datafile.getDataTable().getId().equals(var.getDataTable().getId())) { - throw new IOException("Variable in the subset request does not belong to the datafile."); - } - columnByteBuffers[i] = ByteBuffer.allocate(MAX_COLUMN_BUFFER); - columnTotalLengths[i] = extractColumnLength(columnEndOffsets, var.getFileOrder()); - columnStartOffsets[i] = extractColumnOffset(columnEndOffsets, var.getFileOrder()); - if (columnTotalLengths[i] < MAX_COLUMN_BUFFER) { - columnByteBuffers[i].limit((int)columnTotalLengths[i]); - } - fileChannel.position(columnStartOffsets[i]); - columnBufferSizes[i] = fileChannel.read(columnByteBuffers[i]); - columnBufferOffsets[i] = 0; - columnTotalOffsets[i] = columnBufferSizes[i]; - i++; - } - } - } - - private int getVarCount() { - return varcount; } - private void setVarCount(int varcount) { - this.varcount = varcount; - } - - private int getCaseCount() { - return casecount; - } - - private void setCaseCount(int casecount) { - this.casecount = casecount; - } - - - /* - * Note that this method operates on the *absolute* column number, i.e. - * the number of the physical column in the tabular file. This is stored - * in DataVariable.FileOrder. - * This "column number" should not be confused with the number of column - * in the subset request; a user can request any number of variable - * columns, in an order that doesn't have to follow the physical order - * of the columns in the file. - */ - private long extractColumnOffset(long[] columnEndOffsets, int column) throws IOException { - if (columnEndOffsets == null || columnEndOffsets.length <= column) { - throw new IOException("Offsets table not initialized; or column out of bounds."); - } - long columnOffset; - - if (column > 0) { - columnOffset = columnEndOffsets[column - 1]; - } else { - columnOffset = getVarCount() * 8; - } - return columnOffset; - } - - /* - * See the comment for the method above. + /** + * This class used to be much more complex. There were methods for subsetting + * from fixed-width field files; including using the optimized, "90 deg. rotated" + * versions of such files (i.e. you create a *columns-wise* copy of your data + * file in which the columns are stored sequentially, and a table of byte + * offsets of each column. You can then read individual variable columns + * for cheap; at the expense of doubling the storage size of your tabular + * data files. These methods were not used, so they were deleted (in Jan. 2024 + * prior to 6.2. + * Please consult git history if you are interested in looking at that code. */ - private long extractColumnLength(long[] columnEndOffsets, int column) throws IOException { - if (columnEndOffsets == null || columnEndOffsets.length <= column) { - throw new IOException("Offsets table not initialized; or column out of bounds."); - } - long columnLength; - - if (column > 0) { - columnLength = columnEndOffsets[column] - columnEndOffsets[column - 1]; - } else { - columnLength = columnEndOffsets[0] - varcount * 8; - } - - return columnLength; - } - - - private void bufferMoreColumnBytes(int column) throws IOException { - if (columnTotalOffsets[column] >= columnTotalLengths[column]) { - throw new IOException("attempt to buffer bytes past the column boundary"); - } - fileChannel.position(columnStartOffsets[column] + columnTotalOffsets[column]); - - columnByteBuffers[column].clear(); - if (columnTotalLengths[column] < columnTotalOffsets[column] + MAX_COLUMN_BUFFER) { - dbgLog.fine("Limiting the buffer to "+(columnTotalLengths[column] - columnTotalOffsets[column])+" bytes"); - columnByteBuffers[column].limit((int) (columnTotalLengths[column] - columnTotalOffsets[column])); - } - columnBufferSizes[column] = fileChannel.read(columnByteBuffers[column]); - dbgLog.fine("Read "+columnBufferSizes[column]+" bytes for subset column "+column); - columnBufferOffsets[column] = 0; - columnTotalOffsets[column] += columnBufferSizes[column]; - } - - public byte[] readColumnEntryBytes(int column) { - return readColumnEntryBytes(column, true); - } - - - public byte[] readColumnEntryBytes(int column, boolean addTabs) { - byte[] leftover = null; - byte[] ret = null; - - if (columnBufferOffsets[column] >= columnBufferSizes[column]) { - try { - bufferMoreColumnBytes(column); - if (columnBufferSizes[column] < 1) { - return null; - } - } catch (IOException ioe) { - return null; - } - } - - int byteindex = columnBufferOffsets[column]; - try { - while (columnByteBuffers[column].array()[byteindex] != '\n') { - byteindex++; - if (byteindex == columnBufferSizes[column]) { - // save the leftover: - if (leftover == null) { - leftover = new byte[columnBufferSizes[column] - columnBufferOffsets[column]]; - System.arraycopy(columnByteBuffers[column].array(), columnBufferOffsets[column], leftover, 0, columnBufferSizes[column] - columnBufferOffsets[column]); - } else { - byte[] merged = new byte[leftover.length + columnBufferSizes[column]]; - - System.arraycopy(leftover, 0, merged, 0, leftover.length); - System.arraycopy(columnByteBuffers[column].array(), 0, merged, leftover.length, columnBufferSizes[column]); - leftover = merged; - merged = null; - } - // read more bytes: - bufferMoreColumnBytes(column); - if (columnBufferSizes[column] < 1) { - return null; - } - byteindex = 0; - } - } - - // presumably, we have found our '\n': - if (leftover == null) { - ret = new byte[byteindex - columnBufferOffsets[column] + 1]; - System.arraycopy(columnByteBuffers[column].array(), columnBufferOffsets[column], ret, 0, byteindex - columnBufferOffsets[column] + 1); - } else { - ret = new byte[leftover.length + byteindex + 1]; - System.arraycopy(leftover, 0, ret, 0, leftover.length); - System.arraycopy(columnByteBuffers[column].array(), 0, ret, leftover.length, byteindex + 1); - } - - } catch (IOException ioe) { - return null; - } - - columnBufferOffsets[column] = (byteindex + 1); - - if (column < columnBufferOffsets.length - 1) { - ret[ret.length - 1] = '\t'; - } - return ret; - } - - public int readSingleColumnSubset(byte[] buffer) throws IOException { - if (columnTotalOffsets[0] == columnTotalLengths[0]) { - return -1; - } - - if (columnByteBuffers[0] == null) { - dbgLog.fine("allocating single column subset buffer."); - columnByteBuffers[0] = ByteBuffer.allocate(buffer.length); - } - - int bytesread = fileChannel.read(columnByteBuffers[0]); - dbgLog.fine("single column subset: read "+bytesread+" bytes."); - if (columnTotalOffsets[0] + bytesread > columnTotalLengths[0]) { - bytesread = (int)(columnTotalLengths[0] - columnTotalOffsets[0]); - } - System.arraycopy(columnByteBuffers[0].array(), 0, buffer, 0, bytesread); - - columnTotalOffsets[0] += bytesread; - columnByteBuffers[0].clear(); - return bytesread > 0 ? bytesread : -1; - } - - - public byte[] readSubsetLineBytes() throws IOException { - byte[] ret = null; - int total = 0; - for (int i = 0; i < subsetcount; i++) { - columnEntries[i] = readColumnEntryBytes(i); - if (columnEntries[i] == null) { - throw new IOException("Failed to read subset line entry"); - } - total += columnEntries[i].length; - } - - ret = new byte[total]; - int offset = 0; - for (int i = 0; i < subsetcount; i++) { - System.arraycopy(columnEntries[i], 0, ret, offset, columnEntries[i].length); - offset += columnEntries[i].length; - } - dbgLog.fine("line: "+new String(ret)); - return ret; - } - - - public void close() { - if (fileChannel != null) { - try { - fileChannel.close(); - } catch (IOException ioe) { - // don't care. - } - } - } - public void subsetFile(String infile, String outfile, List columns, Long numCases) { subsetFile(infile, outfile, columns, numCases, "\t"); } @@ -411,11 +132,15 @@ public void subsetFile(InputStream in, String outfile, List columns, Lo * files, OK to use on small files: */ - public static Double[] subsetDoubleVector(InputStream in, int column, int numCases) { + public static Double[] subsetDoubleVector(InputStream in, int column, int numCases, boolean skipHeader) { Double[] retVector = new Double[numCases]; try (Scanner scanner = new Scanner(in)) { scanner.useDelimiter("\\n"); + if (skipHeader) { + skipFirstLine(scanner); + } + for (int caseIndex = 0; caseIndex < numCases; caseIndex++) { if (scanner.hasNext()) { String[] line = (scanner.next()).split("\t", -1); @@ -463,11 +188,15 @@ public static Double[] subsetDoubleVector(InputStream in, int column, int numCas * Same deal as with the method above - straightforward, but (potentially) slow. * Not a resource hog though - will only try to store one vector in memory. */ - public static Float[] subsetFloatVector(InputStream in, int column, int numCases) { + public static Float[] subsetFloatVector(InputStream in, int column, int numCases, boolean skipHeader) { Float[] retVector = new Float[numCases]; try (Scanner scanner = new Scanner(in)) { scanner.useDelimiter("\\n"); + if (skipHeader) { + skipFirstLine(scanner); + } + for (int caseIndex = 0; caseIndex < numCases; caseIndex++) { if (scanner.hasNext()) { String[] line = (scanner.next()).split("\t", -1); @@ -513,11 +242,15 @@ public static Float[] subsetFloatVector(InputStream in, int column, int numCases * Same deal as with the method above - straightforward, but (potentially) slow. * Not a resource hog though - will only try to store one vector in memory. */ - public static Long[] subsetLongVector(InputStream in, int column, int numCases) { + public static Long[] subsetLongVector(InputStream in, int column, int numCases, boolean skipHeader) { Long[] retVector = new Long[numCases]; try (Scanner scanner = new Scanner(in)) { scanner.useDelimiter("\\n"); + if (skipHeader) { + skipFirstLine(scanner); + } + for (int caseIndex = 0; caseIndex < numCases; caseIndex++) { if (scanner.hasNext()) { String[] line = (scanner.next()).split("\t", -1); @@ -549,11 +282,15 @@ public static Long[] subsetLongVector(InputStream in, int column, int numCases) * Same deal as with the method above - straightforward, but (potentially) slow. * Not a resource hog though - will only try to store one vector in memory. */ - public static String[] subsetStringVector(InputStream in, int column, int numCases) { + public static String[] subsetStringVector(InputStream in, int column, int numCases, boolean skipHeader) { String[] retVector = new String[numCases]; try (Scanner scanner = new Scanner(in)) { scanner.useDelimiter("\\n"); + if (skipHeader) { + skipFirstLine(scanner); + } + for (int caseIndex = 0; caseIndex < numCases; caseIndex++) { if (scanner.hasNext()) { String[] line = (scanner.next()).split("\t", -1); @@ -621,819 +358,10 @@ public static String[] subsetStringVector(InputStream in, int column, int numCas } - /* - * Straightforward method for subsetting a tab-delimited data file, extracting - * all the columns representing continuous variables and returning them as - * a 2-dimensional array of Doubles; - * Inefficient on large files, OK to use on small ones. - */ - public static Double[][] subsetDoubleVectors(InputStream in, Set columns, int numCases) throws IOException { - Double[][] retVector = new Double[columns.size()][numCases]; - try (Scanner scanner = new Scanner(in)) { - scanner.useDelimiter("\\n"); - - for (int caseIndex = 0; caseIndex < numCases; caseIndex++) { - if (scanner.hasNext()) { - String[] line = (scanner.next()).split("\t", -1); - int j = 0; - for (Integer i : columns) { - try { - // TODO: verify that NaN and +-Inf are going to be - // handled correctly here! -- L.A. - // NO, "+-Inf" is not handled correctly; see the - // comment further down below. - retVector[j][caseIndex] = new Double(line[i]); - } catch (NumberFormatException ex) { - retVector[j][caseIndex] = null; // missing value - } - j++; - } - } else { - throw new IOException("Tab file has fewer rows than the stored number of cases!"); - } - } - - int tailIndex = numCases; - while (scanner.hasNext()) { - String nextLine = scanner.next(); - if (!"".equals(nextLine)) { - throw new IOException("Tab file has more nonempty rows than the stored number of cases ("+numCases+")! current index: "+tailIndex+", line: "+nextLine); - } - tailIndex++; - } - - } - return retVector; - - } - - public String[] subsetStringVector(DataFile datafile, int column) throws IOException { - return (String[])subsetObjectVector(datafile, column, COLUMN_TYPE_STRING); - } - - public Double[] subsetDoubleVector(DataFile datafile, int column) throws IOException { - return (Double[])subsetObjectVector(datafile, column, COLUMN_TYPE_DOUBLE); - } - - public Long[] subsetLongVector(DataFile datafile, int column) throws IOException { - return (Long[])subsetObjectVector(datafile, column, COLUMN_TYPE_LONG); - } - - // Float methods are temporary; - // In normal operations we'll be treating all the floating point types as - // doubles. I need to be able to handle floats for some 4.0 vs 3.* ingest - // tests. -- L.A. - - public Float[] subsetFloatVector(DataFile datafile, int column) throws IOException { - return (Float[])subsetObjectVector(datafile, column, COLUMN_TYPE_FLOAT); - } - - public String[] subsetStringVector(File tabfile, int column, int varcount, int casecount) throws IOException { - return (String[])subsetObjectVector(tabfile, column, varcount, casecount, COLUMN_TYPE_STRING); - } - - public Double[] subsetDoubleVector(File tabfile, int column, int varcount, int casecount) throws IOException { - return (Double[])subsetObjectVector(tabfile, column, varcount, casecount, COLUMN_TYPE_DOUBLE); - } - - public Long[] subsetLongVector(File tabfile, int column, int varcount, int casecount) throws IOException { - return (Long[])subsetObjectVector(tabfile, column, varcount, casecount, COLUMN_TYPE_LONG); - } - - public Float[] subsetFloatVector(File tabfile, int column, int varcount, int casecount) throws IOException { - return (Float[])subsetObjectVector(tabfile, column, varcount, casecount, COLUMN_TYPE_FLOAT); - } - - public Object[] subsetObjectVector(DataFile dataFile, int column, int columntype) throws IOException { - if (!dataFile.isTabularData()) { - throw new IOException("DataFile is not tabular data."); - } - - int varcount = dataFile.getDataTable().getVarQuantity().intValue(); - int casecount = dataFile.getDataTable().getCaseQuantity().intValue(); - - if (column >= varcount) { - throw new IOException("Column "+column+" is out of bounds."); - } - - StorageIO dataAccess = dataFile.getStorageIO(); - if (!dataAccess.isLocalFile()) { - throw new IOException("Subsetting is supported on local files only!"); - } - - //File tabfile = datafile.getFileSystemLocation().toFile(); - File tabfile = dataAccess.getFileSystemPath().toFile(); - - if (columntype == COLUMN_TYPE_STRING) { - String filename = dataFile.getFileMetadata().getLabel(); - if (filename != null) { - filename = filename.replaceFirst("^_", ""); - Integer fnumvalue = null; - try { - fnumvalue = new Integer(filename); - } catch (Exception ex){ - fnumvalue = null; - } - if (fnumvalue != null) { - //if ((fnumvalue.intValue() < 112497)) { // && (fnumvalue.intValue() > 60015)) { - if ((fnumvalue.intValue() < 111931)) { // && (fnumvalue.intValue() > 60015)) { - if (!(fnumvalue.intValue() == 60007 - || fnumvalue.intValue() == 59997 - || fnumvalue.intValue() == 60015 - || fnumvalue.intValue() == 59948 - || fnumvalue.intValue() == 60012 - || fnumvalue.intValue() == 52585 - || fnumvalue.intValue() == 60005 - || fnumvalue.intValue() == 60002 - || fnumvalue.intValue() == 59954 - || fnumvalue.intValue() == 60008 - || fnumvalue.intValue() == 54972 - || fnumvalue.intValue() == 55010 - || fnumvalue.intValue() == 54996 - || fnumvalue.intValue() == 53527 - || fnumvalue.intValue() == 53546 - || fnumvalue.intValue() == 55002 - || fnumvalue.intValue() == 55006 - || fnumvalue.intValue() == 54998 - || fnumvalue.intValue() == 52552 - // SPSS/SAV cases with similar issue - compat mode must be disabled - //|| fnumvalue.intValue() == 101826 // temporary - tricky file with accents and v. 16... - || fnumvalue.intValue() == 54618 // another SAV file, with long strings... - || fnumvalue.intValue() == 54619 // [same] - || fnumvalue.intValue() == 57983 - || fnumvalue.intValue() == 58262 - || fnumvalue.intValue() == 58288 - || fnumvalue.intValue() == 58656 - || fnumvalue.intValue() == 59144 - // || fnumvalue.intValue() == 69626 [nope!] - )) { - dbgLog.info("\"Old\" file name detected; using \"compatibility mode\" for a character vector subset;"); - return subsetObjectVector(tabfile, column, varcount, casecount, columntype, true); - } - } - } - } + private static void skipFirstLine(Scanner scanner) { + if (!scanner.hasNext()) { + throw new RuntimeException("Failed to read the variable name header line from the tab-delimited file!"); } - - return subsetObjectVector(tabfile, column, varcount, casecount, columntype); - } - - public Object[] subsetObjectVector(File tabfile, int column, int varcount, int casecount, int columntype) throws IOException { - return subsetObjectVector(tabfile, column, varcount, casecount, columntype, false); - } - - - - public Object[] subsetObjectVector(File tabfile, int column, int varcount, int casecount, int columntype, boolean compatmode) throws IOException { - - Object[] retVector = null; - - boolean isString = false; - boolean isDouble = false; - boolean isLong = false; - boolean isFloat = false; - - //Locale loc = new Locale("en", "US"); - - if (columntype == COLUMN_TYPE_STRING) { - isString = true; - retVector = new String[casecount]; - } else if (columntype == COLUMN_TYPE_DOUBLE) { - isDouble = true; - retVector = new Double[casecount]; - } else if (columntype == COLUMN_TYPE_LONG) { - isLong = true; - retVector = new Long[casecount]; - } else if (columntype == COLUMN_TYPE_FLOAT){ - isFloat = true; - retVector = new Float[casecount]; - } else { - throw new IOException("Unsupported column type: "+columntype); - } - - File rotatedImageFile = getRotatedImage(tabfile, varcount, casecount); - long[] columnEndOffsets = extractColumnOffsets(rotatedImageFile, varcount, casecount); - long columnOffset = 0; - long columnLength = 0; - - if (column > 0) { - columnOffset = columnEndOffsets[column - 1]; - columnLength = columnEndOffsets[column] - columnEndOffsets[column - 1]; - } else { - columnOffset = varcount * 8; - columnLength = columnEndOffsets[0] - varcount * 8; - } - int caseindex = 0; - - try (FileChannel fc = (FileChannel.open(Paths.get(rotatedImageFile.getAbsolutePath()), - StandardOpenOption.READ))) { - fc.position(columnOffset); - int MAX_COLUMN_BUFFER = 8192; - - ByteBuffer in = ByteBuffer.allocate(MAX_COLUMN_BUFFER); - - if (columnLength < MAX_COLUMN_BUFFER) { - in.limit((int) (columnLength)); - } - - long bytesRead = 0; - long bytesReadTotal = 0; - - int byteoffset = 0; - byte[] leftover = null; - - while (bytesReadTotal < columnLength) { - bytesRead = fc.read(in); - byte[] columnBytes = in.array(); - int bytecount = 0; - - while (bytecount < bytesRead) { - if (columnBytes[bytecount] == '\n') { - /* - String token = new String(columnBytes, byteoffset, bytecount-byteoffset, "UTF8"); - - if (leftover != null) { - String leftoverString = new String (leftover, "UTF8"); - token = leftoverString + token; - leftover = null; - } - */ - /* - * Note that the way I was doing it at first - above - - * was not quite the correct way - because I was creating UTF8 - * strings from the leftover bytes, and the bytes in the - * current buffer *separately*; which means, if a multi-byte - * UTF8 character got split in the middle between one buffer - * and the next, both chunks of it would become junk - * characters, on each side! - * The correct way of doing it, of course, is to create a - * merged byte buffer, and then turn it into a UTF8 string. - * -- L.A. 4.0 - */ - String token = null; - - if (leftover == null) { - token = new String(columnBytes, byteoffset, bytecount - byteoffset, "UTF8"); - } else { - byte[] merged = new byte[leftover.length + bytecount - byteoffset]; - - System.arraycopy(leftover, 0, merged, 0, leftover.length); - System.arraycopy(columnBytes, byteoffset, merged, leftover.length, bytecount - byteoffset); - token = new String(merged, "UTF8"); - leftover = null; - merged = null; - } - - if (isString) { - if ("".equals(token)) { - // An empty string is a string missing value! - // An empty string in quotes is an empty string! - retVector[caseindex] = null; - } else { - // Strip the outer quotes: - token = token.replaceFirst("^\\\"", ""); - token = token.replaceFirst("\\\"$", ""); - - // We need to restore the special characters that - // are stored in tab files escaped - quotes, new lines - // and tabs. Before we do that however, we need to - // take care of any escaped backslashes stored in - // the tab file. I.e., "foo\t" should be transformed - // to "foo"; but "foo\\t" should be transformed - // to "foo\t". This way new lines and tabs that were - // already escaped in the original data are not - // going to be transformed to unescaped tab and - // new line characters! - - String[] splitTokens = token.split(Matcher.quoteReplacement("\\\\"), -2); - - // (note that it's important to use the 2-argument version - // of String.split(), and set the limit argument to a - // negative value; otherwise any trailing backslashes - // are lost.) - - for (int i = 0; i < splitTokens.length; i++) { - splitTokens[i] = splitTokens[i].replaceAll(Matcher.quoteReplacement("\\\""), "\""); - splitTokens[i] = splitTokens[i].replaceAll(Matcher.quoteReplacement("\\t"), "\t"); - splitTokens[i] = splitTokens[i].replaceAll(Matcher.quoteReplacement("\\n"), "\n"); - splitTokens[i] = splitTokens[i].replaceAll(Matcher.quoteReplacement("\\r"), "\r"); - } - // TODO: - // Make (some of?) the above optional; for ex., we - // do need to restore the newlines when calculating UNFs; - // But if we are subsetting these vectors in order to - // create a new tab-delimited file, they will - // actually break things! -- L.A. Jul. 28 2014 - - token = StringUtils.join(splitTokens, '\\'); - - // "compatibility mode" - a hack, to be able to produce - // unfs identical to those produced by the "early" - // unf5 jar; will be removed in production 4.0. - // -- L.A. (TODO: ...) - if (compatmode && !"".equals(token)) { - if (token.length() > 128) { - if ("".equals(token.trim())) { - // don't ask... - token = token.substring(0, 129); - } else { - token = token.substring(0, 128); - // token = String.format(loc, "%.128s", token); - token = token.trim(); - // dbgLog.info("formatted and trimmed: "+token); - } - } else { - if ("".equals(token.trim())) { - // again, don't ask; - // - this replicates some bugginness - // that happens inside unf5; - token = "null"; - } else { - token = token.trim(); - } - } - } - - retVector[caseindex] = token; - } - } else if (isDouble) { - try { - // TODO: verify that NaN and +-Inf are - // handled correctly here! -- L.A. - // Verified: new Double("nan") works correctly, - // resulting in Double.NaN; - // Double("[+-]Inf") doesn't work however; - // (the constructor appears to be expecting it - // to be spelled as "Infinity", "-Infinity", etc. - if ("inf".equalsIgnoreCase(token) || "+inf".equalsIgnoreCase(token)) { - retVector[caseindex] = java.lang.Double.POSITIVE_INFINITY; - } else if ("-inf".equalsIgnoreCase(token)) { - retVector[caseindex] = java.lang.Double.NEGATIVE_INFINITY; - } else if (token == null || token.equals("")) { - // missing value: - retVector[caseindex] = null; - } else { - retVector[caseindex] = new Double(token); - } - } catch (NumberFormatException ex) { - dbgLog.warning("NumberFormatException thrown for " + token + " as Double"); - - retVector[caseindex] = null; // missing value - // TODO: ? - } - } else if (isLong) { - try { - retVector[caseindex] = new Long(token); - } catch (NumberFormatException ex) { - retVector[caseindex] = null; // assume missing value - } - } else if (isFloat) { - try { - if ("inf".equalsIgnoreCase(token) || "+inf".equalsIgnoreCase(token)) { - retVector[caseindex] = java.lang.Float.POSITIVE_INFINITY; - } else if ("-inf".equalsIgnoreCase(token)) { - retVector[caseindex] = java.lang.Float.NEGATIVE_INFINITY; - } else if (token == null || token.equals("")) { - // missing value: - retVector[caseindex] = null; - } else { - retVector[caseindex] = new Float(token); - } - } catch (NumberFormatException ex) { - dbgLog.warning("NumberFormatException thrown for " + token + " as Float"); - retVector[caseindex] = null; // assume missing value (TODO: ?) - } - } - caseindex++; - - if (bytecount == bytesRead - 1) { - byteoffset = 0; - } else { - byteoffset = bytecount + 1; - } - } else { - if (bytecount == bytesRead - 1) { - // We've reached the end of the buffer; - // This means we'll save whatever unused bytes left in - // it - i.e., the bytes between the last new line - // encountered and the end - in the leftover buffer. - - // *EXCEPT*, there may be a case of a very long String - // that is actually longer than MAX_COLUMN_BUFFER, in - // which case it is possible that we've read through - // an entire buffer of bytes without finding any - // new lines... in this case we may need to add this - // entire byte buffer to an already existing leftover - // buffer! - if (leftover == null) { - leftover = new byte[(int) bytesRead - byteoffset]; - System.arraycopy(columnBytes, byteoffset, leftover, 0, (int) bytesRead - byteoffset); - } else { - if (byteoffset != 0) { - throw new IOException("Reached the end of the byte buffer, with some leftover left from the last read; yet the offset is not zero!"); - } - byte[] merged = new byte[leftover.length + (int) bytesRead]; - - System.arraycopy(leftover, 0, merged, 0, leftover.length); - System.arraycopy(columnBytes, byteoffset, merged, leftover.length, (int) bytesRead); - // leftover = null; - leftover = merged; - merged = null; - } - byteoffset = 0; - - } - } - bytecount++; - } - - bytesReadTotal += bytesRead; - in.clear(); - if (columnLength - bytesReadTotal < MAX_COLUMN_BUFFER) { - in.limit((int) (columnLength - bytesReadTotal)); - } - } - - } - - if (caseindex != casecount) { - throw new IOException("Faile to read "+casecount+" tokens for column "+column); - //System.out.println("read "+caseindex+" tokens instead of expected "+casecount+"."); - } - - return retVector; - } - - private long[] extractColumnOffsets (File rotatedImageFile, int varcount, int casecount) throws IOException { - long[] byteOffsets = new long[varcount]; - - try (BufferedInputStream rotfileStream = new BufferedInputStream(new FileInputStream(rotatedImageFile))) { - - byte[] offsetHeader = new byte[varcount * 8]; - - int readlen = rotfileStream.read(offsetHeader); - - if (readlen != varcount * 8) { - throw new IOException("Could not read " + varcount * 8 + " header bytes from the rotated file."); - } - - for (int varindex = 0; varindex < varcount; varindex++) { - byte[] offsetBytes = new byte[8]; - System.arraycopy(offsetHeader, varindex * 8, offsetBytes, 0, 8); - - ByteBuffer offsetByteBuffer = ByteBuffer.wrap(offsetBytes); - byteOffsets[varindex] = offsetByteBuffer.getLong(); - - // System.out.println(byteOffsets[varindex]); - } - - } - - return byteOffsets; - } - - private File getRotatedImage(File tabfile, int varcount, int casecount) throws IOException { - String fileName = tabfile.getAbsolutePath(); - String rotatedImageFileName = fileName + ".90d"; - File rotatedImageFile = new File(rotatedImageFileName); - if (rotatedImageFile.exists()) { - //System.out.println("Image already exists!"); - return rotatedImageFile; - } - - return generateRotatedImage(tabfile, varcount, casecount); - - } - - private File generateRotatedImage (File tabfile, int varcount, int casecount) throws IOException { - // TODO: throw exceptions if bad file, zero varcount, etc. ... - - String fileName = tabfile.getAbsolutePath(); - String rotatedImageFileName = fileName + ".90d"; - - int MAX_OUTPUT_STREAMS = 32; - int MAX_BUFFERED_BYTES = 10 * 1024 * 1024; // 10 MB - for now? - int MAX_COLUMN_BUFFER = 8 * 1024; - - // offsetHeader will contain the byte offsets of the individual column - // vectors in the final rotated image file - byte[] offsetHeader = new byte[varcount * 8]; - int[] bufferedSizes = new int[varcount]; - long[] cachedfileSizes = new long[varcount]; - File[] columnTempFiles = new File[varcount]; - - for (int i = 0; i < varcount; i++) { - bufferedSizes[i] = 0; - cachedfileSizes[i] = 0; - } - - // TODO: adjust MAX_COLUMN_BUFFER here, so that the total size is - // no more than MAX_BUFFERED_BYTES (but no less than 1024 maybe?) - - byte[][] bufferedColumns = new byte [varcount][MAX_COLUMN_BUFFER]; - - // read the tab-delimited file: - - try (FileInputStream tabfileStream = new FileInputStream(tabfile); - Scanner scanner = new Scanner(tabfileStream)) { - scanner.useDelimiter("\\n"); - - for (int caseindex = 0; caseindex < casecount; caseindex++) { - if (scanner.hasNext()) { - String[] line = (scanner.next()).split("\t", -1); - // TODO: throw an exception if there are fewer tab-delimited - // tokens than the number of variables specified. - String token = ""; - int tokensize = 0; - for (int varindex = 0; varindex < varcount; varindex++) { - // TODO: figure out the safest way to convert strings to - // bytes here. Is it going to be safer to use getBytes("UTF8")? - // we are already making the assumption that the values - // in the tab file are in UTF8. -- L.A. - token = line[varindex] + "\n"; - tokensize = token.getBytes().length; - if (bufferedSizes[varindex] + tokensize > MAX_COLUMN_BUFFER) { - // fill the buffer and dump its contents into the temp file: - // (do note that there may be *several* MAX_COLUMN_BUFFERs - // worth of bytes in the token!) - - int tokenoffset = 0; - - if (bufferedSizes[varindex] != MAX_COLUMN_BUFFER) { - tokenoffset = MAX_COLUMN_BUFFER - bufferedSizes[varindex]; - System.arraycopy(token.getBytes(), 0, bufferedColumns[varindex], bufferedSizes[varindex], tokenoffset); - } // (otherwise the buffer is already full, and we should - // simply dump it into the temp file, without adding any - // extra bytes to it) - - File bufferTempFile = columnTempFiles[varindex]; - if (bufferTempFile == null) { - bufferTempFile = File.createTempFile("columnBufferFile", "bytes"); - columnTempFiles[varindex] = bufferTempFile; - } - - // *append* the contents of the buffer to the end of the - // temp file, if already exists: - try (BufferedOutputStream outputStream = new BufferedOutputStream( - new FileOutputStream(bufferTempFile, true))) { - outputStream.write(bufferedColumns[varindex], 0, MAX_COLUMN_BUFFER); - cachedfileSizes[varindex] += MAX_COLUMN_BUFFER; - - // keep writing MAX_COLUMN_BUFFER-size chunks of bytes into - // the temp file, for as long as there's more than MAX_COLUMN_BUFFER - // bytes left in the token: - - while (tokensize - tokenoffset > MAX_COLUMN_BUFFER) { - outputStream.write(token.getBytes(), tokenoffset, MAX_COLUMN_BUFFER); - cachedfileSizes[varindex] += MAX_COLUMN_BUFFER; - tokenoffset += MAX_COLUMN_BUFFER; - } - - } - - // buffer the remaining bytes and reset the buffered - // byte counter: - - System.arraycopy(token.getBytes(), - tokenoffset, - bufferedColumns[varindex], - 0, - tokensize - tokenoffset); - - bufferedSizes[varindex] = tokensize - tokenoffset; - - } else { - // continue buffering - System.arraycopy(token.getBytes(), 0, bufferedColumns[varindex], bufferedSizes[varindex], tokensize); - bufferedSizes[varindex] += tokensize; - } - } - } else { - throw new IOException("Tab file has fewer rows than the stored number of cases!"); - } - } - } - - // OK, we've created the individual byte vectors of the tab file columns; - // they may be partially saved in temp files and/or in memory. - // We now need to go through all these buffers and create the final - // rotated image file. - - try (BufferedOutputStream finalOut = new BufferedOutputStream( - new FileOutputStream(new File(rotatedImageFileName)))) { - - // but first we should create the offset header and write it out into - // the final file; because it should be at the head, doh! - - long columnOffset = varcount * 8; - // (this is the offset of the first column vector; it is equal to the - // size of the offset header, i.e. varcount * 8 bytes) - - for (int varindex = 0; varindex < varcount; varindex++) { - long totalColumnBytes = cachedfileSizes[varindex] + bufferedSizes[varindex]; - columnOffset += totalColumnBytes; - // totalColumnBytes; - byte[] columnOffsetByteArray = ByteBuffer.allocate(8).putLong(columnOffset).array(); - System.arraycopy(columnOffsetByteArray, 0, offsetHeader, varindex * 8, 8); - } - - finalOut.write(offsetHeader, 0, varcount * 8); - - for (int varindex = 0; varindex < varcount; varindex++) { - long cachedBytesRead = 0; - - // check if there is a cached temp file: - - File cachedTempFile = columnTempFiles[varindex]; - if (cachedTempFile != null) { - byte[] cachedBytes = new byte[MAX_COLUMN_BUFFER]; - try (BufferedInputStream cachedIn = new BufferedInputStream(new FileInputStream(cachedTempFile))) { - int readlen = 0; - while ((readlen = cachedIn.read(cachedBytes)) > -1) { - finalOut.write(cachedBytes, 0, readlen); - cachedBytesRead += readlen; - } - } - - // delete the temp file: - cachedTempFile.delete(); - - } - - if (cachedBytesRead != cachedfileSizes[varindex]) { - throw new IOException("Could not read the correct number of bytes cached for column "+varindex+"; "+ - cachedfileSizes[varindex] + " bytes expected, "+cachedBytesRead+" read."); - } - - // then check if there are any bytes buffered for this column: - - if (bufferedSizes[varindex] > 0) { - finalOut.write(bufferedColumns[varindex], 0, bufferedSizes[varindex]); - } - - } - } - - return new File(rotatedImageFileName); - - } - - /* - * Test method for taking a "rotated" image, and reversing it, reassembling - * all the columns in the original order. Which should result in a file - * byte-for-byte identical file to the original tab-delimited version. - * - * (do note that this method is not efficiently implemented; it's only - * being used for experiments so far, to confirm the accuracy of the - * accuracy of generateRotatedImage(). It should not be used for any - * practical means in the application!) - */ - private void reverseRotatedImage (File rotfile, int varcount, int casecount) throws IOException { - // open the file, read in the offset header: - try (BufferedInputStream rotfileStream = new BufferedInputStream(new FileInputStream(rotfile))) { - byte[] offsetHeader = new byte[varcount * 8]; - long[] byteOffsets = new long[varcount]; - - int readlen = rotfileStream.read(offsetHeader); - - if (readlen != varcount * 8) { - throw new IOException ("Could not read "+varcount*8+" header bytes from the rotated file."); - } - - for (int varindex = 0; varindex < varcount; varindex++) { - byte[] offsetBytes = new byte[8]; - System.arraycopy(offsetHeader, varindex*8, offsetBytes, 0, 8); - - ByteBuffer offsetByteBuffer = ByteBuffer.wrap(offsetBytes); - byteOffsets[varindex] = offsetByteBuffer.getLong(); - - //System.out.println(byteOffsets[varindex]); - } - - String [][] reversedMatrix = new String[casecount][varcount]; - - long offset = varcount * 8; - byte[] columnBytes; - - for (int varindex = 0; varindex < varcount; varindex++) { - long columnLength = byteOffsets[varindex] - offset; - - - - columnBytes = new byte[(int)columnLength]; - readlen = rotfileStream.read(columnBytes); - - if (readlen != columnLength) { - throw new IOException ("Could not read "+columnBytes+" bytes for column "+varindex); - } - /* - String columnString = new String(columnBytes); - //System.out.print(columnString); - String[] values = columnString.split("\n", -1); - - if (values.length < casecount) { - throw new IOException("count mismatch: "+values.length+" tokens found for column "+varindex); - } - - for (int caseindex = 0; caseindex < casecount; caseindex++) { - reversedMatrix[caseindex][varindex] = values[caseindex]; - }*/ - - int bytecount = 0; - int byteoffset = 0; - int caseindex = 0; - //System.out.println("generating value vector for column "+varindex); - while (bytecount < columnLength) { - if (columnBytes[bytecount] == '\n') { - String token = new String(columnBytes, byteoffset, bytecount-byteoffset); - reversedMatrix[caseindex++][varindex] = token; - byteoffset = bytecount + 1; - } - bytecount++; - } - - if (caseindex != casecount) { - throw new IOException("count mismatch: "+caseindex+" tokens found for column "+varindex); - } - offset = byteOffsets[varindex]; - } - - for (int caseindex = 0; caseindex < casecount; caseindex++) { - for (int varindex = 0; varindex < varcount; varindex++) { - System.out.print(reversedMatrix[caseindex][varindex]); - if (varindex < varcount-1) { - System.out.print("\t"); - } else { - System.out.print("\n"); - } - } - } - - } - - - } - - /** - * main() method, for testing - * usage: java edu.harvard.iq.dataverse.dataaccess.TabularSubsetGenerator testfile.tab varcount casecount column type - * make sure the CLASSPATH contains ... - * - */ - - public static void main(String[] args) { - - String tabFileName = args[0]; - int varcount = new Integer(args[1]).intValue(); - int casecount = new Integer(args[2]).intValue(); - int column = new Integer(args[3]).intValue(); - String type = args[4]; - - File tabFile = new File(tabFileName); - File rotatedImageFile = null; - - TabularSubsetGenerator subsetGenerator = new TabularSubsetGenerator(); - - /* - try { - rotatedImageFile = subsetGenerator.getRotatedImage(tabFile, varcount, casecount); - } catch (IOException ex) { - System.out.println(ex.getMessage()); - } - */ - - //System.out.println("\nFinished generating \"rotated\" column image file."); - - //System.out.println("\nOffsets:"); - - MathContext doubleMathContext = new MathContext(15, RoundingMode.HALF_EVEN); - String FORMAT_IEEE754 = "%+#.15e"; - - try { - //subsetGenerator.reverseRotatedImage(rotatedImageFile, varcount, casecount); - //String[] columns = subsetGenerator.subsetStringVector(tabFile, column, varcount, casecount); - if ("string".equals(type)) { - String[] columns = subsetGenerator.subsetStringVector(tabFile, column, varcount, casecount); - for (int i = 0; i < casecount; i++) { - System.out.println(columns[i]); - } - } else { - - Double[] columns = subsetGenerator.subsetDoubleVector(tabFile, column, varcount, casecount); - for (int i = 0; i < casecount; i++) { - if (columns[i] != null) { - BigDecimal outBigDecimal = new BigDecimal(columns[i], doubleMathContext); - System.out.println(String.format(FORMAT_IEEE754, outBigDecimal)); - } else { - System.out.println("NA"); - } - //System.out.println(columns[i]); - } - } - } catch (IOException ex) { - System.out.println(ex.getMessage()); - } - } -} - - + scanner.next(); + } +} \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/TabularSubsetInputStream.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/TabularSubsetInputStream.java deleted file mode 100644 index 89e033353c1..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/TabularSubsetInputStream.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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.dataaccess; - -import edu.harvard.iq.dataverse.DataFile; -import edu.harvard.iq.dataverse.datavariable.DataVariable; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import java.util.logging.Logger; - -/** - * - * @author Leonid Andreev - */ -public class TabularSubsetInputStream extends InputStream { - private static final Logger logger = Logger.getLogger(TabularSubsetInputStream.class.getCanonicalName()); - - private TabularSubsetGenerator subsetGenerator = null; - private int numberOfSubsetVariables; - private int numberOfObservations; - private int numberOfObservationsRead = 0; - private byte[] leftoverBytes = null; - - public TabularSubsetInputStream(DataFile datafile, List variables) throws IOException { - if (datafile == null) { - throw new IOException("Null datafile in subset request"); - } - if (!datafile.isTabularData()) { - throw new IOException("Subset requested on a non-tabular data file"); - } - numberOfObservations = datafile.getDataTable().getCaseQuantity().intValue(); - - if (variables == null || variables.size() < 1) { - throw new IOException("Null or empty list of variables in subset request."); - } - numberOfSubsetVariables = variables.size(); - subsetGenerator = new TabularSubsetGenerator(datafile, variables); - - } - - //@Override - public int read() throws IOException { - throw new IOException("read() method not implemented; do not use."); - } - - //@Override - public int read(byte[] b) throws IOException { - // TODO: - // Move this code into TabularSubsetGenerator - logger.fine("subset input stream: read request, on a "+b.length+" byte buffer;"); - - if (numberOfSubsetVariables == 1) { - logger.fine("calling the single variable subset read method"); - return subsetGenerator.readSingleColumnSubset(b); - } - - int bytesread = 0; - byte [] linebuffer; - - // do we have a leftover? - if (leftoverBytes != null) { - if (leftoverBytes.length < b.length) { - System.arraycopy(leftoverBytes, 0, b, 0, leftoverBytes.length); - bytesread = leftoverBytes.length; - leftoverBytes = null; - - } else { - // shouldn't really happen... unless it's a very large subset, - // or a very long string, etc. - System.arraycopy(leftoverBytes, 0, b, 0, b.length); - byte[] tmp = new byte[leftoverBytes.length - b.length]; - System.arraycopy(leftoverBytes, b.length, tmp, 0, leftoverBytes.length - b.length); - leftoverBytes = tmp; - tmp = null; - return b.length; - } - } - - while (bytesread < b.length && numberOfObservationsRead < numberOfObservations) { - linebuffer = subsetGenerator.readSubsetLineBytes(); - numberOfObservationsRead++; - - if (bytesread + linebuffer.length < b.length) { - // copy linebuffer into the return buffer: - System.arraycopy(linebuffer, 0, b, bytesread, linebuffer.length); - bytesread += linebuffer.length; - } else { - System.arraycopy(linebuffer, 0, b, bytesread, b.length - bytesread); - // save the leftover; - if (bytesread + linebuffer.length > b.length) { - leftoverBytes = new byte[bytesread + linebuffer.length - b.length]; - System.arraycopy(linebuffer, b.length - bytesread, leftoverBytes, 0, bytesread + linebuffer.length - b.length); - } - return b.length; - } - } - - // and this means we've reached the end of the tab file! - - return bytesread > 0 ? bytesread : -1; - } - - //@Override - public void close() { - if (subsetGenerator != null) { - subsetGenerator.close(); - } - } -} diff --git a/src/main/java/edu/harvard/iq/dataverse/export/DDIExportServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/export/DDIExportServiceBean.java index 5119b4b96c7..edd01ae98a3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/export/DDIExportServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/export/DDIExportServiceBean.java @@ -545,6 +545,16 @@ private void createDataFileDDI(XMLStreamWriter xmlw, Set excludedFieldSe List vars = variableService.findByDataTableId(dt.getId()); if (checkField("catgry", excludedFieldSet, includedFieldSet)) { if (checkIsWithoutFrequencies(vars)) { + // @todo: the method called here to calculate frequencies + // when they are missing from the database (for whatever + // reasons) subsets the physical tab-delimited file and + // calculates them in real time. this is very expensive operation + // potentially. let's make sure that, when we do this, we + // save the resulting frequencies in the database, so that + // we don't have to do this again. Also, let's double check + // whether the "checkIsWithoutFrequencies()" method is doing + // the right thing - as it appears to return true when there + // are no categorical variables in the DataTable (?) calculateFrequencies(df, vars); } } @@ -580,6 +590,7 @@ private boolean checkIsWithoutFrequencies(List vars) { private void calculateFrequencies(DataFile df, List vars) { + // @todo: see the comment in the part of the code that calls this method try { DataConverter dc = new DataConverter(); File tabFile = dc.downloadFromStorageIO(df.getStorageIO()); diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java index 233f746fb17..9bacafd173f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java @@ -726,27 +726,17 @@ public void produceSummaryStatistics(DataFile dataFile, File generatedTabularFil } public void produceContinuousSummaryStatistics(DataFile dataFile, File generatedTabularFile) throws IOException { - - /* - // quick, but memory-inefficient way: - // - this method just loads the entire file-worth of continuous vectors - // into a Double[][] matrix. - //Double[][] variableVectors = subsetContinuousVectors(dataFile); - //calculateContinuousSummaryStatistics(dataFile, variableVectors); - - // A more sophisticated way: this subsets one column at a time, using - // the new optimized subsetting that does not have to read any extra - // bytes from the file to extract the column: - - TabularSubsetGenerator subsetGenerator = new TabularSubsetGenerator(); - */ for (int i = 0; i < dataFile.getDataTable().getVarQuantity(); i++) { if (dataFile.getDataTable().getDataVariables().get(i).isIntervalContinuous()) { logger.fine("subsetting continuous vector"); if ("float".equals(dataFile.getDataTable().getDataVariables().get(i).getFormat())) { - Float[] variableVector = TabularSubsetGenerator.subsetFloatVector(new FileInputStream(generatedTabularFile), i, dataFile.getDataTable().getCaseQuantity().intValue()); + Float[] variableVector = TabularSubsetGenerator.subsetFloatVector( + new FileInputStream(generatedTabularFile), + i, + dataFile.getDataTable().getCaseQuantity().intValue(), + dataFile.getDataTable().isStoredWithVariableHeader()); logger.fine("Calculating summary statistics on a Float vector;"); calculateContinuousSummaryStatistics(dataFile, i, variableVector); // calculate the UNF while we are at it: @@ -754,7 +744,11 @@ public void produceContinuousSummaryStatistics(DataFile dataFile, File generated calculateUNF(dataFile, i, variableVector); variableVector = null; } else { - Double[] variableVector = TabularSubsetGenerator.subsetDoubleVector(new FileInputStream(generatedTabularFile), i, dataFile.getDataTable().getCaseQuantity().intValue()); + Double[] variableVector = TabularSubsetGenerator.subsetDoubleVector( + new FileInputStream(generatedTabularFile), + i, + dataFile.getDataTable().getCaseQuantity().intValue(), + dataFile.getDataTable().isStoredWithVariableHeader()); logger.fine("Calculating summary statistics on a Double vector;"); calculateContinuousSummaryStatistics(dataFile, i, variableVector); // calculate the UNF while we are at it: @@ -776,7 +770,11 @@ public void produceDiscreteNumericSummaryStatistics(DataFile dataFile, File gene && dataFile.getDataTable().getDataVariables().get(i).isTypeNumeric()) { logger.fine("subsetting discrete-numeric vector"); - Long[] variableVector = TabularSubsetGenerator.subsetLongVector(new FileInputStream(generatedTabularFile), i, dataFile.getDataTable().getCaseQuantity().intValue()); + Long[] variableVector = TabularSubsetGenerator.subsetLongVector( + new FileInputStream(generatedTabularFile), + i, + dataFile.getDataTable().getCaseQuantity().intValue(), + dataFile.getDataTable().isStoredWithVariableHeader()); // We are discussing calculating the same summary stats for // all numerics (the same kind of sumstats that we've been calculating // for numeric continuous type) -- L.A. Jul. 2014 @@ -810,7 +808,11 @@ public void produceCharacterSummaryStatistics(DataFile dataFile, File generatedT if (dataFile.getDataTable().getDataVariables().get(i).isTypeCharacter()) { logger.fine("subsetting character vector"); - String[] variableVector = TabularSubsetGenerator.subsetStringVector(new FileInputStream(generatedTabularFile), i, dataFile.getDataTable().getCaseQuantity().intValue()); + String[] variableVector = TabularSubsetGenerator.subsetStringVector( + new FileInputStream(generatedTabularFile), + i, + dataFile.getDataTable().getCaseQuantity().intValue(), + dataFile.getDataTable().isStoredWithVariableHeader()); //calculateCharacterSummaryStatistics(dataFile, i, variableVector); // calculate the UNF while we are at it: logger.fine("Calculating UNF on a String vector"); @@ -828,20 +830,29 @@ public static void produceFrequencyStatistics(DataFile dataFile, File generatedT produceFrequencies(generatedTabularFile, vars); } - public static void produceFrequencies( File generatedTabularFile, List vars) throws IOException { + public static void produceFrequencies(File generatedTabularFile, List vars) throws IOException { for (int i = 0; i < vars.size(); i++) { Collection cats = vars.get(i).getCategories(); int caseQuantity = vars.get(i).getDataTable().getCaseQuantity().intValue(); boolean isNumeric = vars.get(i).isTypeNumeric(); + boolean skipVariableHeaderLine = vars.get(i).getDataTable().isStoredWithVariableHeader(); Object[] variableVector = null; if (cats.size() > 0) { if (isNumeric) { - variableVector = TabularSubsetGenerator.subsetFloatVector(new FileInputStream(generatedTabularFile), i, caseQuantity); + variableVector = TabularSubsetGenerator.subsetFloatVector( + new FileInputStream(generatedTabularFile), + i, + caseQuantity, + skipVariableHeaderLine); } else { - variableVector = TabularSubsetGenerator.subsetStringVector(new FileInputStream(generatedTabularFile), i, caseQuantity); + variableVector = TabularSubsetGenerator.subsetStringVector( + new FileInputStream(generatedTabularFile), + i, + caseQuantity, + skipVariableHeaderLine); } if (variableVector != null) { Hashtable freq = calculateFrequency(variableVector); @@ -923,6 +934,7 @@ public boolean ingestAsTabular(Long datafile_id) { DataFile dataFile = fileService.find(datafile_id); boolean ingestSuccessful = false; boolean forceTypeCheck = false; + boolean storingWithVariableHeader = systemConfig.isStoringIngestedFilesWithHeaders(); // Never attempt to ingest a file that's already ingested! if (dataFile.isTabularData()) { @@ -1024,11 +1036,7 @@ public boolean ingestAsTabular(Long datafile_id) { TabularDataIngest tabDataIngest = null; try { - if (additionalData != null) { - tabDataIngest = ingestPlugin.read(inputStream, additionalData); - } else { - tabDataIngest = ingestPlugin.read(inputStream, null); - } + tabDataIngest = ingestPlugin.read(inputStream, storingWithVariableHeader, additionalData); } catch (IOException ingestEx) { dataFile.SetIngestProblem(); FileUtil.createIngestFailureReport(dataFile, ingestEx.getMessage()); @@ -1081,6 +1089,7 @@ public boolean ingestAsTabular(Long datafile_id) { dataFile.setDataTable(tabDataIngest.getDataTable()); tabDataIngest.getDataTable().setDataFile(dataFile); tabDataIngest.getDataTable().setOriginalFileName(originalFileName); + dataFile.getDataTable().setStoredWithVariableHeader(storingWithVariableHeader); try { produceSummaryStatistics(dataFile, tabFile); @@ -1172,6 +1181,7 @@ public boolean ingestAsTabular(Long datafile_id) { // Replace contents of the file with the tab-delimited data produced: dataAccess.savePath(Paths.get(tabFile.getAbsolutePath())); + // Reset the file size: dataFile.setFilesize(dataAccess.getSize()); @@ -2297,7 +2307,7 @@ public static void main(String[] args) { TabularDataIngest tabDataIngest = null; try { - tabDataIngest = ingestPlugin.read(fileInputStream, null); + tabDataIngest = ingestPlugin.read(fileInputStream, false, null); } catch (IOException ingestEx) { System.err.println("Caught an exception trying to ingest file "+file+"."); System.exit(1); diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/TabularDataFileReader.java b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/TabularDataFileReader.java index 223b171dfb5..0f23a3d9781 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/TabularDataFileReader.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/TabularDataFileReader.java @@ -20,10 +20,13 @@ package edu.harvard.iq.dataverse.ingest.tabulardata; +import edu.harvard.iq.dataverse.datavariable.DataVariable; import edu.harvard.iq.dataverse.ingest.tabulardata.spi.*; //import edu.harvard.iq.dataverse.ingest.plugin.metadata.*; import java.io.*; import static java.lang.System.*; +import java.util.Iterator; +import java.util.List; import java.util.regex.Matcher; /** @@ -98,7 +101,7 @@ public void setDataLanguageEncoding(String dataLanguageEncoding) { * * @throws java.io.IOException if a reading error occurs. */ - public abstract TabularDataIngest read(BufferedInputStream stream, File dataFile) + public abstract TabularDataIngest read(BufferedInputStream stream, boolean storeWithVariableHeader, File dataFile) throws IOException; @@ -176,5 +179,26 @@ protected String escapeCharacterString(String rawString) { return escapedString; } + + protected String generateVariableHeader(List dvs) { + String varHeader = null; + + if (dvs != null) { + Iterator iter = dvs.iterator(); + DataVariable dv; + + if (iter.hasNext()) { + dv = iter.next(); + varHeader = dv.getName(); + } + + while (iter.hasNext()) { + dv = iter.next(); + varHeader = varHeader + "\t" + dv.getName(); + } + } + + return varHeader; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/csv/CSVFileReader.java b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/csv/CSVFileReader.java index 57f76df3802..f8816ababb4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/csv/CSVFileReader.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/csv/CSVFileReader.java @@ -110,7 +110,7 @@ private void init() throws IOException { * @throws java.io.IOException if a reading error occurs. */ @Override - public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws IOException { + public TabularDataIngest read(BufferedInputStream stream, boolean saveWithVariableHeader, File dataFile) throws IOException { init(); if (stream == null) { @@ -124,7 +124,7 @@ public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws File tabFileDestination = File.createTempFile("data-", ".tab"); PrintWriter tabFileWriter = new PrintWriter(tabFileDestination.getAbsolutePath()); - int lineCount = readFile(localBufferedReader, dataTable, tabFileWriter); + int lineCount = readFile(localBufferedReader, dataTable, saveWithVariableHeader, tabFileWriter); logger.fine("Tab file produced: " + tabFileDestination.getAbsolutePath()); @@ -136,14 +136,17 @@ public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws } - public int readFile(BufferedReader csvReader, DataTable dataTable, PrintWriter finalOut) throws IOException { + public int readFile(BufferedReader csvReader, DataTable dataTable, boolean saveWithVariableHeader, PrintWriter finalOut) throws IOException { List variableList = new ArrayList<>(); CSVParser parser = new CSVParser(csvReader, inFormat.withHeader()); Map headers = parser.getHeaderMap(); int i = 0; + String variableNameHeader = null; + for (String varName : headers.keySet()) { + // @todo: is .keySet() guaranteed to return the names in the right order? if (varName == null || varName.isEmpty()) { // TODO: // Add a sensible variable name validation algorithm. @@ -158,6 +161,13 @@ public int readFile(BufferedReader csvReader, DataTable dataTable, PrintWriter f dv.setTypeCharacter(); dv.setIntervalDiscrete(); + + if (saveWithVariableHeader) { + variableNameHeader = variableNameHeader == null + ? varName + : variableNameHeader.concat("\t" + varName); + } + i++; } @@ -342,6 +352,14 @@ public int readFile(BufferedReader csvReader, DataTable dataTable, PrintWriter f try (BufferedReader secondPassReader = new BufferedReader(new FileReader(firstPassTempFile))) { parser = new CSVParser(secondPassReader, inFormat.withHeader()); String[] caseRow = new String[headers.size()]; + + // Save the variable name header, if requested + if (saveWithVariableHeader) { + if (variableNameHeader == null) { + throw new IOException("failed to generate the Variable Names header"); + } + finalOut.println(variableNameHeader); + } for (CSVRecord record : parser) { if (!record.isConsistent()) { diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/DTAFileReader.java b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/DTAFileReader.java index 2dec701592e..73818f8fb62 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/DTAFileReader.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/DTAFileReader.java @@ -505,7 +505,7 @@ private void init() throws IOException { } @Override - public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws IOException { + public TabularDataIngest read(BufferedInputStream stream, boolean storeWithVariableHeader, File dataFile) throws IOException { dbgLog.info("***** DTAFileReader: read() start *****"); if (dataFile != null) { @@ -519,7 +519,7 @@ public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws if (releaseNumber!=104) { decodeExpansionFields(stream); } - decodeData(stream); + decodeData(stream, storeWithVariableHeader); decodeValueLabels(stream); ingesteddata.setDataTable(dataTable); @@ -1665,7 +1665,7 @@ private void parseValueLabelsReleasel108(BufferedInputStream stream) throws IOEx dbgLog.fine("parseValueLabelsRelease108(): end"); } - private void decodeData(BufferedInputStream stream) throws IOException { + private void decodeData(BufferedInputStream stream, boolean saveWithVariableHeader) throws IOException { dbgLog.fine("\n***** decodeData(): start *****"); @@ -1719,6 +1719,11 @@ private void decodeData(BufferedInputStream stream) throws IOException { BUT, this needs to be reviewed/confirmed etc! */ //String[][] dateFormat = new String[nvar][nobs]; + + // add the variable header here, if needed + if (saveWithVariableHeader) { + pwout.println(generateVariableHeader(dataTable.getDataVariables())); + } for (int i = 0; i < nobs; i++) { byte[] dataRowBytes = new byte[bytes_per_row]; diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/NewDTAFileReader.java b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/NewDTAFileReader.java index 22581834676..53607d541de 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/NewDTAFileReader.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/NewDTAFileReader.java @@ -339,7 +339,7 @@ private void init() throws IOException { } @Override - public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws IOException { + public TabularDataIngest read(BufferedInputStream stream, boolean storeWithVariableHeader, File dataFile) throws IOException { logger.fine("NewDTAFileReader: read() start"); // shit ton of diagnostics (still) needed here!! -- L.A. @@ -363,7 +363,13 @@ public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws // "characteristics" - STATA-proprietary information // (we are skipping it) readCharacteristics(dataReader); - readData(dataReader); + + String variableHeaderLine = null; + + if (storeWithVariableHeader) { + variableHeaderLine = generateVariableHeader(dataTable.getDataVariables()); + } + readData(dataReader, variableHeaderLine); // (potentially) large, (potentially) non-ASCII character strings // saved outside the section, and referenced @@ -707,7 +713,7 @@ private void readCharacteristics(DataReader reader) throws IOException { } - private void readData(DataReader reader) throws IOException { + private void readData(DataReader reader, String variableHeaderLine) throws IOException { logger.fine("Data section; at offset " + reader.getByteOffset() + "; dta map offset: " + dtaMap.getOffset_data()); logger.fine("readData(): start"); reader.readOpeningTag(TAG_DATA); @@ -731,6 +737,11 @@ private void readData(DataReader reader) throws IOException { FileOutputStream fileOutTab = new FileOutputStream(tabDelimitedDataFile); PrintWriter pwout = new PrintWriter(new OutputStreamWriter(fileOutTab, "utf8"), true); + // add the variable header here, if needed + if (variableHeaderLine != null) { + pwout.println(variableHeaderLine); + } + logger.fine("Beginning to read data stream."); for (int i = 0; i < nobs; i++) { @@ -999,6 +1010,8 @@ private void readSTRLs(DataReader reader) throws IOException { int nobs = dataTable.getCaseQuantity().intValue(); String[] line; + + //@todo: adjust for the case of storing the file with the variable header for (int obsindex = 0; obsindex < nobs; obsindex++) { if (scanner.hasNext()) { diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/por/PORFileReader.java b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/por/PORFileReader.java index c90b0ea6950..2ee966c3e31 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/por/PORFileReader.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/por/PORFileReader.java @@ -180,7 +180,7 @@ private void init() throws IOException { } @Override - public TabularDataIngest read(BufferedInputStream stream, File additionalData) throws IOException{ + public TabularDataIngest read(BufferedInputStream stream, boolean storeWithVariableHeader, File additionalData) throws IOException{ dbgLog.fine("PORFileReader: read() start"); if (additionalData != null) { @@ -226,7 +226,7 @@ public TabularDataIngest read(BufferedInputStream stream, File additionalData) t headerId = "8S"; } - decode(headerId, bfReader); + decode(headerId, bfReader, storeWithVariableHeader); // for last iteration @@ -382,7 +382,7 @@ public TabularDataIngest read(BufferedInputStream stream, File additionalData) t return ingesteddata; } - private void decode(String headerId, BufferedReader reader) throws IOException{ + private void decode(String headerId, BufferedReader reader, boolean storeWithVariableHeader) throws IOException{ if (headerId.equals("1")) decodeProductName(reader); else if (headerId.equals("2")) decodeLicensee(reader); else if (headerId.equals("3")) decodeFileLabel(reader); @@ -398,7 +398,7 @@ private void decode(String headerId, BufferedReader reader) throws IOException{ else if (headerId.equals("C")) decodeVariableLabel(reader); else if (headerId.equals("D")) decodeValueLabel(reader); else if (headerId.equals("E")) decodeDocument(reader); - else if (headerId.equals("F")) decodeData(reader); + else if (headerId.equals("F")) decodeData(reader, storeWithVariableHeader); } @@ -1099,7 +1099,7 @@ private void decodeDocument(BufferedReader reader) throws IOException { } - private void decodeData(BufferedReader reader) throws IOException { + private void decodeData(BufferedReader reader, boolean storeWithVariableHeader) throws IOException { dbgLog.fine("decodeData(): start"); // TODO: get rid of this "variableTypeFinal"; -- L.A. 4.0 beta int[] variableTypeFinal= new int[varQnty]; @@ -1126,6 +1126,9 @@ private void decodeData(BufferedReader reader) throws IOException { // contents (variable) checker concering decimals Arrays.fill(variableTypeFinal, 0); + if (storeWithVariableHeader) { + pwout.println(StringUtils.join(variableNameList, "\t")); + } // raw-case counter int j = 0; // case diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/rdata/RDATAFileReader.java b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/rdata/RDATAFileReader.java index eb1353fd792..50f2f89e354 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/rdata/RDATAFileReader.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/rdata/RDATAFileReader.java @@ -473,7 +473,7 @@ private void init() throws IOException { * @throws java.io.IOException if a reading error occurs. */ @Override - public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws IOException { + public TabularDataIngest read(BufferedInputStream stream, boolean saveWithVariableHeader, File dataFile) throws IOException { init(); @@ -509,7 +509,7 @@ public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws File tabFileDestination = File.createTempFile("data-", ".tab"); PrintWriter tabFileWriter = new PrintWriter(tabFileDestination.getAbsolutePath(), "UTF-8"); - int lineCount = csvFileReader.read(localBufferedReader, dataTable, tabFileWriter); + int lineCount = csvFileReader.read(localBufferedReader, dataTable, saveWithVariableHeader, tabFileWriter); LOG.fine("RDATAFileReader: successfully read "+lineCount+" lines of tab-delimited data."); diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/rdata/RTabFileParser.java b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/rdata/RTabFileParser.java index f60b7733463..fbe7e401b57 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/rdata/RTabFileParser.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/rdata/RTabFileParser.java @@ -61,8 +61,8 @@ public RTabFileParser (char delimiterChar) { // should be used. - public int read(BufferedReader csvReader, DataTable dataTable, PrintWriter pwout) throws IOException { - dbgLog.warning("RTabFileParser: Inside R Tab file parser"); + public int read(BufferedReader csvReader, DataTable dataTable, boolean saveWithVariableHeader, PrintWriter pwout) throws IOException { + dbgLog.fine("RTabFileParser: Inside R Tab file parser"); int varQnty = 0; @@ -94,14 +94,17 @@ public int read(BufferedReader csvReader, DataTable dataTable, PrintWriter pwout boolean[] isTimeVariable = new boolean[varQnty]; boolean[] isBooleanVariable = new boolean[varQnty]; + String variableNameHeader = null; + if (dataTable.getDataVariables() != null) { for (int i = 0; i < varQnty; i++) { DataVariable var = dataTable.getDataVariables().get(i); if (var == null) { - // throw exception! + throw new IOException ("null dataVariable passed to the parser"); + } if (var.getType() == null) { - // throw exception! + throw new IOException ("null dataVariable type passed to the parser"); } if (var.isTypeCharacter()) { isCharacterVariable[i] = true; @@ -128,13 +131,24 @@ public int read(BufferedReader csvReader, DataTable dataTable, PrintWriter pwout } } } else { - // throw excepion "unknown variable format type" - ? + throw new IOException ("unknown dataVariable format passed to the parser"); } - + if (saveWithVariableHeader) { + variableNameHeader = variableNameHeader == null + ? var.getName() + : variableNameHeader.concat("\t" + var.getName()); + } } } else { - // throw exception! + throw new IOException ("null dataVariables list passed to the parser"); + } + + if (saveWithVariableHeader) { + if (variableNameHeader == null) { + throw new IOException ("failed to generate the Variable Names header"); + } + pwout.println(variableNameHeader); } while ((line = csvReader.readLine()) != null) { diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/sav/SAVFileReader.java b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/sav/SAVFileReader.java index 682b8f1166c..5eecbdfb666 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/sav/SAVFileReader.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/sav/SAVFileReader.java @@ -338,7 +338,7 @@ private void init() throws IOException { } } - public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws IOException{ + public TabularDataIngest read(BufferedInputStream stream, boolean storeWithVariableHeader, File dataFile) throws IOException{ dbgLog.info("SAVFileReader: read() start"); if (dataFile != null) { @@ -422,7 +422,7 @@ public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws methodCurrentlyExecuted = "decodeRecordTypeData"; dbgLog.fine("***** SAVFileReader: executing method decodeRecordTypeData"); - decodeRecordTypeData(stream); + decodeRecordTypeData(stream, storeWithVariableHeader); } catch (IllegalArgumentException e) { @@ -2308,7 +2308,7 @@ void decodeRecordType999(BufferedInputStream stream) throws IOException { - void decodeRecordTypeData(BufferedInputStream stream) throws IOException { + void decodeRecordTypeData(BufferedInputStream stream, boolean storeWithVariableHeader) throws IOException { dbgLog.fine("decodeRecordTypeData(): start"); ///String fileUnfValue = null; @@ -2320,9 +2320,9 @@ void decodeRecordTypeData(BufferedInputStream stream) throws IOException { throw new IllegalArgumentException("stream == null!"); } if (isDataSectionCompressed){ - decodeRecordTypeDataCompressed(stream); + decodeRecordTypeDataCompressed(stream, storeWithVariableHeader); } else { - decodeRecordTypeDataUnCompressed(stream); + decodeRecordTypeDataUnCompressed(stream, storeWithVariableHeader); } /* UNF calculation was here... */ @@ -2362,7 +2362,7 @@ PrintWriter createOutputWriter (BufferedInputStream stream) throws IOException { } - void decodeRecordTypeDataCompressed(BufferedInputStream stream) throws IOException { + void decodeRecordTypeDataCompressed(BufferedInputStream stream, boolean storeWithVariableHeader) throws IOException { dbgLog.fine("***** decodeRecordTypeDataCompressed(): start *****"); @@ -2395,7 +2395,10 @@ void decodeRecordTypeDataCompressed(BufferedInputStream stream) throws IOExcepti dbgLog.fine("printFormatTable:\n" + printFormatTable); variableFormatTypeList = new String[varQnty]; - + // write the variable header out, if instructed to do so + if (storeWithVariableHeader) { + pwout.println(generateVariableHeader(dataTable.getDataVariables())); + } for (int i = 0; i < varQnty; i++) { variableFormatTypeList[i] = SPSSConstants.FORMAT_CATEGORY_TABLE.get( @@ -2947,7 +2950,7 @@ void decodeRecordTypeDataCompressed(BufferedInputStream stream) throws IOExcepti } - void decodeRecordTypeDataUnCompressed(BufferedInputStream stream) throws IOException { + void decodeRecordTypeDataUnCompressed(BufferedInputStream stream, boolean storeWithVariableHeader) throws IOException { dbgLog.fine("***** decodeRecordTypeDataUnCompressed(): start *****"); if (stream ==null){ @@ -3013,6 +3016,11 @@ void decodeRecordTypeDataUnCompressed(BufferedInputStream stream) throws IOExcep ///dataTable2 = new Object[varQnty][caseQnty]; // storage of date formats to pass to UNF ///dateFormats = new String[varQnty][caseQnty]; + + // write the variable header out, if instructed to do so + if (storeWithVariableHeader) { + pwout.println(generateVariableHeader(dataTable.getDataVariables())); + } try { for (int i = 0; ; i++){ // case-wise loop diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/xlsx/XLSXFileReader.java b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/xlsx/XLSXFileReader.java index ea3f3868f24..ef91793690e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/xlsx/XLSXFileReader.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/xlsx/XLSXFileReader.java @@ -36,7 +36,6 @@ import org.apache.commons.lang3.StringUtils; import org.apache.poi.xssf.eventusermodel.XSSFReader; -import org.apache.poi.xssf.usermodel.XSSFRichTextString; import org.apache.poi.xssf.model.SharedStrings; import org.apache.poi.openxml4j.opc.OPCPackage; import org.xml.sax.Attributes; @@ -81,7 +80,9 @@ private void init() throws IOException { * @throws java.io.IOException if a reading error occurs. */ @Override - public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws IOException { + public TabularDataIngest read(BufferedInputStream stream, boolean storeWithVariableHeader, File dataFile) throws IOException { + // @todo: implement handling of "saveWithVariableHeader" option + init(); TabularDataIngest ingesteddata = new TabularDataIngest(); @@ -118,6 +119,10 @@ public TabularDataIngest read(BufferedInputStream stream, File dataFile) throws String[] caseRow = new String[varQnty]; String[] valueTokens; + // add the variable header here, if needed + if (storeWithVariableHeader) { + finalWriter.println(generateVariableHeader(dataTable.getDataVariables())); + } while ((line = secondPassReader.readLine()) != null) { // chop the line: @@ -549,7 +554,7 @@ public static void main(String[] args) throws Exception { BufferedInputStream xlsxInputStream = new BufferedInputStream(new FileInputStream(new File(args[0]))); - TabularDataIngest dataIngest = testReader.read(xlsxInputStream, null); + TabularDataIngest dataIngest = testReader.read(xlsxInputStream, false, null); dataTable = dataIngest.getDataTable(); diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java index 627cef08d8b..3b7632f3d9e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java @@ -598,7 +598,12 @@ Whether Harvesting (OAI) service is enabled * Allows an instance admin to disable Solr search facets on the collection * and dataset pages instantly */ - DisableSolrFacets + DisableSolrFacets, + /** + * When ingesting tabular data files, store the generated tab-delimited + * files *with* the variable names line up top. + */ + StoreIngestedTabularFilesWithVarHeaders ; @Override diff --git a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java index 3c6992f8ec3..ded394833f1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java @@ -1173,4 +1173,12 @@ public boolean isStorageQuotasEnforced() { public Long getTestStorageQuotaLimit() { return settingsService.getValueForKeyAsLong(SettingsServiceBean.Key.StorageQuotaSizeInBytes); } + /** + * Should we store tab-delimited files produced during ingest *with* the + * variable name header line included? + * @return boolean - defaults to false. + */ + public boolean isStoringIngestedFilesWithHeaders() { + return settingsService.isTrueForKey(SettingsServiceBean.Key.StoreIngestedTabularFilesWithVarHeaders, false); + } } diff --git a/src/main/resources/db/migration/V6.1.0.2__8524-store-tabular-files-with-varheaders.sql b/src/main/resources/db/migration/V6.1.0.2__8524-store-tabular-files-with-varheaders.sql new file mode 100644 index 00000000000..7c52a00107a --- /dev/null +++ b/src/main/resources/db/migration/V6.1.0.2__8524-store-tabular-files-with-varheaders.sql @@ -0,0 +1 @@ +ALTER TABLE datatable ADD COLUMN IF NOT EXISTS storedWithVariableHeader BOOLEAN DEFAULT FALSE; diff --git a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java index 915f82a6de2..cfc6f9335b3 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java @@ -16,6 +16,7 @@ import io.restassured.path.xml.XmlPath; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; +import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.SystemConfig; import java.io.File; import java.io.IOException; @@ -33,6 +34,8 @@ import jakarta.json.JsonObjectBuilder; import static jakarta.ws.rs.core.Response.Status.*; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import org.hamcrest.CoreMatchers; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterAll; @@ -2483,4 +2486,129 @@ public void testCollectionStorageQuotas() { UtilIT.deleteSetting(SettingsServiceBean.Key.UseStorageQuotas); } + + @Test + public void testIngestWithAndWithoutVariableHeader() throws NoSuchAlgorithmException { + msgt("testIngestWithAndWithoutVariableHeader"); + + // The compact Stata file we'll be using for this test: + // (this file is provided by Stata inc. - it's genuine quality) + String pathToFile = "scripts/search/data/tabular/stata13-auto.dta"; + // The pre-calculated MD5 signature of the *complete* tab-delimited + // file as seen by the final Access API user (i.e., with the variable + // header line in it): + String tabularFileMD5 = "f298c2567cc8eb544e36ad83edf6f595"; + // Expected byte sizes of the generated tab-delimited file as stored, + // with and without the header: + int tabularFileSizeWoutHeader = 4026; + int tabularFileSizeWithHeader = 4113; + + String apiToken = createUserGetToken(); + String dataverseAlias = createDataverseGetAlias(apiToken); + Integer datasetIdA = createDatasetGetId(dataverseAlias, apiToken); + + // Before we do anything else, make sure that the instance is configured + // the "old" way, i.e., to store ingested files without the headers: + UtilIT.deleteSetting(SettingsServiceBean.Key.StoreIngestedTabularFilesWithVarHeaders); + + Response addResponse = UtilIT.uploadFileViaNative(datasetIdA.toString(), pathToFile, apiToken); + addResponse.prettyPrint(); + + addResponse.then().assertThat() + .body("data.files[0].dataFile.contentType", equalTo("application/x-stata-13")) + .body("data.files[0].label", equalTo("stata13-auto.dta")) + .statusCode(OK.getStatusCode()); + + Long fileIdA = JsonPath.from(addResponse.body().asString()).getLong("data.files[0].dataFile.id"); + assertNotNull(fileIdA); + + // Give file time to ingest + assertTrue(UtilIT.sleepForLock(datasetIdA.longValue(), "Ingest", apiToken, UtilIT.MAXIMUM_INGEST_LOCK_DURATION), "Failed test if Ingest Lock exceeds max duration " + pathToFile + "(A)"); + + // Check the metadata to confirm that the file has ingested: + + Response fileDataResponse = UtilIT.getFileData(fileIdA.toString(), apiToken); + fileDataResponse.prettyPrint(); + fileDataResponse.then().assertThat() + .body("data.dataFile.filename", equalTo("stata13-auto.tab")) + .body("data.dataFile.contentType", equalTo("text/tab-separated-values")) + .body("data.dataFile.filesize", equalTo(tabularFileSizeWoutHeader)) + .statusCode(OK.getStatusCode()); + + + // Download the file, verify the checksum: + + Response fileDownloadResponse = UtilIT.downloadFile(fileIdA.intValue(), apiToken); + fileDownloadResponse.then().assertThat() + .statusCode(OK.getStatusCode()); + + byte[] fileDownloadBytes = fileDownloadResponse.body().asByteArray(); + MessageDigest messageDigest = MessageDigest.getInstance("MD5"); + messageDigest.update(fileDownloadBytes); + byte[] rawDigestBytes = messageDigest.digest(); + String tabularFileMD5calculated = FileUtil.checksumDigestToString(rawDigestBytes); + + msgt("md5 of the downloaded file (saved without the variable name header): "+tabularFileMD5calculated); + + assertEquals(tabularFileMD5, tabularFileMD5calculated); + + // Repeat the whole thing, in another dataset (because we will be uploading + // an identical file), but with the "store with the header setting enabled): + + UtilIT.enableSetting(SettingsServiceBean.Key.StoreIngestedTabularFilesWithVarHeaders); + + Integer datasetIdB = createDatasetGetId(dataverseAlias, apiToken); + + addResponse = UtilIT.uploadFileViaNative(datasetIdB.toString(), pathToFile, apiToken); + addResponse.prettyPrint(); + + addResponse.then().assertThat() + .body("data.files[0].dataFile.contentType", equalTo("application/x-stata-13")) + .body("data.files[0].label", equalTo("stata13-auto.dta")) + .statusCode(OK.getStatusCode()); + + Long fileIdB = JsonPath.from(addResponse.body().asString()).getLong("data.files[0].dataFile.id"); + assertNotNull(fileIdB); + + // Give file time to ingest + assertTrue(UtilIT.sleepForLock(datasetIdB.longValue(), "Ingest", apiToken, UtilIT.MAXIMUM_INGEST_LOCK_DURATION), "Failed test if Ingest Lock exceeds max duration " + pathToFile + "(B)"); + + // Check the metadata to confirm that the file has ingested: + + fileDataResponse = UtilIT.getFileData(fileIdB.toString(), apiToken); + fileDataResponse.prettyPrint(); + fileDataResponse.then().assertThat() + .body("data.dataFile.filename", equalTo("stata13-auto.tab")) + .body("data.dataFile.contentType", equalTo("text/tab-separated-values")) + .body("data.dataFile.filesize", equalTo(tabularFileSizeWithHeader)) + .statusCode(OK.getStatusCode()); + + + // Download the file, verify the checksum, again + + fileDownloadResponse = UtilIT.downloadFile(fileIdB.intValue(), apiToken); + fileDownloadResponse.then().assertThat() + .statusCode(OK.getStatusCode()); + + fileDownloadBytes = fileDownloadResponse.body().asByteArray(); + messageDigest.reset(); + messageDigest.update(fileDownloadBytes); + rawDigestBytes = messageDigest.digest(); + tabularFileMD5calculated = FileUtil.checksumDigestToString(rawDigestBytes); + + msgt("md5 of the downloaded file (saved with the variable name header): "+tabularFileMD5calculated); + + assertEquals(tabularFileMD5, tabularFileMD5calculated); + + // In other words, whether the file was saved with, or without the header, + // as downloaded by the user, the end result must be the same in both cases! + // In other words, whether that first line with the variable names is already + // in the physical file, or added by Dataverse on the fly, the downloaded + // content must be identical. + + UtilIT.deleteSetting(SettingsServiceBean.Key.StoreIngestedTabularFilesWithVarHeaders); + + // @todo: cleanup? + } + } diff --git a/src/test/java/edu/harvard/iq/dataverse/ingest/IngestFrequencyTest.java b/src/test/java/edu/harvard/iq/dataverse/ingest/IngestFrequencyTest.java index 96e314324ab..ca64bcc794f 100644 --- a/src/test/java/edu/harvard/iq/dataverse/ingest/IngestFrequencyTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/ingest/IngestFrequencyTest.java @@ -99,7 +99,7 @@ private DataFile readFileCalcFreq(String fileName, String type ) { TabularDataIngest tabDataIngest = null; try { - tabDataIngest = ingestPlugin.read(fileInputStream, null); + tabDataIngest = ingestPlugin.read(fileInputStream, false, null); } catch (IOException ingestEx) { tabDataIngest = null; System.out.println("Caught an exception trying to ingest file " + fileName + ": " + ingestEx.getLocalizedMessage()); diff --git a/src/test/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/csv/CSVFileReaderTest.java b/src/test/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/csv/CSVFileReaderTest.java index fc066ef195e..9afb35918a4 100644 --- a/src/test/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/csv/CSVFileReaderTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/csv/CSVFileReaderTest.java @@ -52,7 +52,7 @@ public void testRead() { try (BufferedInputStream stream = new BufferedInputStream( new FileInputStream(testFile))) { CSVFileReader instance = new CSVFileReader(new CSVFileReaderSpi(), ','); - File outFile = instance.read(stream, null).getTabDelimitedFile(); + File outFile = instance.read(stream, false, null).getTabDelimitedFile(); result = new BufferedReader(new FileReader(outFile)); logger.fine("Final pass: " + outFile.getPath()); } catch (IOException ex) { @@ -104,7 +104,7 @@ public void testVariables() { try (BufferedInputStream stream = new BufferedInputStream( new FileInputStream(testFile))) { CSVFileReader instance = new CSVFileReader(new CSVFileReaderSpi(), ','); - result = instance.read(stream, null).getDataTable(); + result = instance.read(stream, false, null).getDataTable(); } catch (IOException ex) { fail("" + ex); } @@ -154,7 +154,7 @@ public void testSubset() { new FileInputStream(testFile))) { CSVFileReader instance = new CSVFileReader(new CSVFileReaderSpi(), ','); - ingestResult = instance.read(stream, null); + ingestResult = instance.read(stream, false, null); generatedTabFile = ingestResult.getTabDelimitedFile(); generatedDataTable = ingestResult.getDataTable(); @@ -195,7 +195,7 @@ public void testSubset() { fail("Failed to open generated tab-delimited file for reading" + ioex); } - Double[] columnVector = TabularSubsetGenerator.subsetDoubleVector(generatedTabInputStream, i, generatedDataTable.getCaseQuantity().intValue()); + Double[] columnVector = TabularSubsetGenerator.subsetDoubleVector(generatedTabInputStream, i, generatedDataTable.getCaseQuantity().intValue(), false); assertArrayEquals(floatVectors[vectorCount++], columnVector, "column " + i + ":"); } @@ -229,7 +229,7 @@ public void testSubset() { fail("Failed to open generated tab-delimited file for reading" + ioex); } - Long[] columnVector = TabularSubsetGenerator.subsetLongVector(generatedTabInputStream, i, generatedDataTable.getCaseQuantity().intValue()); + Long[] columnVector = TabularSubsetGenerator.subsetLongVector(generatedTabInputStream, i, generatedDataTable.getCaseQuantity().intValue(), false); assertArrayEquals(longVectors[vectorCount++], columnVector, "column " + i + ":"); } @@ -256,7 +256,7 @@ public void testSubset() { fail("Failed to open generated tab-delimited file for reading" + ioex); } - String[] columnVector = TabularSubsetGenerator.subsetStringVector(generatedTabInputStream, i, generatedDataTable.getCaseQuantity().intValue()); + String[] columnVector = TabularSubsetGenerator.subsetStringVector(generatedTabInputStream, i, generatedDataTable.getCaseQuantity().intValue(), false); assertArrayEquals(stringVectors[vectorCount++], columnVector, "column " + i + ":"); } @@ -298,7 +298,7 @@ public void testVariableUNFs() { new FileInputStream(testFile))) { CSVFileReader instance = new CSVFileReader(new CSVFileReaderSpi(), ','); - ingestResult = instance.read(stream, null); + ingestResult = instance.read(stream, false, null); generatedTabFile = ingestResult.getTabDelimitedFile(); generatedDataTable = ingestResult.getDataTable(); @@ -327,7 +327,7 @@ public void testVariableUNFs() { fail("Failed to open generated tab-delimited file for reading" + ioex); } - Double[] columnVector = TabularSubsetGenerator.subsetDoubleVector(generatedTabInputStream, i, generatedDataTable.getCaseQuantity().intValue()); + Double[] columnVector = TabularSubsetGenerator.subsetDoubleVector(generatedTabInputStream, i, generatedDataTable.getCaseQuantity().intValue(), false); try { unf = UNFUtil.calculateUNF(columnVector); } catch (IOException | UnfException ioex) { @@ -345,7 +345,7 @@ public void testVariableUNFs() { fail("Failed to open generated tab-delimited file for reading" + ioex); } - Long[] columnVector = TabularSubsetGenerator.subsetLongVector(generatedTabInputStream, i, generatedDataTable.getCaseQuantity().intValue()); + Long[] columnVector = TabularSubsetGenerator.subsetLongVector(generatedTabInputStream, i, generatedDataTable.getCaseQuantity().intValue(), false); try { unf = UNFUtil.calculateUNF(columnVector); @@ -363,7 +363,7 @@ public void testVariableUNFs() { fail("Failed to open generated tab-delimited file for reading" + ioex); } - String[] columnVector = TabularSubsetGenerator.subsetStringVector(generatedTabInputStream, i, generatedDataTable.getCaseQuantity().intValue()); + String[] columnVector = TabularSubsetGenerator.subsetStringVector(generatedTabInputStream, i, generatedDataTable.getCaseQuantity().intValue(), false); String[] dateFormats = null; @@ -401,7 +401,7 @@ public void testVariableUNFs() { public void testBrokenCSV() { String brokenFile = "src/test/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/csv/BrokenCSV.csv"; try { - new CSVFileReader(new CSVFileReaderSpi(), ',').read(null, null); + new CSVFileReader(new CSVFileReaderSpi(), ',').read(null, false, null); fail("IOException not thrown on null csv"); } catch (NullPointerException ex) { String expMessage = null; @@ -412,7 +412,7 @@ public void testBrokenCSV() { } try (BufferedInputStream stream = new BufferedInputStream( new FileInputStream(brokenFile))) { - new CSVFileReader(new CSVFileReaderSpi(), ',').read(stream, null); + new CSVFileReader(new CSVFileReaderSpi(), ',').read(stream, false, null); fail("IOException was not thrown when collumns do not align."); } catch (IOException ex) { String expMessage = BundleUtil.getStringFromBundle("ingest.csv.recordMismatch", diff --git a/src/test/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/DTAFileReaderTest.java b/src/test/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/DTAFileReaderTest.java index 113e9be6b54..8af36d6466d 100644 --- a/src/test/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/DTAFileReaderTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/DTAFileReaderTest.java @@ -16,7 +16,7 @@ public class DTAFileReaderTest { @Test public void testOs() throws IOException { - TabularDataIngest result = instance.read(new BufferedInputStream(new FileInputStream(new File("scripts/search/data/tabular/50by1000.dta"))), nullDataFile); + TabularDataIngest result = instance.read(new BufferedInputStream(new FileInputStream(new File("scripts/search/data/tabular/50by1000.dta"))), false, nullDataFile); assertEquals("application/x-stata", result.getDataTable().getOriginalFileFormat()); assertEquals("rel_8_or_9", result.getDataTable().getOriginalFormatVersion()); assertEquals(50, result.getDataTable().getDataVariables().size()); diff --git a/src/test/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/NewDTAFileReaderTest.java b/src/test/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/NewDTAFileReaderTest.java index c963346b05e..0f14054f472 100644 --- a/src/test/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/NewDTAFileReaderTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/ingest/tabulardata/impl/plugins/dta/NewDTAFileReaderTest.java @@ -25,7 +25,7 @@ public void testAuto() throws IOException { instance = new NewDTAFileReader(null, 117); // From https://www.stata-press.com/data/r13/auto.dta // `strings` shows "
117" - TabularDataIngest result = instance.read(new BufferedInputStream(new FileInputStream(new File("scripts/search/data/tabular/stata13-auto.dta"))), nullDataFile); + TabularDataIngest result = instance.read(new BufferedInputStream(new FileInputStream(new File("scripts/search/data/tabular/stata13-auto.dta"))), false, nullDataFile); assertEquals("application/x-stata", result.getDataTable().getOriginalFileFormat()); assertEquals("STATA 13", result.getDataTable().getOriginalFormatVersion()); assertEquals(12, result.getDataTable().getDataVariables().size()); @@ -39,7 +39,7 @@ public void testAuto() throws IOException { @Test public void testStrl() throws IOException { instance = new NewDTAFileReader(null, 118); - TabularDataIngest result = instance.read(new BufferedInputStream(new FileInputStream(new File(base + "strl.dta"))), nullDataFile); + TabularDataIngest result = instance.read(new BufferedInputStream(new FileInputStream(new File(base + "strl.dta"))), false, nullDataFile); DataTable table = result.getDataTable(); assertEquals("application/x-stata", table.getOriginalFileFormat()); assertEquals("STATA 14", table.getOriginalFormatVersion()); @@ -58,7 +58,7 @@ public void testStrl() throws IOException { @Test public void testDates() throws IOException { instance = new NewDTAFileReader(null, 118); - TabularDataIngest result = instance.read(new BufferedInputStream(new FileInputStream(new File(base + "dates.dta"))), nullDataFile); + TabularDataIngest result = instance.read(new BufferedInputStream(new FileInputStream(new File(base + "dates.dta"))), false, nullDataFile); DataTable table = result.getDataTable(); assertEquals("application/x-stata", table.getOriginalFileFormat()); assertEquals("STATA 14", table.getOriginalFormatVersion()); @@ -77,7 +77,7 @@ public void testDates() throws IOException { @Test void testNull() { instance = new NewDTAFileReader(null, 117); - assertThrows(IOException.class, () -> instance.read(null, new File(""))); + assertThrows(IOException.class, () -> instance.read(null, false, new File(""))); } // TODO: Can we create a small file to check into the code base that exercises the value-label names non-zero offset issue? @@ -87,7 +87,7 @@ public void testFirstCategoryNonZeroOffset() throws IOException { instance = new NewDTAFileReader(null, 117); // https://dataverse.harvard.edu/file.xhtml?fileId=2865667 Stata 13 HouseImputingCivilRightsInfo.dta md5=7dd144f27cdb9f8d1c3f4eb9c4744c42 - TabularDataIngest result = instance.read(new BufferedInputStream(new FileInputStream(new File("/tmp/HouseImputingCivilRightsInfo.dta"))), nullDataFile); + TabularDataIngest result = instance.read(new BufferedInputStream(new FileInputStream(new File("/tmp/HouseImputingCivilRightsInfo.dta"))), false, nullDataFile); assertEquals("application/x-stata", result.getDataTable().getOriginalFileFormat()); assertEquals("STATA 13", result.getDataTable().getOriginalFormatVersion()); assertEquals(5, result.getDataTable().getDataVariables().size()); @@ -107,7 +107,7 @@ public void testFirstCategoryNonZeroOffset() throws IOException { public void testFirstCategoryNonZeroOffset1() throws IOException { instance = new NewDTAFileReader(null, 118); // https://dataverse.harvard.edu/file.xhtml?fileId=3140457 Stata 14: 2018_04_06_Aggregated_dataset_v2.dta - TabularDataIngest result = instance.read(new BufferedInputStream(new FileInputStream(new File("/tmp/2018_04_06_Aggregated_dataset_v2.dta"))), nullDataFile); + TabularDataIngest result = instance.read(new BufferedInputStream(new FileInputStream(new File("/tmp/2018_04_06_Aggregated_dataset_v2.dta"))), false, nullDataFile); assertEquals("application/x-stata", result.getDataTable().getOriginalFileFormat()); assertEquals("STATA 14", result.getDataTable().getOriginalFormatVersion()); assertEquals(227, result.getDataTable().getDataVariables().size()); @@ -136,7 +136,7 @@ public void test33k() throws IOException { @Test public void testCharacteristics() throws IOException { instance = new NewDTAFileReader(null, 117); - TabularDataIngest result = instance.read(new BufferedInputStream(new FileInputStream(new File("/tmp/15aa6802ee5-5d2ed1bf55a5.dta"))), nullDataFile); + TabularDataIngest result = instance.read(new BufferedInputStream(new FileInputStream(new File("/tmp/15aa6802ee5-5d2ed1bf55a5.dta"))), false, nullDataFile); assertEquals("application/x-stata", result.getDataTable().getOriginalFileFormat()); assertEquals("STATA 13", result.getDataTable().getOriginalFormatVersion()); assertEquals(441, result.getDataTable().getDataVariables().size()); From 4542b213103ecc18cbf50617696c2997a2a9723d Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 7 Feb 2024 16:36:49 -0500 Subject: [PATCH 290/689] refactor handleVersion, add includeDeaccession param #10240 --- doc/sphinx-guides/source/api/native-api.rst | 11 ++++-- .../iq/dataverse/api/AbstractApiBean.java | 31 +++++++++++++++ .../harvard/iq/dataverse/api/Datasets.java | 23 +---------- .../edu/harvard/iq/dataverse/api/Files.java | 38 ++----------------- .../edu/harvard/iq/dataverse/api/FilesIT.java | 11 ++++-- .../edu/harvard/iq/dataverse/api/UtilIT.java | 8 ++++ 6 files changed, 60 insertions(+), 62 deletions(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index d6f88df3235..5be73c01194 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -3529,7 +3529,11 @@ The fully expanded example above (without environment variables) looks like this curl "https://demo.dataverse.org/api/files/42/versions/:latest-published/citation" -When the dataset version is a draft or deaccessioned, authentication is required: +When the dataset version is a draft or deaccessioned, authentication is required. + +By default, deaccessioned dataset versions are not included in the search when applying the :latest or :latest-published identifiers. Additionally, when filtering by a specific version tag, you will get a "unauthorized" error if the version is deaccessioned and you do not enable the ``includeDeaccessioned`` option described below. + +If you want to include deaccessioned dataset versions, you must set ``includeDeaccessioned`` query parameter to ``true``. .. code-block:: bash @@ -3537,14 +3541,15 @@ When the dataset version is a draft or deaccessioned, authentication is required export SERVER_URL=https://demo.dataverse.org export FILE_ID=42 export DATASET_VERSION=:draft + export INCLUDE_DEACCESSIONED=true - curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/files/$FILE_ID/versions/$DATASET_VERSION/citation" + curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/files/$FILE_ID/versions/$DATASET_VERSION/citation?includeDeaccessioned=$INCLUDE_DEACCESSIONED" 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/files/42/versions/:draft/citation + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" "https://demo.dataverse.org/api/files/42/versions/:draft/citation?includeDeaccessioned=true" If your file has a persistent identifier (PID, such as a DOI), you can pass it using the technique described under :ref:`get-json-rep-of-file`. 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 bc94d7f0bcc..3c3e68c4e44 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -2,6 +2,7 @@ import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.actionlogging.ActionLogServiceBean; +import static edu.harvard.iq.dataverse.api.Datasets.handleVersion; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.authorization.RoleAssignee; @@ -15,6 +16,10 @@ 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.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.externaltools.ExternalToolServiceBean; import edu.harvard.iq.dataverse.license.LicenseServiceBean; import edu.harvard.iq.dataverse.locality.StorageSiteServiceBean; @@ -390,6 +395,32 @@ protected Dataset findDatasetOrDie(String id) throws WrappedResponse { } } } + + protected DatasetVersion findDatasetVersionOrDie(final DataverseRequest req, String versionNumber, final Dataset ds, boolean includeDeaccessioned, boolean checkPermsWhenDeaccessioned) throws WrappedResponse { + DatasetVersion dsv = execCommand(handleVersion(versionNumber, new Datasets.DsVersionHandler>() { + + @Override + public Command handleLatest() { + return new GetLatestAccessibleDatasetVersionCommand(req, ds, includeDeaccessioned, checkPermsWhenDeaccessioned); + } + + @Override + public Command handleDraft() { + return new GetDraftDatasetVersionCommand(req, ds); + } + + @Override + public Command handleSpecific(long major, long minor) { + return new GetSpecificPublishedDatasetVersionCommand(req, ds, major, minor, includeDeaccessioned, checkPermsWhenDeaccessioned); + } + + @Override + public Command handleLatestPublished() { + return new GetLatestPublishedDatasetVersionCommand(req, ds, includeDeaccessioned, checkPermsWhenDeaccessioned); + } + })); + return dsv; + } protected DataFile findDataFileOrDie(String id) throws WrappedResponse { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index e3505cbbb33..2181f189fc0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2727,28 +2727,7 @@ private DatasetVersion getDatasetVersionOrDie(final DataverseRequest req, String * Will allow to define when the permissions should be checked when a deaccesioned dataset is requested. If the user doesn't have edit permissions will result in an error. */ private DatasetVersion getDatasetVersionOrDie(final DataverseRequest req, String versionNumber, final Dataset ds, UriInfo uriInfo, HttpHeaders headers, boolean includeDeaccessioned, boolean checkPermsWhenDeaccessioned) throws WrappedResponse { - DatasetVersion dsv = execCommand(handleVersion(versionNumber, new DsVersionHandler>() { - - @Override - public Command handleLatest() { - return new GetLatestAccessibleDatasetVersionCommand(req, ds, includeDeaccessioned, checkPermsWhenDeaccessioned); - } - - @Override - public Command handleDraft() { - return new GetDraftDatasetVersionCommand(req, ds); - } - - @Override - public Command handleSpecific(long major, long minor) { - return new GetSpecificPublishedDatasetVersionCommand(req, ds, major, minor, includeDeaccessioned, checkPermsWhenDeaccessioned); - } - - @Override - public Command handleLatestPublished() { - return new GetLatestPublishedDatasetVersionCommand(req, ds, includeDeaccessioned, checkPermsWhenDeaccessioned); - } - })); + DatasetVersion dsv = findDatasetVersionOrDie(req, versionNumber, ds, includeDeaccessioned, checkPermsWhenDeaccessioned); if (dsv == null || dsv.getId() == null) { throw new WrappedResponse(notFound("Dataset version " + versionNumber + " of dataset " + ds.getId() + " not found")); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index f7cdf2df10b..69bdebb2dd5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -19,7 +19,6 @@ import edu.harvard.iq.dataverse.GuestbookResponseServiceBean; import edu.harvard.iq.dataverse.TermsOfUseAndAccessValidator; import edu.harvard.iq.dataverse.UserNotificationServiceBean; -import static edu.harvard.iq.dataverse.api.Datasets.handleVersion; import edu.harvard.iq.dataverse.api.auth.AuthRequired; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.ApiToken; @@ -34,11 +33,7 @@ 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.impl.GetDataFileCommand; -import edu.harvard.iq.dataverse.engine.command.impl.GetDraftDatasetVersionCommand; import edu.harvard.iq.dataverse.engine.command.impl.GetDraftFileMetadataIfAvailableCommand; -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.RedetectFileTypeCommand; import edu.harvard.iq.dataverse.engine.command.impl.RestrictFileCommand; import edu.harvard.iq.dataverse.engine.command.impl.UningestFileCommand; @@ -941,44 +936,19 @@ public Response getHasBeenDeleted(@Context ContainerRequestContext crc, @PathPar /** * @param fileIdOrPersistentId Database ID or PID of the data file. - * @param dsVersionString The version of the dataset, such as 1.0, :draft, + * @param versionNumber The version of the dataset, such as 1.0, :draft, * :latest-published, etc. + * @param includeDeaccessioned Defaults to false. */ @GET @AuthRequired @Path("{id}/versions/{dsVersionString}/citation") - public Response getFileCitationByVersion(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @PathParam("dsVersionString") String dsVersionString) { + public Response getFileCitationByVersion(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @PathParam("dsVersionString") String versionNumber, @QueryParam("includeDeaccessioned") boolean includeDeaccessioned) { try { DataverseRequest req = createDataverseRequest(getRequestUser(crc)); final DataFile df = execCommand(new GetDataFileCommand(req, findDataFileOrDie(fileIdOrPersistentId))); Dataset ds = df.getOwner(); - // Adapted from getDatasetVersionOrDie - DatasetVersion dsv = execCommand(handleVersion(dsVersionString, new Datasets.DsVersionHandler>() { - - boolean includeDeaccessioned = true; - boolean checkPermsWhenDeaccessioned = true; - - @Override - public Command handleLatest() { - return new GetLatestAccessibleDatasetVersionCommand(req, ds); - } - - @Override - public Command handleDraft() { - return new GetDraftDatasetVersionCommand(req, ds); - } - - @Override - public Command handleSpecific(long major, long minor) { - return new GetSpecificPublishedDatasetVersionCommand(req, ds, major, minor, includeDeaccessioned, checkPermsWhenDeaccessioned); - } - - @Override - public Command handleLatestPublished() { - return new GetLatestPublishedDatasetVersionCommand(req, ds); - } - })); - + DatasetVersion dsv = findDatasetVersionOrDie(req, versionNumber, ds, includeDeaccessioned, true); if (dsv == null) { return unauthorized(BundleUtil.getStringFromBundle("files.api.no.draftOrUnauth")); } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java index f30d7e803ee..dbdb12a3e8d 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java @@ -2698,9 +2698,14 @@ public void testFileCitationByVersion() throws IOException { deaccessionDataset.prettyPrint(); deaccessionDataset.then().assertThat().statusCode(OK.getStatusCode()); - Response getFileCitationV1PostDeaccessionAuthor = UtilIT.getFileCitation(fileId, "1.0", apiToken); - getFileCitationV1PostDeaccessionAuthor.prettyPrint(); - getFileCitationV1PostDeaccessionAuthor.then().assertThat() + Response getFileCitationV1PostDeaccessionAuthorDefault = UtilIT.getFileCitation(fileId, "1.0", apiToken); + getFileCitationV1PostDeaccessionAuthorDefault.prettyPrint(); + getFileCitationV1PostDeaccessionAuthorDefault.then().assertThat() + .statusCode(UNAUTHORIZED.getStatusCode()); + + Response getFileCitationV1PostDeaccessionAuthorIncludeDeaccessioned = UtilIT.getFileCitation(fileId, "1.0", true, apiToken); + getFileCitationV1PostDeaccessionAuthorIncludeDeaccessioned.prettyPrint(); + getFileCitationV1PostDeaccessionAuthorIncludeDeaccessioned.then().assertThat() .statusCode(OK.getStatusCode()) .body("data.message", equalTo("Finch, Fiona, " + currentYear + ", \"Darwin's Finches\", " + pidAsUrl + ", Root, V1, DEACCESSIONED VERSION; coffeeshop.png [fileName]")); 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 b51d6af75a9..f307275af1f 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -3475,10 +3475,18 @@ static Response getDatasetVersionCitation(Integer datasetId, String version, boo } static Response getFileCitation(Integer fileId, String datasetVersion, String apiToken) { + Boolean includeDeaccessioned = null; + return getFileCitation(fileId, datasetVersion, includeDeaccessioned, apiToken); + } + + static Response getFileCitation(Integer fileId, String datasetVersion, Boolean includeDeaccessioned, String apiToken) { RequestSpecification requestSpecification = given(); if (apiToken != null) { requestSpecification.header(API_TOKEN_HTTP_HEADER, apiToken); } + if (includeDeaccessioned != null) { + requestSpecification.queryParam("includeDeaccessioned", includeDeaccessioned); + } return requestSpecification.get("/api/files/" + fileId + "/versions/" + datasetVersion + "/citation"); } From 397dbfb7eb68848650ca8a233462b91532a92970 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 8 Feb 2024 10:41:11 +0000 Subject: [PATCH 291/689] Changed: getFileData endpoint using new commands through DsVersionHandler --- .../edu/harvard/iq/dataverse/api/Files.java | 77 ++++++++----------- ...etDraftFileMetadataIfAvailableCommand.java | 1 - ...etLatestAccessibleFileMetadataCommand.java | 3 +- 3 files changed, 32 insertions(+), 49 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index 95117162094..be2f093fdcf 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -25,15 +25,11 @@ import edu.harvard.iq.dataverse.datasetutility.DataFileTagException; import edu.harvard.iq.dataverse.datasetutility.NoFilesException; import edu.harvard.iq.dataverse.datasetutility.OptionalFileParams; +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.impl.GetDataFileCommand; -import edu.harvard.iq.dataverse.engine.command.impl.GetDraftFileMetadataIfAvailableCommand; -import edu.harvard.iq.dataverse.engine.command.impl.RedetectFileTypeCommand; -import edu.harvard.iq.dataverse.engine.command.impl.RestrictFileCommand; -import edu.harvard.iq.dataverse.engine.command.impl.UningestFileCommand; -import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand; +import edu.harvard.iq.dataverse.engine.command.impl.*; import edu.harvard.iq.dataverse.export.ExportService; import io.gdcc.spi.export.ExportException; import edu.harvard.iq.dataverse.externaltools.ExternalTool; @@ -49,7 +45,8 @@ import edu.harvard.iq.dataverse.util.SystemConfig; import edu.harvard.iq.dataverse.util.URLTokenUtil; -import static edu.harvard.iq.dataverse.api.ApiConstants.DS_VERSION_DRAFT; +import static edu.harvard.iq.dataverse.api.ApiConstants.*; +import static edu.harvard.iq.dataverse.api.Datasets.handleVersion; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; import edu.harvard.iq.dataverse.util.json.JsonUtil; import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; @@ -503,70 +500,58 @@ public Response updateFileMetadata(@Context ContainerRequestContext crc, @FormDa @AuthRequired @Path("{id}") public Response getFileData(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @Context UriInfo uriInfo, @Context HttpHeaders headers) { - return getFileDataResponse(getRequestUser(crc), fileIdOrPersistentId, uriInfo, headers, null); + return response( req -> getFileDataResponse(req, fileIdOrPersistentId, uriInfo, headers, DS_VERSION_LATEST), getRequestUser(crc)); } @GET @AuthRequired @Path("{id}/versions/{datasetVersionId}") public Response getFileData(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @PathParam("datasetVersionId") String datasetVersionId, @Context UriInfo uriInfo, @Context HttpHeaders headers) { - return getFileDataResponse(getRequestUser(crc), fileIdOrPersistentId, uriInfo, headers, datasetVersionId); + return response( req -> getFileDataResponse(req, fileIdOrPersistentId, uriInfo, headers, datasetVersionId), getRequestUser(crc)); } - private Response getFileDataResponse(User user, String fileIdOrPersistentId, UriInfo uriInfo, HttpHeaders headers, String datasetVersionId){ - - DataverseRequest req; - try { - req = createDataverseRequest(user); - } catch (Exception e) { - return error(BAD_REQUEST, "Error attempting to request information. Maybe a bad API token?"); - } - final DataFile df; + private Response getFileDataResponse(final DataverseRequest req, String fileIdOrPersistentId, UriInfo uriInfo, HttpHeaders headers, String datasetVersionId) throws WrappedResponse { + final DataFile dataFile; try { - df = execCommand(new GetDataFileCommand(req, findDataFileOrDie(fileIdOrPersistentId))); + dataFile = execCommand(new GetDataFileCommand(req, findDataFileOrDie(fileIdOrPersistentId))); } catch (Exception e) { return error(BAD_REQUEST, "Error attempting get the requested data file."); } - FileMetadata fm; - - if (datasetVersionId != null && datasetVersionId.equals(DS_VERSION_DRAFT)) { - try { - fm = execCommand(new GetDraftFileMetadataIfAvailableCommand(req, df)); - } catch (WrappedResponse w) { - return error(BAD_REQUEST, "An error occurred getting a draft version, you may not have permission to access unpublished data on this dataset."); + FileMetadata fileMetadata = execCommand(handleVersion(datasetVersionId, new Datasets.DsVersionHandler<>() { + @Override + public Command handleLatest() { + return new GetLatestAccessibleFileMetadataCommand(req, dataFile); } - if (null == fm) { - return error(BAD_REQUEST, BundleUtil.getStringFromBundle("files.api.no.draft")); + + @Override + public Command handleDraft() { + return new GetDraftFileMetadataIfAvailableCommand(req, dataFile); } - } else { - //first get latest published - //if not available get draft if permissible - try { - fm = df.getLatestPublishedFileMetadata(); + @Override + public Command handleSpecific(long major, long minor) { + return new GetSpecificPublishedFileMetadataByDatasetVersionCommand(req, dataFile, major, minor); + } - } catch (UnsupportedOperationException e) { - try { - fm = execCommand(new GetDraftFileMetadataIfAvailableCommand(req, df)); - } catch (WrappedResponse w) { - return error(BAD_REQUEST, "An error occurred getting a draft version, you may not have permission to access unpublished data on this dataset."); - } - if (null == fm) { - return error(BAD_REQUEST, BundleUtil.getStringFromBundle("files.api.no.draft")); - } + @Override + public Command handleLatestPublished() { + return new GetLatestPublishedFileMetadataCommand(req, dataFile); } + })); + if (fileMetadata == null) { + throw new WrappedResponse(notFound("FileMetadata for DataFile with id " + fileIdOrPersistentId + " in dataset version " + datasetVersionId + " not found")); } - if (fm.getDatasetVersion().isReleased()) { - MakeDataCountLoggingServiceBean.MakeDataCountEntry entry = new MakeDataCountLoggingServiceBean.MakeDataCountEntry(uriInfo, headers, dvRequestService, df); + if (fileMetadata.getDatasetVersion().isReleased()) { + MakeDataCountLoggingServiceBean.MakeDataCountEntry entry = new MakeDataCountLoggingServiceBean.MakeDataCountEntry(uriInfo, headers, dvRequestService, dataFile); mdcLogService.logEntry(entry); } return Response.ok(Json.createObjectBuilder() - .add("status", ApiConstants.STATUS_OK) - .add("data", json(fm)).build()) + .add("status", ApiConstants.STATUS_OK) + .add("data", json(fileMetadata)).build()) .type(MediaType.APPLICATION_JSON) .build(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDraftFileMetadataIfAvailableCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDraftFileMetadataIfAvailableCommand.java index 4673f45412a..e0f8ca1fcf8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDraftFileMetadataIfAvailableCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDraftFileMetadataIfAvailableCommand.java @@ -1,7 +1,6 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.DataFile; -import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.FileMetadata; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.engine.command.AbstractCommand; diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java index 306221ed86c..980563a5489 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java @@ -2,14 +2,13 @@ import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.FileMetadata; -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; -@RequiredPermissions(Permission.ViewUnpublishedDataset) +@RequiredPermissions({}) public class GetLatestAccessibleFileMetadataCommand extends AbstractCommand { private final DataFile dataFile; From 65de2a532956f690aad73b1784e9ef1d0f55a3e3 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 8 Feb 2024 10:49:21 +0000 Subject: [PATCH 292/689] Refactor: using Bundle string in response --- .../edu/harvard/iq/dataverse/api/Files.java | 17 ++--------------- src/main/java/propertyFiles/Bundle.properties | 1 + 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index be2f093fdcf..a8e6aa74a42 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -2,20 +2,7 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; -import edu.harvard.iq.dataverse.DataFile; -import edu.harvard.iq.dataverse.DataFileServiceBean; -import edu.harvard.iq.dataverse.Dataset; -import edu.harvard.iq.dataverse.DatasetLock; -import edu.harvard.iq.dataverse.DatasetServiceBean; -import edu.harvard.iq.dataverse.DatasetVersion; -import edu.harvard.iq.dataverse.DatasetVersionServiceBean; -import edu.harvard.iq.dataverse.DataverseRequestServiceBean; -import edu.harvard.iq.dataverse.DataverseServiceBean; -import edu.harvard.iq.dataverse.EjbDataverseEngine; -import edu.harvard.iq.dataverse.FileMetadata; -import edu.harvard.iq.dataverse.GuestbookResponseServiceBean; -import edu.harvard.iq.dataverse.TermsOfUseAndAccessValidator; -import edu.harvard.iq.dataverse.UserNotificationServiceBean; +import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.api.auth.AuthRequired; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.ApiToken; @@ -541,7 +528,7 @@ public Command handleLatestPublished() { })); if (fileMetadata == null) { - throw new WrappedResponse(notFound("FileMetadata for DataFile with id " + fileIdOrPersistentId + " in dataset version " + datasetVersionId + " not found")); + throw new WrappedResponse(notFound(BundleUtil.getStringFromBundle("files.api.notFoundInVersion", Arrays.asList(fileIdOrPersistentId, datasetVersionId)))); } if (fileMetadata.getDatasetVersion().isReleased()) { diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 157f2ecaf54..4ef78c8fe7f 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -2633,6 +2633,7 @@ admin.api.deleteUser.success=Authenticated User {0} deleted. #Files.java files.api.metadata.update.duplicateFile=Filename already exists at {0} files.api.no.draft=No draft available for this file +files.api.notFoundInVersion="File metadata for file with id {0} in dataset version {1} not found" files.api.only.tabular.supported=This operation is only available for tabular files. #Datasets.java From 153d7d38ef46827a2e4d7651eec3de7b3ee2c1b7 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 8 Feb 2024 11:10:17 +0000 Subject: [PATCH 293/689] Changed: FilesIT testGetFileInfo restructure for upcoming new tests --- .../edu/harvard/iq/dataverse/api/FilesIT.java | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java index 915f82a6de2..feeeb40c133 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java @@ -1036,7 +1036,7 @@ public void testRestrictFile() { } - @Test + @Test public void testRestrictAddedFile() { msgt("testRestrictAddedFile"); @@ -1141,9 +1141,6 @@ public void testAccessFacet() { UtilIT.setSetting(SettingsServiceBean.Key.PublicInstall, "false"); } - - - @Test public void test_AddFileBadUploadFormat() { @@ -1398,14 +1395,13 @@ public void testDataSizeInDataverse() throws InterruptedException { assertEquals(magicControlString, JsonPath.from(datasetDownloadSizeResponse.body().asString()).getString("data.message")); } - + @Test public void testGetFileInfo() { - Response createUser = UtilIT.createRandomUser(); String username = UtilIT.getUsernameFromResponse(createUser); String apiToken = UtilIT.getApiTokenFromResponse(createUser); - Response makeSuperUser = UtilIT.makeSuperUser(username); + UtilIT.makeSuperUser(username); String dataverseAlias = createDataverseGetAlias(apiToken); Integer datasetId = createDatasetGetId(dataverseAlias, apiToken); @@ -1416,29 +1412,23 @@ public void testGetFileInfo() { String pathToFile = "scripts/search/data/binary/trees.png"; Response addResponse = UtilIT.uploadFileViaNative(datasetId.toString(), pathToFile, apiToken); + // The following tests cover cases where no version ID is specified in the endpoint + // Superuser should get to see draft file data String dataFileId = addResponse.getBody().jsonPath().getString("data.files[0].dataFile.id"); - msgt("datafile id: " + dataFileId); - - addResponse.prettyPrint(); - Response getFileDataResponse = UtilIT.getFileData(dataFileId, apiToken); - - getFileDataResponse.prettyPrint(); getFileDataResponse.then().assertThat() .body("data.label", equalTo("trees.png")) .body("data.dataFile.filename", equalTo("trees.png")) .body("data.dataFile.contentType", equalTo("image/png")) .body("data.dataFile.filesize", equalTo(8361)) .statusCode(OK.getStatusCode()); - + + // Regular user should not get to see draft file data getFileDataResponse = UtilIT.getFileData(dataFileId, apiTokenRegular); getFileDataResponse.then().assertThat() .statusCode(BAD_REQUEST.getStatusCode()); - // ------------------------- // Publish dataverse and dataset - // ------------------------- - msg("Publish dataverse and dataset"); Response publishDataversetResp = UtilIT.publishDataverseViaSword(dataverseAlias, apiToken); publishDataversetResp.then().assertThat() .statusCode(OK.getStatusCode()); @@ -1446,12 +1436,17 @@ public void testGetFileInfo() { Response publishDatasetResp = UtilIT.publishDatasetViaNativeApi(datasetId, "major", apiToken); publishDatasetResp.then().assertThat() .statusCode(OK.getStatusCode()); - //regular user should get to see file data + + // Regular user should get to see published file data getFileDataResponse = UtilIT.getFileData(dataFileId, apiTokenRegular); getFileDataResponse.then().assertThat() .statusCode(OK.getStatusCode()); - //cleanup + // The following tests cover cases where a version ID is specified in the endpoint + + // TODO + + // Cleanup Response destroyDatasetResponse = UtilIT.destroyDataset(datasetId, apiToken); assertEquals(200, destroyDatasetResponse.getStatusCode()); From 2fb5247386aacc53742b1d3657e68b3c5df7d420 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 8 Feb 2024 11:39:36 +0000 Subject: [PATCH 294/689] Changed: UtilIT getFileData to support new datasetVersionId optional param --- .../edu/harvard/iq/dataverse/api/UtilIT.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) 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 ec41248a65f..c2d43584b22 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -1084,11 +1084,17 @@ static Response getFileMetadata(String fileIdOrPersistentId, String optionalForm .urlEncodingEnabled(false) .get("/api/access/datafile/" + idInPath + "/metadata" + optionalFormatInPath + optionalQueryParam); } - - static Response getFileData(String fileId, String apiToken) { - return given() - .header(API_TOKEN_HTTP_HEADER, apiToken) - .get("/api/files/" + fileId ); + + static Response getFileData(String fileId, String apiToken) { + return getFileData(fileId, apiToken, null); + } + + static Response getFileData(String fileId, String apiToken, String datasetVersionId) { + RequestSpecification requestSpec = given().header(API_TOKEN_HTTP_HEADER, apiToken); + if (datasetVersionId != null) { + requestSpec.queryParam("datasetVersionId", datasetVersionId); + } + return requestSpec.get("/api/files/" + fileId); } static Response testIngest(String fileName, String fileType) { From e98ea11a75ca9b808085ca861d86047cc200b77a Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 8 Feb 2024 13:34:54 +0000 Subject: [PATCH 295/689] Changed: do not overwrite findDataFileOrDie or GetDataFileCommand with bad request --- .../iq/dataverse/api/AbstractApiBean.java | 1 - .../edu/harvard/iq/dataverse/api/Files.java | 8 +-- .../command/impl/GetDataFileCommand.java | 17 +++--- .../edu/harvard/iq/dataverse/api/FilesIT.java | 54 +++++++++++++++---- .../edu/harvard/iq/dataverse/api/UtilIT.java | 12 ++--- 5 files changed, 58 insertions(+), 34 deletions(-) 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 bc94d7f0bcc..fe9ee518d75 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -392,7 +392,6 @@ protected Dataset findDatasetOrDie(String id) throws WrappedResponse { } protected DataFile findDataFileOrDie(String id) throws WrappedResponse { - DataFile datafile; if (id.equals(PERSISTENT_ID_KEY)) { String persistentId = getRequestParameter(PERSISTENT_ID_KEY.substring(1)); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index a8e6aa74a42..4116bf18973 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -498,13 +498,7 @@ public Response getFileData(@Context ContainerRequestContext crc, @PathParam("id } private Response getFileDataResponse(final DataverseRequest req, String fileIdOrPersistentId, UriInfo uriInfo, HttpHeaders headers, String datasetVersionId) throws WrappedResponse { - final DataFile dataFile; - try { - dataFile = execCommand(new GetDataFileCommand(req, findDataFileOrDie(fileIdOrPersistentId))); - } catch (Exception e) { - return error(BAD_REQUEST, "Error attempting get the requested data file."); - } - + final DataFile dataFile = execCommand(new GetDataFileCommand(req, findDataFileOrDie(fileIdOrPersistentId))); FileMetadata fileMetadata = execCommand(handleVersion(datasetVersionId, new Datasets.DsVersionHandler<>() { @Override public Command handleLatest() { diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDataFileCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDataFileCommand.java index fdf47bbd2dd..369f3cbfda6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDataFileCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDataFileCommand.java @@ -11,35 +11,34 @@ 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; /** - * * @author Matthew */ // no annotations here, since permissions are dynamically decided // based off GetDatasetCommand for similar permissions checking public class GetDataFileCommand extends AbstractCommand { - private final DataFile df; + private final DataFile dataFile; - public GetDataFileCommand(DataverseRequest aRequest, DataFile anAffectedDataset) { - super(aRequest, anAffectedDataset); - df = anAffectedDataset; + public GetDataFileCommand(DataverseRequest aRequest, DataFile dataFile) { + super(aRequest, dataFile); + this.dataFile = dataFile; } @Override public DataFile execute(CommandContext ctxt) throws CommandException { - return df; + return dataFile; } @Override public Map> getRequiredPermissions() { return Collections.singletonMap("", - df.isReleased() ? Collections.emptySet() - : Collections.singleton(Permission.ViewUnpublishedDataset)); + dataFile.isReleased() ? Collections.emptySet() + : Collections.singleton(Permission.ViewUnpublishedDataset)); } - } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java index feeeb40c133..d84b0ed77ac 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java @@ -12,6 +12,7 @@ import io.restassured.path.json.JsonPath; import static edu.harvard.iq.dataverse.api.ApiConstants.DS_VERSION_DRAFT; +import static edu.harvard.iq.dataverse.api.ApiConstants.DS_VERSION_LATEST_PUBLISHED; import static io.restassured.path.json.JsonPath.with; import io.restassured.path.xml.XmlPath; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; @@ -1400,22 +1401,22 @@ public void testDataSizeInDataverse() throws InterruptedException { public void testGetFileInfo() { Response createUser = UtilIT.createRandomUser(); String username = UtilIT.getUsernameFromResponse(createUser); - String apiToken = UtilIT.getApiTokenFromResponse(createUser); + String superUserApiToken = UtilIT.getApiTokenFromResponse(createUser); UtilIT.makeSuperUser(username); - String dataverseAlias = createDataverseGetAlias(apiToken); - Integer datasetId = createDatasetGetId(dataverseAlias, apiToken); + String dataverseAlias = createDataverseGetAlias(superUserApiToken); + Integer datasetId = createDatasetGetId(dataverseAlias, superUserApiToken); createUser = UtilIT.createRandomUser(); String apiTokenRegular = UtilIT.getApiTokenFromResponse(createUser); msg("Add a non-tabular file"); String pathToFile = "scripts/search/data/binary/trees.png"; - Response addResponse = UtilIT.uploadFileViaNative(datasetId.toString(), pathToFile, apiToken); + Response addResponse = UtilIT.uploadFileViaNative(datasetId.toString(), pathToFile, superUserApiToken); // The following tests cover cases where no version ID is specified in the endpoint // Superuser should get to see draft file data String dataFileId = addResponse.getBody().jsonPath().getString("data.files[0].dataFile.id"); - Response getFileDataResponse = UtilIT.getFileData(dataFileId, apiToken); + Response getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken); getFileDataResponse.then().assertThat() .body("data.label", equalTo("trees.png")) .body("data.dataFile.filename", equalTo("trees.png")) @@ -1426,14 +1427,14 @@ public void testGetFileInfo() { // Regular user should not get to see draft file data getFileDataResponse = UtilIT.getFileData(dataFileId, apiTokenRegular); getFileDataResponse.then().assertThat() - .statusCode(BAD_REQUEST.getStatusCode()); + .statusCode(UNAUTHORIZED.getStatusCode()); // Publish dataverse and dataset - Response publishDataversetResp = UtilIT.publishDataverseViaSword(dataverseAlias, apiToken); - publishDataversetResp.then().assertThat() + Response publishDataverseResp = UtilIT.publishDataverseViaSword(dataverseAlias, superUserApiToken); + publishDataverseResp.then().assertThat() .statusCode(OK.getStatusCode()); - Response publishDatasetResp = UtilIT.publishDatasetViaNativeApi(datasetId, "major", apiToken); + Response publishDatasetResp = UtilIT.publishDatasetViaNativeApi(datasetId, "major", superUserApiToken); publishDatasetResp.then().assertThat() .statusCode(OK.getStatusCode()); @@ -1443,14 +1444,45 @@ public void testGetFileInfo() { .statusCode(OK.getStatusCode()); // The following tests cover cases where a version ID is specified in the endpoint + // Superuser should not get to see draft file data when no draft version exists + getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_DRAFT); + getFileDataResponse.then().assertThat() + .statusCode(NOT_FOUND.getStatusCode()); + + // Update the file metadata + String newFileName = "trees_2.png"; + JsonObjectBuilder updateFileMetadata = Json.createObjectBuilder() + .add("label", newFileName); + Response updateFileMetadataResponse = UtilIT.updateFileMetadata(dataFileId, updateFileMetadata.build().toString(), superUserApiToken); + updateFileMetadataResponse.then().statusCode(OK.getStatusCode()); + // Superuser should get to see draft file data + getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_DRAFT); + getFileDataResponse.then().assertThat() + .statusCode(OK.getStatusCode()); + + // Regular user should not get to see draft file data + getFileDataResponse = UtilIT.getFileData(dataFileId, apiTokenRegular, DS_VERSION_DRAFT); + getFileDataResponse.then().assertThat() + .statusCode(UNAUTHORIZED.getStatusCode()); + + // Publish dataset once again + publishDatasetResp = UtilIT.publishDatasetViaNativeApi(datasetId, "major", superUserApiToken); + publishDatasetResp.then().assertThat() + .statusCode(OK.getStatusCode()); + + // Regular user should get to see latest published file data + getFileDataResponse = UtilIT.getFileData(dataFileId, apiTokenRegular, DS_VERSION_LATEST_PUBLISHED); + getFileDataResponse.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.label", equalTo(newFileName)); // TODO // Cleanup - Response destroyDatasetResponse = UtilIT.destroyDataset(datasetId, apiToken); + Response destroyDatasetResponse = UtilIT.destroyDataset(datasetId, superUserApiToken); assertEquals(200, destroyDatasetResponse.getStatusCode()); - Response deleteDataverseResponse = UtilIT.deleteDataverse(dataverseAlias, apiToken); + Response deleteDataverseResponse = UtilIT.deleteDataverse(dataverseAlias, superUserApiToken); assertEquals(200, deleteDataverseResponse.getStatusCode()); Response deleteUserResponse = UtilIT.deleteUser(username); 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 c2d43584b22..f6f2c9a3c03 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -1086,15 +1086,15 @@ static Response getFileMetadata(String fileIdOrPersistentId, String optionalForm } static Response getFileData(String fileId, String apiToken) { - return getFileData(fileId, apiToken, null); + return given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .get("/api/files/" + fileId); } static Response getFileData(String fileId, String apiToken, String datasetVersionId) { - RequestSpecification requestSpec = given().header(API_TOKEN_HTTP_HEADER, apiToken); - if (datasetVersionId != null) { - requestSpec.queryParam("datasetVersionId", datasetVersionId); - } - return requestSpec.get("/api/files/" + fileId); + return given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .get("/api/files/" + fileId + "/versions/" + datasetVersionId); } static Response testIngest(String fileName, String fileType) { From 8abeaf06ce1c24a5b0cc7c17954c307727746703 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Thu, 8 Feb 2024 09:06:57 -0500 Subject: [PATCH 296/689] #10286 add breadcrumbs to dataset api --- .../harvard/iq/dataverse/api/Datasets.java | 5 +- .../iq/dataverse/util/json/JsonPrinter.java | 49 ++++++++++++++++++- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index e3505cbbb33..60c07815b71 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -186,11 +186,12 @@ public interface DsVersionHandler { @GET @AuthRequired @Path("{id}") - public Response getDataset(@Context ContainerRequestContext crc, @PathParam("id") String id, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) { + public Response getDataset(@Context ContainerRequestContext crc, @PathParam("id") String id, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response, @QueryParam("breadcrumbs") Boolean breadcrumbs) { return response( req -> { final Dataset retrieved = execCommand(new GetDatasetCommand(req, findDatasetOrDie(id))); final DatasetVersion latest = execCommand(new GetLatestAccessibleDatasetVersionCommand(req, retrieved)); - final JsonObjectBuilder jsonbuilder = json(retrieved); + Boolean includeBreadcrumbs = breadcrumbs == null ? false : breadcrumbs; + final JsonObjectBuilder jsonbuilder = json(retrieved, includeBreadcrumbs); //Report MDC if this is a released version (could be draft if user has access, or user may not have access at all and is not getting metadata beyond the minimum) if((latest != null) && latest.isReleased()) { MakeDataCountLoggingServiceBean.MakeDataCountEntry entry = new MakeDataCountEntry(uriInfo, headers, dvRequestService, retrieved); 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 2eaf6b64579..197c46ac474 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 @@ -55,6 +55,7 @@ import jakarta.ejb.Singleton; import jakarta.json.JsonArray; import jakarta.json.JsonObject; +import java.math.BigDecimal; /** * Convert objects to Json. @@ -303,6 +304,45 @@ public static JsonArrayBuilder json(List dataverseContacts) { } return jsonArrayOfContacts; } + + public static JsonArrayBuilder getBreadcrumbsFromDvObject(DvObject dvObject) { + + List ownerList = new ArrayList(); + + while (dvObject != null) { + ownerList.add(dvObject); + dvObject = dvObject.getOwner(); + } + + JsonArrayBuilder jsonArrayOfBreadcrumbs = Json.createArrayBuilder(); + + for (DvObject dvo : ownerList){ + JsonObjectBuilder breadcrumbObject = jsonObjectBuilder(); + if (dvo.isInstanceofDataverse()){ + Dataverse in = (Dataverse) dvo; + breadcrumbObject.add("identifier", in.getAlias()); + } + if (dvo.isInstanceofDataset() || dvo.isInstanceofDataFile() ){ + if (dvo.getIdentifier() != null){ + breadcrumbObject.add("identifier", dvo.getIdentifier()); + } else { + breadcrumbObject.add("identifier", dvo.getId()); + } + } + if (dvo.isInstanceofDataverse()){ + breadcrumbObject.add("type", "DATAVERSE"); + } + if (dvo.isInstanceofDataset()){ + breadcrumbObject.add("type", "DATASET"); + } + if (dvo.isInstanceofDataFile()){ + breadcrumbObject.add("type", "DATAFILE"); + } + breadcrumbObject.add("displayName", dvo.getDisplayName()); + jsonArrayOfBreadcrumbs.add(breadcrumbObject); + } + return jsonArrayOfBreadcrumbs; + } public static JsonObjectBuilder json( DataverseTheme theme ) { final NullSafeJsonBuilder baseObject = jsonObjectBuilder() @@ -326,8 +366,12 @@ public static JsonObjectBuilder json(BuiltinUser user) { .add("id", user.getId()) .add("userName", user.getUserName()); } + + public static JsonObjectBuilder json(Dataset ds){ + return json(ds, false); + } - public static JsonObjectBuilder json(Dataset ds) { + public static JsonObjectBuilder json(Dataset ds, Boolean includeBreadcrumbs) { JsonObjectBuilder bld = jsonObjectBuilder() .add("id", ds.getId()) .add("identifier", ds.getIdentifier()) @@ -340,6 +384,9 @@ public static JsonObjectBuilder json(Dataset ds) { if (DvObjectContainer.isMetadataLanguageSet(ds.getMetadataLanguage())) { bld.add("metadataLanguage", ds.getMetadataLanguage()); } + if (includeBreadcrumbs){ + bld.add("ownerArray", getBreadcrumbsFromDvObject(ds)); + } return bld; } From 572b9cbbd0264b068bb324c69a6d2a2a06f6337e Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Thu, 8 Feb 2024 09:30:19 -0500 Subject: [PATCH 297/689] #10286 move owner type to beginning --- .../iq/dataverse/util/json/JsonPrinter.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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 197c46ac474..0803001fbfd 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 @@ -318,6 +318,15 @@ public static JsonArrayBuilder getBreadcrumbsFromDvObject(DvObject dvObject) { for (DvObject dvo : ownerList){ JsonObjectBuilder breadcrumbObject = jsonObjectBuilder(); + if (dvo.isInstanceofDataverse()){ + breadcrumbObject.add("type", "DATAVERSE"); + } + if (dvo.isInstanceofDataset()){ + breadcrumbObject.add("type", "DATASET"); + } + if (dvo.isInstanceofDataFile()){ + breadcrumbObject.add("type", "DATAFILE"); + } if (dvo.isInstanceofDataverse()){ Dataverse in = (Dataverse) dvo; breadcrumbObject.add("identifier", in.getAlias()); @@ -329,15 +338,6 @@ public static JsonArrayBuilder getBreadcrumbsFromDvObject(DvObject dvObject) { breadcrumbObject.add("identifier", dvo.getId()); } } - if (dvo.isInstanceofDataverse()){ - breadcrumbObject.add("type", "DATAVERSE"); - } - if (dvo.isInstanceofDataset()){ - breadcrumbObject.add("type", "DATASET"); - } - if (dvo.isInstanceofDataFile()){ - breadcrumbObject.add("type", "DATAFILE"); - } breadcrumbObject.add("displayName", dvo.getDisplayName()); jsonArrayOfBreadcrumbs.add(breadcrumbObject); } From 6702918abddafb4bd66965bc3e133fdb4be133c1 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Thu, 8 Feb 2024 14:02:08 -0500 Subject: [PATCH 298/689] #10271 fix tool tests w/arrays --- .../edu/harvard/iq/dataverse/api/TestApi.java | 28 ++++++++++++++++ .../iq/dataverse/api/ExternalToolsIT.java | 33 +++++++++---------- .../edu/harvard/iq/dataverse/api/UtilIT.java | 15 +++++++++ 3 files changed, 59 insertions(+), 17 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java b/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java index 10510013495..b9db44b2671 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java @@ -44,6 +44,34 @@ public Response getExternalToolsforFile(@PathParam("id") String idSupplied, @Que return wr.getResponse(); } } + + @GET + @Path("datasets/{id}/externalTool/{toolId}") + public Response getExternalToolforDatasetById(@PathParam("id") String idSupplied, @PathParam("toolId") String toolId, @QueryParam("type") String typeSupplied) { + ExternalTool.Type type; + try { + type = ExternalTool.Type.fromString(typeSupplied); + } catch (IllegalArgumentException ex) { + return error(BAD_REQUEST, ex.getLocalizedMessage()); + } + Dataset dataset; + try { + dataset = findDatasetOrDie(idSupplied); + JsonArrayBuilder tools = Json.createArrayBuilder(); + List datasetTools = externalToolService.findDatasetToolsByType(type); + for (ExternalTool tool : datasetTools) { + ApiToken apiToken = externalToolService.getApiToken(getRequestApiKey()); + ExternalToolHandler externalToolHandler = new ExternalToolHandler(tool, dataset, apiToken, null); + JsonObjectBuilder toolToJson = externalToolService.getToolAsJsonWithQueryParameters(externalToolHandler); + if (tool.getId().toString().equals(toolId)) { + return ok(toolToJson); + } + } + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + return error(BAD_REQUEST, "Could not find external tool with id of " + toolId); + } @Path("files/{id}/externalTools") @GET diff --git a/src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java index 9a280f475a1..22abf6fa2e3 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/ExternalToolsIT.java @@ -197,7 +197,7 @@ public void testDatasetLevelTool1() { .statusCode(OK.getStatusCode()) .body("data.displayName", CoreMatchers.equalTo("DatasetTool1")); - long toolId = JsonPath.from(addExternalTool.getBody().asString()).getLong("data.id"); + Long toolId = JsonPath.from(addExternalTool.getBody().asString()).getLong("data.id"); Response getExternalToolsByDatasetIdInvalidType = UtilIT.getExternalToolsForDataset(datasetId.toString(), "invalidType", apiToken); getExternalToolsByDatasetIdInvalidType.prettyPrint(); @@ -205,12 +205,12 @@ public void testDatasetLevelTool1() { .statusCode(BAD_REQUEST.getStatusCode()) .body("message", CoreMatchers.equalTo("Type must be one of these values: [explore, configure, preview, query].")); - Response getExternalToolsByDatasetId = UtilIT.getExternalToolsForDataset(datasetId.toString(), "explore", apiToken); + Response getExternalToolsByDatasetId = UtilIT.getExternalToolForDatasetById(datasetId.toString(), "explore", apiToken, toolId.toString()); getExternalToolsByDatasetId.prettyPrint(); getExternalToolsByDatasetId.then().assertThat() - .body("data[0].displayName", CoreMatchers.equalTo("DatasetTool1")) - .body("data[0].scope", CoreMatchers.equalTo("dataset")) - .body("data[0].toolUrlWithQueryParams", CoreMatchers.equalTo("http://datasettool1.com?datasetPid=" + datasetPid + "&key=" + apiToken)) + .body("data.displayName", CoreMatchers.equalTo("DatasetTool1")) + .body("data.scope", CoreMatchers.equalTo("dataset")) + .body("data.toolUrlWithQueryParams", CoreMatchers.equalTo("http://datasettool1.com?datasetPid=" + datasetPid + "&key=" + apiToken)) .statusCode(OK.getStatusCode()); //Delete the tool added by this test... @@ -271,15 +271,14 @@ public void testDatasetLevelToolConfigure() { .statusCode(OK.getStatusCode()) .body("data.displayName", CoreMatchers.equalTo("Dataset Configurator")); - long toolId = JsonPath.from(addExternalTool.getBody().asString()).getLong("data.id"); - - Response getExternalToolsByDatasetId = UtilIT.getExternalToolsForDataset(datasetId.toString(), "configure", apiToken); + Long toolId = JsonPath.from(addExternalTool.getBody().asString()).getLong("data.id"); + Response getExternalToolsByDatasetId = UtilIT.getExternalToolForDatasetById(datasetId.toString(), "configure", apiToken, toolId.toString()); getExternalToolsByDatasetId.prettyPrint(); getExternalToolsByDatasetId.then().assertThat() - .body("data[0].displayName", CoreMatchers.equalTo("Dataset Configurator")) - .body("data[0].scope", CoreMatchers.equalTo("dataset")) - .body("data[0].types[0]", CoreMatchers.equalTo("configure")) - .body("data[0].toolUrlWithQueryParams", CoreMatchers.equalTo("https://datasetconfigurator.com?datasetPid=" + datasetPid)) + .body("data.displayName", CoreMatchers.equalTo("Dataset Configurator")) + .body("data.scope", CoreMatchers.equalTo("dataset")) + .body("data.types[0]", CoreMatchers.equalTo("configure")) + .body("data.toolUrlWithQueryParams", CoreMatchers.equalTo("https://datasetconfigurator.com?datasetPid=" + datasetPid)) .statusCode(OK.getStatusCode()); //Delete the tool added by this test... @@ -594,7 +593,7 @@ public void testFileLevelToolWithAuxFileReq() throws IOException { .statusCode(OK.getStatusCode()) .body("data.displayName", CoreMatchers.equalTo("HDF5 Tool")); - long toolId = JsonPath.from(addExternalTool.getBody().asString()).getLong("data.id"); + Long toolId = JsonPath.from(addExternalTool.getBody().asString()).getLong("data.id"); Response getTool = UtilIT.getExternalTool(toolId); getTool.prettyPrint(); @@ -610,13 +609,13 @@ public void testFileLevelToolWithAuxFileReq() throws IOException { .body("data", Matchers.hasSize(0)); // The tool shows for a true HDF5 file. The NcML aux file is available. Requirements met. - Response getToolsForTrueHdf5 = UtilIT.getExternalToolsForFile(trueHdf5.toString(), "preview", apiToken); + Response getToolsForTrueHdf5 = UtilIT.getExternalToolForFileById(trueHdf5.toString(), "preview", apiToken, toolId.toString()); getToolsForTrueHdf5.prettyPrint(); getToolsForTrueHdf5.then().assertThat() .statusCode(OK.getStatusCode()) - .body("data[0].displayName", CoreMatchers.equalTo("HDF5 Tool")) - .body("data[0].scope", CoreMatchers.equalTo("file")) - .body("data[0].contentType", CoreMatchers.equalTo("application/x-hdf5")); + .body("data.displayName", CoreMatchers.equalTo("HDF5 Tool")) + .body("data.scope", CoreMatchers.equalTo("file")) + .body("data.contentType", CoreMatchers.equalTo("application/x-hdf5")); //Delete the tool added by this test... Response deleteExternalTool = UtilIT.deleteExternalTool(toolId); 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 ec41248a65f..d67b45b645b 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -2339,6 +2339,21 @@ static Response getExternalToolsForDataset(String idOrPersistentIdOfDataset, Str } return requestSpecification.get("/api/admin/test/datasets/" + idInPath + "/externalTools?type=" + type + optionalQueryParam); } + + static Response getExternalToolForDatasetById(String idOrPersistentIdOfDataset, String type, String apiToken, String toolId) { + String idInPath = idOrPersistentIdOfDataset; // Assume it's a number. + String optionalQueryParam = ""; // If idOrPersistentId is a number we'll just put it in the path. + if (!NumberUtils.isCreatable(idOrPersistentIdOfDataset)) { + idInPath = ":persistentId"; + optionalQueryParam = "&persistentId=" + idOrPersistentIdOfDataset; + } + RequestSpecification requestSpecification = given(); + if (apiToken != null) { + requestSpecification = given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, apiToken); + } + return requestSpecification.get("/api/admin/test/datasets/" + idInPath + "/externalTool/" + toolId + "?type=" + type + optionalQueryParam); + } static Response getExternalToolsForFile(String idOrPersistentIdOfFile, String type, String apiToken) { String idInPath = idOrPersistentIdOfFile; // Assume it's a number. From 889e942f353953aff9df192c46fa2c0ebd4b0c51 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Thu, 8 Feb 2024 14:32:44 -0500 Subject: [PATCH 299/689] #10286 update pathparam name/terms --- .../harvard/iq/dataverse/api/Datasets.java | 6 ++-- .../iq/dataverse/util/json/JsonPrinter.java | 32 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 60c07815b71..02eb13e32d4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -186,12 +186,12 @@ public interface DsVersionHandler { @GET @AuthRequired @Path("{id}") - public Response getDataset(@Context ContainerRequestContext crc, @PathParam("id") String id, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response, @QueryParam("breadcrumbs") Boolean breadcrumbs) { + public Response getDataset(@Context ContainerRequestContext crc, @PathParam("id") String id, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response, @QueryParam("returnOwners") Boolean returnOwners) { return response( req -> { final Dataset retrieved = execCommand(new GetDatasetCommand(req, findDatasetOrDie(id))); final DatasetVersion latest = execCommand(new GetLatestAccessibleDatasetVersionCommand(req, retrieved)); - Boolean includeBreadcrumbs = breadcrumbs == null ? false : breadcrumbs; - final JsonObjectBuilder jsonbuilder = json(retrieved, includeBreadcrumbs); + Boolean includeOwners = returnOwners == null ? false : returnOwners; + final JsonObjectBuilder jsonbuilder = json(retrieved, includeOwners); //Report MDC if this is a released version (could be draft if user has access, or user may not have access at all and is not getting metadata beyond the minimum) if((latest != null) && latest.isReleased()) { MakeDataCountLoggingServiceBean.MakeDataCountEntry entry = new MakeDataCountEntry(uriInfo, headers, dvRequestService, retrieved); 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 0803001fbfd..7d9bede9a61 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 @@ -305,43 +305,43 @@ public static JsonArrayBuilder json(List dataverseContacts) { return jsonArrayOfContacts; } - public static JsonArrayBuilder getBreadcrumbsFromDvObject(DvObject dvObject) { + public static JsonArrayBuilder getOwnersFromDvObject(DvObject dvObject) { List ownerList = new ArrayList(); - + dvObject = dvObject.getOwner(); // We're going to ignore the object itself while (dvObject != null) { ownerList.add(dvObject); dvObject = dvObject.getOwner(); } - JsonArrayBuilder jsonArrayOfBreadcrumbs = Json.createArrayBuilder(); + JsonArrayBuilder jsonArrayOfOwners = Json.createArrayBuilder(); for (DvObject dvo : ownerList){ - JsonObjectBuilder breadcrumbObject = jsonObjectBuilder(); + JsonObjectBuilder ownerObject = jsonObjectBuilder(); if (dvo.isInstanceofDataverse()){ - breadcrumbObject.add("type", "DATAVERSE"); + ownerObject.add("type", "DATAVERSE"); } if (dvo.isInstanceofDataset()){ - breadcrumbObject.add("type", "DATASET"); + ownerObject.add("type", "DATASET"); } if (dvo.isInstanceofDataFile()){ - breadcrumbObject.add("type", "DATAFILE"); + ownerObject.add("type", "DATAFILE"); } if (dvo.isInstanceofDataverse()){ Dataverse in = (Dataverse) dvo; - breadcrumbObject.add("identifier", in.getAlias()); + ownerObject.add("identifier", in.getAlias()); } if (dvo.isInstanceofDataset() || dvo.isInstanceofDataFile() ){ if (dvo.getIdentifier() != null){ - breadcrumbObject.add("identifier", dvo.getIdentifier()); + ownerObject.add("identifier", dvo.getIdentifier()); } else { - breadcrumbObject.add("identifier", dvo.getId()); + ownerObject.add("identifier", dvo.getId()); } } - breadcrumbObject.add("displayName", dvo.getDisplayName()); - jsonArrayOfBreadcrumbs.add(breadcrumbObject); + ownerObject.add("displayName", dvo.getDisplayName()); + jsonArrayOfOwners.add(ownerObject); } - return jsonArrayOfBreadcrumbs; + return jsonArrayOfOwners; } public static JsonObjectBuilder json( DataverseTheme theme ) { @@ -371,7 +371,7 @@ public static JsonObjectBuilder json(Dataset ds){ return json(ds, false); } - public static JsonObjectBuilder json(Dataset ds, Boolean includeBreadcrumbs) { + public static JsonObjectBuilder json(Dataset ds, Boolean includeOwners) { JsonObjectBuilder bld = jsonObjectBuilder() .add("id", ds.getId()) .add("identifier", ds.getIdentifier()) @@ -384,8 +384,8 @@ public static JsonObjectBuilder json(Dataset ds, Boolean includeBreadcrumbs) { if (DvObjectContainer.isMetadataLanguageSet(ds.getMetadataLanguage())) { bld.add("metadataLanguage", ds.getMetadataLanguage()); } - if (includeBreadcrumbs){ - bld.add("ownerArray", getBreadcrumbsFromDvObject(ds)); + if (includeOwners){ + bld.add("ownerArray", getOwnersFromDvObject(ds)); } return bld; } From 244cb1a7a3ed87cc747ede3bd7da967e6f5e2938 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Fri, 9 Feb 2024 12:24:58 -0500 Subject: [PATCH 300/689] support citation for files with PIDs #10240 --- .../edu/harvard/iq/dataverse/api/Files.java | 2 +- .../iq/dataverse/DataCitationTest.java | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index 69bdebb2dd5..440577d1518 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -958,7 +958,7 @@ public Response getFileCitationByVersion(@Context ContainerRequestContext crc, @ if (fm == null) { return notFound(BundleUtil.getStringFromBundle("files.api.fileNotFound")); } - boolean direct = false; + boolean direct = df.isIdentifierRegistered(); DataCitation citation = new DataCitation(fm, direct); return ok(citation.toString(true)); } catch (WrappedResponse ex) { diff --git a/src/test/java/edu/harvard/iq/dataverse/DataCitationTest.java b/src/test/java/edu/harvard/iq/dataverse/DataCitationTest.java index 4097adb0be6..23a7efedca7 100644 --- a/src/test/java/edu/harvard/iq/dataverse/DataCitationTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/DataCitationTest.java @@ -378,6 +378,36 @@ public void testTitleWithQuotes() throws ParseException { } + @Test + public void testFileCitationToStringHtml() throws ParseException { + DatasetVersion dsv = createATestDatasetVersion("Dataset Title", true); + FileMetadata fileMetadata = new FileMetadata(); + fileMetadata.setLabel("foo.txt"); + fileMetadata.setDataFile(new DataFile()); + dsv.setVersionState(DatasetVersion.VersionState.RELEASED); + fileMetadata.setDatasetVersion(dsv); + dsv.setDataset(dsv.getDataset()); + DataCitation fileCitation = new DataCitation(fileMetadata, false); + assertEquals("First Last, 1955, \"Dataset Title\", https://doi.org/10.5072/FK2/LK0D1H, LibraScholar, V1; foo.txt [fileName]", fileCitation.toString(true)); + } + + @Test + public void testFileCitationToStringHtmlFilePid() throws ParseException { + DatasetVersion dsv = createATestDatasetVersion("Dataset Title", true); + FileMetadata fileMetadata = new FileMetadata(); + fileMetadata.setLabel("foo.txt"); + DataFile dataFile = new DataFile(); + dataFile.setProtocol("doi"); + dataFile.setAuthority("10.42"); + dataFile.setIdentifier("myFilePid"); + fileMetadata.setDataFile(dataFile); + dsv.setVersionState(DatasetVersion.VersionState.RELEASED); + fileMetadata.setDatasetVersion(dsv); + dsv.setDataset(dsv.getDataset()); + DataCitation fileCitation = new DataCitation(fileMetadata, true); + assertEquals("First Last, 1955, \"foo.txt\", Dataset Title, https://doi.org/10.42/myFilePid, LibraScholar, V1", fileCitation.toString(true)); + } + private DatasetVersion createATestDatasetVersion(String withTitle, boolean withAuthor) throws ParseException { Dataverse dataverse = new Dataverse(); @@ -400,6 +430,7 @@ private DatasetVersion createATestDatasetVersion(String withTitle, boolean withA fields.add(createTitleField(withTitle)); } if (withAuthor) { + // TODO: "Last, First" would make more sense. fields.add(createAuthorField("First Last")); } From c95ceb282648d7c386e8dce88aec416a872af567 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 9 Feb 2024 18:42:10 +0100 Subject: [PATCH 301/689] fix(ct): make base image comply with OpenShift file permissions To enable the user with a random, arbitrary UID to write into the overlay filesystem, we need to set proper file permissions. This should not affect users on Docker or other K8s distributions, as the security is more lenient there. It is not ideal to write into overlayfs, as it impacts performance and may lead to unintended side effects. This is a workaround to at least get going. See https://docs.openshift.com/container-platform/4.14/openshift_images/create-images.html#use-uid_create-images for a detailed reference --- modules/container-base/src/main/docker/Dockerfile | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/modules/container-base/src/main/docker/Dockerfile b/modules/container-base/src/main/docker/Dockerfile index 97aa4cd2792..3d2e1f782f2 100644 --- a/modules/container-base/src/main/docker/Dockerfile +++ b/modules/container-base/src/main/docker/Dockerfile @@ -84,8 +84,11 @@ RUN < Date: Fri, 9 Feb 2024 18:43:58 +0100 Subject: [PATCH 302/689] fix(ct): make location of boot scripts configurable By defining pre- and postboot file locations within the Dockerfile, it wasn't able to change the location by changing CONFIG_DIR env var. This is fixed now, allowing simpler backing of the dir location with an (ephemeral) volume. --- modules/container-base/src/main/docker/Dockerfile | 2 -- .../container-base/src/main/docker/scripts/entrypoint.sh | 6 ++++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/container-base/src/main/docker/Dockerfile b/modules/container-base/src/main/docker/Dockerfile index 3d2e1f782f2..663b3d9dd51 100644 --- a/modules/container-base/src/main/docker/Dockerfile +++ b/modules/container-base/src/main/docker/Dockerfile @@ -49,8 +49,6 @@ ENV PAYARA_DIR="${HOME_DIR}/appserver" \ ENV PATH="${PATH}:${PAYARA_DIR}/bin:${SCRIPT_DIR}" \ DOMAIN_DIR="${PAYARA_DIR}/glassfish/domains/${DOMAIN_NAME}" \ DEPLOY_PROPS="" \ - PREBOOT_COMMANDS="${CONFIG_DIR}/pre-boot-commands.asadmin" \ - POSTBOOT_COMMANDS="${CONFIG_DIR}/post-boot-commands.asadmin" \ JVM_ARGS="" \ MEM_MAX_RAM_PERCENTAGE="70.0" \ MEM_XSS="512k" \ diff --git a/modules/container-base/src/main/docker/scripts/entrypoint.sh b/modules/container-base/src/main/docker/scripts/entrypoint.sh index 47933bd42e2..bd7031db9f0 100644 --- a/modules/container-base/src/main/docker/scripts/entrypoint.sh +++ b/modules/container-base/src/main/docker/scripts/entrypoint.sh @@ -10,6 +10,12 @@ # and zombies under control. If the ENTRYPOINT command is changed, it will still use dumb-init because shebang. # dumb-init takes care to send any signals to subshells, too! (Which might run in the background...) +# We do not define these variables within our Dockerfile so the location can be changed when trying to avoid +# writes to the overlay filesystem. (CONFIG_DIR is defined within the Dockerfile, but might be overridden.) +${PREBOOT_COMMANDS:="${CONFIG_DIR}/pre-boot-commands.asadmin"} +export PREBOOT_COMMANDS +${POSTBOOT_COMMANDS:="${CONFIG_DIR}/post-boot-commands.asadmin"} +export POSTBOOT_COMMANDS # Execute any scripts BEFORE the appserver starts for f in "${SCRIPT_DIR}"/init_* "${SCRIPT_DIR}"/init.d/*; do From d77cf4a9b2d6d07b66414704c8d389ed3fd40257 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 9 Feb 2024 18:44:49 +0100 Subject: [PATCH 303/689] style(ct): fix typos in base image Dockerfile --- modules/container-base/src/main/docker/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/container-base/src/main/docker/Dockerfile b/modules/container-base/src/main/docker/Dockerfile index 663b3d9dd51..5fbbdd0c1e5 100644 --- a/modules/container-base/src/main/docker/Dockerfile +++ b/modules/container-base/src/main/docker/Dockerfile @@ -155,7 +155,7 @@ RUN < Date: Fri, 9 Feb 2024 18:45:35 +0100 Subject: [PATCH 304/689] fix(ct): make DV preboot file end up in config dir The location where to create the temporary file was wrong, fixed now. --- .../src/main/docker/scripts/init_1_generate_devmode_commands.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/container-base/src/main/docker/scripts/init_1_generate_devmode_commands.sh b/modules/container-base/src/main/docker/scripts/init_1_generate_devmode_commands.sh index bb0984332f7..28e7fd68b97 100644 --- a/modules/container-base/src/main/docker/scripts/init_1_generate_devmode_commands.sh +++ b/modules/container-base/src/main/docker/scripts/init_1_generate_devmode_commands.sh @@ -16,7 +16,7 @@ ENABLE_JMX=${ENABLE_JMX:-0} ENABLE_JDWP=${ENABLE_JDWP:-0} ENABLE_RELOAD=${ENABLE_RELOAD:-0} -DV_PREBOOT=${PAYARA_DIR}/dataverse_preboot +DV_PREBOOT=${CONFIG_DIR}/dataverse_preboot echo "# Dataverse preboot configuration for Payara" > "${DV_PREBOOT}" # 1. Configure JMX (enabled by default on port 8686, but requires SSL) From 8547dbf4222597dc48c83f45104bb9165dcce843 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 9 Feb 2024 18:51:48 +0100 Subject: [PATCH 305/689] feat(k8s): initial commit of new module --- modules/container-k8s/pom.xml | 59 +++++++++++++++ .../src/main/jkube/dataverse-datasets-pvc.yml | 6 ++ .../src/main/jkube/dataverse-deployment.yaml | 72 +++++++++++++++++++ .../src/main/jkube/dataverse-docroot-pvc.yml | 6 ++ .../src/main/jkube/dataverse-storage-pvc.yml | 6 ++ .../src/main/jkube/dataverse-uploads-pvc.yaml | 6 ++ .../main/jkube/deps/postgres-deployment.yml | 31 ++++++++ .../src/main/jkube/deps/postgres-pvc.yml | 6 ++ .../src/main/jkube/deps/postgres-svc.yml | 5 ++ .../container-k8s/src/main/jkube/profiles.yml | 12 ++++ 10 files changed, 209 insertions(+) create mode 100644 modules/container-k8s/pom.xml create mode 100644 modules/container-k8s/src/main/jkube/dataverse-datasets-pvc.yml create mode 100644 modules/container-k8s/src/main/jkube/dataverse-deployment.yaml create mode 100644 modules/container-k8s/src/main/jkube/dataverse-docroot-pvc.yml create mode 100644 modules/container-k8s/src/main/jkube/dataverse-storage-pvc.yml create mode 100644 modules/container-k8s/src/main/jkube/dataverse-uploads-pvc.yaml create mode 100644 modules/container-k8s/src/main/jkube/deps/postgres-deployment.yml create mode 100644 modules/container-k8s/src/main/jkube/deps/postgres-pvc.yml create mode 100644 modules/container-k8s/src/main/jkube/deps/postgres-svc.yml create mode 100644 modules/container-k8s/src/main/jkube/profiles.yml diff --git a/modules/container-k8s/pom.xml b/modules/container-k8s/pom.xml new file mode 100644 index 00000000000..470abb753ae --- /dev/null +++ b/modules/container-k8s/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + + edu.harvard.iq + dataverse-parent + ${revision} + ../dataverse-parent + + + io.gdcc + container-k8s + ${packaging.type} + Container Kubernetes Materials + This module provides resources to run Dataverse on OpenShift or plain Kubernetes + + + + poikilotherm + Oliver Bertuch + github@bertuch.eu + Europe/Berlin + + maintainer + + + + + + + + pom + + + + + ct + + true + dataverse-k8s + + + + + + org.eclipse.jkube + kubernetes-maven-plugin + 1.16.0 + + + + + + + + + \ No newline at end of file diff --git a/modules/container-k8s/src/main/jkube/dataverse-datasets-pvc.yml b/modules/container-k8s/src/main/jkube/dataverse-datasets-pvc.yml new file mode 100644 index 00000000000..50e9ccb3c92 --- /dev/null +++ b/modules/container-k8s/src/main/jkube/dataverse-datasets-pvc.yml @@ -0,0 +1,6 @@ +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Mi diff --git a/modules/container-k8s/src/main/jkube/dataverse-deployment.yaml b/modules/container-k8s/src/main/jkube/dataverse-deployment.yaml new file mode 100644 index 00000000000..5d1ed67f635 --- /dev/null +++ b/modules/container-k8s/src/main/jkube/dataverse-deployment.yaml @@ -0,0 +1,72 @@ +spec: + replicas: 1 + template: + spec: + containers: + - name: dataverse + image: ghcr.io/gdcc/dataverse:openshift-poc + imagePullPolicy: Always + resources: + requests: + memory: "1Gi" + limits: + memory: "2Gi" + ports: + - containerPort: 8080 + readinessProbe: + httpGet: + path: /api/info/version + port: 8080 + #args: + # - bash + # - -c + # - "ls -laZ /opt/payara/config; touch /opt/payara/config/test" + env: + - name: DATAVERSE_DB_HOST + value: postgres + - name: DATAVERSE_DB_USER + value: dataverse + - name: DATAVERSE_DB_PASSWORD + value: supersecret + volumeMounts: + - name: storage + mountPath: /dv + - name: datasets + mountPath: /dv/store + - name: docroot + mountPath: /dv/docroot + - name: uploads + mountPath: /dv/uploads + - name: config + mountPath: /opt/payara/config + - name: dvtemp + mountPath: /dv/temp + - name: tmp + mountPath: /tmp + - name: heapdumps + mountPath: /dumps + - name: bootstrap + image: ghcr.io/gdcc/configbaker:openshift-poc + restartPolicy: Never + args: ["bootstrap.sh", "-u", "http://localhost:8080", "-t", "3m", "dev"] + volumes: + - name: storage + persistentVolumeClaim: + claimName: dataverse-storage + - name: datasets + persistentVolumeClaim: + claimName: dataverse-datasets + - name: docroot + persistentVolumeClaim: + claimName: dataverse-docroot + - name: uploads + persistentVolumeClaim: + claimName: dataverse-uploads + - name: config + emptyDir: {} + - name: dvtemp + emptyDir: {} + - name: tmp + emptyDir: {} + - name: heapdumps + emptyDir: {} \ No newline at end of file diff --git a/modules/container-k8s/src/main/jkube/dataverse-docroot-pvc.yml b/modules/container-k8s/src/main/jkube/dataverse-docroot-pvc.yml new file mode 100644 index 00000000000..50e9ccb3c92 --- /dev/null +++ b/modules/container-k8s/src/main/jkube/dataverse-docroot-pvc.yml @@ -0,0 +1,6 @@ +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Mi diff --git a/modules/container-k8s/src/main/jkube/dataverse-storage-pvc.yml b/modules/container-k8s/src/main/jkube/dataverse-storage-pvc.yml new file mode 100644 index 00000000000..50e9ccb3c92 --- /dev/null +++ b/modules/container-k8s/src/main/jkube/dataverse-storage-pvc.yml @@ -0,0 +1,6 @@ +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Mi diff --git a/modules/container-k8s/src/main/jkube/dataverse-uploads-pvc.yaml b/modules/container-k8s/src/main/jkube/dataverse-uploads-pvc.yaml new file mode 100644 index 00000000000..50e9ccb3c92 --- /dev/null +++ b/modules/container-k8s/src/main/jkube/dataverse-uploads-pvc.yaml @@ -0,0 +1,6 @@ +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Mi diff --git a/modules/container-k8s/src/main/jkube/deps/postgres-deployment.yml b/modules/container-k8s/src/main/jkube/deps/postgres-deployment.yml new file mode 100644 index 00000000000..c5290982642 --- /dev/null +++ b/modules/container-k8s/src/main/jkube/deps/postgres-deployment.yml @@ -0,0 +1,31 @@ +spec: + replicas: 1 + strategy: + type: Recreate + template: + spec: + containers: + - name: postgres + image: postgres:13 + ports: + - containerPort: 5432 + env: + - name: POSTGRES_USER + value: dataverse + - name: POSTGRES_PASSWORD + value: supersecret + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + volumeMounts: + - name: postgresql-persistent-storage + mountPath: /var/lib/postgresql/data + readinessProbe: + exec: + command: ["pg_isready"] + initialDelaySeconds: 5 + failureThreshold: 100 + periodSeconds: 5 + volumes: + - name: postgresql-persistent-storage + persistentVolumeClaim: + claimName: postgres \ No newline at end of file diff --git a/modules/container-k8s/src/main/jkube/deps/postgres-pvc.yml b/modules/container-k8s/src/main/jkube/deps/postgres-pvc.yml new file mode 100644 index 00000000000..9cefb651bd4 --- /dev/null +++ b/modules/container-k8s/src/main/jkube/deps/postgres-pvc.yml @@ -0,0 +1,6 @@ +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 300Mi diff --git a/modules/container-k8s/src/main/jkube/deps/postgres-svc.yml b/modules/container-k8s/src/main/jkube/deps/postgres-svc.yml new file mode 100644 index 00000000000..fc75438b31c --- /dev/null +++ b/modules/container-k8s/src/main/jkube/deps/postgres-svc.yml @@ -0,0 +1,5 @@ +spec: + ports: + - port: 5432 + targetPort: 5432 + protocol: TCP \ No newline at end of file diff --git a/modules/container-k8s/src/main/jkube/profiles.yml b/modules/container-k8s/src/main/jkube/profiles.yml new file mode 100644 index 00000000000..8443a9cf54c --- /dev/null +++ b/modules/container-k8s/src/main/jkube/profiles.yml @@ -0,0 +1,12 @@ +- name: deps + extends: default +- name: default + enricher: + excludes: + - jkube-volume-permission + - jkube-project-label +- name: security-hardening + enricher: + excludes: + - jkube-volume-permission + - jkube-project-label From b3321d4ad6760155672f67a95a4d9463eb5f3b1f Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Fri, 9 Feb 2024 13:29:41 -0500 Subject: [PATCH 306/689] Add content to deaccession info message --- src/main/java/propertyFiles/Bundle.properties | 7 ++++--- src/main/webapp/dataset.xhtml | 7 ++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 157f2ecaf54..34e16e36eac 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -2002,7 +2002,8 @@ file.deleteFileDialog.immediate=The file will be deleted after you click on the file.deleteFileDialog.multiple.immediate=The file(s) will be deleted after you click on the Delete button. file.deleteFileDialog.header=Delete Files file.deleteFileDialog.failed.tip=Files will not be removed from previously published versions of the dataset. -file.deaccessionDialog.tip=Once you deaccession this dataset it will no longer be viewable by the public. +file.deaccessionDialog.tip.permanent=Deaccession is permanent. +file.deaccessionDialog.tip=This dataset will no longer be public and a tumbstone will display the reason for deaccessioning.
Please read the documentation if you have any questions. file.deaccessionDialog.version=Version file.deaccessionDialog.reason.question1=Which version(s) do you want to deaccession? file.deaccessionDialog.reason.question2=What is the reason for deaccession? @@ -2016,8 +2017,8 @@ file.deaccessionDialog.reason.selectItem.other=Other (Please type reason in spac file.deaccessionDialog.enterInfo=Please enter additional information about the reason for deaccession. file.deaccessionDialog.leaveURL=If applicable, please leave a URL where this dataset can be accessed after deaccessioning. file.deaccessionDialog.leaveURL.watermark=Optional dataset site, http://... -file.deaccessionDialog.deaccession.tip=Are you sure you want to deaccession? The selected version(s) will no longer be viewable by the public. -file.deaccessionDialog.deaccessionDataset.tip=Are you sure you want to deaccession this dataset? It will no longer be viewable by the public. +file.deaccessionDialog.deaccession.tip=Are you sure you want to deaccession? This is permanent and the selected version(s) will no longer be viewable by the public. +file.deaccessionDialog.deaccessionDataset.tip=Are you sure you want to deaccession this dataset? This is permanent an it will no longer be viewable by the public. file.deaccessionDialog.dialog.selectVersion.error=Please select version(s) for deaccessioning. file.deaccessionDialog.dialog.reason.error=Please select reason for deaccessioning. file.deaccessionDialog.dialog.url.error=Please enter valid forwarding URL. diff --git a/src/main/webapp/dataset.xhtml b/src/main/webapp/dataset.xhtml index e50e68ec162..2afae295082 100644 --- a/src/main/webapp/dataset.xhtml +++ b/src/main/webapp/dataset.xhtml @@ -1221,7 +1221,12 @@ -

#{bundle['file.deaccessionDialog.tip']}

+
+   +


+ +

+
Date: Fri, 9 Feb 2024 17:12:24 -0500 Subject: [PATCH 307/689] #10286 add owner array to file api --- .../edu/harvard/iq/dataverse/api/Files.java | 17 ++++++++++------- .../iq/dataverse/util/json/JsonPrinter.java | 17 ++++++++++++++--- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index 5d400ee1438..155d8953d15 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -504,18 +504,21 @@ public Response updateFileMetadata(@Context ContainerRequestContext crc, @FormDa @GET @AuthRequired @Path("{id}/draft") - public Response getFileDataDraft(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) throws WrappedResponse, Exception { - return getFileDataResponse(getRequestUser(crc), fileIdOrPersistentId, uriInfo, headers, response, true); + public Response getFileDataDraft(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response, @QueryParam("returnOwners") Boolean returnOwners) throws WrappedResponse, Exception { + Boolean includeOwners = returnOwners == null ? false : returnOwners; + return getFileDataResponse(getRequestUser(crc), fileIdOrPersistentId, uriInfo, headers, response, true, includeOwners); } @GET @AuthRequired @Path("{id}") - public Response getFileData(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) throws WrappedResponse, Exception { - return getFileDataResponse(getRequestUser(crc), fileIdOrPersistentId, uriInfo, headers, response, false); + public Response getFileData(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response, @QueryParam("returnOwners") Boolean returnOwners) throws WrappedResponse, Exception { + Boolean includeOwners = returnOwners == null ? false : returnOwners; + System.out.print("includeOwners: " + includeOwners); + return getFileDataResponse(getRequestUser(crc), fileIdOrPersistentId, uriInfo, headers, response, false, includeOwners); } - private Response getFileDataResponse(User user, String fileIdOrPersistentId, UriInfo uriInfo, HttpHeaders headers, HttpServletResponse response, boolean draft ){ + private Response getFileDataResponse(User user, String fileIdOrPersistentId, UriInfo uriInfo, HttpHeaders headers, HttpServletResponse response, boolean draft, boolean includeOwners ){ DataverseRequest req; try { @@ -565,10 +568,10 @@ private Response getFileDataResponse(User user, String fileIdOrPersistentId, Uri MakeDataCountLoggingServiceBean.MakeDataCountEntry entry = new MakeDataCountLoggingServiceBean.MakeDataCountEntry(uriInfo, headers, dvRequestService, df); mdcLogService.logEntry(entry); } - + return Response.ok(Json.createObjectBuilder() .add("status", ApiConstants.STATUS_OK) - .add("data", json(fm)).build()) + .add("data", json(fm, includeOwners)).build()) .type(MediaType.APPLICATION_JSON) .build(); } 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 7d9bede9a61..d88015145b3 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 @@ -639,8 +639,12 @@ public static JsonObjectBuilder json(DatasetFieldType fld) { return fieldsBld; } + + public static JsonObjectBuilder json(FileMetadata fmd){ + return json(fmd, false); + } - public static JsonObjectBuilder json(FileMetadata fmd) { + public static JsonObjectBuilder json(FileMetadata fmd, Boolean includeOwners) { return jsonObjectBuilder() // deprecated: .add("category", fmd.getCategory()) // TODO: uh, figure out what to do here... it's deprecated @@ -655,7 +659,7 @@ public static JsonObjectBuilder json(FileMetadata fmd) { .add("version", fmd.getVersion()) .add("datasetVersionId", fmd.getDatasetVersion().getId()) .add("categories", getFileCategories(fmd)) - .add("dataFile", JsonPrinter.json(fmd.getDataFile(), fmd, false)); + .add("dataFile", JsonPrinter.json(fmd.getDataFile(), fmd, false, includeOwners)); } public static JsonObjectBuilder json(AuxiliaryFile auxFile) { @@ -674,7 +678,11 @@ public static JsonObjectBuilder json(DataFile df) { return JsonPrinter.json(df, null, false); } - public static JsonObjectBuilder json(DataFile df, FileMetadata fileMetadata, boolean forExportDataProvider) { + public static JsonObjectBuilder json(DataFile df, FileMetadata fileMetadata, boolean forExportDataProvider){ + return json(df, fileMetadata, forExportDataProvider, false); + } + + public static JsonObjectBuilder json(DataFile df, FileMetadata fileMetadata, boolean forExportDataProvider, Boolean includeOwners) { // File names are no longer stored in the DataFile entity; // (they are instead in the FileMetadata (as "labels") - this way // the filename can change between versions... @@ -750,6 +758,9 @@ public static JsonObjectBuilder json(DataFile df, FileMetadata fileMetadata, boo ? JsonPrinter.jsonVarGroup(fileMetadata.getVarGroups()) : null); } + if (includeOwners){ + builder.add("ownerArray", getOwnersFromDvObject(df)); + } return builder; } From 43f61e6334c087648476737dfc7d32ff5bc16d1f Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Mon, 12 Feb 2024 13:29:34 -0500 Subject: [PATCH 308/689] #10286 add owner array to view dv --- .../java/edu/harvard/iq/dataverse/api/Dataverses.java | 6 ++++-- .../edu/harvard/iq/dataverse/util/json/JsonPrinter.java | 8 +++++--- 2 files changed, 9 insertions(+), 5 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 6c1bf42c02a..66aec38adfa 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -610,10 +610,12 @@ private Dataset parseDataset(String datasetJson) throws WrappedResponse { @GET @AuthRequired @Path("{identifier}") - public Response viewDataverse(@Context ContainerRequestContext crc, @PathParam("identifier") String idtf) { + public Response viewDataverse(@Context ContainerRequestContext crc, @PathParam("identifier") String idtf, @QueryParam("returnOwners") Boolean returnOwners) { + Boolean includeOwners = returnOwners == null ? false : returnOwners; return response(req -> ok( json(execCommand(new GetDataverseCommand(req, findDataverseOrDie(idtf))), - settingsService.isTrueForKey(SettingsServiceBean.Key.ExcludeEmailFromExport, false) + settingsService.isTrueForKey(SettingsServiceBean.Key.ExcludeEmailFromExport, false), + includeOwners )), getRequestUser(crc)); } 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 d88015145b3..6f750eaddac 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 @@ -258,11 +258,11 @@ public static JsonObjectBuilder json(Workflow wf){ } public static JsonObjectBuilder json(Dataverse dv) { - return json(dv, false); + return json(dv, false, false); } //TODO: Once we upgrade to Java EE 8 we can remove objects from the builder, and this email removal can be done in a better place. - public static JsonObjectBuilder json(Dataverse dv, Boolean hideEmail) { + public static JsonObjectBuilder json(Dataverse dv, Boolean hideEmail, Boolean includeOwners) { JsonObjectBuilder bld = jsonObjectBuilder() .add("id", dv.getId()) .add("alias", dv.getAlias()) @@ -271,7 +271,9 @@ public static JsonObjectBuilder json(Dataverse dv, Boolean hideEmail) { if(!hideEmail) { bld.add("dataverseContacts", JsonPrinter.json(dv.getDataverseContacts())); } - + if (includeOwners){ + bld.add("ownerArray", getOwnersFromDvObject(dv)); + } bld.add("permissionRoot", dv.isPermissionRoot()) .add("description", dv.getDescription()) .add("dataverseType", dv.getDataverseType().name()); From 1898c148512aec9943971f546c9dc7e53b537147 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Mon, 12 Feb 2024 15:39:10 -0500 Subject: [PATCH 309/689] #10286 add test for get ds api --- .../harvard/iq/dataverse/api/DatasetsIT.java | 28 +++++++++++++++++++ .../edu/harvard/iq/dataverse/api/UtilIT.java | 9 ++++++ 2 files changed, 37 insertions(+) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index e1c4b901116..3703a0d39c3 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -1887,6 +1887,34 @@ public void testDeleteDatasetWhileFileIngesting() { .statusCode(FORBIDDEN.getStatusCode()); } + + @Test + public void testGetIncludeOwnerArray() { + + Response createUser = UtilIT.createRandomUser(); + createUser.then().assertThat() + .statusCode(OK.getStatusCode()); + String username = UtilIT.getUsernameFromResponse(createUser); + String apiToken = UtilIT.getApiTokenFromResponse(createUser); + + Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken); + createDataverseResponse.prettyPrint(); + createDataverseResponse.then().assertThat() + .statusCode(CREATED.getStatusCode()); + String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + + Response createDatasetResponse = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, apiToken); + createDatasetResponse.prettyPrint(); + createDatasetResponse.then().assertThat() + .statusCode(CREATED.getStatusCode()); + Integer datasetId = JsonPath.from(createDatasetResponse.body().asString()).getInt("data.id"); + String persistentId = JsonPath.from(createDatasetResponse.body().asString()).getString("data.persistentId"); + logger.info("Dataset created with id " + datasetId + " and persistent id " + persistentId); + + Response getDatasetWithOwners = UtilIT.getDatasetWithOwners(persistentId, apiToken, true); + getDatasetWithOwners.prettyPrint(); + getDatasetWithOwners.then().assertThat().body("data.ownerArray[0].identifier", equalTo(dataverseAlias)); + } /** * In order for this test to pass you must have the Data Capture Module ( 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 ec41248a65f..0598bb80ea6 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -1476,6 +1476,15 @@ static Response getDatasetVersion(String persistentId, String versionNumber, Str + persistentId + (excludeFiles ? "&excludeFiles=true" : "")); } + + static Response getDatasetWithOwners(String persistentId, String apiToken, boolean returnOwners) { + return given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .get("/api/datasets/:persistentId/" + + "?persistentId=" + + persistentId + + (returnOwners ? "&returnOwners=true" : "")); + } static Response getMetadataBlockFromDatasetVersion(String persistentId, String versionNumber, String metadataBlock, String apiToken) { return given() From f612f7a0e3dce2e7e4cb0a5664986adcd2868667 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 13 Feb 2024 13:25:04 -0500 Subject: [PATCH 310/689] change flyway numbering --- ...straints.sql => V6.1.0.3__9983-missing-unique-constraints.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V6.1.0.4__9983-missing-unique-constraints.sql => V6.1.0.3__9983-missing-unique-constraints.sql} (100%) diff --git a/src/main/resources/db/migration/V6.1.0.4__9983-missing-unique-constraints.sql b/src/main/resources/db/migration/V6.1.0.3__9983-missing-unique-constraints.sql similarity index 100% rename from src/main/resources/db/migration/V6.1.0.4__9983-missing-unique-constraints.sql rename to src/main/resources/db/migration/V6.1.0.3__9983-missing-unique-constraints.sql From f3fae4bf754572986fb815324a3a69a2632a87cc Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 13 Feb 2024 14:33:58 -0500 Subject: [PATCH 311/689] add clarity --- doc/release-notes/9983-unique-constraints.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/9983-unique-constraints.md b/doc/release-notes/9983-unique-constraints.md index 1e37d75d88d..d889beb0718 100644 --- a/doc/release-notes/9983-unique-constraints.md +++ b/doc/release-notes/9983-unique-constraints.md @@ -11,4 +11,4 @@ and then removing any duplicate rows (where count>1). -TODO: Add note about reloading metadata blocks after upgrade. \ No newline at end of file +TODO: Whoever puts the release notes together should make sure there is the standard note about reloading metadata blocks for the citation, astrophysics, and biomedical blocks (plus any others from other PRs) after upgrading. \ No newline at end of file From 240e851c7ba43b5037097aa2f0e0e827f2564f12 Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Tue, 13 Feb 2024 16:38:03 -0500 Subject: [PATCH 312/689] Ingest/Uningest from file page --- .../edu/harvard/iq/dataverse/FilePage.java | 112 ++++++++++++++++++ .../webapp/file-edit-button-fragment.xhtml | 16 +++ 2 files changed, 128 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/FilePage.java b/src/main/java/edu/harvard/iq/dataverse/FilePage.java index 479c8a429c6..b6706acd4ff 100644 --- a/src/main/java/edu/harvard/iq/dataverse/FilePage.java +++ b/src/main/java/edu/harvard/iq/dataverse/FilePage.java @@ -475,6 +475,112 @@ public String restrictFile(boolean restricted) throws CommandException{ return returnToDraftVersion(); } + public String ingestFile() throws CommandException{ + + User u = session.getUser(); + if(!u.isAuthenticated() || !(permissionService.permissionsFor(u, file).contains(Permission.PublishDataset))) { + //Shouldn't happen (choice not displayed for users who don't have the right permission), but check anyway + logger.warning("User: " + u.getIdentifier() + " tried to ingest a file"); + JH.addMessage(FacesMessage.SEVERITY_WARN, BundleUtil.getStringFromBundle("file.ingest.cantIngestFileWarning")); + return null; + } + + DataFile dataFile = fileMetadata.getDataFile(); + editDataset = dataFile.getOwner(); + + if (dataFile.isTabularData()) { + JH.addMessage(FacesMessage.SEVERITY_WARN, BundleUtil.getStringFromBundle("file.ingest.alreadyIngestedWarning")); + return null; + } + + boolean ingestLock = dataset.isLockedFor(DatasetLock.Reason.Ingest); + + if (ingestLock) { + JH.addMessage(FacesMessage.SEVERITY_WARN, BundleUtil.getStringFromBundle("file.ingest.ingestInProgressWarning")); + return null; + } + + if (!FileUtil.canIngestAsTabular(dataFile)) { + JH.addMessage(FacesMessage.SEVERITY_WARN, BundleUtil.getStringFromBundle("file.ingest.cantIngestFileWarning")); + return null; + + } + + dataFile.SetIngestScheduled(); + + if (dataFile.getIngestRequest() == null) { + dataFile.setIngestRequest(new IngestRequest(dataFile)); + } + + dataFile.getIngestRequest().setForceTypeCheck(true); + + // update the datafile, to save the newIngest request in the database: + save(); + + // queue the data ingest job for asynchronous execution: + String status = ingestService.startIngestJobs(editDataset.getId(), new ArrayList<>(Arrays.asList(dataFile)), (AuthenticatedUser) session.getUser()); + + if (!StringUtil.isEmpty(status)) { + // This most likely indicates some sort of a problem (for example, + // the ingest job was not put on the JMS queue because of the size + // of the file). But we are still returning the OK status - because + // from the point of view of the API, it's a success - we have + // successfully gone through the process of trying to schedule the + // ingest job... + + logger.warning("Ingest Status for file: " + dataFile.getId() + " : " + status); + } + logger.info("File: " + dataFile.getId() + " ingest queued"); + + init(); + JsfHelper.addInfoMessage(BundleUtil.getStringFromBundle("file.ingest.ingestQueued")); + return returnToDraftVersion(); + } + + public String uningestFile() throws CommandException { + + if (!file.isTabularData()) { + if(file.isIngestProblem()) { + User u = session.getUser(); + if(!u.isAuthenticated() || !(permissionService.permissionsFor(u, file).contains(Permission.PublishDataset))) { + logger.warning("User: " + u.getIdentifier() + " tried to uningest a file"); + //Shouldn't happen (choice not displayed for users who don't have the right permission), but check anyway + JH.addMessage(FacesMessage.SEVERITY_WARN, BundleUtil.getStringFromBundle("file.ingest.cantUningestFileWarning")); + return null; + } + file.setIngestDone(); + file.setIngestReport(null); + } else { + JH.addMessage(FacesMessage.SEVERITY_WARN, BundleUtil.getStringFromBundle("file.ingest.cantUningestFileWarning")); + return null; + } + } else { + commandEngine.submit(new UningestFileCommand(dvRequestService.getDataverseRequest(), file)); + Long dataFileId = file.getId(); + file = datafileService.find(dataFileId); + } + editDataset = file.getOwner(); + if (editDataset.isReleased()) { + try { + ExportService instance = ExportService.getInstance(); + instance.exportAllFormats(editDataset); + + } catch (ExportException ex) { + // Something went wrong! + // Just like with indexing, a failure to export is not a fatal + // condition. We'll just log the error as a warning and keep + // going: + logger.log(Level.WARNING, "Uningest: Exception while exporting:{0}", ex.getMessage()); + } + } + save(); + //Refresh filemetadata with file title, etc. + init(); + JH.addMessage(FacesMessage.SEVERITY_INFO, BundleUtil.getStringFromBundle("file.uningest.complete")); + return returnToDraftVersion(); + } + + private List filesToBeDeleted = new ArrayList<>(); public String deleteFile() { @@ -948,6 +1054,12 @@ public boolean isPubliclyDownloadable() { return FileUtil.isPubliclyDownloadable(fileMetadata); } + public boolean isIngestable() { + DataFile f = fileMetadata.getDataFile(); + //Datafile is an ingestable type and hasn't been ingested yet or had an ingest fail + return (FileUtil.canIngestAsTabular(f)&&!(f.isTabularData() || f.isIngestProblem())); + } + private Boolean lockedFromEditsVar; private Boolean lockedFromDownloadVar; diff --git a/src/main/webapp/file-edit-button-fragment.xhtml b/src/main/webapp/file-edit-button-fragment.xhtml index 4dac1613266..e08de716cda 100644 --- a/src/main/webapp/file-edit-button-fragment.xhtml +++ b/src/main/webapp/file-edit-button-fragment.xhtml @@ -77,6 +77,22 @@ + + + +
  • + + + +
  • +
    + +
  • + + + +
  • +
    From fcdc24611d26889ba32fda351490c0ae657aef7e Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 13 Feb 2024 17:00:07 -0500 Subject: [PATCH 313/689] missing imports/@EJB --- src/main/java/edu/harvard/iq/dataverse/FilePage.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/FilePage.java b/src/main/java/edu/harvard/iq/dataverse/FilePage.java index b6706acd4ff..4e5843964e7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/FilePage.java +++ b/src/main/java/edu/harvard/iq/dataverse/FilePage.java @@ -21,6 +21,7 @@ import edu.harvard.iq.dataverse.engine.command.impl.CreateNewDatasetCommand; import edu.harvard.iq.dataverse.engine.command.impl.PersistProvFreeFormCommand; import edu.harvard.iq.dataverse.engine.command.impl.RestrictFileCommand; +import edu.harvard.iq.dataverse.engine.command.impl.UningestFileCommand; import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand; import edu.harvard.iq.dataverse.export.ExportService; import io.gdcc.spi.export.ExportException; @@ -28,6 +29,8 @@ import edu.harvard.iq.dataverse.externaltools.ExternalTool; import edu.harvard.iq.dataverse.externaltools.ExternalToolHandler; import edu.harvard.iq.dataverse.externaltools.ExternalToolServiceBean; +import edu.harvard.iq.dataverse.ingest.IngestRequest; +import edu.harvard.iq.dataverse.ingest.IngestServiceBean; import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean; import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean.MakeDataCountEntry; import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean; @@ -35,6 +38,8 @@ import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.JsfHelper; +import edu.harvard.iq.dataverse.util.StringUtil; + import static edu.harvard.iq.dataverse.util.JsfHelper.JH; import edu.harvard.iq.dataverse.util.SystemConfig; @@ -45,6 +50,7 @@ import java.util.Comparator; import java.util.List; import java.util.Set; +import java.util.logging.Level; import java.util.logging.Logger; import jakarta.ejb.EJB; import jakarta.ejb.EJBException; @@ -112,10 +118,10 @@ public class FilePage implements java.io.Serializable { GuestbookResponseServiceBean guestbookResponseService; @EJB AuthenticationServiceBean authService; - @EJB DatasetServiceBean datasetService; - + @EJB + IngestServiceBean ingestService; @EJB SystemConfig systemConfig; @@ -209,7 +215,7 @@ public String init() { // If this DatasetVersion is unpublished and permission is doesn't have permissions: // > Go to the Login page // - // Check permisisons + // Check permissions Boolean authorized = (fileMetadata.getDatasetVersion().isReleased()) || (!fileMetadata.getDatasetVersion().isReleased() && this.canViewUnpublishedDataset()); From 15ae19e36250e3a467452cfd41287df1cfe8bd3a Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 13 Feb 2024 17:00:28 -0500 Subject: [PATCH 314/689] Change command to publish perm --- .../dataverse/engine/command/impl/UningestFileCommand.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UningestFileCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UningestFileCommand.java index 3e85630dd59..e9791809cb2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UningestFileCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UningestFileCommand.java @@ -33,7 +33,7 @@ * @author skraffmi * @author Leonid Andreev */ -@RequiredPermissions({}) +@RequiredPermissions(Permission.PublishDataset) public class UningestFileCommand extends AbstractVoidCommand { private static final Logger logger = Logger.getLogger(UningestFileCommand.class.getCanonicalName()); @@ -48,8 +48,8 @@ public UningestFileCommand(DataverseRequest aRequest, DataFile uningest) { protected void executeImpl(CommandContext ctxt) throws CommandException { // first check if user is a superuser - if ( (!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser() ) ) { - throw new PermissionException("Uningest File can only be called by Superusers.", + if (!(getUser() instanceof AuthenticatedUser)) { + throw new PermissionException("Uningest File can only be called by User with the PublishDataset permission.", this, Collections.singleton(Permission.EditDataset), uningest); } From 262fb267a2025872d8f537e937ad31dc0a25a156 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 13 Feb 2024 17:49:09 -0500 Subject: [PATCH 315/689] superuser only in command, add docs --- .../user/tabulardataingest/ingestprocess.rst | 20 ++++++++++++++++++- .../command/impl/UningestFileCommand.java | 10 +++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/doc/sphinx-guides/source/user/tabulardataingest/ingestprocess.rst b/doc/sphinx-guides/source/user/tabulardataingest/ingestprocess.rst index 33ae9b555e6..9e82ff12b9b 100644 --- a/doc/sphinx-guides/source/user/tabulardataingest/ingestprocess.rst +++ b/doc/sphinx-guides/source/user/tabulardataingest/ingestprocess.rst @@ -32,7 +32,7 @@ format. (more info below) Tabular Data and Metadata -========================== +========================= Data vs. Metadata ----------------- @@ -56,3 +56,21 @@ the Dataverse Software was originally based on the `DDI Codebook `_ format. You can see an example of DDI output under the :ref:`data-variable-metadata-access` section of the :doc:`/api/dataaccess` section of the API Guide. + +Uningest and Reingest +===================== + +Ingest will only work for files whose content can be interpreted as a table. +Multi-sheets spreadsheets and CSV files with different number of entries per row are two examples where ingest will fail. +This is non-fatal. The Dataverse software will not produce a .tab version of the file and will show a warning to users +who can see the draft version of the dataset containing the file that will indicate why ingest failed. When the file is published as +part of the dataset, there will be no indication that ingest was attempted and failed. + +If the warning message is a concern, the Dataverse software includes both an API call (see the Files section of the :doc:`/api/native-api` guide) +and an Edit/Uningest menu option displayed on the file page, that allow a file to be Uningested. These are only available to superusers. +Uningest will remove the warning. Uningest can also be done for a file that was successfully ingested. +This will remove the .tab version of the file that was generated. + +If a file is a tabular format but was never ingested, .e.g. due to the ingest file size limit being lower in the past, or if ingest had failed, +e.g. in a prior Dataverse version, an reingest API (see the Files section of the :doc:`/api/native-api` guide) and a file page Edit/Reingest option +in the user interface allow ingest to be tried again. As with Uningest, this fucntionality is only available to superusers. diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UningestFileCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UningestFileCommand.java index e9791809cb2..ba04c4d7931 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UningestFileCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/UningestFileCommand.java @@ -33,7 +33,7 @@ * @author skraffmi * @author Leonid Andreev */ -@RequiredPermissions(Permission.PublishDataset) +@RequiredPermissions({}) public class UningestFileCommand extends AbstractVoidCommand { private static final Logger logger = Logger.getLogger(UningestFileCommand.class.getCanonicalName()); @@ -47,10 +47,10 @@ public UningestFileCommand(DataverseRequest aRequest, DataFile uningest) { @Override protected void executeImpl(CommandContext ctxt) throws CommandException { - // first check if user is a superuser - if (!(getUser() instanceof AuthenticatedUser)) { - throw new PermissionException("Uningest File can only be called by User with the PublishDataset permission.", - this, Collections.singleton(Permission.EditDataset), uningest); + // first check if user is a superuser + if ((!(getUser() instanceof AuthenticatedUser) || !getUser().isSuperuser())) { + throw new PermissionException("Uningest File can only be called by Superusers.", this, + Collections.singleton(Permission.EditDataset), uningest); } // is this actually a tabular data file? From 130cfba92e9f3ced2e9497ba74b2f17b20bfec77 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 13 Feb 2024 17:51:37 -0500 Subject: [PATCH 316/689] release note --- doc/release-notes/10318-uningest-and-reingest.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 doc/release-notes/10318-uningest-and-reingest.md diff --git a/doc/release-notes/10318-uningest-and-reingest.md b/doc/release-notes/10318-uningest-and-reingest.md new file mode 100644 index 00000000000..7465f934330 --- /dev/null +++ b/doc/release-notes/10318-uningest-and-reingest.md @@ -0,0 +1,2 @@ +New Uningest/Reingest options are available in the File Page Edit menu for superusers, allowing ingest errors to be cleared and for +ingest to be retried (e.g. after a Dataverse version update or if ingest size limits are changed). From 67d004fb5719f94389610227e070494f0d652ecd Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 13 Feb 2024 22:16:33 -0500 Subject: [PATCH 317/689] add redeploy tab for Netbeans #9590 --- .../source/container/dev-usage.rst | 58 ++++++++++++++++-- .../source/container/img/netbeans-compile.png | Bin 0 -> 99396 bytes .../source/container/img/netbeans-run.png | Bin 0 -> 124521 bytes .../container/img/netbeans-servers-common.png | Bin 0 -> 89185 bytes .../container/img/netbeans-servers-java.png | Bin 0 -> 68487 bytes .../source/developers/classic-dev-env.rst | 2 + 6 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 doc/sphinx-guides/source/container/img/netbeans-compile.png create mode 100644 doc/sphinx-guides/source/container/img/netbeans-run.png create mode 100644 doc/sphinx-guides/source/container/img/netbeans-servers-common.png create mode 100644 doc/sphinx-guides/source/container/img/netbeans-servers-java.png diff --git a/doc/sphinx-guides/source/container/dev-usage.rst b/doc/sphinx-guides/source/container/dev-usage.rst index 9fc9058eada..85b1b3e5f05 100644 --- a/doc/sphinx-guides/source/container/dev-usage.rst +++ b/doc/sphinx-guides/source/container/dev-usage.rst @@ -154,24 +154,29 @@ IDE-triggered re-deployments You have at least two options: -1. Use plugins for different IDEs by Payara to ease the burden of redeploying an application during development to a running Payara application server. +1. Use builtin features of IDEs or plugins for different IDEs by Payara to ease the burden of redeploying an application during development to a running Payara application server. Their guides contain `documentation on Payara IDE plugins `_. 2. Use a paid product like `JRebel `_. The main difference between the first and the second option is support for hot deploys of non-class files plus limitations in what the JVM HotswapAgent can do for you. Find more `details in a blog article by JRebel `_. -When opting for Payara tools, please follow these steps: +When opting for builtin features or Payara tools, please follow these steps: 1. | Download the Payara appserver to your machine, unzip and note the location for later. - | - See this guide for which version, in doubt lookup using + | - See :ref:`payara` for which version or run the following command | ``mvn help:evaluate -Dexpression=payara.version -q -DforceStdout`` - | - Can be downloaded from `Maven Central `_. + | - To download, see :ref:`payara` or try `Maven Central `_. 2. Install Payara tools plugin in your IDE: .. tabs:: + .. group-tab:: Netbeans + + This step is not necessary for Netbeans. The feature is builtin. + .. group-tab:: IntelliJ + **Requires IntelliJ Ultimate!** (Note that `free educational licenses `_ are available) @@ -180,6 +185,28 @@ When opting for Payara tools, please follow these steps: 3. Configure a connection to the application server: .. tabs:: + .. group-tab:: Netbeans + + Unzip Payara to ``/usr/local/payara6`` as explained in :ref:`install-payara-dev`. + + Launch Netbeans and click "Tools" and then "Servers". Click "Add Server" and select "Payara Server" and set the installation location to ``/usr/local/payara6``. Use the settings in the screenshot below. Most of the defaults are fine. + + Under "Common", the password should be "admin". Make sure "Enable Hot Deploy" is checked. + + .. image:: img/netbeans-servers-common.png + + Under "Java", change the debug port to 9009. + + .. image:: img/netbeans-servers-java.png + + Open the project properties (under "File"), navigate to "Compile" and make sure "Compile on Save" is checked. + + .. image:: img/netbeans-compile.png + + Under "Run", select "Payara Server" under "Server" and make sure "Deploy on Save" is checked. + + .. image:: img/netbeans-run.png + .. group-tab:: IntelliJ Create a new running configuration with a "Remote Payara". (Open dialog by clicking "Run", then "Edit Configurations") @@ -212,13 +239,32 @@ When opting for Payara tools, please follow these steps: .. image:: img/intellij-payara-config-server-behaviour.png 4. | Start all the containers. Follow the cheat sheet above, but take care to skip application deployment: - | - When using the Maven commands, append ``-Dapp.deploy.skip``. - | - When using Docker Compose, prepend the command with ``SKIP_DEPLOY=1``. + | - When using the Maven commands, append ``-Dapp.deploy.skip``. For example: + | ``mvn -Pct docker:run -Dapp.deploy.skip`` + | - When using Docker Compose, prepend the command with ``SKIP_DEPLOY=1``. For example: + | ``SKIP_DEPLOY=1 docker compose -f docker-compose-dev.yml up`` | - Note: the Admin Console can be reached at http://localhost:4848 or https://localhost:4949 5. To deploy the application to the running server, use the configured tools to deploy. Using the "Run" configuration only deploys and enables redeploys, while running "Debug" enables hot swapping of classes via JDWP. .. tabs:: + .. group-tab:: Netbeans + + Click "Debug" then "Debug Project". After some time, Dataverse will be deployed. + + Try making a code change, perhaps to ``Info.java``. + + Click "Debug" and then "Apply Code Changes". If the change was correctly applied, you should see output similar to this: + + .. code-block:: + + Classes to reload: + edu.harvard.iq.dataverse.api.Info + + Code updated + + Check to make sure the change is live by visiting, for example, http://localhost:8080/api/info/version + .. group-tab:: IntelliJ Choose "Run" or "Debug" in the toolbar. diff --git a/doc/sphinx-guides/source/container/img/netbeans-compile.png b/doc/sphinx-guides/source/container/img/netbeans-compile.png new file mode 100644 index 0000000000000000000000000000000000000000..e429695ccb01c0170ee612ef1e701aebb68029c7 GIT binary patch literal 99396 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 OK%OVP-KZym)BX!yWK!4w literal 0 HcmV?d00001 diff --git a/doc/sphinx-guides/source/container/img/netbeans-run.png b/doc/sphinx-guides/source/container/img/netbeans-run.png new file mode 100644 index 0000000000000000000000000000000000000000..00f8af23cc5a0292622ab9fb26d950f87becb3c6 GIT binary patch literal 124521 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%0D)CeV?U@;wm}A&9i(KwsMMG+v@8#o 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%0D)U3i?}Pw{|xb?=5# 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
    CB5t0Wn1;Ni>X3?ql)_CKU)bApArcg`9&>`yuXG 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
  • From 3d7f72a532841a1ce0a9635158b04a60ea080bf1 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Feb 2024 16:21:39 -0500 Subject: [PATCH 335/689] add uningest for a problem logic in api --- .../edu/harvard/iq/dataverse/api/Files.java | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index 5d400ee1438..1f0e0801c68 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -51,6 +51,7 @@ import edu.harvard.iq.dataverse.util.SystemConfig; import edu.harvard.iq.dataverse.util.URLTokenUtil; +import static edu.harvard.iq.dataverse.util.JsfHelper.JH; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; import edu.harvard.iq.dataverse.util.json.JsonUtil; import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; @@ -65,6 +66,7 @@ import java.util.logging.Logger; import jakarta.ejb.EJB; import jakarta.ejb.EJBException; +import jakarta.faces.application.FacesMessage; import jakarta.inject.Inject; import jakarta.json.Json; import jakarta.json.JsonArray; @@ -637,7 +639,27 @@ public Response uningestDatafile(@Context ContainerRequestContext crc, @PathPara if (dataFile == null) { return error(Response.Status.NOT_FOUND, "File not found for given id."); } - + if (!dataFile.isTabularData()) { + // Ingest never succeeded, either there was a failure or this is not a tabular + // data file + // We allow anyone who can publish to uningest in order to clear a problem + if (dataFile.isIngestProblem()) { + try { + AuthenticatedUser au = getRequestAuthenticatedUserOrDie(crc); + if (!(permissionSvc.permissionsFor(au, dataFile).contains(Permission.PublishDataset))) { + return forbidden( + "Uningesting to remove an ingest problem can only be done by those who can publish the dataset"); + } + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + dataFile.setIngestDone(); + dataFile.setIngestReport(null); + } else { + return error(Response.Status.BAD_REQUEST, + BundleUtil.getStringFromBundle("file.ingest.cantUningestFileWarning")); + } + } if (!dataFile.isTabularData()) { return error(Response.Status.BAD_REQUEST, "Cannot uningest non-tabular file."); } From 7de7f43c99d7f79a7a4a255e75bfa08506361a41 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Feb 2024 16:21:56 -0500 Subject: [PATCH 336/689] update docs --- .../source/user/tabulardataingest/ingestprocess.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/sphinx-guides/source/user/tabulardataingest/ingestprocess.rst b/doc/sphinx-guides/source/user/tabulardataingest/ingestprocess.rst index 4dce441de4a..418eb2206c8 100644 --- a/doc/sphinx-guides/source/user/tabulardataingest/ingestprocess.rst +++ b/doc/sphinx-guides/source/user/tabulardataingest/ingestprocess.rst @@ -67,9 +67,10 @@ who can see the draft version of the dataset containing the file that will indic part of the dataset, there will be no indication that ingest was attempted and failed. If the warning message is a concern, the Dataverse software includes both an API call (see :ref:`file-uningest` in the :doc:`/api/native-api` guide) -and an Edit/Uningest menu option displayed on the file page, that allow a file to be uningested. These are only available to superusers. -Uningest will remove the warning. Uningest can also be done for a file that was successfully ingested. -This will remove the .tab version of the file that was generated. +and an Edit/Uningest menu option displayed on the file page, that allow a file to be uningested by anone who can publish the dataset. + +Uningest will remove the warning. Uningest can also be done for a file that was successfully ingested. This is only available to superusers. +This will remove the variable-level metadata and the .tab version of the file that was generated. If a file is a tabular format but was never ingested, .e.g. due to the ingest file size limit being lower in the past, or if ingest had failed, e.g. in a prior Dataverse version, an reingest API (see :ref:`file-reingest` in the :doc:`/api/native-api` guide) and a file page Edit/Reingest option From 51fe60c095f52e26a6f1be7587c5323de7107993 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Feb 2024 16:26:50 -0500 Subject: [PATCH 337/689] fix logic --- .../edu/harvard/iq/dataverse/api/Files.java | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index 1f0e0801c68..d48ae3247b5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -655,27 +655,24 @@ public Response uningestDatafile(@Context ContainerRequestContext crc, @PathPara } dataFile.setIngestDone(); dataFile.setIngestReport(null); + return ok("Datafile " + dataFile.getId() + " uningested."); } else { return error(Response.Status.BAD_REQUEST, - BundleUtil.getStringFromBundle("file.ingest.cantUningestFileWarning")); + BundleUtil.getStringFromBundle("Cannot uningest non-tabular file.")); + } + } else { + try { + DataverseRequest req = createDataverseRequest(getRequestUser(crc)); + execCommand(new UningestFileCommand(req, dataFile)); + Long dataFileId = dataFile.getId(); + dataFile = fileService.find(dataFileId); + Dataset theDataset = dataFile.getOwner(); + exportDatasetMetadata(settingsService, theDataset); + return ok("Datafile " + dataFileId + " uningested."); + } catch (WrappedResponse wr) { + return wr.getResponse(); } } - if (!dataFile.isTabularData()) { - return error(Response.Status.BAD_REQUEST, "Cannot uningest non-tabular file."); - } - - try { - DataverseRequest req = createDataverseRequest(getRequestUser(crc)); - execCommand(new UningestFileCommand(req, dataFile)); - Long dataFileId = dataFile.getId(); - dataFile = fileService.find(dataFileId); - Dataset theDataset = dataFile.getOwner(); - exportDatasetMetadata(settingsService, theDataset); - return ok("Datafile " + dataFileId + " uningested."); - } catch (WrappedResponse wr) { - return wr.getResponse(); - } - } // reingest attempts to queue an *existing* DataFile From 31d7cbcea224d253325f9baa3b4f4f1d8e802882 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Feb 2024 17:38:19 -0500 Subject: [PATCH 338/689] typo/merge issues --- src/main/webapp/file-edit-button-fragment.xhtml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/file-edit-button-fragment.xhtml b/src/main/webapp/file-edit-button-fragment.xhtml index fd455521c98..30c3f6e7938 100644 --- a/src/main/webapp/file-edit-button-fragment.xhtml +++ b/src/main/webapp/file-edit-button-fragment.xhtml @@ -79,9 +79,9 @@ - +
  • - +
  • From 057d2c3c5d9a00b38354416dbaa70ee6637bbe43 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Feb 2024 17:49:28 -0500 Subject: [PATCH 339/689] missing save --- src/main/java/edu/harvard/iq/dataverse/api/Files.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index d48ae3247b5..f735ecfdec8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -655,6 +655,7 @@ public Response uningestDatafile(@Context ContainerRequestContext crc, @PathPara } dataFile.setIngestDone(); dataFile.setIngestReport(null); + fileService.save(dataFile); return ok("Datafile " + dataFile.getId() + " uningested."); } else { return error(Response.Status.BAD_REQUEST, From beb5bf6847469ab9b41b3128837d5c9d4daddf24 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Feb 2024 17:55:59 -0500 Subject: [PATCH 340/689] tweak api doc --- doc/sphinx-guides/source/api/native-api.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 8cfa5deb96c..1b04d7c9e12 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -2859,7 +2859,10 @@ The fully expanded example above (without environment variables) looks like this Uningest a File ~~~~~~~~~~~~~~~ -Reverse the tabular data ingest process performed on a file where ``ID`` is the database id or ``PERSISTENT_ID`` is the persistent id (DOI or Handle) of the file to process. Note that this requires "superuser" credentials. +Reverse the tabular data ingest process performed on a file where ``ID`` is the database id or ``PERSISTENT_ID`` is the persistent id (DOI or Handle) of the file to process. + +Note that this requires "superuser" credentials to undo a successful ingest and remove the variable-level metadata and .tab version of the file. +It can also be used by a user who can publish the dataset to clear the error from an unsuccessful ingest. A curl example using an ``ID``: From 00d418912d88e202a390a6c2d70d80efb0ec5bfc Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Feb 2024 18:02:53 -0500 Subject: [PATCH 341/689] changelog, release note tweaks --- doc/release-notes/10318-uningest-and-reingest.md | 5 +++-- doc/sphinx-guides/source/api/changelog.rst | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/release-notes/10318-uningest-and-reingest.md b/doc/release-notes/10318-uningest-and-reingest.md index 7465f934330..9f6f81b4818 100644 --- a/doc/release-notes/10318-uningest-and-reingest.md +++ b/doc/release-notes/10318-uningest-and-reingest.md @@ -1,2 +1,3 @@ -New Uningest/Reingest options are available in the File Page Edit menu for superusers, allowing ingest errors to be cleared and for -ingest to be retried (e.g. after a Dataverse version update or if ingest size limits are changed). +New Uningest/Reingest options are available in the File Page Edit menu, allowing ingest errors to be cleared (by users who can published the associated dataset) +and (by suerpsuers) for a successful ingest to be undone or retried (e.g. after a Dataverse version update or if ingest size limits are changed). +The /api/files//uningest api also now allows users who can publish the dataset to undo an ingest failure. diff --git a/doc/sphinx-guides/source/api/changelog.rst b/doc/sphinx-guides/source/api/changelog.rst index d272086fa2e..99414550c4b 100644 --- a/doc/sphinx-guides/source/api/changelog.rst +++ b/doc/sphinx-guides/source/api/changelog.rst @@ -12,6 +12,7 @@ v6.2 - **/api/datasets/{id}/versions/{versionId}**: The includeFiles parameter has been renamed to excludeFiles. The default behavior remains the same, which is to include files. However, when excludeFiles is set to true, the files will be excluded. A bug that caused the API to only return a deaccessioned dataset if the user had edit privileges has been fixed. - **/api/datasets/{id}/versions**: The includeFiles parameter has been renamed to excludeFiles. The default behavior remains the same, which is to include files. However, when excludeFiles is set to true, the files will be excluded. +- **/api/files/$ID/uningest**: Can now be used by users with the ability to publish the dataset to undo a failed ingest. (Removing a successful ingest still requires being superuser) v6.1 ---- From ab0abaf83ca854214bf2a589ab642d4187ffa3fd Mon Sep 17 00:00:00 2001 From: Guillermo Portas Date: Thu, 15 Feb 2024 13:45:34 +0000 Subject: [PATCH 342/689] Removed: double quotes in docs for DATASET_VERSION value --- 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 5be73c01194..4038ec4340d 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -3519,7 +3519,7 @@ When the dataset version is published, authentication is not required: export SERVER_URL=https://demo.dataverse.org export FILE_ID=42 - export DATASET_VERSION=":latest-published" + export DATASET_VERSION=:latest-published curl "$SERVER_URL/api/files/$FILE_ID/versions/$DATASET_VERSION/citation" From d82c730b9f3ef0b1ba570878ea1814ea51dc073e Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Thu, 15 Feb 2024 11:00:39 -0500 Subject: [PATCH 343/689] adding harvesting feature to handle missing controlled values --- ...92-harvest-metadata-values-not-in-cvv-list | 6 ++++ .../settings/SettingsServiceBean.java | 7 +++- .../iq/dataverse/util/SystemConfig.java | 7 ++++ .../iq/dataverse/util/json/JsonParser.java | 34 +++++++++++-------- .../iq/dataverse/api/HarvestingClientsIT.java | 31 ++++++++++++++--- 5 files changed, 64 insertions(+), 21 deletions(-) create mode 100644 doc/release-notes/9992-harvest-metadata-values-not-in-cvv-list diff --git a/doc/release-notes/9992-harvest-metadata-values-not-in-cvv-list b/doc/release-notes/9992-harvest-metadata-values-not-in-cvv-list new file mode 100644 index 00000000000..64ea2e1166a --- /dev/null +++ b/doc/release-notes/9992-harvest-metadata-values-not-in-cvv-list @@ -0,0 +1,6 @@ + +`AllowHarvestingMissingCVV` setting to enable/disable allowing datasets to be harvested with Controlled Vocabulary Values that existed in the originating Dataverse Project but are not in the harvesting Dataverse Project. +The default value of this setting is false/no which will cause the harvesting of the dataset to fail. +By activating this feature (true/yes) the value in question will be removed from the list of values and the dataset will be harvested without the missing value. + +`curl http://localhost:8080/api/admin/settings/:AllowHarvestingMissingCVV -X PUT -d yes` diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java index 3b7632f3d9e..6ed17d93ee3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java @@ -603,7 +603,12 @@ Whether Harvesting (OAI) service is enabled * When ingesting tabular data files, store the generated tab-delimited * files *with* the variable names line up top. */ - StoreIngestedTabularFilesWithVarHeaders + StoreIngestedTabularFilesWithVarHeaders, + + /** + * Should we ignore missing controlled vocabulary values when harvesting + */ + AllowHarvestingMissingCVV ; @Override diff --git a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java index ded394833f1..b2127cc263d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java @@ -1181,4 +1181,11 @@ public Long getTestStorageQuotaLimit() { public boolean isStoringIngestedFilesWithHeaders() { return settingsService.isTrueForKey(SettingsServiceBean.Key.StoreIngestedTabularFilesWithVarHeaders, false); } + + /** + * Should we ignore missing controlled vocabulary values when harvesting + */ + public boolean allowHarvestingMissingCVV() { + return settingsService.isTrueForKey(SettingsServiceBean.Key.AllowHarvestingMissingCVV, false); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java index 984c607aac7..cd93f4719cd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java @@ -38,7 +38,6 @@ import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -69,7 +68,8 @@ public class JsonParser { MetadataBlockServiceBean blockService; SettingsServiceBean settingsService; LicenseServiceBean licenseService; - HarvestingClient harvestingClient = null; + HarvestingClient harvestingClient = null; + boolean allowHarvestingMissingCVV = false; /** * if lenient, we will accept alternate spellings for controlled vocabulary values @@ -93,6 +93,7 @@ public JsonParser(DatasetFieldServiceBean datasetFieldSvc, MetadataBlockServiceB this.settingsService = settingsService; this.licenseService = licenseService; this.harvestingClient = harvestingClient; + this.allowHarvestingMissingCVV = (harvestingClient != null && settingsService.isTrueForKey(SettingsServiceBean.Key.AllowHarvestingMissingCVV, false)); } public JsonParser() { @@ -931,30 +932,30 @@ private String jsonValueToString(JsonValue jv) { } public List parseControlledVocabularyValue(DatasetFieldType cvvType, JsonObject json) throws JsonParseException { + List vals = new LinkedList<>(); try { if (cvvType.isAllowMultiples()) { try { json.getJsonArray("value").getValuesAs(JsonObject.class); } catch (ClassCastException cce) { throw new JsonParseException("Invalid values submitted for " + cvvType.getName() + ". It should be an array of values."); - } - List vals = new LinkedList<>(); + } for (JsonString strVal : json.getJsonArray("value").getValuesAs(JsonString.class)) { String strValue = strVal.getString(); ControlledVocabularyValue cvv = datasetFieldSvc.findControlledVocabularyValueByDatasetFieldTypeAndStrValue(cvvType, strValue, lenient); - if (cvv == null) { + if (cvv == null && !allowHarvestingMissingCVV) { throw new ControlledVocabularyException("Value '" + strValue + "' does not exist in type '" + cvvType.getName() + "'", cvvType, strValue); } - // Only add value to the list if it is not a duplicate - if (strValue.equals("Other")) { - System.out.println("vals = " + vals + ", contains: " + vals.contains(cvv)); - } - if (!vals.contains(cvv)) { - vals.add(cvv); + if (cvv != null) { + // Only add value to the list if it is not a duplicate + if (strValue.equals("Other")) { + System.out.println("vals = " + vals + ", contains: " + vals.contains(cvv)); + } + if (!vals.contains(cvv)) { + vals.add(cvv); + } } } - return vals; - } else { try { json.getString("value"); @@ -963,11 +964,14 @@ public List parseControlledVocabularyValue(DatasetFie } String strValue = json.getString("value", ""); ControlledVocabularyValue cvv = datasetFieldSvc.findControlledVocabularyValueByDatasetFieldTypeAndStrValue(cvvType, strValue, lenient); - if (cvv == null) { + if (cvv == null && !allowHarvestingMissingCVV) { throw new ControlledVocabularyException("Value '" + strValue + "' does not exist in type '" + cvvType.getName() + "'", cvvType, strValue); } - return Collections.singletonList(cvv); + if (cvv != null) { + vals.add(cvv); + } } + return vals; } catch (ClassCastException cce) { throw new JsonParseException("Invalid values submitted for " + cvvType.getName()); } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java index d5388e510d2..36ef947e105 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java @@ -2,6 +2,8 @@ import java.util.logging.Logger; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import io.restassured.RestAssured; @@ -37,8 +39,8 @@ public class HarvestingClientsIT { private static final String ARCHIVE_URL = "https://demo.dataverse.org"; private static final String HARVEST_METADATA_FORMAT = "oai_dc"; private static final String ARCHIVE_DESCRIPTION = "RestAssured harvesting client test"; - private static final String CONTROL_OAI_SET = "controlTestSet"; - private static final int DATASETS_IN_CONTROL_SET = 7; + private static final String CONTROL_OAI_SET = "controlTestSet2"; + private static final int DATASETS_IN_CONTROL_SET = 8; private static String normalUserAPIKey; private static String adminUserAPIKey; private static String harvestCollectionAlias; @@ -54,6 +56,10 @@ public static void setUpClass() { setupCollection(); } + @AfterEach + public void cleanup() { + UtilIT.deleteSetting(SettingsServiceBean.Key.AllowHarvestingMissingCVV); + } private static void setupUsers() { Response cu0 = UtilIT.createRandomUser(); @@ -157,9 +163,24 @@ public void testCreateEditDeleteClient() throws InterruptedException { logger.info("rDelete.getStatusCode(): " + rDelete.getStatusCode()); assertEquals(OK.getStatusCode(), rDelete.getStatusCode()); } - + + @Test + public void testHarvestingClientRun_AllowHarvestingMissingCVV_True() throws InterruptedException { + harvestingClientRun(true); + } @Test - public void testHarvestingClientRun() throws InterruptedException { + public void testHarvestingClientRun_AllowHarvestingMissingCVV_False() throws InterruptedException { + harvestingClientRun(false); + } + + private void harvestingClientRun(boolean allowHarvestingMissingCVV) throws InterruptedException { + int expectedNumberOfSetsHarvested = allowHarvestingMissingCVV ? DATASETS_IN_CONTROL_SET : DATASETS_IN_CONTROL_SET - 1; + if (allowHarvestingMissingCVV) { + UtilIT.enableSetting(SettingsServiceBean.Key.AllowHarvestingMissingCVV); + } else { + UtilIT.deleteSetting(SettingsServiceBean.Key.AllowHarvestingMissingCVV); + } + // This test will create a client and attempt to perform an actual // harvest and validate the resulting harvested content. @@ -242,7 +263,7 @@ public void testHarvestingClientRun() throws InterruptedException { assertEquals(harvestTimeStamp, responseJsonPath.getString("data.lastNonEmpty")); // d) Confirm that the correct number of datasets have been harvested: - assertEquals(DATASETS_IN_CONTROL_SET, responseJsonPath.getInt("data.lastDatasetsHarvested")); + assertEquals(expectedNumberOfSetsHarvested, responseJsonPath.getInt("data.lastDatasetsHarvested")); // ok, it looks like the harvest has completed successfully. break; From 34d7802622f6d38fec9debcd6ee88798c20bd358 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Thu, 15 Feb 2024 11:42:11 -0500 Subject: [PATCH 344/689] add .md to release notes file --- ...n-cvv-list => 9992-harvest-metadata-values-not-in-cvv-list.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename doc/release-notes/{9992-harvest-metadata-values-not-in-cvv-list => 9992-harvest-metadata-values-not-in-cvv-list.md} (100%) diff --git a/doc/release-notes/9992-harvest-metadata-values-not-in-cvv-list b/doc/release-notes/9992-harvest-metadata-values-not-in-cvv-list.md similarity index 100% rename from doc/release-notes/9992-harvest-metadata-values-not-in-cvv-list rename to doc/release-notes/9992-harvest-metadata-values-not-in-cvv-list.md From 87b5a38bd5511a169f1ccae9d3bb966f2e3cb6b6 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 15 Feb 2024 17:01:59 -0500 Subject: [PATCH 345/689] typo #10318 --- .../source/user/tabulardataingest/ingestprocess.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/user/tabulardataingest/ingestprocess.rst b/doc/sphinx-guides/source/user/tabulardataingest/ingestprocess.rst index 418eb2206c8..1e481a54da6 100644 --- a/doc/sphinx-guides/source/user/tabulardataingest/ingestprocess.rst +++ b/doc/sphinx-guides/source/user/tabulardataingest/ingestprocess.rst @@ -67,7 +67,7 @@ who can see the draft version of the dataset containing the file that will indic part of the dataset, there will be no indication that ingest was attempted and failed. If the warning message is a concern, the Dataverse software includes both an API call (see :ref:`file-uningest` in the :doc:`/api/native-api` guide) -and an Edit/Uningest menu option displayed on the file page, that allow a file to be uningested by anone who can publish the dataset. +and an Edit/Uningest menu option displayed on the file page, that allow a file to be uningested by anyone who can publish the dataset. Uningest will remove the warning. Uningest can also be done for a file that was successfully ingested. This is only available to superusers. This will remove the variable-level metadata and the .tab version of the file that was generated. From 95ce492f221a5980729c6b26528feb1ac681c56b Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 16 Feb 2024 10:38:56 +0000 Subject: [PATCH 346/689] Changed: includeDeaccessioned optional param in getLatestPublishedFileMetadata, and existing usages changed --- .../edu/harvard/iq/dataverse/DataFile.java | 8 +++-- .../edu/harvard/iq/dataverse/api/EditDDI.java | 2 +- .../edu/harvard/iq/dataverse/api/Files.java | 2 +- ...stractGetPublishedFileMetadataCommand.java | 31 +++++++++++++++++++ ...etDraftFileMetadataIfAvailableCommand.java | 4 +-- ...etLatestAccessibleFileMetadataCommand.java | 4 +-- ...GetLatestPublishedFileMetadataCommand.java | 19 +++++------- ...edFileMetadataByDatasetVersionCommand.java | 22 +++---------- .../MakeDataCountLoggingServiceBean.java | 2 +- 9 files changed, 56 insertions(+), 38 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractGetPublishedFileMetadataCommand.java diff --git a/src/main/java/edu/harvard/iq/dataverse/DataFile.java b/src/main/java/edu/harvard/iq/dataverse/DataFile.java index 818cade1eef..de13a83e204 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataFile.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataFile.java @@ -567,9 +567,13 @@ public FileMetadata getLatestFileMetadata() { return resultFileMetadata; } - public FileMetadata getLatestPublishedFileMetadata() throws UnsupportedOperationException { + public FileMetadata getLatestPublishedFileMetadata(boolean includeDeaccessioned) throws UnsupportedOperationException { FileMetadata resultFileMetadata = fileMetadatas.stream() - .filter(metadata -> !metadata.getDatasetVersion().getVersionState().equals(VersionState.DRAFT)) + .filter(metadata -> { + VersionState versionState = metadata.getDatasetVersion().getVersionState(); + return (!versionState.equals(VersionState.DRAFT) && + !(versionState.equals(VersionState.DEACCESSIONED) && !includeDeaccessioned)); + }) .reduce(null, this::getTheNewerFileMetadata); if (resultFileMetadata == null) { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/EditDDI.java b/src/main/java/edu/harvard/iq/dataverse/api/EditDDI.java index 1b74ab5479e..d6aee0b7bfc 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/EditDDI.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/EditDDI.java @@ -124,7 +124,7 @@ public Response edit(@Context ContainerRequestContext crc, InputStream body, @Pa if (!latestVersion.isWorkingCopy()) { //for new draft version - FileMetadata latestFml = dataFile.getLatestPublishedFileMetadata(); + FileMetadata latestFml = dataFile.getLatestPublishedFileMetadata(true); boolean groupUpdate = newGroups(varGroupMap, latestFml); boolean varUpdate = varUpdates(mapVarToVarMet, latestFml, neededToUpdateVM, true); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index 55d65bae96b..b7494c5daec 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -580,7 +580,7 @@ public Response getFileMetadata(@Context ContainerRequestContext crc, @PathParam return error(BAD_REQUEST, BundleUtil.getStringFromBundle("files.api.no.draft")); } } else { - fm = df.getLatestPublishedFileMetadata(); + fm = df.getLatestPublishedFileMetadata(false); MakeDataCountLoggingServiceBean.MakeDataCountEntry entry = new MakeDataCountLoggingServiceBean.MakeDataCountEntry(uriInfo, headers, dvRequestService, df); mdcLogService.logEntry(entry); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractGetPublishedFileMetadataCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractGetPublishedFileMetadataCommand.java new file mode 100644 index 00000000000..82d0ac3491b --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractGetPublishedFileMetadataCommand.java @@ -0,0 +1,31 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetVersion; +import edu.harvard.iq.dataverse.FileMetadata; +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; + +@RequiredPermissions({}) +abstract class AbstractGetPublishedFileMetadataCommand extends AbstractCommand { + protected final DataFile dataFile; + protected final boolean includeDeaccessioned; + + public AbstractGetPublishedFileMetadataCommand(DataverseRequest request, DataFile dataFile, boolean includeDeaccessioned) { + super(request, dataFile); + this.dataFile = dataFile; + this.includeDeaccessioned = includeDeaccessioned; + } + + protected boolean isDatasetVersionAccessible(DatasetVersion datasetVersion, Dataset ownerDataset, CommandContext ctxt) { + return datasetVersion.isReleased() || isDatasetVersionDeaccessionedAndAccessible(datasetVersion, ownerDataset, ctxt); + } + + private boolean isDatasetVersionDeaccessionedAndAccessible(DatasetVersion datasetVersion, Dataset ownerDataset, CommandContext ctxt) { + return includeDeaccessioned && datasetVersion.isDeaccessioned() && ctxt.permissions().requestOn(getRequest(), ownerDataset).has(Permission.EditDataset); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDraftFileMetadataIfAvailableCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDraftFileMetadataIfAvailableCommand.java index e0f8ca1fcf8..8ed058d79f8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDraftFileMetadataIfAvailableCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetDraftFileMetadataIfAvailableCommand.java @@ -16,8 +16,8 @@ public class GetDraftFileMetadataIfAvailableCommand extends AbstractCommand { private final DataFile dataFile; - public GetDraftFileMetadataIfAvailableCommand(DataverseRequest aRequest, DataFile dataFile) { - super(aRequest, dataFile); + public GetDraftFileMetadataIfAvailableCommand(DataverseRequest request, DataFile dataFile) { + super(request, dataFile); this.dataFile = dataFile; } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java index fa80b75c593..98913d63471 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java @@ -14,8 +14,8 @@ public class GetLatestAccessibleFileMetadataCommand extends AbstractCommand { - private final DataFile dataFile; - private final boolean includeDeaccessioned; +public class GetLatestPublishedFileMetadataCommand extends AbstractGetPublishedFileMetadataCommand { - public GetLatestPublishedFileMetadataCommand(DataverseRequest aRequest, DataFile dataFile, boolean includeDeaccessioned) { - super(aRequest, dataFile); - this.dataFile = dataFile; - this.includeDeaccessioned = includeDeaccessioned; + public GetLatestPublishedFileMetadataCommand(DataverseRequest request, DataFile dataFile, boolean includeDeaccessioned) { + super(request, dataFile, includeDeaccessioned); } @Override public FileMetadata execute(CommandContext ctxt) throws CommandException { try { - return dataFile.getLatestPublishedFileMetadata(); + FileMetadata fileMetadata = dataFile.getLatestPublishedFileMetadata(includeDeaccessioned); + if (isDatasetVersionAccessible(fileMetadata.getDatasetVersion(), dataFile.getOwner(), ctxt)) { + return fileMetadata; + } + return null; } catch (UnsupportedOperationException e) { return null; } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetSpecificPublishedFileMetadataByDatasetVersionCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetSpecificPublishedFileMetadataByDatasetVersionCommand.java index 82350d3bd95..deffbfb57ee 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetSpecificPublishedFileMetadataByDatasetVersionCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetSpecificPublishedFileMetadataByDatasetVersionCommand.java @@ -1,29 +1,20 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.DataFile; -import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.FileMetadata; -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; -@RequiredPermissions({}) -public class GetSpecificPublishedFileMetadataByDatasetVersionCommand extends AbstractCommand { +public class GetSpecificPublishedFileMetadataByDatasetVersionCommand extends AbstractGetPublishedFileMetadataCommand { private final long majorVersion; private final long minorVersion; - private final DataFile dataFile; - private final boolean includeDeaccessioned; - public GetSpecificPublishedFileMetadataByDatasetVersionCommand(DataverseRequest aRequest, DataFile dataFile, long majorVersion, long minorVersion, boolean includeDeaccessioned) { - super(aRequest, dataFile); - this.dataFile = dataFile; + public GetSpecificPublishedFileMetadataByDatasetVersionCommand(DataverseRequest request, DataFile dataFile, long majorVersion, long minorVersion, boolean includeDeaccessioned) { + super(request, dataFile, includeDeaccessioned); this.majorVersion = majorVersion; this.minorVersion = minorVersion; - this.includeDeaccessioned = includeDeaccessioned; } @Override @@ -36,13 +27,8 @@ public FileMetadata execute(CommandContext ctxt) throws CommandException { private boolean isRequestedVersionFileMetadata(FileMetadata fileMetadata, CommandContext ctxt) { DatasetVersion datasetVersion = fileMetadata.getDatasetVersion(); - Dataset ownerDataset = dataFile.getOwner(); - return (datasetVersion.isReleased() || isDatasetVersionDeaccessionedAndAccessible(datasetVersion, ownerDataset, ctxt)) + return isDatasetVersionAccessible(datasetVersion, dataFile.getOwner(), ctxt) && datasetVersion.getVersionNumber().equals(majorVersion) && datasetVersion.getMinorVersionNumber().equals(minorVersion); } - - private boolean isDatasetVersionDeaccessionedAndAccessible(DatasetVersion datasetVersion, Dataset ownerDataset, CommandContext ctxt) { - return includeDeaccessioned && datasetVersion.isDeaccessioned() && ctxt.permissions().requestOn(getRequest(), ownerDataset).has(Permission.EditDataset); - } } diff --git a/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountLoggingServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountLoggingServiceBean.java index 5edf2fde0c3..a3f09d190ca 100644 --- a/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountLoggingServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountLoggingServiceBean.java @@ -133,7 +133,7 @@ public MakeDataCountEntry(FacesContext fc, DataverseRequestServiceBean dvRequest //Exception thrown if no published metadata exists for DataFile //This is passed a DataFile to log the file downloaded. uriInfo and headers are passed in lieu of FacesContext public MakeDataCountEntry(UriInfo uriInfo, HttpHeaders headers, DataverseRequestServiceBean dvRequestService, DataFile df) throws UnsupportedOperationException{ - this(null, dvRequestService, df.getLatestPublishedFileMetadata().getDatasetVersion()); + this(null, dvRequestService, df.getLatestPublishedFileMetadata(false).getDatasetVersion()); if(uriInfo != null) { setRequestUrl(uriInfo.getRequestUri().toString()); From 6d79c6b1dc313263dd5b01e87e1817c7d00dd703 Mon Sep 17 00:00:00 2001 From: Don Sizemore Date: Fri, 16 Feb 2024 07:19:46 -0500 Subject: [PATCH 347/689] #10326 update installation-main.rst to reference Python installer --- doc/sphinx-guides/source/installation/installation-main.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/installation/installation-main.rst b/doc/sphinx-guides/source/installation/installation-main.rst index 46c1b0b0af3..bc51a8e19f5 100755 --- a/doc/sphinx-guides/source/installation/installation-main.rst +++ b/doc/sphinx-guides/source/installation/installation-main.rst @@ -68,7 +68,7 @@ The script will prompt you for some configuration values. If this is a test/eval If desired, these default values can be configured by creating a ``default.config`` (example :download:`here <../../../../scripts/installer/default.config>`) file in the installer's working directory with new values (if this file isn't present, the above defaults will be used). -This allows the installer to be run in non-interactive mode (with ``./install -y -f > install.out 2> install.err``), which can allow for easier interaction with automated provisioning tools. +This allows the installer to be run in non-interactive mode (with ``./install.py -y -f > install.out 2> install.err``), which can allow for easier interaction with automated provisioning tools. All the Payara configuration tasks performed by the installer are isolated in the shell script ``dvinstall/as-setup.sh`` (as ``asadmin`` commands). From 213b0256fc41ec745979f4a85dd3e259e2c218c1 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 16 Feb 2024 14:46:50 +0100 Subject: [PATCH 348/689] test(mail): add more tests for mail session producer with invalid config Also fix minor linting with visibility of test methods. --- .../iq/dataverse/MailServiceBeanTest.java | 4 +- .../dataverse/util/MailSessionProducerIT.java | 48 +++++++++++++++++-- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java index f8a01c53298..afcc12949d6 100644 --- a/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanTest.java @@ -35,11 +35,11 @@ class Delegation { * We need to reset the BrandingUtil mocks for every test, as we rely on them being set to default. */ @BeforeEach - private void setup() { + void setup() { BrandingUtilTest.setupMocks(); } @AfterAll - private static void tearDown() { + static void tearDown() { BrandingUtilTest.tearDownMocks(); } diff --git a/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java b/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java index dcf04b7644a..8280578a343 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java @@ -3,6 +3,7 @@ import edu.harvard.iq.dataverse.DataverseServiceBean; import edu.harvard.iq.dataverse.MailServiceBean; import edu.harvard.iq.dataverse.branding.BrandingUtil; +import edu.harvard.iq.dataverse.branding.BrandingUtilTest; import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.testing.JvmSetting; @@ -10,14 +11,12 @@ import edu.harvard.iq.dataverse.util.testing.Tags; import io.restassured.RestAssured; import jakarta.mail.Session; -import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.testcontainers.containers.GenericContainer; @@ -30,6 +29,9 @@ import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -43,6 +45,8 @@ @Tag(Tags.USES_TESTCONTAINERS) @Testcontainers(disabledWithoutDocker = true) @ExtendWith(MockitoExtension.class) +@LocalJvmSettings +@JvmSetting(key = JvmSettings.SYSTEM_EMAIL, value = "test@test.com") class MailSessionProducerIT { private static final Integer PORT_SMTP = 1025; @@ -51,15 +55,21 @@ class MailSessionProducerIT { static SettingsServiceBean settingsServiceBean = Mockito.mock(SettingsServiceBean.class);; static DataverseServiceBean dataverseServiceBean = Mockito.mock(DataverseServiceBean.class);; + /** + * We need to reset the BrandingUtil mocks for every test, as we rely on them being set to default. + */ @BeforeAll static void setUp() { // Setup mocks behavior, inject as deps BrandingUtil.injectServices(dataverseServiceBean, settingsServiceBean); } + @AfterAll + static void tearDown() { + BrandingUtilTest.tearDownMocks(); + } @Nested @LocalJvmSettings - @JvmSetting(key = JvmSettings.SYSTEM_EMAIL, value = "test@test.com") @JvmSetting(key = JvmSettings.MAIL_MTA_HOST, method = "tcSmtpHost") @JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, method = "tcSmtpPort", varArgs = "port") class WithoutAuthentication { @@ -113,7 +123,6 @@ void createSession() { @Nested @LocalJvmSettings - @JvmSetting(key = JvmSettings.SYSTEM_EMAIL, value = "test@test.com") @JvmSetting(key = JvmSettings.MAIL_MTA_HOST, method = "tcSmtpHost") @JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, method = "tcSmtpPort", varArgs = "port") @JvmSetting(key = JvmSettings.MAIL_MTA_AUTH, value = "yes") @@ -169,4 +178,33 @@ void createSession() { } + @Nested + @LocalJvmSettings + class InvalidConfiguration { + @Test + @JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, value = "1234", varArgs = "invalid") + void invalidConfigItemsAreIgnoredOnSessionBuild() { + assertDoesNotThrow(() -> new MailSessionProducer().getSession()); + + Session mailSession = new MailSessionProducer().getSession(); + MailServiceBean mailer = new MailServiceBean(mailSession, settingsServiceBean); + assertFalse(mailer.sendSystemEmail("test@example.org", "Test", "Test", false)); + } + + @Test + @JvmSetting(key = JvmSettings.MAIL_MTA_HOST, value = "foobar") + void invalidHostnameIsFailingWhenSending() { + assertDoesNotThrow(() -> new MailSessionProducer().getSession()); + + Session mailSession = new MailSessionProducer().getSession(); + MailServiceBean mailer = new MailServiceBean(mailSession, settingsServiceBean); + assertFalse(mailer.sendSystemEmail("test@example.org", "Test", "Test", false)); + } + + @Test + @JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, varArgs = "port" , value = "foobar") + void invalidPortWithLetters() { + assertThrows(IllegalArgumentException.class, () -> new MailSessionProducer().getSession()); + } + } } \ No newline at end of file From 32d2fa4fa8a26783f6055d1715cb667a3b3ae4d1 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Fri, 16 Feb 2024 11:06:18 -0500 Subject: [PATCH 349/689] modify for comments --- .../iq/dataverse/util/json/JsonParser.java | 11 +++----- .../iq/dataverse/api/HarvestingClientsIT.java | 25 ++++++++++--------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java index cd93f4719cd..bd756fffdbf 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java @@ -946,14 +946,9 @@ public List parseControlledVocabularyValue(DatasetFie if (cvv == null && !allowHarvestingMissingCVV) { throw new ControlledVocabularyException("Value '" + strValue + "' does not exist in type '" + cvvType.getName() + "'", cvvType, strValue); } - if (cvv != null) { - // Only add value to the list if it is not a duplicate - if (strValue.equals("Other")) { - System.out.println("vals = " + vals + ", contains: " + vals.contains(cvv)); - } - if (!vals.contains(cvv)) { - vals.add(cvv); - } + // Only add value to the list if it is not a duplicate + if (cvv != null && !vals.contains(cvv)) { + vals.add(cvv); } } } else { diff --git a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java index 36ef947e105..71d4fc14ad5 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java @@ -43,7 +43,8 @@ public class HarvestingClientsIT { private static final int DATASETS_IN_CONTROL_SET = 8; private static String normalUserAPIKey; private static String adminUserAPIKey; - private static String harvestCollectionAlias; + private static String harvestCollectionAlias; + String clientApiPath = null; @BeforeAll public static void setUpClass() { @@ -59,6 +60,15 @@ public static void setUpClass() { @AfterEach public void cleanup() { UtilIT.deleteSetting(SettingsServiceBean.Key.AllowHarvestingMissingCVV); + // Cleanup: delete the client + if (clientApiPath != null) { + Response deleteResponse = given() + .header(UtilIT.API_TOKEN_HTTP_HEADER, adminUserAPIKey) + .delete(clientApiPath); + System.out.println("deleteResponse.getStatusCode(): " + deleteResponse.getStatusCode()); + assertEquals(OK.getStatusCode(), deleteResponse.getStatusCode()); + clientApiPath = null; + } } private static void setupUsers() { @@ -191,7 +201,7 @@ private void harvestingClientRun(boolean allowHarvestingMissingCVV) throws Inte String nickName = "h" + UtilIT.getRandomString(6); - String clientApiPath = String.format(HARVEST_CLIENTS_API+"%s", nickName); + clientApiPath = String.format(HARVEST_CLIENTS_API+"%s", nickName); String clientJson = String.format("{\"dataverseAlias\":\"%s\"," + "\"type\":\"oai\"," + "\"harvestUrl\":\"%s\"," @@ -279,15 +289,6 @@ private void harvestingClientRun(boolean allowHarvestingMissingCVV) throws Inte // datasets have been harvested. This may or may not be necessary, seeing // how we have already confirmed the number of successfully harvested // datasets from the control set; somewhat hard to imagine a practical - // situation where that would not be enough (?). - - // Cleanup: delete the client - - Response deleteResponse = given() - .header(UtilIT.API_TOKEN_HTTP_HEADER, adminUserAPIKey) - .delete(clientApiPath); - System.out.println("deleteResponse.getStatusCode(): " + deleteResponse.getStatusCode()); - assertEquals(OK.getStatusCode(), deleteResponse.getStatusCode()); - + // situation where that would not be enough (?). } } From 97678807454cd3834f5dc4a59c50599f326e14dd Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Fri, 16 Feb 2024 13:09:54 -0500 Subject: [PATCH 350/689] addressing review comments --- .../iq/dataverse/util/json/JsonParser.java | 29 +++++++++++-------- .../iq/dataverse/api/HarvestingClientsIT.java | 10 +++---- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java index bd756fffdbf..ac7b6bb4067 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java @@ -38,6 +38,7 @@ import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -69,7 +70,6 @@ public class JsonParser { SettingsServiceBean settingsService; LicenseServiceBean licenseService; HarvestingClient harvestingClient = null; - boolean allowHarvestingMissingCVV = false; /** * if lenient, we will accept alternate spellings for controlled vocabulary values @@ -93,7 +93,6 @@ public JsonParser(DatasetFieldServiceBean datasetFieldSvc, MetadataBlockServiceB this.settingsService = settingsService; this.licenseService = licenseService; this.harvestingClient = harvestingClient; - this.allowHarvestingMissingCVV = (harvestingClient != null && settingsService.isTrueForKey(SettingsServiceBean.Key.AllowHarvestingMissingCVV, false)); } public JsonParser() { @@ -738,7 +737,14 @@ public DatasetField parseField(JsonObject json, Boolean testType) throws JsonPar ret.setDatasetFieldType(type); - + + // If Harvesting, CVV values may differ between the Dataverse installations, so we won't enforce them + if (harvestingClient != null && type.isControlledVocabulary() && + settingsService.isTrueForKey(SettingsServiceBean.Key.AllowHarvestingMissingCVV, false)) { + type.setAllowControlledVocabulary(false); + logger.warning("Harvesting: Skipping Controlled Vocabulary. Treating values as primitives"); + } + if (type.isCompound()) { List vals = parseCompoundValue(type, json, testType); for (DatasetFieldCompoundValue dsfcv : vals) { @@ -930,9 +936,8 @@ private String jsonValueToString(JsonValue jv) { default: return jv.toString(); } } - + public List parseControlledVocabularyValue(DatasetFieldType cvvType, JsonObject json) throws JsonParseException { - List vals = new LinkedList<>(); try { if (cvvType.isAllowMultiples()) { try { @@ -940,17 +945,20 @@ public List parseControlledVocabularyValue(DatasetFie } catch (ClassCastException cce) { throw new JsonParseException("Invalid values submitted for " + cvvType.getName() + ". It should be an array of values."); } + List vals = new LinkedList<>(); for (JsonString strVal : json.getJsonArray("value").getValuesAs(JsonString.class)) { String strValue = strVal.getString(); ControlledVocabularyValue cvv = datasetFieldSvc.findControlledVocabularyValueByDatasetFieldTypeAndStrValue(cvvType, strValue, lenient); - if (cvv == null && !allowHarvestingMissingCVV) { + if (cvv == null) { throw new ControlledVocabularyException("Value '" + strValue + "' does not exist in type '" + cvvType.getName() + "'", cvvType, strValue); } // Only add value to the list if it is not a duplicate - if (cvv != null && !vals.contains(cvv)) { + if (!vals.contains(cvv)) { vals.add(cvv); } } + return vals; + } else { try { json.getString("value"); @@ -959,14 +967,11 @@ public List parseControlledVocabularyValue(DatasetFie } String strValue = json.getString("value", ""); ControlledVocabularyValue cvv = datasetFieldSvc.findControlledVocabularyValueByDatasetFieldTypeAndStrValue(cvvType, strValue, lenient); - if (cvv == null && !allowHarvestingMissingCVV) { + if (cvv == null) { throw new ControlledVocabularyException("Value '" + strValue + "' does not exist in type '" + cvvType.getName() + "'", cvvType, strValue); } - if (cvv != null) { - vals.add(cvv); - } + return Collections.singletonList(cvv); } - return vals; } catch (ClassCastException cce) { throw new JsonParseException("Invalid values submitted for " + cvvType.getName()); } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java index 71d4fc14ad5..9b83c4c1c9a 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java @@ -60,14 +60,12 @@ public static void setUpClass() { @AfterEach public void cleanup() { UtilIT.deleteSetting(SettingsServiceBean.Key.AllowHarvestingMissingCVV); - // Cleanup: delete the client if (clientApiPath != null) { Response deleteResponse = given() .header(UtilIT.API_TOKEN_HTTP_HEADER, adminUserAPIKey) .delete(clientApiPath); - System.out.println("deleteResponse.getStatusCode(): " + deleteResponse.getStatusCode()); - assertEquals(OK.getStatusCode(), deleteResponse.getStatusCode()); clientApiPath = null; + System.out.println("deleteResponse.getStatusCode(): " + deleteResponse.getStatusCode()); } } @@ -175,11 +173,11 @@ public void testCreateEditDeleteClient() throws InterruptedException { } @Test - public void testHarvestingClientRun_AllowHarvestingMissingCVV_True() throws InterruptedException { - harvestingClientRun(true); + public void testHarvestingClientRun_AllowHarvestingMissingCVV_False() throws InterruptedException { + harvestingClientRun(false); } @Test - public void testHarvestingClientRun_AllowHarvestingMissingCVV_False() throws InterruptedException { + public void testHarvestingClientRun_AllowHarvestingMissingCVV_True() throws InterruptedException { harvestingClientRun(false); } From 61fa5719e73c7d3605268cca0fe4670cac9f4439 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 19 Feb 2024 01:00:36 +0000 Subject: [PATCH 351/689] Fixed: includeDeaccessioned wrong behavior in getFileInfo --- .../edu/harvard/iq/dataverse/DataFile.java | 12 +-- .../edu/harvard/iq/dataverse/api/EditDDI.java | 2 +- .../edu/harvard/iq/dataverse/api/Files.java | 2 +- ...GetLatestPublishedFileMetadataCommand.java | 16 ++-- .../MakeDataCountLoggingServiceBean.java | 2 +- .../edu/harvard/iq/dataverse/api/FilesIT.java | 84 +++++++++++++++---- .../edu/harvard/iq/dataverse/api/UtilIT.java | 5 ++ 7 files changed, 89 insertions(+), 34 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataFile.java b/src/main/java/edu/harvard/iq/dataverse/DataFile.java index de13a83e204..25ec40de845 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataFile.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataFile.java @@ -567,14 +567,10 @@ public FileMetadata getLatestFileMetadata() { return resultFileMetadata; } - public FileMetadata getLatestPublishedFileMetadata(boolean includeDeaccessioned) throws UnsupportedOperationException { + public FileMetadata getLatestPublishedFileMetadata() throws UnsupportedOperationException { FileMetadata resultFileMetadata = fileMetadatas.stream() - .filter(metadata -> { - VersionState versionState = metadata.getDatasetVersion().getVersionState(); - return (!versionState.equals(VersionState.DRAFT) && - !(versionState.equals(VersionState.DEACCESSIONED) && !includeDeaccessioned)); - }) - .reduce(null, this::getTheNewerFileMetadata); + .filter(metadata -> !metadata.getDatasetVersion().getVersionState().equals(VersionState.DRAFT)) + .reduce(null, DataFile::getTheNewerFileMetadata); if (resultFileMetadata == null) { throw new UnsupportedOperationException("No published metadata version for DataFile " + this.getId()); @@ -583,7 +579,7 @@ public FileMetadata getLatestPublishedFileMetadata(boolean includeDeaccessioned) return resultFileMetadata; } - private FileMetadata getTheNewerFileMetadata(FileMetadata current, FileMetadata candidate) { + public static FileMetadata getTheNewerFileMetadata(FileMetadata current, FileMetadata candidate) { if (current == null) { return candidate; } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/EditDDI.java b/src/main/java/edu/harvard/iq/dataverse/api/EditDDI.java index d6aee0b7bfc..1b74ab5479e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/EditDDI.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/EditDDI.java @@ -124,7 +124,7 @@ public Response edit(@Context ContainerRequestContext crc, InputStream body, @Pa if (!latestVersion.isWorkingCopy()) { //for new draft version - FileMetadata latestFml = dataFile.getLatestPublishedFileMetadata(true); + FileMetadata latestFml = dataFile.getLatestPublishedFileMetadata(); boolean groupUpdate = newGroups(varGroupMap, latestFml); boolean varUpdate = varUpdates(mapVarToVarMet, latestFml, neededToUpdateVM, true); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index b7494c5daec..55d65bae96b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -580,7 +580,7 @@ public Response getFileMetadata(@Context ContainerRequestContext crc, @PathParam return error(BAD_REQUEST, BundleUtil.getStringFromBundle("files.api.no.draft")); } } else { - fm = df.getLatestPublishedFileMetadata(false); + fm = df.getLatestPublishedFileMetadata(); MakeDataCountLoggingServiceBean.MakeDataCountEntry entry = new MakeDataCountLoggingServiceBean.MakeDataCountEntry(uriInfo, headers, dvRequestService, df); mdcLogService.logEntry(entry); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedFileMetadataCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedFileMetadataCommand.java index ea58cd4e7eb..7c07766748c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedFileMetadataCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedFileMetadataCommand.java @@ -1,6 +1,7 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.FileMetadata; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; @@ -14,14 +15,11 @@ public GetLatestPublishedFileMetadataCommand(DataverseRequest request, DataFile @Override public FileMetadata execute(CommandContext ctxt) throws CommandException { - try { - FileMetadata fileMetadata = dataFile.getLatestPublishedFileMetadata(includeDeaccessioned); - if (isDatasetVersionAccessible(fileMetadata.getDatasetVersion(), dataFile.getOwner(), ctxt)) { - return fileMetadata; - } - return null; - } catch (UnsupportedOperationException e) { - return null; - } + return dataFile.getFileMetadatas().stream().filter(fileMetadata -> { + DatasetVersion.VersionState versionState = fileMetadata.getDatasetVersion().getVersionState(); + return (!versionState.equals(DatasetVersion.VersionState.DRAFT) + && !(versionState.equals(DatasetVersion.VersionState.DEACCESSIONED) && !includeDeaccessioned) + && isDatasetVersionAccessible(fileMetadata.getDatasetVersion(), dataFile.getOwner(), ctxt)); + }).reduce(null, DataFile::getTheNewerFileMetadata); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountLoggingServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountLoggingServiceBean.java index a3f09d190ca..5edf2fde0c3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountLoggingServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountLoggingServiceBean.java @@ -133,7 +133,7 @@ public MakeDataCountEntry(FacesContext fc, DataverseRequestServiceBean dvRequest //Exception thrown if no published metadata exists for DataFile //This is passed a DataFile to log the file downloaded. uriInfo and headers are passed in lieu of FacesContext public MakeDataCountEntry(UriInfo uriInfo, HttpHeaders headers, DataverseRequestServiceBean dvRequestService, DataFile df) throws UnsupportedOperationException{ - this(null, dvRequestService, df.getLatestPublishedFileMetadata(false).getDatasetVersion()); + this(null, dvRequestService, df.getLatestPublishedFileMetadata().getDatasetVersion()); if(uriInfo != null) { setRequestUrl(uriInfo.getRequestUri().toString()); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java index 4fb667d8955..4e1be85af56 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java @@ -1402,14 +1402,15 @@ public void testDataSizeInDataverse() throws InterruptedException { @Test public void testGetFileInfo() { Response createUser = UtilIT.createRandomUser(); - String username = UtilIT.getUsernameFromResponse(createUser); + String superUserUsername = UtilIT.getUsernameFromResponse(createUser); String superUserApiToken = UtilIT.getApiTokenFromResponse(createUser); - UtilIT.makeSuperUser(username); + UtilIT.makeSuperUser(superUserUsername); String dataverseAlias = createDataverseGetAlias(superUserApiToken); Integer datasetId = createDatasetGetId(dataverseAlias, superUserApiToken); createUser = UtilIT.createRandomUser(); - String apiTokenRegular = UtilIT.getApiTokenFromResponse(createUser); + String regularUsername = UtilIT.getUsernameFromResponse(createUser); + String regularApiToken = UtilIT.getApiTokenFromResponse(createUser); msg("Add a non-tabular file"); String pathToFile = "scripts/search/data/binary/trees.png"; @@ -1427,7 +1428,7 @@ public void testGetFileInfo() { .statusCode(OK.getStatusCode()); // Regular user should not get to see draft file data - getFileDataResponse = UtilIT.getFileData(dataFileId, apiTokenRegular); + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken); getFileDataResponse.then().assertThat() .statusCode(UNAUTHORIZED.getStatusCode()); @@ -1441,7 +1442,7 @@ public void testGetFileInfo() { .statusCode(OK.getStatusCode()); // Regular user should get to see published file data - getFileDataResponse = UtilIT.getFileData(dataFileId, apiTokenRegular); + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken); getFileDataResponse.then().assertThat() .statusCode(OK.getStatusCode()); @@ -1464,7 +1465,7 @@ public void testGetFileInfo() { .statusCode(OK.getStatusCode()); // Regular user should not get to see draft file data - getFileDataResponse = UtilIT.getFileData(dataFileId, apiTokenRegular, DS_VERSION_DRAFT); + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_DRAFT); getFileDataResponse.then().assertThat() .statusCode(UNAUTHORIZED.getStatusCode()); @@ -1481,13 +1482,13 @@ public void testGetFileInfo() { updateFileMetadataResponse.then().statusCode(OK.getStatusCode()); // Regular user should get to see latest published file data - getFileDataResponse = UtilIT.getFileData(dataFileId, apiTokenRegular, DS_VERSION_LATEST_PUBLISHED); + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST_PUBLISHED); getFileDataResponse.then().assertThat() .statusCode(OK.getStatusCode()) .body("data.label", equalTo(newFileNameFirstUpdate)); // Regular user should get to see latest published file data if latest is requested - getFileDataResponse = UtilIT.getFileData(dataFileId, apiTokenRegular, DS_VERSION_LATEST); + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST); getFileDataResponse.then().assertThat() .statusCode(OK.getStatusCode()) .body("data.label", equalTo(newFileNameFirstUpdate)); @@ -1504,25 +1505,80 @@ public void testGetFileInfo() { .statusCode(OK.getStatusCode()); // Regular user should get to see file data by specific version number - getFileDataResponse = UtilIT.getFileData(dataFileId, apiTokenRegular, "2.0"); + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, "2.0"); getFileDataResponse.then().assertThat() .statusCode(OK.getStatusCode()) .body("data.label", equalTo(newFileNameFirstUpdate)); - getFileDataResponse = UtilIT.getFileData(dataFileId, apiTokenRegular, "3.0"); + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, "3.0"); getFileDataResponse.then().assertThat() .statusCode(OK.getStatusCode()) .body("data.label", equalTo(newFileNameSecondUpdate)); + // The following tests cover cases where the dataset version is deaccessioned + Response deaccessionDatasetResponse = UtilIT.deaccessionDataset(datasetId, "3.0", "Test reason", null, superUserApiToken); + deaccessionDatasetResponse.then().assertThat().statusCode(OK.getStatusCode()); + + // Superuser should get to see file data if the latest version is deaccessioned filtering by latest and includeDeaccessioned is true + getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_LATEST, true); + getFileDataResponse.then().assertThat() + .body("data.label", equalTo(newFileNameSecondUpdate)) + .statusCode(OK.getStatusCode()); + + // Superuser should get to see version 2.0 file data if the latest version is deaccessioned filtering by latest and includeDeaccessioned is false + getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_LATEST, false); + getFileDataResponse.then().assertThat() + .body("data.label", equalTo(newFileNameFirstUpdate)) + .statusCode(OK.getStatusCode()); + + // Regular user should get to see version 2.0 file data if the latest version is deaccessioned filtering by latest and includeDeaccessioned is false + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST, true); + getFileDataResponse.then().assertThat() + .body("data.label", equalTo(newFileNameFirstUpdate)) + .statusCode(OK.getStatusCode()); + + // Update the file metadata + String newFileNameThirdUpdate = "trees_4.png"; + updateFileMetadata = Json.createObjectBuilder() + .add("label", newFileNameThirdUpdate); + updateFileMetadataResponse = UtilIT.updateFileMetadata(dataFileId, updateFileMetadata.build().toString(), superUserApiToken); + updateFileMetadataResponse.then().statusCode(OK.getStatusCode()); + + // Superuser should get to see draft file data if draft exists filtering by latest and includeDeaccessioned is true + getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_LATEST, true); + getFileDataResponse.then().assertThat() + .body("data.label", equalTo(newFileNameThirdUpdate)) + .statusCode(OK.getStatusCode()); + + // Regular user should get to see version 2.0 file data if the latest version is deaccessioned and draft exists filtering by latest and includeDeaccessioned is true + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST, true); + getFileDataResponse.then().assertThat() + .body("data.label", equalTo(newFileNameFirstUpdate)) + .statusCode(OK.getStatusCode()); + + // Publish dataset once again + publishDatasetResp = UtilIT.publishDatasetViaNativeApi(datasetId, "major", superUserApiToken); + publishDatasetResp.then().assertThat() + .statusCode(OK.getStatusCode()); + + // Regular user should get to see file data if the latest version is not deaccessioned filtering by latest and includeDeaccessioned is true + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST, true); + getFileDataResponse.then().assertThat() + .body("data.label", equalTo(newFileNameThirdUpdate)) + .statusCode(OK.getStatusCode()); + // Cleanup Response destroyDatasetResponse = UtilIT.destroyDataset(datasetId, superUserApiToken); - assertEquals(200, destroyDatasetResponse.getStatusCode()); + destroyDatasetResponse.then().assertThat().statusCode(OK.getStatusCode()); Response deleteDataverseResponse = UtilIT.deleteDataverse(dataverseAlias, superUserApiToken); - assertEquals(200, deleteDataverseResponse.getStatusCode()); + deleteDataverseResponse.then().assertThat().statusCode(OK.getStatusCode()); - Response deleteUserResponse = UtilIT.deleteUser(username); - assertEquals(200, deleteUserResponse.getStatusCode()); + Response deleteUserResponse = UtilIT.deleteUser(superUserUsername); + deleteUserResponse.then().assertThat().statusCode(OK.getStatusCode()); + + deleteUserResponse = UtilIT.deleteUser(regularUsername); + deleteUserResponse.then().assertThat().statusCode(OK.getStatusCode()); } @Test 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 a3d894c7a52..a63d0521a24 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -1092,8 +1092,13 @@ static Response getFileData(String fileId, String apiToken) { } static Response getFileData(String fileId, String apiToken, String datasetVersionId) { + return getFileData(fileId, apiToken, datasetVersionId, false); + } + + static Response getFileData(String fileId, String apiToken, String datasetVersionId, boolean includeDeaccessioned) { return given() .header(API_TOKEN_HTTP_HEADER, apiToken) + .queryParam("includeDeaccessioned", includeDeaccessioned) .get("/api/files/" + fileId + "/versions/" + datasetVersionId); } From d0b745499bfcb8da33575eea66f6aedfa220d849 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 19 Feb 2024 10:00:09 +0000 Subject: [PATCH 352/689] Added: IT testGetFileInfo cases --- .../edu/harvard/iq/dataverse/api/FilesIT.java | 79 ++++++++++++++++++- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java index 4e1be85af56..ad86127e231 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java @@ -1420,9 +1420,10 @@ public void testGetFileInfo() { // Superuser should get to see draft file data String dataFileId = addResponse.getBody().jsonPath().getString("data.files[0].dataFile.id"); Response getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken); + String newFileName = "trees.png"; getFileDataResponse.then().assertThat() - .body("data.label", equalTo("trees.png")) - .body("data.dataFile.filename", equalTo("trees.png")) + .body("data.label", equalTo(newFileName)) + .body("data.dataFile.filename", equalTo(newFileName)) .body("data.dataFile.contentType", equalTo("image/png")) .body("data.dataFile.filesize", equalTo(8361)) .statusCode(OK.getStatusCode()); @@ -1444,7 +1445,8 @@ public void testGetFileInfo() { // Regular user should get to see published file data getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken); getFileDataResponse.then().assertThat() - .statusCode(OK.getStatusCode()); + .statusCode(OK.getStatusCode()) + .body("data.label", equalTo(newFileName)); // The following tests cover cases where a version ID is specified in the endpoint // Superuser should not get to see draft file data when no draft version exists @@ -1452,6 +1454,12 @@ public void testGetFileInfo() { getFileDataResponse.then().assertThat() .statusCode(NOT_FOUND.getStatusCode()); + // Regular user should get to see file data from specific version filtering by tag + getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, "1.0"); + getFileDataResponse.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.label", equalTo(newFileName)); + // Update the file metadata String newFileNameFirstUpdate = "trees_2.png"; JsonObjectBuilder updateFileMetadata = Json.createObjectBuilder() @@ -1525,18 +1533,63 @@ public void testGetFileInfo() { .body("data.label", equalTo(newFileNameSecondUpdate)) .statusCode(OK.getStatusCode()); + // Superuser should get to see file data if the latest version is deaccessioned filtering by latest published and includeDeaccessioned is true + getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_LATEST_PUBLISHED, true); + getFileDataResponse.then().assertThat() + .body("data.label", equalTo(newFileNameSecondUpdate)) + .statusCode(OK.getStatusCode()); + // Superuser should get to see version 2.0 file data if the latest version is deaccessioned filtering by latest and includeDeaccessioned is false getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_LATEST, false); getFileDataResponse.then().assertThat() .body("data.label", equalTo(newFileNameFirstUpdate)) .statusCode(OK.getStatusCode()); - // Regular user should get to see version 2.0 file data if the latest version is deaccessioned filtering by latest and includeDeaccessioned is false + // Superuser should get to see version 2.0 file data if the latest version is deaccessioned filtering by latest published and includeDeaccessioned is false + getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_LATEST_PUBLISHED, false); + getFileDataResponse.then().assertThat() + .body("data.label", equalTo(newFileNameFirstUpdate)) + .statusCode(OK.getStatusCode()); + + // Superuser should get to see file data from specific deaccessioned version filtering by tag and includeDeaccessioned is true + getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, "3.0", true); + getFileDataResponse.then().assertThat() + .body("data.label", equalTo(newFileNameSecondUpdate)) + .statusCode(OK.getStatusCode()); + + // Superuser should not get to see file data from specific deaccessioned version filtering by tag and includeDeaccessioned is false + getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, "3.0", false); + getFileDataResponse.then().assertThat() + .statusCode(NOT_FOUND.getStatusCode()); + + // Regular user should get to see version 2.0 file data if the latest version is deaccessioned filtering by latest and includeDeaccessioned is true getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST, true); getFileDataResponse.then().assertThat() .body("data.label", equalTo(newFileNameFirstUpdate)) .statusCode(OK.getStatusCode()); + // Regular user should get to see version 2.0 file data if the latest version is deaccessioned filtering by latest published and includeDeaccessioned is true + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST_PUBLISHED, true); + getFileDataResponse.then().assertThat() + .body("data.label", equalTo(newFileNameFirstUpdate)) + .statusCode(OK.getStatusCode()); + + // Regular user should get to see version 2.0 file data if the latest version is deaccessioned filtering by latest published and includeDeaccessioned is false + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST_PUBLISHED, false); + getFileDataResponse.then().assertThat() + .body("data.label", equalTo(newFileNameFirstUpdate)) + .statusCode(OK.getStatusCode()); + + // Regular user should not get to see file data from specific deaccessioned version filtering by tag and includeDeaccessioned is true + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, "3.0", true); + getFileDataResponse.then().assertThat() + .statusCode(NOT_FOUND.getStatusCode()); + + // Regular user should not get to see file data from specific deaccessioned version filtering by tag and includeDeaccessioned is false + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, "3.0", false); + getFileDataResponse.then().assertThat() + .statusCode(NOT_FOUND.getStatusCode()); + // Update the file metadata String newFileNameThirdUpdate = "trees_4.png"; updateFileMetadata = Json.createObjectBuilder() @@ -1550,12 +1603,24 @@ public void testGetFileInfo() { .body("data.label", equalTo(newFileNameThirdUpdate)) .statusCode(OK.getStatusCode()); + // Superuser should get to see latest published file data if draft exists filtering by latest published and includeDeaccessioned is true + getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_LATEST_PUBLISHED, true); + getFileDataResponse.then().assertThat() + .body("data.label", equalTo(newFileNameSecondUpdate)) + .statusCode(OK.getStatusCode()); + // Regular user should get to see version 2.0 file data if the latest version is deaccessioned and draft exists filtering by latest and includeDeaccessioned is true getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST, true); getFileDataResponse.then().assertThat() .body("data.label", equalTo(newFileNameFirstUpdate)) .statusCode(OK.getStatusCode()); + // Regular user should get to see version 2.0 file data if the latest version is deaccessioned and draft exists filtering by latest published and includeDeaccessioned is true + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST_PUBLISHED, true); + getFileDataResponse.then().assertThat() + .body("data.label", equalTo(newFileNameFirstUpdate)) + .statusCode(OK.getStatusCode()); + // Publish dataset once again publishDatasetResp = UtilIT.publishDatasetViaNativeApi(datasetId, "major", superUserApiToken); publishDatasetResp.then().assertThat() @@ -1567,6 +1632,12 @@ public void testGetFileInfo() { .body("data.label", equalTo(newFileNameThirdUpdate)) .statusCode(OK.getStatusCode()); + // Regular user should get to see file data if the latest version is not deaccessioned filtering by latest published and includeDeaccessioned is true + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST_PUBLISHED, true); + getFileDataResponse.then().assertThat() + .body("data.label", equalTo(newFileNameThirdUpdate)) + .statusCode(OK.getStatusCode()); + // Cleanup Response destroyDatasetResponse = UtilIT.destroyDataset(datasetId, superUserApiToken); destroyDatasetResponse.then().assertThat().statusCode(OK.getStatusCode()); From a267adc66ceb02cd5864d7bf40d4637838c1c1d6 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 19 Feb 2024 10:38:32 +0000 Subject: [PATCH 353/689] Removed: commented code in json printer for DatasetVersion --- .../iq/dataverse/util/json/JsonPrinter.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) 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 2eaf6b64579..93e214159cf 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 @@ -356,9 +356,6 @@ public static JsonObjectBuilder json(DatasetVersion dsv, boolean includeFiles) { } public static JsonObjectBuilder json(DatasetVersion dsv, List anonymizedFieldTypeNamesList, boolean includeFiles) { - /* return json(dsv, null, includeFiles, null); - } - public static JsonObjectBuilder json(DatasetVersion dsv, List anonymizedFieldTypeNamesList, boolean includeFiles, Long numberOfFiles) {*/ Dataset dataset = dsv.getDataset(); JsonObjectBuilder bld = jsonObjectBuilder() .add("id", dsv.getId()).add("datasetId", dataset.getId()) @@ -374,8 +371,7 @@ public static JsonObjectBuilder json(DatasetVersion dsv, List anonymized .add("alternativePersistentId", dataset.getAlternativePersistentIdentifier()) .add("publicationDate", dataset.getPublicationDateFormattedYYYYMMDD()) .add("citationDate", dataset.getCitationDateFormattedYYYYMMDD()); - //.add("numberOfFiles", numberOfFiles); - + License license = DatasetUtil.getLicense(dsv); if (license != null) { bld.add("license", jsonLicense(dsv)); @@ -593,6 +589,18 @@ public static JsonObjectBuilder json(DatasetFieldType fld) { return fieldsBld; } + /* + + versionId: number +displayName: string +versionNumber: {majorNumber?: number, minorNumber?: number} +publishingStatus: string +citation: string +isLatest: boolean +isInReview: boolean +latestVersionPublishingStatus: string + */ + public static JsonObjectBuilder json(FileMetadata fmd) { return jsonObjectBuilder() // deprecated: .add("category", fmd.getCategory()) From ff2e86c19a232ac9821d045e284bb0f27bcd909b Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 19 Feb 2024 11:38:03 +0000 Subject: [PATCH 354/689] Added: returnDatasetVersion optional parameter to getFileInfo API endpoint --- .../edu/harvard/iq/dataverse/api/Files.java | 14 +++--- .../iq/dataverse/util/json/JsonPrinter.java | 27 +++++++---- .../edu/harvard/iq/dataverse/api/FilesIT.java | 47 ++++++++++++------- .../edu/harvard/iq/dataverse/api/UtilIT.java | 5 +- 4 files changed, 59 insertions(+), 34 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index fa8332c6138..d07950d5c37 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -49,10 +49,7 @@ import jakarta.ejb.EJB; import jakarta.ejb.EJBException; import jakarta.inject.Inject; -import jakarta.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonString; -import jakarta.json.JsonValue; +import jakarta.json.*; import jakarta.json.stream.JsonParsingException; import jakarta.servlet.http.HttpServletResponse; import jakarta.ws.rs.*; @@ -489,9 +486,10 @@ public Response updateFileMetadata(@Context ContainerRequestContext crc, @FormDa public Response getFileData(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @QueryParam("includeDeaccessioned") boolean includeDeaccessioned, + @QueryParam("returnDatasetVersion") boolean returnDatasetVersion, @Context UriInfo uriInfo, @Context HttpHeaders headers) { - return response( req -> getFileDataResponse(req, fileIdOrPersistentId, DS_VERSION_LATEST, includeDeaccessioned, uriInfo, headers), getRequestUser(crc)); + return response( req -> getFileDataResponse(req, fileIdOrPersistentId, DS_VERSION_LATEST, includeDeaccessioned, returnDatasetVersion, uriInfo, headers), getRequestUser(crc)); } @GET @@ -501,15 +499,17 @@ public Response getFileData(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @PathParam("datasetVersionId") String datasetVersionId, @QueryParam("includeDeaccessioned") boolean includeDeaccessioned, + @QueryParam("returnDatasetVersion") boolean returnDatasetVersion, @Context UriInfo uriInfo, @Context HttpHeaders headers) { - return response( req -> getFileDataResponse(req, fileIdOrPersistentId, datasetVersionId, includeDeaccessioned, uriInfo, headers), getRequestUser(crc)); + return response( req -> getFileDataResponse(req, fileIdOrPersistentId, datasetVersionId, includeDeaccessioned, returnDatasetVersion, uriInfo, headers), getRequestUser(crc)); } private Response getFileDataResponse(final DataverseRequest req, String fileIdOrPersistentId, String datasetVersionId, boolean includeDeaccessioned, + boolean returnDatasetVersion, UriInfo uriInfo, HttpHeaders headers) throws WrappedResponse { final DataFile dataFile = execCommand(new GetDataFileCommand(req, findDataFileOrDie(fileIdOrPersistentId))); @@ -546,7 +546,7 @@ public Command handleLatestPublished() { return Response.ok(Json.createObjectBuilder() .add("status", ApiConstants.STATUS_OK) - .add("data", json(fileMetadata)).build()) + .add("data", json(fileMetadata, returnDatasetVersion)).build()) .type(MediaType.APPLICATION_JSON) .build(); } 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 93e214159cf..df93727a666 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 @@ -602,28 +602,38 @@ public static JsonObjectBuilder json(DatasetFieldType fld) { */ public static JsonObjectBuilder json(FileMetadata fmd) { - return jsonObjectBuilder() + return json(fmd, false); + } + + public static JsonObjectBuilder json(FileMetadata fmd, boolean printDatasetVersion) { + NullSafeJsonBuilder builder = jsonObjectBuilder() // deprecated: .add("category", fmd.getCategory()) - // TODO: uh, figure out what to do here... it's deprecated - // in a sense that there's no longer the category field in the - // fileMetadata object; but there are now multiple, oneToMany file + // TODO: uh, figure out what to do here... it's deprecated + // in a sense that there's no longer the category field in the + // fileMetadata object; but there are now multiple, oneToMany file // categories - and we probably need to export them too!) -- L.A. 4.5 - // DONE: catgegories by name + // DONE: catgegories by name .add("description", fmd.getDescription()) .add("label", fmd.getLabel()) // "label" is the filename - .add("restricted", fmd.isRestricted()) + .add("restricted", fmd.isRestricted()) .add("directoryLabel", fmd.getDirectoryLabel()) .add("version", fmd.getVersion()) .add("datasetVersionId", fmd.getDatasetVersion().getId()) .add("categories", getFileCategories(fmd)) .add("dataFile", JsonPrinter.json(fmd.getDataFile(), fmd, false)); + + if (printDatasetVersion) { + builder.add("datasetVersion", json(fmd.getDatasetVersion(), false)); + } + + return builder; } - public static JsonObjectBuilder json(AuxiliaryFile auxFile) { + public static JsonObjectBuilder json(AuxiliaryFile auxFile) { return jsonObjectBuilder() .add("formatTag", auxFile.getFormatTag()) .add("formatVersion", auxFile.getFormatVersion()) // "label" is the filename - .add("origin", auxFile.getOrigin()) + .add("origin", auxFile.getOrigin()) .add("isPublic", auxFile.getIsPublic()) .add("type", auxFile.getType()) .add("contentType", auxFile.getContentType()) @@ -631,6 +641,7 @@ public static JsonObjectBuilder json(AuxiliaryFile auxFile) { .add("checksum", auxFile.getChecksum()) .add("dataFile", JsonPrinter.json(auxFile.getDataFile())); } + public static JsonObjectBuilder json(DataFile df) { return JsonPrinter.json(df, null, false); } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java index d436b4129c4..125240b76b7 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java @@ -1530,65 +1530,65 @@ public void testGetFileInfo() { deaccessionDatasetResponse.then().assertThat().statusCode(OK.getStatusCode()); // Superuser should get to see file data if the latest version is deaccessioned filtering by latest and includeDeaccessioned is true - getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_LATEST, true); + getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_LATEST, true, false); getFileDataResponse.then().assertThat() .body("data.label", equalTo(newFileNameSecondUpdate)) .statusCode(OK.getStatusCode()); // Superuser should get to see file data if the latest version is deaccessioned filtering by latest published and includeDeaccessioned is true - getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_LATEST_PUBLISHED, true); + getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_LATEST_PUBLISHED, true, false); getFileDataResponse.then().assertThat() .body("data.label", equalTo(newFileNameSecondUpdate)) .statusCode(OK.getStatusCode()); // Superuser should get to see version 2.0 file data if the latest version is deaccessioned filtering by latest and includeDeaccessioned is false - getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_LATEST, false); + getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_LATEST, false, false); getFileDataResponse.then().assertThat() .body("data.label", equalTo(newFileNameFirstUpdate)) .statusCode(OK.getStatusCode()); // Superuser should get to see version 2.0 file data if the latest version is deaccessioned filtering by latest published and includeDeaccessioned is false - getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_LATEST_PUBLISHED, false); + getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_LATEST_PUBLISHED, false, false); getFileDataResponse.then().assertThat() .body("data.label", equalTo(newFileNameFirstUpdate)) .statusCode(OK.getStatusCode()); // Superuser should get to see file data from specific deaccessioned version filtering by tag and includeDeaccessioned is true - getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, "3.0", true); + getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, "3.0", true, false); getFileDataResponse.then().assertThat() .body("data.label", equalTo(newFileNameSecondUpdate)) .statusCode(OK.getStatusCode()); // Superuser should not get to see file data from specific deaccessioned version filtering by tag and includeDeaccessioned is false - getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, "3.0", false); + getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, "3.0", false, false); getFileDataResponse.then().assertThat() .statusCode(NOT_FOUND.getStatusCode()); // Regular user should get to see version 2.0 file data if the latest version is deaccessioned filtering by latest and includeDeaccessioned is true - getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST, true); + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST, true, false); getFileDataResponse.then().assertThat() .body("data.label", equalTo(newFileNameFirstUpdate)) .statusCode(OK.getStatusCode()); // Regular user should get to see version 2.0 file data if the latest version is deaccessioned filtering by latest published and includeDeaccessioned is true - getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST_PUBLISHED, true); + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST_PUBLISHED, true, false); getFileDataResponse.then().assertThat() .body("data.label", equalTo(newFileNameFirstUpdate)) .statusCode(OK.getStatusCode()); // Regular user should get to see version 2.0 file data if the latest version is deaccessioned filtering by latest published and includeDeaccessioned is false - getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST_PUBLISHED, false); + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST_PUBLISHED, false, false); getFileDataResponse.then().assertThat() .body("data.label", equalTo(newFileNameFirstUpdate)) .statusCode(OK.getStatusCode()); // Regular user should not get to see file data from specific deaccessioned version filtering by tag and includeDeaccessioned is true - getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, "3.0", true); + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, "3.0", true, false); getFileDataResponse.then().assertThat() .statusCode(NOT_FOUND.getStatusCode()); // Regular user should not get to see file data from specific deaccessioned version filtering by tag and includeDeaccessioned is false - getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, "3.0", false); + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, "3.0", false, false); getFileDataResponse.then().assertThat() .statusCode(NOT_FOUND.getStatusCode()); @@ -1600,25 +1600,25 @@ public void testGetFileInfo() { updateFileMetadataResponse.then().statusCode(OK.getStatusCode()); // Superuser should get to see draft file data if draft exists filtering by latest and includeDeaccessioned is true - getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_LATEST, true); + getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_LATEST, true, false); getFileDataResponse.then().assertThat() .body("data.label", equalTo(newFileNameThirdUpdate)) .statusCode(OK.getStatusCode()); // Superuser should get to see latest published file data if draft exists filtering by latest published and includeDeaccessioned is true - getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_LATEST_PUBLISHED, true); + getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_LATEST_PUBLISHED, true, false); getFileDataResponse.then().assertThat() .body("data.label", equalTo(newFileNameSecondUpdate)) .statusCode(OK.getStatusCode()); // Regular user should get to see version 2.0 file data if the latest version is deaccessioned and draft exists filtering by latest and includeDeaccessioned is true - getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST, true); + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST, true, false); getFileDataResponse.then().assertThat() .body("data.label", equalTo(newFileNameFirstUpdate)) .statusCode(OK.getStatusCode()); // Regular user should get to see version 2.0 file data if the latest version is deaccessioned and draft exists filtering by latest published and includeDeaccessioned is true - getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST_PUBLISHED, true); + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST_PUBLISHED, true, false); getFileDataResponse.then().assertThat() .body("data.label", equalTo(newFileNameFirstUpdate)) .statusCode(OK.getStatusCode()); @@ -1629,17 +1629,30 @@ public void testGetFileInfo() { .statusCode(OK.getStatusCode()); // Regular user should get to see file data if the latest version is not deaccessioned filtering by latest and includeDeaccessioned is true - getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST, true); + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST, true, false); getFileDataResponse.then().assertThat() .body("data.label", equalTo(newFileNameThirdUpdate)) .statusCode(OK.getStatusCode()); // Regular user should get to see file data if the latest version is not deaccessioned filtering by latest published and includeDeaccessioned is true - getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST_PUBLISHED, true); + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, DS_VERSION_LATEST_PUBLISHED, true, false); getFileDataResponse.then().assertThat() .body("data.label", equalTo(newFileNameThirdUpdate)) .statusCode(OK.getStatusCode()); + // The following tests cover cases where the user requests to include the dataset version information in the response + // User should get to see dataset version info in the response if returnDatasetVersion is true + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, "1.0", false, true); + getFileDataResponse.then().assertThat() + .body("data.datasetVersion.versionState", equalTo("RELEASED")) + .statusCode(OK.getStatusCode()); + + // User should not get to see dataset version info in the response if returnDatasetVersion is false + getFileDataResponse = UtilIT.getFileData(dataFileId, regularApiToken, "1.0", false, false); + getFileDataResponse.then().assertThat() + .body("data.datasetVersion", equalTo(null)) + .statusCode(OK.getStatusCode()); + // Cleanup Response destroyDatasetResponse = UtilIT.destroyDataset(datasetId, superUserApiToken); destroyDatasetResponse.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 410401514b1..9d728688f5f 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -1092,13 +1092,14 @@ static Response getFileData(String fileId, String apiToken) { } static Response getFileData(String fileId, String apiToken, String datasetVersionId) { - return getFileData(fileId, apiToken, datasetVersionId, false); + return getFileData(fileId, apiToken, datasetVersionId, false, false); } - static Response getFileData(String fileId, String apiToken, String datasetVersionId, boolean includeDeaccessioned) { + static Response getFileData(String fileId, String apiToken, String datasetVersionId, boolean includeDeaccessioned, boolean returnDatasetVersion) { return given() .header(API_TOKEN_HTTP_HEADER, apiToken) .queryParam("includeDeaccessioned", includeDeaccessioned) + .queryParam("returnDatasetVersion", returnDatasetVersion) .get("/api/files/" + fileId + "/versions/" + datasetVersionId); } From d4eedc2288f35a8be8ce25f8e48d20fac85aecdb Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 19 Feb 2024 12:01:00 +0000 Subject: [PATCH 355/689] Added: extended docs for Get JSON Representation of a File --- doc/sphinx-guides/source/api/native-api.rst | 38 +++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 4038ec4340d..3a0731d8c3f 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -2724,6 +2724,8 @@ Get JSON Representation of a File .. note:: Files can be accessed using persistent identifiers. This is done by passing the constant ``:persistentId`` where the numeric id of the file is expected, and then passing the actual persistent id as a query parameter with the name ``persistentId``. +This endpoint returns the file metadata present in the latest dataset version. + Example: Getting the file whose DOI is *10.5072/FK2/J8SJZB*: .. code-block:: bash @@ -2790,6 +2792,42 @@ The fully expanded example above (without environment variables) looks like this The file id can be extracted from the response retrieved from the API which uses the persistent identifier (``/api/datasets/:persistentId/?persistentId=$PERSISTENT_IDENTIFIER``). +By default, files from deaccessioned dataset versions are not included in the search. If no accessible dataset draft version exists, the search of the latest published file will ignore dataset deaccessioned versions unless ``includeDeaccessioned`` query parameter is set to ``true``. + +Usage example: + +.. code-block:: bash + + export SERVER_URL=https://demo.dataverse.org + export PERSISTENT_IDENTIFIER=doi:10.5072/FK2/J8SJZB + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/files/:persistentId/?persistentId=$PERSISTENT_IDENTIFIER&includeDeaccessioned=true" + +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/files/:persistentId/?persistentId=doi:10.5072/FK2/J8SJZB&includeDeaccessioned=true" + +If you want to include the dataset version of the file in the response, there is an optional parameter for this called ``returnDatasetVersion`` whose default value is ``false``. + +Usage example: + +.. code-block:: bash + + export SERVER_URL=https://demo.dataverse.org + export PERSISTENT_IDENTIFIER=doi:10.5072/FK2/J8SJZB + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/files/:persistentId/?persistentId=$PERSISTENT_IDENTIFIER&returnDatasetVersion=true" + +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/files/:persistentId/?persistentId=doi:10.5072/FK2/J8SJZB&returnDatasetVersion=true" + Adding Files ~~~~~~~~~~~~ From ab60747d339aeef63cb4100d4407deec30f7aef5 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 19 Feb 2024 12:36:34 +0000 Subject: [PATCH 356/689] Added: docs for Get JSON Representation of a File given a Dataset Version --- doc/sphinx-guides/source/api/native-api.rst | 64 +++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 3a0731d8c3f..3d33be1ca45 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -2828,6 +2828,70 @@ 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/files/:persistentId/?persistentId=doi:10.5072/FK2/J8SJZB&returnDatasetVersion=true" +Get JSON Representation of a File given a Dataset Version +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. note:: Files can be accessed using persistent identifiers. This is done by passing the constant ``:persistentId`` where the numeric id of the file is expected, and then passing the actual persistent id as a query parameter with the name ``persistentId``. + +This endpoint returns the file metadata present in the requested dataset version. To specify the dataset version, you can use ``:latest-published``, or ``:latest``, or ``:draft`` or ``1.0`` or any other style listed under :ref:`dataset-version-specifiers`. + +Example: Getting the file whose DOI is *10.5072/FK2/J8SJZB* present in the published dataset version ``1.0``: + +.. code-block:: bash + + export SERVER_URL=https://demo.dataverse.org + export PERSISTENT_IDENTIFIER=doi:10.5072/FK2/J8SJZB + export DATASET_VERSION=1.0 + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/files/:persistentId/versions/$DATASET_VERSION?persistentId=$PERSISTENT_IDENTIFIER" + +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/files/:persistentId/versions/1.0?persistentId=doi:10.5072/FK2/J8SJZB" + +You may obtain a not found error depending on whether or not the specified version exists or you have permission to view it. + +By default, files from deaccessioned dataset versions are not included in the search unless ``includeDeaccessioned`` query parameter is set to ``true``. + +Usage example: + +.. code-block:: bash + + export SERVER_URL=https://demo.dataverse.org + export PERSISTENT_IDENTIFIER=doi:10.5072/FK2/J8SJZB + export DATASET_VERSION=:latest-published + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/files/:persistentId/versions/$DATASET_VERSION?persistentId=$PERSISTENT_IDENTIFIER&includeDeaccessioned=true" + +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/files/:persistentId/versions/:latest-published?persistentId=doi:10.5072/FK2/J8SJZB&includeDeaccessioned=true" + +If you want to include the dataset version of the file in the response, there is an optional parameter for this called ``returnDatasetVersion`` whose default value is ``false``. + +Usage example: + +.. code-block:: bash + + export SERVER_URL=https://demo.dataverse.org + export PERSISTENT_IDENTIFIER=doi:10.5072/FK2/J8SJZB + export DATASET_VERSION=:draft + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/files/:persistentId/versions/$DATASET_VERSION?persistentId=$PERSISTENT_IDENTIFIER&returnDatasetVersion=true" + +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/files/:persistentId/versions/:draft?persistentId=doi:10.5072/FK2/J8SJZB&returnDatasetVersion=true" + Adding Files ~~~~~~~~~~~~ From e5dbfa1510950bc8b0cb37bbf226cff0722938c8 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 19 Feb 2024 12:44:28 +0000 Subject: [PATCH 357/689] Added: release notes for #10280 --- doc/release-notes/10280-get-file-api-extension.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 doc/release-notes/10280-get-file-api-extension.md diff --git a/doc/release-notes/10280-get-file-api-extension.md b/doc/release-notes/10280-get-file-api-extension.md new file mode 100644 index 00000000000..fcca0afd78b --- /dev/null +++ b/doc/release-notes/10280-get-file-api-extension.md @@ -0,0 +1,8 @@ +The API endpoint `api/files/{id}` has been extended to support the following optional query parameters: + +- `includeDeaccessioned`: Indicates whether or not to consider deaccessioned dataset versions in the latest file search. (Default: `false`). +- `returnDatasetVersion`: Indicates whether or not to include the dataset version of the file in the response. (Default: `false`). + +A new endpoint `api/files/{id}/versions/{datasetVersionId}` has been created. This endpoint returns the file metadata present in the requested dataset version. To specify the dataset version, you can use ``:latest-published``, or ``:latest``, or ``:draft`` or ``1.0`` or any other available version identifier. + +The endpoint supports the `includeDeaccessioned` and `returnDatasetVersion` optional query parameters, as does the `api/files/{id}` endpoint. From 084fa3219a7bbe609f6180eba50f380d9a450247 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 20 Feb 2024 08:25:48 +0100 Subject: [PATCH 358/689] chore(test): remove leftover JUnit 4 rules --- src/test/java/org/junit/rules/TestRule.java | 11 ----------- src/test/java/org/junit/runners/model/Statement.java | 11 ----------- 2 files changed, 22 deletions(-) delete mode 100644 src/test/java/org/junit/rules/TestRule.java delete mode 100644 src/test/java/org/junit/runners/model/Statement.java diff --git a/src/test/java/org/junit/rules/TestRule.java b/src/test/java/org/junit/rules/TestRule.java deleted file mode 100644 index 4f94d8e6922..00000000000 --- a/src/test/java/org/junit/rules/TestRule.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.junit.rules; - -/** - * "Fake" class used as a replacement for Junit4-dependent classes. - * See more at: - * GenericContainer run from Jupiter tests shouldn't require JUnit 4.x library on runtime classpath - * . - */ -@SuppressWarnings("unused") -public interface TestRule { -} diff --git a/src/test/java/org/junit/runners/model/Statement.java b/src/test/java/org/junit/runners/model/Statement.java deleted file mode 100644 index b80ca0abc86..00000000000 --- a/src/test/java/org/junit/runners/model/Statement.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.junit.runners.model; - -/** - * "Fake" class used as a replacement for Junit4-dependent classes. - * See more at: - * GenericContainer run from Jupiter tests shouldn't require JUnit 4.x library on runtime classpath - * . - */ -@SuppressWarnings("unused") -public class Statement { -} From 4d3904f66f20bc78a0fba718557543fa694280a2 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 20 Feb 2024 08:30:15 +0100 Subject: [PATCH 359/689] test(mail): verify SMTP over SSL/TLS works Adding an integration test with self-signed certificates to enable verification SMTP over SSL works. --- .../dataverse/util/MailSessionProducerIT.java | 60 +++++++++++++++++++ src/test/resources/mail/cert.pem | 24 ++++++++ src/test/resources/mail/key.pem | 28 +++++++++ 3 files changed, 112 insertions(+) create mode 100644 src/test/resources/mail/cert.pem create mode 100644 src/test/resources/mail/key.pem diff --git a/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java b/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java index 8280578a343..c4893652153 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java @@ -23,6 +23,7 @@ import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.MountableFile; import java.util.Map; @@ -118,6 +119,65 @@ void createSession() { } + @Nested + @LocalJvmSettings + @JvmSetting(key = JvmSettings.MAIL_MTA_HOST, method = "tcSmtpHost") + @JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, method = "tcSmtpPort", varArgs = "port") + @JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, varArgs = "ssl.enable", value = "true") + @JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, varArgs = "ssl.trust", value = "*") + class WithSSLWithoutAuthentication { + @Container + static GenericContainer maildev = new GenericContainer<>("maildev/maildev:2.1.0") + .withCopyFileToContainer(MountableFile.forClasspathResource("mail/cert.pem"), "/cert.pem") + .withCopyFileToContainer(MountableFile.forClasspathResource("mail/key.pem"), "/key.pem") + .withExposedPorts(PORT_HTTP, PORT_SMTP) + .withEnv(Map.of( + "MAILDEV_INCOMING_SECURE", "true", + "MAILDEV_INCOMING_CERT", "/cert.pem", + "MAILDEV_INCOMING_KEY", "/key.pem" + )) + .waitingFor(Wait.forHttp("/")); + + static String tcSmtpHost() { + return maildev.getHost(); + } + + static String tcSmtpPort() { + return maildev.getMappedPort(PORT_SMTP).toString(); + } + + @BeforeAll + static void setup() { + RestAssured.baseURI = "http://" + tcSmtpHost(); + RestAssured.port = maildev.getMappedPort(PORT_HTTP); + } + + @Test + void createSession() { + given().when().get("/email") + .then() + .statusCode(200) + .body("size()", is(0)); + + // given + Session session = new MailSessionProducer().getSession(); + MailServiceBean mailer = new MailServiceBean(session, settingsServiceBean); + + // when + boolean sent = mailer.sendSystemEmail("test@example.org", "Test", "Test", false); + + // then + assertTrue(sent); + //RestAssured.get("/email").body().prettyPrint(); + given().when().get("/email") + .then() + .statusCode(200) + .body("size()", is(1)) + .body("[0].subject", equalTo("Test")); + } + + } + static final String username = "testuser"; static final String password = "supersecret"; diff --git a/src/test/resources/mail/cert.pem b/src/test/resources/mail/cert.pem new file mode 100644 index 00000000000..6115183d413 --- /dev/null +++ b/src/test/resources/mail/cert.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEFTCCAv0CFAIjr/AvBVg4EX5/rk5+eFdfsquOMA0GCSqGSIb3DQEBCwUAMIHG +MQswCQYDVQQGEwJEVjEaMBgGA1UECAwRRGF0YXZlcnNlIENvdW50cnkxFzAVBgNV +BAcMDkRhdGF2ZXJzZSBDaXR5MS4wLAYDVQQKDCVHbG9iYWwgRGF0YXZlcnNlIENv +bW11bml0eSBDb25zb3J0aXVtMRswGQYDVQQLDBJUZXN0aW5nIERlcGFydG1lbnQx +FDASBgNVBAMMC2V4YW1wbGUub3JnMR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1w +bGUub3JnMB4XDTI0MDIyMDA3MTkxOVoXDTM0MDIxNzA3MTkxOVowgcYxCzAJBgNV +BAYTAkRWMRowGAYDVQQIDBFEYXRhdmVyc2UgQ291bnRyeTEXMBUGA1UEBwwORGF0 +YXZlcnNlIENpdHkxLjAsBgNVBAoMJUdsb2JhbCBEYXRhdmVyc2UgQ29tbXVuaXR5 +IENvbnNvcnRpdW0xGzAZBgNVBAsMElRlc3RpbmcgRGVwYXJ0bWVudDEUMBIGA1UE +AwwLZXhhbXBsZS5vcmcxHzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5vcmcw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzQ55QKM/sVJMb9c5MKtc/ +YW3+MlCrCnGlo42DCjl6noZg8Gji4dOEMo29UcRtYqhOsx7HOXZ5ulj3YKiBfzht ++QV/ZofhMIN9F/N5XCi4MRPorFz+mPck5NDzH1SqYn5zGm5APPqFJlwBWxDKEfqe +6ir5gG91MzHHuJJSQq3nrSDq+/DXRwg/7L2O7da6pBqti7nYU0T5ql88nddkRhR8 +7NdeZndI+UVmkcnal/3ZpybW8ZNzpiP8nCJO3ASz9kXRC3cITS0zgKxl6USDZs+8 +NAM6R0r8icB89L+i8bOfbyU7nkN9T+xUTTOmalSmsYrMIedIBmcB7NuqbXPLEpeJ +AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAA4U/uhswbeJB0gX4vfVqYf30A131Rvu +J4eaVrVLzuByP1R0MvbBCMMYZBlDVDhiFqRh4KdoVWBvTfxf/4McYZ1FhXkgRlOb +mv/mxVBqnXEu5msviApYmoLzMqgd91F3T4CWs66QIWVTJYh2McRKLG0+IfGp3aox +YKC/W2RPsUO2fKFnUDkYetXMuWg1KJYKuqE6u2lcoV3uHFphXplClnlwN+IwtWWY +cgfNBBRpwx6RXTk2XXgpCKYRBthBu1rowp7qiAwX7R5am6wDx0EIbevfR32bDReX +oAV8c9soJWwAUwH63jqq7KTO8Dg1oGHveZMk4HHGkCqZeGCjbDPaak4= +-----END CERTIFICATE----- diff --git a/src/test/resources/mail/key.pem b/src/test/resources/mail/key.pem new file mode 100644 index 00000000000..84d34efdce8 --- /dev/null +++ b/src/test/resources/mail/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzQ55QKM/sVJMb +9c5MKtc/YW3+MlCrCnGlo42DCjl6noZg8Gji4dOEMo29UcRtYqhOsx7HOXZ5ulj3 +YKiBfzht+QV/ZofhMIN9F/N5XCi4MRPorFz+mPck5NDzH1SqYn5zGm5APPqFJlwB +WxDKEfqe6ir5gG91MzHHuJJSQq3nrSDq+/DXRwg/7L2O7da6pBqti7nYU0T5ql88 +nddkRhR87NdeZndI+UVmkcnal/3ZpybW8ZNzpiP8nCJO3ASz9kXRC3cITS0zgKxl +6USDZs+8NAM6R0r8icB89L+i8bOfbyU7nkN9T+xUTTOmalSmsYrMIedIBmcB7Nuq +bXPLEpeJAgMBAAECggEAQ3h3TQ9XVslsRxFIsLVNJ49JoWuZng7DwIai3AfMo4Cn +7jN+HqrFfBO08mUkq9D+rQRQ2MYhd+Zx1sXcFkVmXUnlTlKuYMzsKHiLzIkp0E20 +gxXguHilSI8Qr/kCWlDQ7AyuI2JwHg5WgbIfSxbiP86+FwNGsBNxMI0hEXIEV1ZY +OFXO6AWO63D4zwbwMT30k8cjfyjGvjEtoGmjnBJcrJLSADCIWLcFCw+Cm8vcRkCd +BEpfRzeEos/NVdOqCpi1ea3OkGAY94mXxz6gaFRbeJFj9b6st7oVZLBOiMx1eafH +hgB9JkfVtDogl9B13MkqRN8WAiOgAjIo2Ukq8x1ZkwKBgQD88sdh8k1eldO9UXG1 +BjEsB2mEnzp1hvjuRlMQtnvOjDakbqozzbNQlq9YJxocphLyUPM/BKTsIGp0SPpd +vo0lgspDJ5eLnHd/Xf/guYvKg90NsHZR6V7hf9Z4JcrwrwvXpf7Lp/m95Jwd930j +/kPXw25gRFmpJ8Q9ciIk0PF0NwKBgQC1bUTK8iarZHhDGnR+/AhjkfSnb0z725Qb +w7MYRvicRNWT0wnk3njMMfXYS0rbxw7O5LlSoyCf+n6dGtHqJWCS1+lYuCjCz1vr +hMVFbpcEhob0OAhg8YMgzQRsmeJcBm8slVEOrmmVhQQZPRBjAaQw2f6cjW/ZhzZd +JHSiDw3yPwKBgQDLSleB2Zni3al56v3mzh4w05gzVUFHeX2RCoXx1ad1He1AhAxY +bAakSyaLQ4nR4osxomuMhzAA8iB8araFJwMLVa03AZfjRZIolCR0uMqnrQi42syN +EnEF7JcyorUScKyk2S0JAmxN+HCcCO7TQaPGwbNwvR4OO/6Un6jfS+nySwKBgH6n +4bashkJwyWRPO7TKzjB03I9nLB9Hk4YugQEZysWNaGzij62vgjVLS43MQl5cAQJ+ +usHuEACfJ3UWHCWSInFhOg4twob9q/YnonBuXA9UuzITTAYhlKF5fvUyGMyV0VcW +hpfxOtSfH9Vew+naY32XMiCovMTnmBQ+Nw5L5DiRAoGAV5/JT4z57Y+8npBCRr1m +NJZBXjQ8rmjYBCs+jOQ48wK2mEgcgARIgVGgi9MZZ2BUFHPThGS1o4OYE+fdqD95 +bvg1XInVpNwebLP6UZa9xZ8oGd3Auxfsav1WJB+CZo2tOX5Qt+GnwiumEr3Dlf1d +UVXDNM5A/sl1IDL3T3IEdSw= +-----END PRIVATE KEY----- From 53e964ae68b227793cde00774108adeef586eebb Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 20 Feb 2024 08:30:50 +0100 Subject: [PATCH 360/689] style(mail): update deprecation tags for DV v6.2 --- .../harvard/iq/dataverse/settings/SettingsServiceBean.java | 2 +- .../edu/harvard/iq/dataverse/util/MailSessionProducer.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java index 45189ac6c3a..63566b62395 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java @@ -232,7 +232,7 @@ public enum Key { * @deprecated Please replace usages with {@link edu.harvard.iq.dataverse.MailServiceBean#getSystemAddress}, * which is backward compatible with this setting. */ - @Deprecated(since = "6.1", forRemoval = true) + @Deprecated(since = "6.2", forRemoval = true) SystemEmail, /* size limit for Tabular data file ingests */ /* (can be set separately for specific ingestable formats; in which diff --git a/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java b/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java index 25f5970274e..13fedb94014 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java @@ -49,7 +49,7 @@ public class MailSessionProducer { * No direct JNDI lookup on the field to avoid deployment failures when not present. * @deprecated This should be removed with the next major release of Dataverse, as it would be a breaking change. */ - @Deprecated(forRemoval = true, since = "6.1") + @Deprecated(forRemoval = true, since = "6.2") Session appserverProvidedSession; public MailSessionProducer() { @@ -124,7 +124,7 @@ Properties getMailProperties() { * @return True if injected as resource from app server, false otherwise * @deprecated This is supposed to be removed when {@link #appserverProvidedSession} is removed. */ - @Deprecated(forRemoval = true, since = "6.1") + @Deprecated(forRemoval = true, since = "6.2") public boolean hasSessionFromAppServer() { return this.appserverProvidedSession != null; } From abcb131e79bd7f16ee8e003b8d7bf2e33f3e0259 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 20 Feb 2024 08:32:11 +0100 Subject: [PATCH 361/689] style(settings): ignore SonarCube rule S115 for DB settings The DB settings names are not compliant with usual Java enum name rules. Ignoring to avoid unnecessary clutter, hiding more important problems. --- .../edu/harvard/iq/dataverse/settings/SettingsServiceBean.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java index 63566b62395..864307d536f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java @@ -45,6 +45,7 @@ public class SettingsServiceBean { * over your shoulder when typing strings in various places of a large app. * So there. */ + @SuppressWarnings("java:S115") public enum Key { AllowApiTokenLookupViaApi, /** From b0d268d281331549f5c8b9ef17f760131686d079 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 20 Feb 2024 10:39:06 +0100 Subject: [PATCH 362/689] doc(settings): add section on secure password storage in security section The section about securing your installation was missing hints about how to store and access passwords in a safe manner. Now having a single place to reference from everywhere makes the config bits for passwords much more readable, as we do not need to provide as many examples. --- .../source/installation/config.rst | 121 ++++++++++-------- 1 file changed, 71 insertions(+), 50 deletions(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index c233e594fa7..32c61009524 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -88,6 +88,51 @@ See the :ref:`payara` section of :doc:`prerequisites` for details and init scrip Related to this is that you should remove ``/root/.payara/pass`` to ensure that Payara isn't ever accidentally started as root. Without the password, Payara won't be able to start as root, which is a good thing. +.. _secure-password-storage: + +Secure Password Storage +^^^^^^^^^^^^^^^^^^^^^^^ + +In development or demo scenarios, we suggest not to store passwords in files permanently. +We recommend the use of at least environment variables or production-grade mechanisms to supply passwords. + +In a production setup, permanently storing passwords as plaintext should be avoided at all cost. +Environment variables are dangerous in shared environments and containers, as they may be easily exploited; we suggest not to use them. +Depending on your deployment model and environment, you can make use of the following techniques to securely store and access passwords. + +**Password Aliases** + +A `password alias`_ allows you to have a plaintext reference to an encrypted password stored on the server, with the alias being used wherever the password is needed. +This method is especially useful in a classic deployment, as it does not require any external secrets management. + +Password aliases are consumable as a MicroProfile Config source and can be referrenced by their name in a `property expression`_. +You may also reference them within a `variable substitution`_, e.g. in your ``domain.xml``. + +Creation example for an alias named *my.alias.name*: + +.. code-block:: shell + + echo "AS_ADMIN_ALIASPASSWORD=changeme" > /tmp/p.txt + asadmin create-password-alias --passwordfile "/tmp/p.txt" "my.alias.name" + rm /tmp/p.txt + +Note: omitting the ``--passwordfile`` parameter allows creating the alias in an interactive fashion with a prompt. + +**Secrets Files** + +Payara has a builtin MicroProfile Config source to consume values from files in a directory on your filesystem. +This `directory config source`_ is most useful and secure with external secrets management in place, temporarily mounting cleartext passwords as files. +Examples are Kubernetes / OpenShift `Secrets `_ or tools like `Vault Agent `_. + +Please follow the `directory config source`_ documentation to learn about its usage. + +**Cloud Providers** + +Running Dataverse on a cloud platform or running an external secret management system like `Vault `_ enables accessing secrets without any intermediate storage of cleartext. +Obviously this is the most secure option for any deployment model, but it may require more resources to set up and maintain - your mileage may vary. + +Take a look at `cloud sources`_ shipped with Payara to learn about their usage. + Enforce Strong Passwords for User Accounts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -365,16 +410,8 @@ Basic Database Settings 1. Any of these settings can be set via system properties (see :ref:`jvm-options` starting at :ref:`dataverse.db.name`), environment variables or other MicroProfile Config mechanisms supported by the app server. `See Payara docs for supported sources `_. -2. Remember to protect your secrets. For passwords, use an environment variable (bare minimum), a password alias named the same - as the key (OK) or use the "dir config source" of Payara (best). - - Alias creation example: - - .. code-block:: shell - - echo "AS_ADMIN_ALIASPASSWORD=changeme" > /tmp/p.txt - asadmin create-password-alias --passwordfile /tmp/p.txt dataverse.db.password - rm /tmp/p.txt +2. Remember to protect your secrets. + See :ref:`secure-password-storage` for more information. 3. Environment variables follow the key, replacing any dot, colon, dash, etc. into an underscore "_" and all uppercase letters. Example: ``dataverse.db.host`` -> ``DATAVERSE_DB_HOST`` @@ -603,6 +640,8 @@ Then create a password alias by running (without changes): The second command will trigger an interactive prompt asking you to input your Swift password. +Note: you may choose a different way to secure this password, depending on your use case. See :ref:`secure-password-storage` for more options. + Second, update the JVM option ``dataverse.files.storage-driver-id`` by running the delete command: ``./asadmin $ASADMIN_OPTS delete-jvm-options "\-Ddataverse.files.storage-driver-id=file"`` @@ -872,9 +911,8 @@ Optionally, you may provide static credentials for each S3 storage using MicroPr You may provide the values for these via any `supported MicroProfile Config API source`_. **WARNING:** - *For security, do not use the sources "environment variable" or "system property" (JVM option) in a production context!* -*Rely on password alias, secrets directory or cloud based sources instead!* +*Rely on password alias, secrets directory or cloud based sources as described at* :ref:`secure-password-storage` *instead!* **NOTE:** @@ -1946,15 +1984,9 @@ dataverse.db.password The PostgreSQL users password to connect with. -Preferrably use a JVM alias, as passwords in environment variables aren't safe. - -.. code-block:: shell - - echo "AS_ADMIN_ALIASPASSWORD=change-me-super-secret" > /tmp/password.txt - asadmin create-password-alias --passwordfile /tmp/password.txt dataverse.db.password - rm /tmp/password.txt +See :ref:`secure-password-storage` to learn about options to securely store this password. -Can also be set via *MicroProfile Config API* sources, e.g. the environment variable ``DATAVERSE_DB_PASSWORD``. +Can also be set via *MicroProfile Config API* sources, e.g. the environment variable ``DATAVERSE_DB_PASSWORD`` (although you shouldn't use environment variables for passwords). dataverse.db.host +++++++++++++++++ @@ -2201,14 +2233,7 @@ Once you have a username from DataCite, you can enter it like this: dataverse.pid.datacite.password +++++++++++++++++++++++++++++++ -Once you have a password from your provider, you should create a password alias. -This avoids storing it in clear text, although you could use a JVM option `to reference -a different place `__. - -``./asadmin create-password-alias dataverse.pid.datacite.password`` - -It will allow you to enter the password while not echoing the characters. -To manage these, read up on `Payara docs about password aliases `__. +Once you have a password from your provider, you should create a password alias called *dataverse.pid.datacite.password* or use another method described at :ref:`secure-password-storage` to safeguard it. **Notes:** @@ -2219,7 +2244,7 @@ To manage these, read up on `Payara docs about password aliases `. Provide a passphrase to decrypt the :ref:`private key file `. +See :ref:`secure-password-storage` for ways to do this securely. The key file may (and should) be encrypted with a passphrase (used for encryption with AES-128). See also chapter 1.4 "Authentication" of the @@ -2260,10 +2286,10 @@ encryption with AES-128). See also chapter 1.4 "Authentication" of the Can also be set via *MicroProfile Config API* sources, e.g. the environment variable ``DATAVERSE_PID_HANDLENET_KEY_PASSPHRASE`` (although you shouldn't use -environment variables for passwords). This setting was formerly known as -``dataverse.handlenet.admprivphrase`` and has been renamed. You should delete -the old JVM option and the wrapped password alias, then recreate as shown for -:ref:`dataverse.pid.datacite.password` but with this option as alias name. +environment variables for passwords). + +This setting was formerly known as ``dataverse.handlenet.admprivphrase`` and has been renamed. +You should delete the old JVM option and the wrapped password alias, then recreate as shown for :ref:`dataverse.pid.datacite.password` but with this option as alias name. .. _dataverse.pid.handlenet.index: @@ -2457,20 +2483,11 @@ The key used to sign a URL is created from the API token of the creating user pl signature-secret makes it impossible for someone who knows an API token from forging signed URLs and provides extra security by making the overall signing key longer. -Since the signature-secret is sensitive, you should treat it like a password. Here is an example how to set your shared secret -with the secure method "password alias": +**WARNING**: +*Since the signature-secret is sensitive, you should treat it like a password.* +*See* :ref:`secure-password-storage` *to learn about ways to safeguard it.* -.. code-block:: shell - - echo "AS_ADMIN_ALIASPASSWORD=change-me-super-secret" > /tmp/password.txt - asadmin create-password-alias --passwordfile /tmp/password.txt dataverse.api.signature-secret - rm /tmp/password.txt - -Can also be set via any `supported MicroProfile Config API source`_, e.g. the environment variable -``DATAVERSE_API_SIGNATURE_SECRET``. - -**WARNING:** For security, do not use the sources "environment variable" or "system property" (JVM option) in a -production context! Rely on password alias, secrets directory or cloud based sources instead! +Can also be set via any `supported MicroProfile Config API source`_, e.g. the environment variable ``DATAVERSE_API_SIGNATURE_SECRET`` (although you shouldn't use environment variables for passwords) . .. _dataverse.api.allow-incomplete-metadata: @@ -4147,10 +4164,7 @@ A true(default)/false option determining whether datafiles listed on the dataset :AllowUserManagementOfOrder +++++++++++++++++++++++++++ -A true/false (default) option determining whether the dataset datafile table display includes checkboxes enabling users to turn folder ordering and/or category ordering (if an order is defined by :CategoryOrder) on and off dynamically. - -.. _supported MicroProfile Config API source: https://docs.payara.fish/community/docs/Technical%20Documentation/MicroProfile/Config/Overview.html - +A true/false (default) option determining whether the dataset datafile table display includes checkboxes enabling users to turn folder ordering and/or category ordering (if an order is defined by :CategoryOrder) on and off dynamically. .. _:UseStorageQuotas: @@ -4173,3 +4187,10 @@ tab. files saved with these headers on S3 - since they no longer have to be generated and added to the streamed file on the fly. The setting is ``false`` by default, preserving the legacy behavior. + +.. _supported MicroProfile Config API source: https://docs.payara.fish/community/docs/Technical%20Documentation/MicroProfile/Config/Overview.html +.. _password alias: https://docs.payara.fish/community/docs/Technical%20Documentation/Payara%20Server%20Documentation/Server%20Configuration%20And%20Management/Configuration%20Options/Password%20Aliases.html +.. _variable substitution: https://docs.payara.fish/community/docs/Technical%20Documentation/Payara%20Server%20Documentation/Server%20Configuration%20And%20Management/Configuration%20Options/Variable%20Substitution/Usage%20of%20Variables.html +.. _property expression: https://download.eclipse.org/microprofile/microprofile-config-3.1/microprofile-config-spec-3.1.html#property-expressions +.. _directory config source: https://docs.payara.fish/community/docs/Technical%20Documentation/MicroProfile/Config/Directory.html +.. _cloud sources: https://docs.payara.fish/community/docs/Technical%20Documentation/MicroProfile/Config/Cloud/Overview.html \ No newline at end of file From ffd69e5cbf7f90f921e9d386c04fe3cfa062104e Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 20 Feb 2024 12:59:48 +0000 Subject: [PATCH 363/689] Added: #10280 release note tweak --- doc/release-notes/10280-get-file-api-extension.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/release-notes/10280-get-file-api-extension.md b/doc/release-notes/10280-get-file-api-extension.md index fcca0afd78b..7ed70e93dc9 100644 --- a/doc/release-notes/10280-get-file-api-extension.md +++ b/doc/release-notes/10280-get-file-api-extension.md @@ -6,3 +6,5 @@ The API endpoint `api/files/{id}` has been extended to support the following opt A new endpoint `api/files/{id}/versions/{datasetVersionId}` has been created. This endpoint returns the file metadata present in the requested dataset version. To specify the dataset version, you can use ``:latest-published``, or ``:latest``, or ``:draft`` or ``1.0`` or any other available version identifier. The endpoint supports the `includeDeaccessioned` and `returnDatasetVersion` optional query parameters, as does the `api/files/{id}` endpoint. + +`api/files/{id}/draft` endpoint is no longer available in favor of the new endpoint `api/files/{id}/versions/{datasetVersionId}`, which can use the version identifier ``:draft`` (`api/files/{id}/versions/:draft`) to obtain the same result. From 7ff5d6a35647cb76d9fe98279dd9021952a9849b Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 20 Feb 2024 08:49:25 -0500 Subject: [PATCH 364/689] adding to test --- .../edu/harvard/iq/dataverse/util/json/JsonParser.java | 8 +++++--- .../edu/harvard/iq/dataverse/api/HarvestingClientsIT.java | 7 +++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java index ac7b6bb4067..4287cab069b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java @@ -70,6 +70,7 @@ public class JsonParser { SettingsServiceBean settingsService; LicenseServiceBean licenseService; HarvestingClient harvestingClient = null; + boolean allowHarvestingMissingCVV = false; /** * if lenient, we will accept alternate spellings for controlled vocabulary values @@ -93,6 +94,8 @@ public JsonParser(DatasetFieldServiceBean datasetFieldSvc, MetadataBlockServiceB this.settingsService = settingsService; this.licenseService = licenseService; this.harvestingClient = harvestingClient; + this.allowHarvestingMissingCVV = harvestingClient != null && + settingsService.isTrueForKey(SettingsServiceBean.Key.AllowHarvestingMissingCVV, false); } public JsonParser() { @@ -739,10 +742,9 @@ public DatasetField parseField(JsonObject json, Boolean testType) throws JsonPar ret.setDatasetFieldType(type); // If Harvesting, CVV values may differ between the Dataverse installations, so we won't enforce them - if (harvestingClient != null && type.isControlledVocabulary() && - settingsService.isTrueForKey(SettingsServiceBean.Key.AllowHarvestingMissingCVV, false)) { + if (allowHarvestingMissingCVV && type.isControlledVocabulary()) { type.setAllowControlledVocabulary(false); - logger.warning("Harvesting: Skipping Controlled Vocabulary. Treating values as primitives"); + logger.info("Harvesting: Skipping Controlled Vocabulary. Treating values as primitives"); } if (type.isCompound()) { diff --git a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java index 9b83c4c1c9a..375eb92a6ab 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java @@ -3,8 +3,7 @@ import java.util.logging.Logger; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import io.restassured.RestAssured; import static io.restassured.RestAssured.given; @@ -19,7 +18,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.api.BeforeAll; /** * This class tests Harvesting Client functionality. @@ -29,6 +27,7 @@ * /api/harvest/clients/ api to run an actual harvest of a control set and * then validate the resulting harvested content. */ +@TestMethodOrder(MethodOrderer.MethodName.class) public class HarvestingClientsIT { private static final Logger logger = Logger.getLogger(HarvestingClientsIT.class.getCanonicalName()); @@ -178,7 +177,7 @@ public void testHarvestingClientRun_AllowHarvestingMissingCVV_False() throws In } @Test public void testHarvestingClientRun_AllowHarvestingMissingCVV_True() throws InterruptedException { - harvestingClientRun(false); + harvestingClientRun(true); } private void harvestingClientRun(boolean allowHarvestingMissingCVV) throws InterruptedException { From f690c47100f216810133ca308ea147dc6ad93ee1 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 20 Feb 2024 15:43:07 +0100 Subject: [PATCH 365/689] feat(installer): make installer use new way to apply mail MTA config Instead of setting a DB setting, we now simply apply system properties. Also, aligned with the way the "from" address is now bound to be the system mail address, this commit removes this subtle difference in the installer as well. --- scripts/installer/as-setup.sh | 16 ++++++++++------ scripts/installer/install.py | 8 -------- scripts/installer/installAppServer.py | 5 +++-- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/scripts/installer/as-setup.sh b/scripts/installer/as-setup.sh index fc5b378cff5..f169dfa5333 100755 --- a/scripts/installer/as-setup.sh +++ b/scripts/installer/as-setup.sh @@ -146,12 +146,10 @@ function final_setup(){ # delete any existing mail/notifyMailSession; configure port, if provided: ./asadmin delete-javamail-resource mail/notifyMailSession - - if [ $SMTP_SERVER_PORT"x" != "x" ] - then - ./asadmin $ASADMIN_OPTS create-javamail-resource --mailhost "$SMTP_SERVER" --mailuser "dataversenotify" --fromaddress "do-not-reply@${HOST_ADDRESS}" --property mail.smtp.port="${SMTP_SERVER_PORT}" mail/notifyMailSession - else - ./asadmin $ASADMIN_OPTS create-javamail-resource --mailhost "$SMTP_SERVER" --mailuser "dataversenotify" --fromaddress "do-not-reply@${HOST_ADDRESS}" mail/notifyMailSession + ./asadmin $ASADMIN_OPTS create-system-properties "dataverse.mail.system-email='${ADMIN_EMAIL}'" + ./asadmin $ASADMIN_OPTS create-system-properties "dataverse.mail.mta.host='${SMTP_SERVER}'" + if [ "x${SMTP_SERVER_PORT}" != "x" ]; then + ./asadmin $ASADMIN_OPTS create-system-properties "dataverse.mail.mta.port='${SMTP_SERVER_PORT}'" fi } @@ -279,6 +277,12 @@ if [ ! -d "$DOMAIN_DIR" ] exit 2 fi +if [ -z "$ADMIN_EMAIL" ] + then + echo "You must specify the system admin email address (ADMIN_EMAIL)." + exit 1 +fi + echo "Setting up your app. server (Payara) to support Dataverse" echo "Payara directory: "$GLASSFISH_ROOT echo "Domain directory: "$DOMAIN_DIR diff --git a/scripts/installer/install.py b/scripts/installer/install.py index 18995695638..2bad29c780e 100644 --- a/scripts/installer/install.py +++ b/scripts/installer/install.py @@ -568,14 +568,6 @@ except: sys.exit("Failure to execute setup-all.sh! aborting.") -# 7b. configure admin email in the application settings -print("configuring system email address...") -returnCode = subprocess.call(["curl", "-X", "PUT", "-d", adminEmail, apiUrl+"/admin/settings/:SystemEmail"]) -if returnCode != 0: - print("\nWARNING: failed to configure the admin email in the Dataverse settings!") -else: - print("\ndone.") - # 8c. configure remote Solr location, if specified if solrLocation != "LOCAL": print("configuring remote Solr location... ("+solrLocation+")") diff --git a/scripts/installer/installAppServer.py b/scripts/installer/installAppServer.py index 698f5ba9a58..7636490c583 100644 --- a/scripts/installer/installAppServer.py +++ b/scripts/installer/installAppServer.py @@ -6,8 +6,9 @@ def runAsadminScript(config): # commands to set up all the app. server (payara6) components for the application. # All the parameters must be passed to that script as environmental # variables: - os.environ['GLASSFISH_DOMAIN'] = "domain1"; - os.environ['ASADMIN_OPTS'] = ""; + os.environ['GLASSFISH_DOMAIN'] = "domain1" + os.environ['ASADMIN_OPTS'] = "" + os.environ['ADMIN_EMAIL'] = config.get('system','ADMIN_EMAIL') os.environ['HOST_ADDRESS'] = config.get('glassfish','HOST_DNS_ADDRESS') os.environ['GLASSFISH_ROOT'] = config.get('glassfish','GLASSFISH_DIRECTORY') From 98244256529959a37b98986226ad71f4cc2b9bcf Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 20 Feb 2024 15:46:12 +0100 Subject: [PATCH 366/689] doc(mail): add mail config paragraphs #7424 --- .../source/installation/config.rst | 154 ++++++++++++++++-- 1 file changed, 142 insertions(+), 12 deletions(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 32c61009524..1d23f9a1277 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -2520,13 +2520,37 @@ See :ref:`discovery-sign-posting` for details. Can also be set via any `supported MicroProfile Config API source`_, e.g. the environment variable ``DATAVERSE_SIGNPOSTING_LEVEL1_ITEM_LIMIT``. +.. _systemEmail: +.. _dataverse.mail.system-email: + +dataverse.mail.system-email ++++++++++++++++++++++++++++ + +This is the email address that "system" emails are sent from such as password reset links, notifications, etc. +It replaces the database setting :ref:`legacySystemEmail` since Dataverse 6.2. + +**WARNING**: Your Dataverse installation will not send mail without this setting in place. + +Note that only the email address is required, which you can supply without the ``<`` and ``>`` signs, but if you include the text, it's the way to customize the name of your support team, which appears in the "from" address in emails as well as in help text in the UI. +If you don't include the text, the installation name (see :ref:`Branding Your Installation`) will appear in the "from" address. +In case you want your system email address to of no-reply style, have a look at :ref:`dataverse.mail.support-email` setting, too. + +Please note that if you're having any trouble sending email, you can refer to "Troubleshooting" under :doc:`installation-main`. + +Can also be set via any `supported MicroProfile Config API source`_, e.g. the environment variable ``DATAVERSE_MAIL_SYSTEM_EMAIL``. + +.. _dataverse.mail.support-email: + dataverse.mail.support-email ++++++++++++++++++++++++++++ -This provides an email address distinct from the :ref:`systemEmail` that will be used as the email address for Contact Forms and Feedback API. This address is used as the To address when the Contact form is launched from the Support entry in the top navigation bar and, if configured via :ref:`dataverse.mail.cc-support-on-contact-email`, as a CC address when the form is launched from a Dataverse/Dataset Contact button. -This allows configuration of a no-reply email address for :ref:`systemEmail` while allowing feedback to go to/be cc'd to the support email address, which would normally accept replies. If not set, the :ref:`systemEmail` is used for the feedback API/contact form email. +This provides an email address distinct from the :ref:`systemEmail` that will be used as the email address for Contact Forms and Feedback API. +This address is used as the To address when the Contact form is launched from the Support entry in the top navigation bar and, if configured via :ref:`dataverse.mail.cc-support-on-contact-email`, as a CC address when the form is launched from a Dataverse/Dataset Contact button. +This allows configuration of a no-reply email address for :ref:`systemEmail` while allowing feedback to go to/be cc'd to the support email address, which would normally accept replies. +If not set, the :ref:`systemEmail` is used for the feedback API/contact form email. -Note that only the email address is required, which you can supply without the ``<`` and ``>`` signs, but if you include the text, it's the way to customize the name of your support team, which appears in the "from" address in emails as well as in help text in the UI. If you don't include the text, the installation name (see :ref:`Branding Your Installation`) will appear in the "from" address. +Note that only the email address is required, which you can supply without the ``<`` and ``>`` signs, but if you include the text, it's the way to customize the name of your support team, which appears in the "from" address in emails as well as in help text in the UI. +If you don't include the text, the installation name (see :ref:`Branding Your Installation`) will appear in the "from" address. Can also be set via any `supported MicroProfile Config API source`_, e.g. the environment variable ``DATAVERSE_MAIL_SUPPORT_EMAIL``. @@ -2535,12 +2559,123 @@ Can also be set via any `supported MicroProfile Config API source`_, e.g. the en dataverse.mail.cc-support-on-contact-email ++++++++++++++++++++++++++++++++++++++++++ -If this setting is true, the contact forms and feedback API will cc the system (:SupportEmail if set, :SystemEmail if not) when sending email to the collection, dataset, or datafile contacts. +If this boolean setting is true, the contact forms and feedback API will cc the system (``dataverse.mail.support-mail`` if set, ``dataverse.mail.system-email`` if not) when sending email to the collection, dataset, or datafile contacts. A CC line is added to the contact form when this setting is true so that users are aware that the cc will occur. The default is false. Can also be set via *MicroProfile Config API* sources, e.g. the environment variable ``DATAVERSE_MAIL_CC_SUPPORT_ON_CONTACT_EMAIL``. +dataverse.mail.debug +++++++++++++++++++++ + +When this boolean setting is true, sending an email will generate more verbose logging, enabling you to analyze mail delivery malfunctions. +Defaults to ``false``. + +Can also be set via *MicroProfile Config API* sources, e.g. the environment variable ``DATAVERSE_MAIL_DEBUG``. + +.. _dataverse.mail.mta: + +dataverse.mail.mta.* +++++++++++++++++++++ + +The following options allow you to configure a target Mail Transfer Agent (MTA) to be used for sending emails to users. +Be advised: as the mail server connection (session) is cached once created, you need to restart Payara when applying configuration changes. + +All can also be set via any `supported MicroProfile Config API source`_, e.g. the environment variable ``DATAVERSE_MAIL_MTA_HOST``. +(For environment variables: simply replace "." and "-" with "_" and write as all caps.) + +The following table describes the most important settings commonly used. + +.. list-table:: + :widths: 15 60 25 + :header-rows: 1 + :align: left + + * - Setting Key + - Description + - Default Value + * - ``dataverse.mail.mta.host`` + - The SMTP server to connect to. + - | *No default* + | (``smtp`` in our :ref:`Dataverse container `) + * - ``dataverse.mail.mta.port`` + - The SMTP server port to connect to. + - ``25`` + * - ``dataverse.mail.mta.auth`` + - If ``true``, attempt to authenticate the user using the AUTH command. + - ``false`` + * - ``dataverse.mail.mta.user`` + - The username to use in an AUTH command. + - *No default* + * - ``dataverse.mail.mta.password`` + - The password to use in an AUTH command. (Might be a token when using XOAUTH2 mechanism) + - *No default* + * - ``dataverse.mail.mta.allow-utf8-addresses`` + - If set to ``true``, UTF-8 strings are allowed in message headers, e.g., in addresses. + This should only be set if the mail server also supports UTF-8. + (Quoted from `Jakarta Mail Javadoc `_) + Setting to ``false`` will also make mail address validation in UI/API fail on UTF-8 chars. + - ``true`` + +**WARNING**: +*For security of your password use only safe ways to store and access it.* +*See* :ref:`secure-password-storage` *to learn about your options.* + +Find below a list of even more options you can use to configure sending mails. +Detailed description for every setting can be found in the table included within the `Jakarta Mail Documentation `_. +(Simply replace ``dataverse.mail.mta.`` with ``mail.smtp.``.) + +* Timeouts: + ``dataverse.mail.mta.connectiontimeout``, + ``dataverse.mail.mta.timeout``, + ``dataverse.mail.mta.writetimeout`` +* SSL/TLS: + ``dataverse.mail.mta.starttls.enable``, + ``dataverse.mail.mta.starttls.required``, + ``dataverse.mail.mta.ssl.enable``, + ``dataverse.mail.mta.ssl.checkserveridentity``, + ``dataverse.mail.mta.ssl.trust``, + ``dataverse.mail.mta.ssl.protocols``, + ``dataverse.mail.mta.ssl.ciphersuites`` +* Proxy Connection: + ``dataverse.mail.mta.proxy.host``, + ``dataverse.mail.mta.proxy.port``, + ``dataverse.mail.mta.proxy.user``, + ``dataverse.mail.mta.proxy.password``, + ``dataverse.mail.mta.socks.host``, + ``dataverse.mail.mta.socks.port`` +* SMTP EHLO command details: + ``dataverse.mail.mta.ehlo``, + ``dataverse.mail.mta.localhost``, + ``dataverse.mail.mta.localaddress``, + ``dataverse.mail.mta.localport`` +* Authentication details: + ``dataverse.mail.mta.auth.mechanisms``, + ``dataverse.mail.mta.auth.login.disable``, + ``dataverse.mail.mta.auth.plain.disable``, + ``dataverse.mail.mta.auth.digest-md5.disable``, + ``dataverse.mail.mta.auth.ntlm.disable``, + ``dataverse.mail.mta.auth.xoauth2.disable``, + ``dataverse.mail.mta.auth.ntlm.domain``, + ``dataverse.mail.mta.auth.ntlm.flag``, + ``dataverse.mail.mta.sasl.enable``, + ``dataverse.mail.mta.sasl.usecanonicalhostname``, + ``dataverse.mail.mta.sasl.mechanisms``, + ``dataverse.mail.mta.sasl.authorizationid``, + ``dataverse.mail.mta.sasl.realm`` +* Miscellaneous: + ``dataverse.mail.mta.allow8bitmime``, + ``dataverse.mail.mta.submitter``, + ``dataverse.mail.mta.dsn.notify``, + ``dataverse.mail.mta.dsn.ret``, + ``dataverse.mail.mta.sendpartial``, + ``dataverse.mail.mta.quitwait``, + ``dataverse.mail.mta.quitonsessionreject``, + ``dataverse.mail.mta.userset``, + ``dataverse.mail.mta.noop.strict``, + ``dataverse.mail.mta.mailextension`` + + dataverse.ui.allow-review-for-incomplete ++++++++++++++++++++++++++++++++++++++++ @@ -2763,18 +2898,13 @@ In Dataverse Software 4.7 and lower, the :doc:`/api/search` required an API toke ``curl -X PUT -d true http://localhost:8080/api/admin/settings/:SearchApiRequiresToken`` -.. _systemEmail: +.. _legacySystemEmail: :SystemEmail ++++++++++++ -This is the email address that "system" emails are sent from such as password reset links. Your Dataverse installation will not send mail without this setting in place. - -``curl -X PUT -d 'LibraScholar SWAT Team ' http://localhost:8080/api/admin/settings/:SystemEmail`` - -Note that only the email address is required, which you can supply without the ``<`` and ``>`` signs, but if you include the text, it's the way to customize the name of your support team, which appears in the "from" address in emails as well as in help text in the UI. If you don't include the text, the installation name (see :ref:`Branding Your Installation`) will appear in the "from" address. - -Please note that if you're having any trouble sending email, you can refer to "Troubleshooting" under :doc:`installation-main`. +Please note that this setting is deprecated since Dataverse 6.2. +It will be picked up for backward compatibility, but please migrate to usage of :ref:`dataverse.mail.system-email`. :HomePageCustomizationFile ++++++++++++++++++++++++++ From 5dcaba9ee23179f43d72b91693f2591ee58a6d17 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 20 Feb 2024 15:47:10 +0100 Subject: [PATCH 367/689] doc(mail): rewrite install docs to match new way of mail config #7424 --- .../source/installation/installation-main.rst | 51 ++++++------------- 1 file changed, 16 insertions(+), 35 deletions(-) diff --git a/doc/sphinx-guides/source/installation/installation-main.rst b/doc/sphinx-guides/source/installation/installation-main.rst index 46c1b0b0af3..d9ae650e37a 100755 --- a/doc/sphinx-guides/source/installation/installation-main.rst +++ b/doc/sphinx-guides/source/installation/installation-main.rst @@ -157,49 +157,30 @@ If your mail host requires a username/password for access, continue to the next Mail Host Configuration & Authentication ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -If you need to alter your mail host address, user, or provide a password to connect with, these settings are easily changed in the Payara admin console or via command line. +If you need to alter your mail host address, user, or provide a password to connect with, these settings are easily changed using JVM options group :ref:`dataverse.mail.mta`. -For the Payara console, load a browser with your domain online, navigate to http://localhost:4848 and on the side panel find JavaMail Sessions. By default, the Dataverse Software uses a session named mail/notifyMailSession for routing outgoing emails. Click this mail session in the window to modify it. +To enable authentication with your mail server, simply configure the following options: -When fine tuning your JavaMail Session, there are a number of fields you can edit. The most important are: +- ``dataverse.mail.mta.auth = true`` +- ``dataverse.mail.mta.username = `` +- ``dataverse.mail.mta.password`` -+ **Mail Host:** Desired mail host’s DNS address (e.g. smtp.gmail.com) -+ **Default User:** Username mail host will recognize (e.g. user\@gmail.com) -+ **Default Sender Address:** Email address that your Dataverse installation will send mail from +**WARNING**: +We strongly recommend not using plaintext storage or environment variables, but relying on :ref:`secure-password-storage`. -Depending on the SMTP server you're using, you may need to add additional properties at the bottom of the page (below "Advanced"). +**WARNING**: +It’s recommended to use an *app password* (for smtp.gmail.com users) or utilize a dedicated/non-personal user account with SMTP server auths so that you do not risk compromising your password. -From the "Add Properties" utility at the bottom, use the “Add Property” button for each entry you need, and include the name / corresponding value as needed. Descriptions are optional, but can be used for your own organizational needs. +If your installation’s mail host uses SSL (like smtp.gmail.com) you’ll need to configure these options: -**Note:** These properties are just an example. You may need different/more/fewer properties all depending on the SMTP server you’re using. +- ``dataverse.mail.mta.ssl.enable = true`` +- ``dataverse.mail.mta.port = 587`` -============================== ============================== - Name Value -============================== ============================== -mail.smtp.auth true -mail.smtp.password [Default User password*] -mail.smtp.port [Port number to route through] -============================== ============================== +**NOTE**: Some mail providers might still support using port 465, which formerly was assigned to be SMTP over SSL (SMTPS). +However, this is no longer standardized and the port has been reassigned by the IANA to a different service. +If your provider supports using port 587, be advised to migrate your configuration. -**\*WARNING**: Entering a password here will *not* conceal it on-screen. It’s recommended to use an *app password* (for smtp.gmail.com users) or utilize a dedicated/non-personal user account with SMTP server auths so that you do not risk compromising your password. - -If your installation’s mail host uses SSL (like smtp.gmail.com) you’ll need these name/value pair properties in place: - -====================================== ============================== - Name Value -====================================== ============================== -mail.smtp.socketFactory.port 465 -mail.smtp.port 465 -mail.smtp.socketFactory.fallback false -mail.smtp.socketFactory.class javax.net.ssl.SSLSocketFactory -====================================== ============================== - -The mail session can also be set from command line. To use this method, you will need to delete your notifyMailSession and create a new one. See the below example: - -- Delete: ``./asadmin delete-javamail-resource mail/notifyMailSession`` -- Create (remove brackets and replace the variables inside): ``./asadmin create-javamail-resource --mailhost [smtp.gmail.com] --mailuser [test\@test\.com] --fromaddress [test\@test\.com] --property mail.smtp.auth=[true]:mail.smtp.password=[password]:mail.smtp.port=[465]:mail.smtp.socketFactory.port=[465]:mail.smtp.socketFactory.fallback=[false]:mail.smtp.socketFactory.class=[javax.net.ssl.SSLSocketFactory] mail/notifyMailSession`` - -Be sure you save the changes made here and then restart your Payara server to test it out. +As the mail server connection (session) is cached once created, you need to restart Payara when applying configuration changes. UnknownHostException While Deploying ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From a48e860a511906296a9f43d807adc271e80bfb42 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 20 Feb 2024 16:10:16 +0100 Subject: [PATCH 368/689] fix(ct): migrate compose and configbaker to use new way of mail config --- docker-compose-dev.yml | 2 ++ modules/container-configbaker/scripts/bootstrap/dev/init.sh | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 6eab84092ed..d43fce37bfc 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -14,6 +14,8 @@ services: DATAVERSE_DB_USER: ${DATAVERSE_DB_USER} ENABLE_JDWP: "1" DATAVERSE_FEATURE_API_BEARER_AUTH: "1" + DATAVERSE_MAIL_SYSTEM_EMAIL: "dataverse@localhost" + DATAVERSE_MAIL_MTA_HOST: "smtp" DATAVERSE_AUTH_OIDC_ENABLED: "1" DATAVERSE_AUTH_OIDC_CLIENT_ID: test DATAVERSE_AUTH_OIDC_CLIENT_SECRET: 94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8 diff --git a/modules/container-configbaker/scripts/bootstrap/dev/init.sh b/modules/container-configbaker/scripts/bootstrap/dev/init.sh index efdaee3d0c3..f8770436652 100644 --- a/modules/container-configbaker/scripts/bootstrap/dev/init.sh +++ b/modules/container-configbaker/scripts/bootstrap/dev/init.sh @@ -9,9 +9,6 @@ export DATAVERSE_URL echo "Running base setup-all.sh (INSECURE MODE)..." "${BOOTSTRAP_DIR}"/base/setup-all.sh --insecure -p=admin1 | tee /tmp/setup-all.sh.out -echo "Setting system mail address..." -curl -X PUT -d "dataverse@localhost" "${DATAVERSE_URL}/api/admin/settings/:SystemEmail" - echo "Setting DOI provider to \"FAKE\"..." curl "${DATAVERSE_URL}/api/admin/settings/:DoiProvider" -X PUT -d FAKE From 6f5cc9f761e49edf2ea67caaeaf67dbc6dbde4df Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 20 Feb 2024 16:13:36 +0100 Subject: [PATCH 369/689] style(mail): update mail config release note --- doc/release-notes/7424-mailsession.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/release-notes/7424-mailsession.md b/doc/release-notes/7424-mailsession.md index 8a3aa3e956b..67e5684e569 100644 --- a/doc/release-notes/7424-mailsession.md +++ b/doc/release-notes/7424-mailsession.md @@ -1,8 +1,10 @@ ## New way to configure mail transfer agent With this release, we deprecate the usage of `asadmin create-javamail-resource` to configure your MTA. -Instead, we provide the ability to configure your SMTP mail host using JVM options with the flexibility of MicroProfile Config. +Instead, we provide the ability to configure your SMTP mail host using JVM options only, with the flexibility of MicroProfile Config. At this point, no action is required if you want to keep your current configuration. Warnings will show in your server logs to inform and remind you about the deprecation. A future major release of Dataverse may remove this way of configuration. + +For more details on how to configure your the connection to your mail provider, please find updated details within the Installation Guide's main installation and configuration section. \ No newline at end of file From 930fc1b1ddd34d9b30be2826c011f9047a2e0774 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 20 Feb 2024 16:16:16 +0100 Subject: [PATCH 370/689] style(mail): update mail config release note about source of from address --- doc/release-notes/7424-mailsession.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/release-notes/7424-mailsession.md b/doc/release-notes/7424-mailsession.md index 67e5684e569..25b1d39a471 100644 --- a/doc/release-notes/7424-mailsession.md +++ b/doc/release-notes/7424-mailsession.md @@ -7,4 +7,6 @@ At this point, no action is required if you want to keep your current configurat Warnings will show in your server logs to inform and remind you about the deprecation. A future major release of Dataverse may remove this way of configuration. -For more details on how to configure your the connection to your mail provider, please find updated details within the Installation Guide's main installation and configuration section. \ No newline at end of file +For more details on how to configure your the connection to your mail provider, please find updated details within the Installation Guide's main installation and configuration section. + +Please note: as there have been problems with mails delivered to SPAM folders when "From" within mail envelope and mail session configuration mismatched, as of this version the sole source for the "From" address is the setting `dataverse.mail.system-email` once you migrate to the new way of configuration. \ No newline at end of file From 2e9d4144cffcf97d7890b10f14f438c950f8ab89 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 20 Feb 2024 10:16:46 -0500 Subject: [PATCH 371/689] fixing CCV datafieldtypes from getting overwritten in database --- .../edu/harvard/iq/dataverse/util/json/JsonParser.java | 8 +------- .../edu/harvard/iq/dataverse/api/HarvestingClientsIT.java | 5 +++-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java index 4287cab069b..16cffb92c8c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java @@ -741,12 +741,6 @@ public DatasetField parseField(JsonObject json, Boolean testType) throws JsonPar ret.setDatasetFieldType(type); - // If Harvesting, CVV values may differ between the Dataverse installations, so we won't enforce them - if (allowHarvestingMissingCVV && type.isControlledVocabulary()) { - type.setAllowControlledVocabulary(false); - logger.info("Harvesting: Skipping Controlled Vocabulary. Treating values as primitives"); - } - if (type.isCompound()) { List vals = parseCompoundValue(type, json, testType); for (DatasetFieldCompoundValue dsfcv : vals) { @@ -754,7 +748,7 @@ public DatasetField parseField(JsonObject json, Boolean testType) throws JsonPar } ret.setDatasetFieldCompoundValues(vals); - } else if (type.isControlledVocabulary()) { + } else if (type.isControlledVocabulary() && !allowHarvestingMissingCVV) { // if allowing missing CVV then fall through to 'primitive' List vals = parseControlledVocabularyValue(type, json); for (ControlledVocabularyValue cvv : vals) { cvv.setDatasetFieldType(type); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java index 375eb92a6ab..1de219e765b 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/HarvestingClientsIT.java @@ -3,12 +3,14 @@ import java.util.logging.Logger; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; -import org.junit.jupiter.api.*; import io.restassured.RestAssured; import static io.restassured.RestAssured.given; import io.restassured.path.json.JsonPath; import io.restassured.response.Response; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import static jakarta.ws.rs.core.Response.Status.CREATED; import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED; @@ -27,7 +29,6 @@ * /api/harvest/clients/ api to run an actual harvest of a control set and * then validate the resulting harvested content. */ -@TestMethodOrder(MethodOrderer.MethodName.class) public class HarvestingClientsIT { private static final Logger logger = Logger.getLogger(HarvestingClientsIT.class.getCanonicalName()); From 2018c87acd9c86cd0131c5fcd8228ce4254ec488 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 21 Feb 2024 10:50:36 +0100 Subject: [PATCH 372/689] style(ct): rename Maven skip deploy option To make SKIP_DEPLOY and the Maven property more alike, rename the Maven property to be "app.skipDeploy". --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index bf5bf16d423..aaa2b49eaae 100644 --- a/pom.xml +++ b/pom.xml @@ -916,7 +916,7 @@ gdcc/dataverse:${app.image.tag} unstable - false + false gdcc/base:${base.image.tag} unstable gdcc/configbaker:${conf.image.tag} @@ -929,7 +929,7 @@ ${postgresql.server.version} ${solr.version} dataverse - ${app.deploy.skip} + ${app.skipDeploy} From 626b2a87cad6a9e610c7a587bb5960997abb47cb Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 21 Feb 2024 10:54:13 +0100 Subject: [PATCH 373/689] doc(ct): rephrase and extend on running container dependencies for hot-reload - Make the description use tabs to be more aligned with the other tabs. - Include option as a tab to make IntelliJ run the compose commands for us --- .../source/container/dev-usage.rst | 49 +++++++++++++----- .../img/intellij-compose-add-new-config.png | Bin 0 -> 25929 bytes .../img/intellij-compose-add-run-payara.png | Bin 0 -> 14908 bytes .../container/img/intellij-compose-setup.png | Bin 0 -> 45986 bytes .../img/intellij-compose-sort-run-payara.png | Bin 0 -> 9725 bytes 5 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 doc/sphinx-guides/source/container/img/intellij-compose-add-new-config.png create mode 100644 doc/sphinx-guides/source/container/img/intellij-compose-add-run-payara.png create mode 100644 doc/sphinx-guides/source/container/img/intellij-compose-setup.png create mode 100644 doc/sphinx-guides/source/container/img/intellij-compose-sort-run-payara.png diff --git a/doc/sphinx-guides/source/container/dev-usage.rst b/doc/sphinx-guides/source/container/dev-usage.rst index 6dbd0276cb3..a8e7efb7edc 100644 --- a/doc/sphinx-guides/source/container/dev-usage.rst +++ b/doc/sphinx-guides/source/container/dev-usage.rst @@ -144,15 +144,13 @@ Alternatives: Redeploying ----------- -Rebuilding and Running Images -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - The safest and most reliable way to redeploy code is to stop the running containers (with Ctrl-c if you started them in the foreground) and then build and run them again with ``mvn -Pct clean package docker:run``. +Safe, but also slowing down the development cycle a lot. -IDE-Triggered Redeployments -^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Hot Re-Deployments +^^^^^^^^^^^^^^^^^^ -Triggering redeployment using an IDE can greatly improve your feedback look when changing code. +Triggering redeployment of changes using an IDE can greatly improve your feedback loop when changing code. You have at least two options: @@ -237,12 +235,39 @@ To make use of builtin features or Payara tools (option 1), please follow these .. image:: img/intellij-payara-config-server-behaviour.png -#. | Start all the containers. Follow the cheat sheet above, but take care to skip application deployment: - | - When using the Maven commands, append ``-Dapp.deploy.skip``. For example: - | ``mvn -Pct docker:run -Dapp.deploy.skip`` - | - When using Docker Compose, prepend the command with ``SKIP_DEPLOY=1``. For example: - | ``SKIP_DEPLOY=1 docker compose -f docker-compose-dev.yml up`` - | - Note: the Admin Console can be reached at http://localhost:4848 or https://localhost:4949 +#. Start all the containers, but take care to skip application deployment. + + .. tabs:: + .. group-tab:: Maven + ``mvn -Pct docker:run -Dapp.skipDeploy`` + + Run above command in your terminal to start containers in foreground and skip deployment. + See cheat sheet above for more options. + Note that this command either assumes you built the :doc:`app-image` first or will download it from Docker Hub. + .. group-tab:: Compose + ``SKIP_DEPLOY=1 docker compose -f docker-compose-dev.yml up`` + + Run above command in your terminal to start containers in foreground and skip deployment. + See cheat sheet above for more options. + Note that this command either assumes you built the :doc:`app-image` first or will download it from Docker Hub. + .. group-tab:: IntelliJ + You can create a service configuration to automatically start services for you. + + **NOTE**: You might need to change the Docker Compose executable in your IDE settings to ``docker`` if you have no ``docker-compose`` bin. + + .. image:: img/intellij-compose-add-new-config.png + + Give your configuration a meaningful name, select the compose file to use (in this case the default one), add the environment variable ``SKIP_DEPLOY=1``, and optionally select the services to start. + + .. image:: img/intellij-compose-setup.png + + Now add this as dependent run configuration in your Payara Run Configuration you created before, in correct order: + + .. image:: img/intellij-compose-add-run-payara.png + .. image:: img/intellij-compose-sort-run-payara.png + + Note: the Admin Console can be reached at http://localhost:4848 or https://localhost:4949 + #. To deploy the application to the running server, use the configured tools to deploy. Using the "Run" configuration only deploys and enables redeploys, while running "Debug" enables hot swapping of classes via JDWP. diff --git a/doc/sphinx-guides/source/container/img/intellij-compose-add-new-config.png b/doc/sphinx-guides/source/container/img/intellij-compose-add-new-config.png new file mode 100644 index 0000000000000000000000000000000000000000..cec9bb357fea359ed18f3595c744cf50cc868845 GIT binary patch literal 25929 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%0AC|v>SpqtZ8=KY@W_J-0!nc1#*UWek5PJ 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%0D3=j^R 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 TxD@Q&C~yEdRoPl;^RWK|h`VSd literal 0 HcmV?d00001 diff --git a/doc/sphinx-guides/source/container/img/intellij-compose-add-run-payara.png b/doc/sphinx-guides/source/container/img/intellij-compose-add-run-payara.png new file mode 100644 index 0000000000000000000000000000000000000000..52a301f7ed58ff6f05ccd9ad99f5293d0e4143e1 GIT binary patch literal 14908 zcmb8WWpo_PlCImbm@H;yu$UPwMvEC+ELqHCi)BHJnVFf-?zdtgfn9tFj_0-+Ur6R8d|65e^p)001CLNq$!X0KgzW+u1NspZjs^lC{qtSUX8g zM*!e!?>`$@{I{=o001#S>btOto8H-)s}uU%%HOMt(FE(5HMz=J02sOmY} z!IG7gR4dhNpW+`p9&KKexyU?}&%mGQ{U_t0+0J?kD6o<`&VPZUs+ub|L7c;Bb0AXi zHrOd8R^)$~>13-77sHwT7BtzMK}ASN==QXbeO86@YI|=yJHM=O8WR4|V~lg|8>g_5 z($y%za_%hzn1q;mx(Y5npj!exy{H86zV|4v}s@h|y_O{i+oB zh-#!7H?8&5_Xt3@$s0W6d%I$`w5H9ox2O6$AaopGyDB+C%-d)#88zkpytS7@%YqQa z*3{IT6A#pBEB!GyQ#>d+uooAB+ipwjBaR3eY~iCRhDphb$Ha3mSDeLws+CIw)&Z4j z0Y7xe2Y%`3=%C058_@+45_C=bNk9YRsvDY!L?*%Cl`RUr)ZrX9c3CB#ZZTiRP2|Hg zVd|EgR68^dnb;;OgEgCO9~(6DJyE%vkv>?`aRrzS}x@s zwpSLd0P=FI^P$wI%dv^2SOXd@Vd17yX#}`1Os}Dti4K4wGIlqQbAd@Ihu7Uxp%@kY zZ`{n5F$kalKvY*5w)S73Cf9;pn0P+_7OtHrHY#)q5W5#U%$MaCz>ELb9{ zLeY}jH1&CTYM!_*`OjHx|FsN|2V3iETEh6Sl|C(B{glc@NH@{oMun1(KsbmaM7Z{V zC1)j5R?itctbkCI60xt!0xs9L{d+XI0qOB1x25a!vHz`T#BH`gG9?r`-*>E zX(iRSF~2e>RXgl670)H?jL`KFct%kU373q&*tpVjMwSI7Gru{O(_fp;Q8fVtsn+_{ zp}PqLFhduGOzCDXL}uk+zpvw!i4X8Q$6kJ^Mr(4pKPu-2L0i^hb*$%)B@K8*r>9QD zWCNDexsVC{6|#s}<~{1?&JD!gTXuEK-om59Ki+fhvN-h(Rx-^bl>y2K&|mfX$^IhA zMfbsXuc;ZyfWd)_`h;yYeEIS#rfX}D_32S@{S3;6-J_E^`09<+?JkCjP>+P~P7OxP zR-Al5|GTp6N6PRTnz0u|wV3=x!XoxI8;M(!WE89c<${WvW|l+en*-dSK%Ltwj<{1f zNPs`+WFc3vn0UyiWt(~lXtK8;)&(4fT*6gUInQXK_>QVxS8T+reNxob)kYbW*DZhu z9WL(rBJ5|UEsY4Lh>d;y;Va=#QEPRf8pMt$;z<6x_mSR<>nDntv~>>_3l&Aw_8tfX zmvg;5SyM^0-V94_O3lS%G%D4oFE8LcNUV1lGZv#RUd_&dwF`J2vyj&Xg!%bFQI$hb z{|X2zWmT#;WBFq&FHO9}3lL=gzNZl^Wl1nzqQhcMMPC~i#}fQa*sdtt#=we++Eyrv z97i)DS`{~ZPHVbQMlOd@hxBdrt4k^`bCHBx8if!CGK&#qE%ZWR6K}$P?aEU8T841@ zvBtp^)U?T ziVb%Fn9VAFA4Tw}Sz|44DZl^oY?s)VI@P1AeG$_;Sh|-;lz`SUS^7X;QXF@?XGT64 zYE0H8ng#CmH<|r+FEE*}UV;z28J_N#GEl(SAVRrY%#IY-gf_H;atE?UpYz5xE_+J- zhY1qxag zZ;9^2mhx&*+Gh<<&h;e_or=@%;(&f9Fwv~0P66|%hE-Fd!}R>PR3nf0r5JL{mP9pN zy^vzo18G$Shn&(ZzS>Mz#L7t`F;-SuZE2rcpBuoUm5W7~;;GDKI<{}|RFAcc{wL$W@Hz6j6fBu7i`jd)~;txQJ+|1ZC~%hcKwLubuN58|u!rrKbi1rsbLcDf=|$>FbmHAfe)f9;@fRxtJ;)x zX^W0IAN>vz=udMsb7VcH^bbbZi&xj%OLsMmPSw^DX_|g|dD=X%LWHw>_~Oqs_>oGx zy4TYwr!gSU_q40*_0i+-Q+y3#Gx#DgHYP@7peoWuc|*WgkSJUn{0cU-mx|no4Hr%e z@u6E3iNE)A0}5(9Vxb6c5yG7;hDyBL9i-YOuvjm`Sk9F|{t9k|RxMSbpyXKo8M(4^ za1t?PzWEwBk|awhONn;CsMBxGws~-3Blx77y?{4)f_U#A=!;_iWCwCOoelNzez`es ztqqBcq!eI?Ow`Zlou&xG=5mAawR!S49*A6;aM7NnW&g*FHN$IC;F9(Jr|n0q<}~Dh zl`9ZrU<^-n0=@Q6!J<_HO^Wy(N%s+&VF6+QKDS2zk*`*l%#Qy43sHyhTT4vrazfu- z>W_PWfRzFD&ott=eIj_cJ_5Iq1bVZ_%j|pqk@6;oa|{VEu(Cu1e$V?BD==Z?bBw7% zJ1pnp*(H0S%S+($iEWkh$-XoWau6b3WS8AZm<^PFv|&%s3KAwQHwyZB&X3%T&2(3@ zbzEdU&vg3KkCHerFk&(?e+%WpB?u`0uEDzEnZZHo`&JzhiaU_5z$;#{3awF&G2D9t zEX>J;jSv&Yk#*aOTDcaiq--ir1S^UeAc?~n7y2vVC?3^oYDktHXcszQ%=fJ?2=Re~ z7|`2Cv53@Cs#gAt^Q*)A1?=*qvJ)Kv?Om5vM=|72#cKQhldC)(CcNP5ldmQ6%+&TC;b=J&*e@N zCvN&kV&4v921+fj4_7L0s!YHgP0M#hx(ew`zIe2ahp*Gnc|UzDIzM!Hd%`C+FkWpX z^+)=86*#^=KI3!r$JIgp$+qdc$>a-MNa0MXKQ|3sQKZf=r%{1n2yi(Qq5lAgv^wXt z4EwYZh=p}6Kyy+J`Gl*6?yw)Wd%;@sgxiDr(WK1E%{T9yoJ7{1ns>Agpt}aWdEQsK zP7p?}#$8>xkZ$$r-JdV@tb0>6C##MF4QkMS3UoB_hGpKR+hLalb|wO?g$pqmboiXG zANmQ7R6HSN$ihs`)VA~*=?PL6`hu0qs1^egAV^7^w7quT-URoj}%V@4FCeahf6Y7$nFYt2Hc+0zg5h z1;93{rNho;m_5a)K6c)55b&%GxJFYALJG;%OB67lBx7Pv=Y(>$&Xk%lEYZR+&bUdS z$29m(%x87o;&FzHqG36(%se+Yi?H~lQ(m11+&ATmqa~h*Z-9Zj*$uIozIucrEW8{& zxXGnOp6;DU7ERex_XMfz@n)O5s4ZivZ(UAAUhOEtBD#U`XU=J&UrvS%og^0UsU z>>amf8(|Tw`rEa31ctE!n|lrN-}OU8u1>~s*YIw}dqGQ?HtB0dKZIB&&39MY)(6rr zKic~zNQvW;&CT9sfVRsnmI3?SKks$9yE4E--5M`sLt>rlfYt+5(PM@a!F;Sn2nrD% zDGx}dTQLjWj5ck?sI)VhK97eI3@>*^tC#&vk_H@D+{&$WMvF-fjEQRhU@2Sj6vmy1yJG$oc?>1yqgP4eN% zC)t1d%jj=~7G@qiRoh-#)gciOnTN+#L+WaK+Sxw=Gm@Dy%=DjsZ7|ZhJ)!HtXwp@% zVc&C9J~$r?npNJs6D{0)DO1EGrGD)JcHTR+=#S0inhkOsH`{M>S)AJ(PR~LDRMW-T zoaBKQ&u6_!U@~7kInPNrE_&6MAGalAzK=I&KAQKrw|TS;9u`z6?p_NMtcLgZTPvwL z9||~e^$=ai7<+%^Zt+-ER}PH99ebo z2FdwzFR9g8ScfjcVx@8P@$01nzcXN|d-uAIt((olE1))aROJ&lw``MIT$XP9nXiXG z2x~``fch0Py7d+<3F0iUm&ClIA9%9d154wH$ojF7RS^zwM(B3%q0fAELpYIA?Pffc66ZJ(v1%lE#c+(p)>14@KsnMO zg+qPDWxtHxf-_(+@v;Z1uG4>=F`4(erl;Qr8IBq!7jZ%U;(n%_N}yw4Nbh}VE#C-> z4`n#VHgo#yCfkj zPVSrMR=C-!_)h1*4&^hMW&kLM2Gc7(eYS=hVXbpx$L+^seP(*>Vn2O@{5u-2uXm7M zBs}VVMWg5a%A%Z*PFl-{@dQFmMEW3hwxjAUN#SZfqT;w%@2gmAU99({D)n8Gk85Bi zSaI*^JG17m%sHwGMexnSpy&B&*d3CqIUy|d zX(#;8y)osDTjD;O#e6(f!RUUv=EUT%dHMpXcZk1w_-yGxPv7NEo(3wH$1d&rou55- zXQt@Yo~P2^v^2?@9I`AQGTcD6o!=-EQ>_i`Awxb$%!d221$=KzV|eqqdhN7slFfPK zSOOZ+sj+=yf*&BH`2ofD4`7DX0gcaLqp)?UZ+OrPuj^mI2d4*HI(Uf;oB+oR%r?ii1R_=>BtLE|F{_4!BmG-;l2$*y_f(`-HwSh9wUr}|n z2>uv`zJ@BOrS%jZkejx@a7;9e?M<_vu>aaSwiI}_M5=R-Y+UzFEEe?-UB6_`{6MjK zsfvjij_z}O+>e7N&`*=yNJ+^t4n$p@ndpCdf&SB5?ivx$e;9~2)LXdAW1f)6(eVnT z;^fnj%Ofv{_2k6Tfm>PvC-N6@&_UI70TZ8Y+B`a!b#WlI;NA-t`11b#yI1RY5dqB` zEC(y+3^1rwzKLq22@6^N$F+o4hlQoSp86m_uT@u< zDTf?@s zCwZS`xNv_MS-xYS0y(sqTOyvCA^hETc|6c4CLCSqN4+!SF_rCB<;+v4l1Uq`Ga20` z?#ertyvVwxRR6Qn(QbdT^GlPSQjqbIe~_+ru0UHfnZ3y(Weqf9t4nsr4=8kWM-CcJ zdB@91ZdBX1*?h$=)!_ucE(72#n!987UfjG)ZY%O4ATHBc69;mB_7OeQnLtBALTY$fFA25BK%m%HzuHF5HhJP1yn?%N>d#xE^- zqho%0LZU!q5T`Ggb-GrD7#@eZ1xCeKFyLp#fI;cEIpOpUv@3v`|1JEg>16%g$A9sJ1u01^s>`YB=N+O4LW)%4ks5tmBT;Bj@ERZY>iqe02 ztcq)-XZhi&+BJjPQy^?HnWZQA-2H@0V7z$CR8S7SMEwo)@|sXnx_U`G1vI!uFuGnCtABj@KlZi!CRMes_m5G8mF{o^G*C5!8;;vU4_{DQ$+F= zUHV6q6{6uf-Z`Dmv+>tG^Wr6MQe~(G3MvNhT+rwh1?ywy7$>vxa0^e-N*u=g#+@gMVO!fqQ(wVNw zKZydkq8m0_bnQlh)!@f15UGVMTSqV~b;1ZK{D~-Q@4)L4JPo!xY!(aZAUS31w-bt* zI@LpgGyeVu!J(#1FlPckxk_gHQJQI$cPHN+QSrN{SZ^89pZvK8;;25ulAS>vgJ#A> zQ;!0p0|I6TBn18$7T*!)elR=-OisGR=#TIF6%hJUA%3s*K+WaUBh$u{Y#ee-Vj_;Q z-}t_t9_npA#}%=Vi8q#vfYTX?=598P%^=8`$!`(a?e}jPEM$?@5Bewe3<5&omM`Wf ztWML&w|Iq2*9$VhV(`P;Yir7gj=>wE3#z+IfpO4Jak4k4hP;-Urch3);&nEQter~! zB?vYfzpF=3+9=r2;%^kP_NSda=CZb*;@WR08E$zD9on`I%Bn%m^_45WI*=_<6~7$D zuU6WSG%%6_V}k|RE#0y>Jhx7C`^QW>2FJ(Dh7^CDe#3_Wbx}%Wbw(-p%kpq)u#^-Xw0RaT@ecOq{Vz;7I5Sx+OhZCcMpyUHG zDNa@m<+1UmC$+_WePL9|Kz1x6S}weEdK^J!IS<#}BT-|~=~PTXK}z)ob2YH~wSQP~ z;0!1|20*Xz=+db#1PcHunfiUu99Vtpe`4Q|`Zm6yE3BzqtgJkg%p_GVrBEu4A2Pa}1gPANdLq;~_<91d zAoyqWlBt&n?(hq&t^ielse@dgWAQ3KbH4<#Ht>zUnz`3PN-Z>wG;x4P-Dd)#jb%9MV^-e)+f=J&KJESom6N|0JS@A0n)p;9Ot2 z;;Z`^#EFc$M$fl696j$3os8D+P#O)^?p+^^wkv8t*`*>;s4I~~>x4FzKjqcU#L$zr z@{Y$}G|ZPnAny5FcILIE#_? z;R1kyU%YChq=(zsiMe7N({(*7S;{Psc~X0(hSTDN{v-+t&<=K@(Y-uxrKbuR1%07j z<#URPqbYYw^3Xxlm-Q%%&RyEExKRYuPh1?t`c*E_=80QxhBb0t21+YL+#4rx*wb+i zj2n4coM_4R9CVh2pKbox3&z-pO-UHGs+2SQ#9psY3q*AK?*u);NG6|$|1^Ey@1Q>q z_v#l4qCPJ~`qt5?AC4#$6TO%70e~HpBcoY^7{tco6B*!fCo*N%Kd_XQ<8iV2m8zyN zmmrEg*)jE5J_894&>{R!IKVr}UfxE_Ay>*9TW^=HSdi8|f`;NSe zHs`%rgzWYSfF+^9kmON9oy>0D5L^+DujIYd;J77xOi9o>Uu#Z)S3fEZ@JiUQOY8F# z5>vkG54Q4E84I+g>n@$)%$7#he_a-8L2!rZ>9D~cIeBf=;g)R5hX>WmkiVM4{n_K$ z8qhsl?_^#;!l(Q=adwD5ZxeXn)@m6tkSBW<|Dss~RbEndLW{0SqYYPFOhc#H77~g^ zb@M5exV;XUK>QrP5b@^0CR?2GeBPg^O4KXIdqZP2Yur$Zi|1wFgUQK|Rdk*vak3j<@Dc!e)6C`_vLf;wWw$ zX^u~3!)dJlp5%+eYgE*lv<7Yw`TB!S1J<4Vi1rz_4eZB~NLLiAlZND*$B)kRSso7H zHFitxm9-aq{~h$N$_ENe{1&CTwZPW9M4s#G85lg&4f79%>+G~>ZcdCq(RJGj%jZir z>dI2sx!gutOvwV6XsCCI=f5Wa|B=d{D)V%yJ(w~AuhyrXo!aSQ-JeVXG+khWmq4ed z@;Vc4LYm9&v6U@FB_OafA5@hmT#m!lh`{r#HLrz{7eaa=>11#``^zY{#(AFDDFZ49RnNrZ_!k) zy}fS6J-)E8Fa$KT(RAk`Omz{d5Bnit(2H zx-!h>jrF>=2OIxzO~OR{YbsY_yR2B96#&55{-+k;)3}-%W*8;|xqtxSukmzVu&U<; z_Po4;?Slgef2aqn~8o;ui-Le1IZ*Yy5 z6|Lvnqp=FzR=EAnKyOMTXStv@K~PsZ=jHx0xsxk}l_Sdi9tMfk9d?>*Ucr9nP@#w! zQ`WdViOgP*Ydo=~h1aEB7-hX=`X)yW29?&Zh8eVGnM{@oL%C0*sSSh5vfI{$(vhEE za4T=_%`-B(7zryGIF`D)2rh@gKqLX1%`$rWI4mHy>+fW?XqCq3_q5O|Z6#RQ1ujqK z)JOrsO{Wb4Gn)?j48t~-%5MW3Nxh)N(IU%xv-{D0Yjk=84WyDaXg7peaoMkPi6K@Xm^vx3Xaig)PX^p{N`)j0PXrbM30k55$AzKb&? z5a}6L%oNJb`wTZhQk5r%jKm}_320X{RPFL4ehaHlW{o^nm!98ulB3x<3qv_?mc~Ly z7c;!8Eai5&#l~XL0T^6uQ`Fh4G!PR5^9pOtuat6fbN`HM@eReBl9@4;W`3bH^RiMA z|lRNY~}bMBnbQi3l|f)Spj0raehx$BrzN~7=1fKg)^-vfAhOLTi;y5 z(*7}Lfyla`=h%i1SBv4BoLH^_V%NT^>d01k`+m7tns8E6m%)y6*Kyq~rLLeCZ^kR4(BAEo4(PZya ze&ClU7Q!(BBfF26#o9-7Zr7yxW}UR-=}LV9QkV4ncVm!}*#^rvW~^e>?C9v;pqh>m z=E;hR*8Ki-e70|WQ?);9qfwc}vtL2&yeFW9ZZH@$7N2abEG`4GHhGb2;wSr7Ov zfm$HUITU}kGOldwAAj_T=b$cPx74aL|GIcjqCsccp?A$5DmCB#vvz-s2^&Goj9zs- zS=Eb3FMc4XuHU@c)Mnh})F=lS((czH8L4wO$HFP%Nh( zUDo*wbX)aeN4DcT4=6;!-sO$?wdvx+t@H6s691>YME#IIe;RPq89w{!i6KViFpYf| zh(0Fm)mV#>_~}aIeY2Ij|LTl$NQnXS{I8!0sa2eRTJ2t+S={yiL5fsHgA;*NKv8|>Q@_0-Yfg-l^B($#^ z@qZP!7>GXaM)@@EGK70l?TDq)l=Z8B%qN5x#>74`G9AwKiFnB5L^MhTP5^Pbi!|D9 z9Y@);FU;2O(v|k4S~*49liVG`B-dH)Z=7SYmM+?5MP{f@f~yOJFm?3s!h~`Wl~4FxY%eT1He*CYYV)Ve*~lsK>FiW$D{m`Yc4pL*`XHLn8ILyl5tbbc+5%DAquP*LvZQ(g*qyq^>CU>O$oqE3-O*e zZ_n!@uc(EMWivT^c*#<$%h^(I_l6ye_2sm*M%e*pMw>PEafV0FBgs~jzOh^D8aX)W zan<>Xwosb#L8urN?DlXjlTY%MFLFT}9iQS~Z19wSrXX=bll$P%!%h{Sq=E+5XMMs3 zeMfWf{EqgnDhyUuyu3|jEY%Vh;u}jl>oJ-7$m)SQ9#2uJMd-+PP_c))Ukq)d+TXsA zM5e5M=~?q-aQ2uxPJI^7{XN(!soojqK~EW#Bh`F*bu8tb|DO0_Y@_QceEXrS!UtT- zG{bQI7#KctQCL3!>!UE3+JP`Kt7P}<@G~K_LOPZ@9tZi0 z-H(JIM+TXpQf2BRPZc(+{n5KlwVQt19}Sh}ymNWlc&weiO`XKV0BQ?$jjJ<`mKLiY zV9Q1)8T#_}Ck!)H55|EfnCN566Z2Kf#XqCHA)c4Va2}jIaf?_6;~cn~?`VG;qK*_; zZzr`J*7cn!yxfITWZ7`)`IKSy-y)1cnN2_ZG}!+m^T@qA-(Z5?St+f(LE1?qCY<}@ zM{WsYVr5@csuk4HdfoH8voJ{}_dxI6TViNf2xXD6t}J&(o=$av4`alsR)nduijID2 zxL@nrq4v-_L~5tm_G~K9(hG;ng1VF)lW}&4Zv%VGPoJ+@KNF1Fc?vNsaKPXn~V0 za`r1c`ZO;kb$9g!{8Nw%w+FS{6EV>V0b%Ru2L)c^OENzsABdUarYqhbqNH0a6P?&j z1fy^&5sdmv$sSMFd;;{=hHJoa@_hi102#Vg7m4U%0Jl9pSF#@{Q@a{Zon3dv8gmufgj~yKuF}z=p}ka)O_9X@92_ zMw_K)tev>Rbv)|%7A<83A*|V&2|VpbP&llt;Zfs2Q%ac4ETbBOrdT9I?2?KIM53Lo zhoD>?12*(+e#RH`wdhh$6g3l~4U9CCF#UpIgTF~m=}kE}Q9H4rGb%l6nmLbr*7qZI z02b6NhnbzN9}!do>W$4ZQEAv=4F;-oyUVU*=-GLBF{fIH`cz!HvJCY~CmE29^R;&9?KHV^nI0+}%TY4mPI-K%Vy2emOpYp&$8H zT(OXFu)KGDcRI0@U;s6gE}-l`9Jkgo>#chHOiTU8PxwN_qvdXf%fi9}8?oZ^kGMp` z*Ppa!=EW+v?@-4NKLZr(kkws1k)0P6gMJxENNs~uB}nMPgI|G6RCe^^!Bvr}_A`e? z%S{AtqUj^;c)*S40M*W}0%xivZ)(zu4IH%(_pgQfh)tDw`@V!XeKV)y6$5`Od_J8W z7m4TLYnRZAS*AX1t7Xhf@%L!VAvg(m;#sP;$^4G)t|z!BVY=_V(hX z3^^D$;ZE~Cn42X9c|^ubi_<~ieY5qtOL92N?3;WNV0wiV_U$N^UDdxcIeMJ zIAbmM`$C9a)7Use8%>;J4w5pDt&5U88{GtN)fOWLlxs$!^J|LlINQE`g>$?=>ctRs z+9P`LIinofM+J?89mX%r`i7>$&vk>(;3sPKgL?wP2}Y-b>B%#e297U*WubeviL!H; z56X9^B=<|YyQUIprbO+BP8({GiT6t~Gl*(l&He|OxV;|{fQ)<(DE^P2>Z>BvCU;3Iso z-VPrm_*V*ggJM=w@{s2D$NrUPdrH*SK!F0X>F+H{1U1d-=_7 zcP7Kbdmwe6^$d;*Gxcs>K9(<~Ivo(hTd_bEUG~VLcLj+7T1E|O=pGzCPU+z~ru~rB z9=V*s+U)7vTq)iAXtk4MdWcnQ-a^=+H*zw8O1ASF`v~Z z_AjU_G89UU9q!wacyhiG%aJ*hEkj0!ELRijlvq}*bit@t57~L3hi>-ZPl;XWsIj2o z`mC>nMMO0DL>34{0{HD+g8_;9G}V2ZhuGzA^nt5DyX$HqS{M^k`KD3Z4c&dt7Zi-- zoGfb0eFdVhTH4sPR#)X(59f|I;Fpg^gZj7Gulad-pM{BP0H4P#=#KYitGG5bajD;3 zdV%n83A6B@^OTTNnQq@uKarb$XLAQ%XK@gr?j&QC!=7}8@4I_zu?&@zk{&VjTY`zD z^q;Yfhewe%e7dF2$xSYBKr2=WuG_bFC}iYR0J}@uwmneZtX(@MEp4>*qGKe5-RiV} zA-S-SYHEr(vW4uYArr36yJ}Oy7eInF-B97#XC+@}LJoBvEgYKOW0>oDa$f)}S`w?- z*FQ$yZWdeJf%mm>gEKR+i%l9YT_#FbT5x`sL~$Fd>c2QQQX;#|^Ryd(-p9r#2M0ql zGc)gbL@RxQxgsH;CMoAa@9vN?HVY7DeSQ5tHZC+V5sR6*I&MC5Gvu?NS5a7l7pj~- z2p)^E`gG;O?Y#NmlpRYo zBtCbR%FcHlQ^zH}^Xhf)kQZBLHmJ{M^thG$e8!>SVWX+;z}^Av%*Bhb9Shs`p?FD)By zT!+7Z!OGrj=)IqBO&2P3A#XPRGeOT4=USq+033X|!CrL-x2;eRF`G4#mZPG&D5K)V zn~W8fF2EuGS3yA>0@LGm{s^%EL%o4kur#f$uC-x4iuYn)bbB=ouEX<3HWhVRI7tp^+NRmBPx5tM_<~ zIt9cAwfLc^(29#S-A5F&S+Nz(3ZwFAC7#6Q_0Lox6=V<=Q0Yxpp)GGaI+{%TJ`v%` z=9ru|cQl?RIxO+ll_X*}&#CEC_g(J>OYY8@c2V6`BnHO|1!X5sd|`%{ACllJa{cms z)!fM)hcEbN8ZagyA&73n=di>{xT|L^eahV%=1)#ka`f-8(r+Y)cvZ#&UsE`2{>Yn+ zWt*~LWAiw{V1^}!u;x?HP0tVgxNsypx$vO{DWlN)M zVPkRjr~!EmyeiCVvXURfC$>pt|A1$7${-}SeT~bcTf>KHm*Oy<)vN_!t~QPv-IdpHxz3TRG|1E@V)gM%x1jthoe7b7(e$1>WrSrU2|G?$D&En`JLo9v zYYb3-V@2ilzW!(FC$8(q#RdYGy8Xn$o-DUSZaw&G$c=x{#m<`~=D%teT@_751#v$a|DVzGbwXu@SpDvT zn~PfjdvnPra`w(D7dB_bSTT#(OB{4!s@u*7^-sELbIDZhR*G_)pCmSU&#>o*H|py# z!-4zX@dt&6cYlS6AHEXPdfyv3b72voK!E#`^O>ud!{_w~hsR<7RJmQDE;ra{BO|j+ z&uG@U(M(PK_+*zEyVAAMtVweUVDYRS>?I@wx`W~AL)0d)4f|oIt6m8Rkb@oFzb~;{ zpr1*UWfiHj`MBMPKi5+|vGAd_Ct`WjaM#>G^gIVC-K|J6fG06jd9IjPjZYLwj#KqT zc|PXcUBpR}=JFTqk>o15W<|JA5mmMV3CvoC?lDpiALOA$>Ry&obv5$6N;a!vMqKMe z{uc^gQuzNz;kwkKPk@3O>yMo>8?~dDd*N|zpVSAiERlmPkS_+(tSO^cyGfIOR#u7f x^*z60>+Y4O4VHRNP9mxa|M&83yY84j}#Q=>6X( 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 CdnSkg literal 0 HcmV?d00001 From 0192758f335c9c493fc1cbe89c42ac2f46594ad3 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 21 Feb 2024 18:19:41 +0100 Subject: [PATCH 374/689] doc(ct): add default admin credentials to base image docs --- doc/sphinx-guides/source/container/base-image.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/sphinx-guides/source/container/base-image.rst b/doc/sphinx-guides/source/container/base-image.rst index b43c201fe9f..c41250d48c5 100644 --- a/doc/sphinx-guides/source/container/base-image.rst +++ b/doc/sphinx-guides/source/container/base-image.rst @@ -361,6 +361,8 @@ Other Hints By default, ``domain1`` is enabled to use the ``G1GC`` garbage collector. +To access the Payara Admin Console or use the ``asadmin`` command, use username ``admin`` and password ``admin``. + For running a Java application within a Linux based container, the support for CGroups is essential. It has been included and activated by default since Java 8u192, Java 11 LTS and later. If you are interested in more details, you can read about those in a few places like https://developers.redhat.com/articles/2022/04/19/java-17-whats-new-openjdks-container-awareness, From c1612dbd4a5c057bb590c95679bd92c75b764cee Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 21 Feb 2024 18:20:29 +0100 Subject: [PATCH 375/689] style(ct): remove some typos and casing for dev usage docs --- doc/sphinx-guides/source/container/dev-usage.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/sphinx-guides/source/container/dev-usage.rst b/doc/sphinx-guides/source/container/dev-usage.rst index a8e7efb7edc..3c2b5934fe8 100644 --- a/doc/sphinx-guides/source/container/dev-usage.rst +++ b/doc/sphinx-guides/source/container/dev-usage.rst @@ -160,14 +160,14 @@ You have at least two options: The main differences between the first and the second options are support for hot deploys of non-class files and limitations in what the JVM HotswapAgent can do for you. Find more details in a `blog article by JRebel `_. -To make use of builtin features or Payara tools (option 1), please follow these steps: +To make use of builtin features or Payara IDE Tools (option 1), please follow these steps: #. | Download the version of Payara shown in :ref:`install-payara-dev` and unzip it to a reasonable location such as ``/usr/local/payara6``. | - Note that Payara can also be downloaded from `Maven Central `_. | - Note that another way to check the expected version of Payara is to run this command: | ``mvn help:evaluate -Dexpression=payara.version -q -DforceStdout`` -#. Install Payara tools plugin in your IDE: +#. Install Payara Tools plugin in your IDE: .. tabs:: .. group-tab:: Netbeans From bb8df95b752536af6ea374c8f73322b2f90f4881 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 21 Feb 2024 18:20:46 +0100 Subject: [PATCH 376/689] fix(ct): make IntelliJ script less dependent - Remove Perl dependency for version number extraction - Rely on `docker cp` instead of mounting the filesystem --- scripts/intellij/cpwebapp.sh | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/scripts/intellij/cpwebapp.sh b/scripts/intellij/cpwebapp.sh index 6ecad367048..a823f8871ce 100755 --- a/scripts/intellij/cpwebapp.sh +++ b/scripts/intellij/cpwebapp.sh @@ -14,20 +14,21 @@ # https://www.jetbrains.com/help/idea/configuring-third-party-tools.html # -PROJECT_DIR=$1 -FILE_TO_COPY=$2 +set -eu + +PROJECT_DIR="$1" +FILE_TO_COPY="$2" RELATIVE_PATH="${FILE_TO_COPY#$PROJECT_DIR/}" # Check if RELATIVE_PATH starts with 'src/main/webapp', otherwise ignore -if [[ $RELATIVE_PATH == src/main/webapp* ]]; then - # Get current version. Any other way to do this? A simple VERSION file would help. - VERSION=`perl -ne 'print $1 if /(.*?)<\/revision>/' ./modules/dataverse-parent/pom.xml` - RELATIVE_PATH_WITHOUT_WEBAPP="${RELATIVE_PATH#src/main/webapp/}" - TARGET_DIR=./docker-dev-volumes/glassfish/applications/dataverse-$VERSION - TARGET_PATH="${TARGET_DIR}/${RELATIVE_PATH_WITHOUT_WEBAPP}" +if [[ "$RELATIVE_PATH" == "src/main/webapp"* ]]; then + # Extract version from POM, so we don't need to have Maven on the PATH + VERSION=$(grep -oPm1 "(?<=)[^<]+" "$PROJECT_DIR/modules/dataverse-parent/pom.xml") - mkdir -p "$(dirname "$TARGET_PATH")" - cp "$FILE_TO_COPY" "$TARGET_PATH" + # Construct the target path by cutting off the local prefix and prepend with in-container path + RELATIVE_PATH_WITHOUT_WEBAPP="${RELATIVE_PATH#src/main/webapp/}" + TARGET_PATH="/opt/payara/appserver/glassfish/domains/domain1/applications/dataverse-$VERSION/${RELATIVE_PATH_WITHOUT_WEBAPP}" - echo "File $FILE_TO_COPY copied to $TARGET_PATH" + # Copy file to container + docker cp "$FILE_TO_COPY" "dev_dataverse:$TARGET_PATH" fi From e9ff6bc2780da61d33226c53e7209528f827e33a Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 21 Feb 2024 18:36:39 +0100 Subject: [PATCH 377/689] doc(ct): add notes about IDE redeployment and add stub for non-code redeployment --- .../source/container/dev-usage.rst | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/doc/sphinx-guides/source/container/dev-usage.rst b/doc/sphinx-guides/source/container/dev-usage.rst index 3c2b5934fe8..aef262f30cf 100644 --- a/doc/sphinx-guides/source/container/dev-usage.rst +++ b/doc/sphinx-guides/source/container/dev-usage.rst @@ -147,11 +147,7 @@ Redeploying The safest and most reliable way to redeploy code is to stop the running containers (with Ctrl-c if you started them in the foreground) and then build and run them again with ``mvn -Pct clean package docker:run``. Safe, but also slowing down the development cycle a lot. -Hot Re-Deployments -^^^^^^^^^^^^^^^^^^ - Triggering redeployment of changes using an IDE can greatly improve your feedback loop when changing code. - You have at least two options: #. Use builtin features of IDEs or `IDE plugins from Payara `_. @@ -160,7 +156,13 @@ You have at least two options: The main differences between the first and the second options are support for hot deploys of non-class files and limitations in what the JVM HotswapAgent can do for you. Find more details in a `blog article by JRebel `_. -To make use of builtin features or Payara IDE Tools (option 1), please follow these steps: +IDE Triggered Code Re-Deployments +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To make use of builtin features or Payara IDE Tools (option 1), please follow steps below. +Note that using this method, you may redeploy a complete WAR or single methods. +Redeploying WARs supports swapping and adding classes and non-code materials, but is slower (still faster than rebuilding containers). +Hotswapping methods requires using JDWP (Debug Mode), but does not allow switching non-code material or adding classes. #. | Download the version of Payara shown in :ref:`install-payara-dev` and unzip it to a reasonable location such as ``/usr/local/payara6``. | - Note that Payara can also be downloaded from `Maven Central `_. @@ -312,6 +314,17 @@ To make use of builtin features or Payara IDE Tools (option 1), please follow th Note: in the background, the bootstrap job will wait for Dataverse to be deployed and responsive. When your IDE automatically opens the URL a newly deployed, not bootstrapped Dataverse application, it might take some more time and page refreshes until the job finishes. +IDE Triggered Non-Code Re-Deployments +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Either redeploy the WAR (see above), use JRebel or look into copying files into the exploded WAR within the running container. +The steps below describe options to enable the later in different IDEs. + +.. tabs:: + .. group-tab:: IntelliJ + TODO + + Using a Debugger ---------------- From 314e2ebf5f678a8cadfb925e0b1ea5c194019b8a Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Wed, 21 Feb 2024 16:18:55 -0500 Subject: [PATCH 378/689] #10286 changes from CR --- .../harvard/iq/dataverse/api/Datasets.java | 15 ++-- .../harvard/iq/dataverse/api/Dataverses.java | 2 +- .../edu/harvard/iq/dataverse/api/Files.java | 10 +-- .../iq/dataverse/util/json/JsonPrinter.java | 89 +++++++++++-------- .../harvard/iq/dataverse/api/DatasetsIT.java | 10 +-- .../iq/dataverse/api/DataversesIT.java | 2 +- .../edu/harvard/iq/dataverse/api/FilesIT.java | 2 +- 7 files changed, 70 insertions(+), 60 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index d19c8bf3915..7d0141641fe 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -186,12 +186,11 @@ public interface DsVersionHandler { @GET @AuthRequired @Path("{id}") - public Response getDataset(@Context ContainerRequestContext crc, @PathParam("id") String id, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response, @QueryParam("returnOwners") Boolean returnOwners) { + public Response getDataset(@Context ContainerRequestContext crc, @PathParam("id") String id, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response, @QueryParam("returnOwners") boolean returnOwners) { return response( req -> { final Dataset retrieved = execCommand(new GetDatasetCommand(req, findDatasetOrDie(id))); final DatasetVersion latest = execCommand(new GetLatestAccessibleDatasetVersionCommand(req, retrieved)); - Boolean includeOwners = returnOwners == null ? false : returnOwners; - final JsonObjectBuilder jsonbuilder = json(retrieved, includeOwners); + final JsonObjectBuilder jsonbuilder = json(retrieved, returnOwners); //Report MDC if this is a released version (could be draft if user has access, or user may not have access at all and is not getting metadata beyond the minimum) if((latest != null) && latest.isReleased()) { MakeDataCountLoggingServiceBean.MakeDataCountEntry entry = new MakeDataCountEntry(uriInfo, headers, dvRequestService, retrieved); @@ -422,6 +421,7 @@ public Response getVersion(@Context ContainerRequestContext crc, @PathParam("versionId") String versionId, @QueryParam("excludeFiles") Boolean excludeFiles, @QueryParam("includeDeaccessioned") boolean includeDeaccessioned, + @QueryParam("returnOwners") boolean includeOwners, @Context UriInfo uriInfo, @Context HttpHeaders headers) { return response( req -> { @@ -440,7 +440,8 @@ public Response getVersion(@Context ContainerRequestContext crc, if (excludeFiles == null ? true : !excludeFiles) { dsv = datasetversionService.findDeep(dsv.getId()); } - return ok(json(dsv, excludeFiles == null ? true : !excludeFiles)); + System.out.print("returnOwners: " + includeOwners); + return ok(json(dsv, null, excludeFiles == null ? true : !excludeFiles, includeOwners)); }, getRequestUser(crc)); } @@ -4387,7 +4388,7 @@ public Response getDatasetSummaryFieldNames() { @GET @Path("privateUrlDatasetVersion/{privateUrlToken}") - public Response getPrivateUrlDatasetVersion(@PathParam("privateUrlToken") String privateUrlToken) { + public Response getPrivateUrlDatasetVersion(@PathParam("privateUrlToken") String privateUrlToken, @QueryParam("returnOwners") boolean returnOwners) { PrivateUrlUser privateUrlUser = privateUrlService.getPrivateUrlUserFromToken(privateUrlToken); if (privateUrlUser == null) { return notFound("Private URL user not found"); @@ -4404,9 +4405,9 @@ public Response getPrivateUrlDatasetVersion(@PathParam("privateUrlToken") String JsonObjectBuilder responseJson; if (isAnonymizedAccess) { List anonymizedFieldTypeNamesList = new ArrayList<>(Arrays.asList(anonymizedFieldTypeNames.split(",\\s"))); - responseJson = json(dsv, anonymizedFieldTypeNamesList, true); + responseJson = json(dsv, anonymizedFieldTypeNamesList, true, returnOwners); } else { - responseJson = json(dsv, true); + responseJson = json(dsv, null, true, returnOwners); } return ok(responseJson); } 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 66aec38adfa..3bcbfdd4d58 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -610,7 +610,7 @@ private Dataset parseDataset(String datasetJson) throws WrappedResponse { @GET @AuthRequired @Path("{identifier}") - public Response viewDataverse(@Context ContainerRequestContext crc, @PathParam("identifier") String idtf, @QueryParam("returnOwners") Boolean returnOwners) { + public Response getDataverse(@Context ContainerRequestContext crc, @PathParam("identifier") String idtf, @QueryParam("returnOwners") Boolean returnOwners) { Boolean includeOwners = returnOwners == null ? false : returnOwners; return response(req -> ok( json(execCommand(new GetDataverseCommand(req, findDataverseOrDie(idtf))), diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index 13e459bc3e8..6efb4766dfa 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -506,17 +506,15 @@ public Response updateFileMetadata(@Context ContainerRequestContext crc, @FormDa @GET @AuthRequired @Path("{id}/draft") - public Response getFileDataDraft(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response, @QueryParam("returnOwners") Boolean returnOwners) throws WrappedResponse, Exception { - Boolean includeOwners = returnOwners == null ? false : returnOwners; - return getFileDataResponse(getRequestUser(crc), fileIdOrPersistentId, uriInfo, headers, response, true, includeOwners); + public Response getFileDataDraft(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response, @QueryParam("returnOwners") boolean returnOwners) throws WrappedResponse, Exception { + return getFileDataResponse(getRequestUser(crc), fileIdOrPersistentId, uriInfo, headers, response, true, returnOwners); } @GET @AuthRequired @Path("{id}") - public Response getFileData(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response, @QueryParam("returnOwners") Boolean returnOwners) throws WrappedResponse, Exception { - Boolean includeOwners = returnOwners == null ? false : returnOwners; - return getFileDataResponse(getRequestUser(crc), fileIdOrPersistentId, uriInfo, headers, response, false, includeOwners); + public Response getFileData(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response, @QueryParam("returnOwners") boolean returnOwners) throws WrappedResponse, Exception { + return getFileDataResponse(getRequestUser(crc), fileIdOrPersistentId, uriInfo, headers, response, false, returnOwners); } private Response getFileDataResponse(User user, String fileIdOrPersistentId, UriInfo uriInfo, HttpHeaders headers, HttpServletResponse response, boolean draft, boolean includeOwners ){ 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 d64f77b3526..05dbc4d6079 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 @@ -272,7 +272,7 @@ public static JsonObjectBuilder json(Dataverse dv, Boolean hideEmail, Boolean in bld.add("dataverseContacts", JsonPrinter.json(dv.getDataverseContacts())); } if (includeOwners){ - bld.add("ownerArray", getOwnersFromDvObject(dv)); + bld.add("isPartOf", getOwnersFromDvObject(dv)); } bld.add("permissionRoot", dv.isPermissionRoot()) .add("description", dv.getDescription()) @@ -307,46 +307,58 @@ public static JsonArrayBuilder json(List dataverseContacts) { return jsonArrayOfContacts; } - public static JsonArrayBuilder getOwnersFromDvObject(DvObject dvObject) { - + public static JsonObjectBuilder getOwnersFromDvObject(DvObject dvObject){ + return getOwnersFromDvObject(dvObject, null); + } + + public static JsonObjectBuilder getOwnersFromDvObject(DvObject dvObject, DatasetVersion dsv) { List ownerList = new ArrayList(); dvObject = dvObject.getOwner(); // We're going to ignore the object itself + //Get "root" to top of list while (dvObject != null) { - ownerList.add(dvObject); + ownerList.add(0, dvObject); dvObject = dvObject.getOwner(); } + //then work "inside out" + JsonObjectBuilder saved = null; + for (DvObject dvo : ownerList) { + saved = addEmbeddedOwnerObject(dvo, saved, dsv); + } + return saved; + } + + private static JsonObjectBuilder addEmbeddedOwnerObject(DvObject dvo, JsonObjectBuilder isPartOf, DatasetVersion dsv ) { + JsonObjectBuilder ownerObject = jsonObjectBuilder(); + + if (dvo.isInstanceofDataverse()) { + ownerObject.add("type", "DATAVERSE"); + Dataverse in = (Dataverse) dvo; + ownerObject.add("identifier", in.getAlias()); + } + + if (dvo.isInstanceofDataset()) { + ownerObject.add("type", "DATASET"); + String versionString = ""; + if (dsv != null){ + versionString = dsv == null ? "" : "&version=" + dsv.getFriendlyVersionNumber(); + } + if (dvo.getGlobalId() != null) { + ownerObject.add("identifier", dvo.getGlobalId().asString() + versionString); + } else { + ownerObject.add("identifier", dvo.getId() ); + } + + } - JsonArrayBuilder jsonArrayOfOwners = Json.createArrayBuilder(); + ownerObject.add("displayName", dvo.getDisplayName()); - for (DvObject dvo : ownerList){ - JsonObjectBuilder ownerObject = jsonObjectBuilder(); - if (dvo.isInstanceofDataverse()){ - ownerObject.add("type", "DATAVERSE"); - } - if (dvo.isInstanceofDataset()){ - ownerObject.add("type", "DATASET"); - } - if (dvo.isInstanceofDataFile()){ - ownerObject.add("type", "DATAFILE"); - } - if (dvo.isInstanceofDataverse()){ - Dataverse in = (Dataverse) dvo; - ownerObject.add("identifier", in.getAlias()); - } - if (dvo.isInstanceofDataset() || dvo.isInstanceofDataFile() ){ - if (dvo.getIdentifier() != null){ - Dataset ds = (Dataset) dvo; - ownerObject.add("identifier", ds.getGlobalId().asString()); - } else { - ownerObject.add("identifier", dvo.getId()); - } - } - ownerObject.add("displayName", dvo.getDisplayName()); - jsonArrayOfOwners.add(ownerObject); + if (isPartOf != null) { + ownerObject.add("isPartOf", isPartOf); } - return jsonArrayOfOwners; + + return ownerObject; } - + public static JsonObjectBuilder json( DataverseTheme theme ) { final NullSafeJsonBuilder baseObject = jsonObjectBuilder() .add("id", theme.getId() ) @@ -388,7 +400,7 @@ public static JsonObjectBuilder json(Dataset ds, Boolean includeOwners) { bld.add("metadataLanguage", ds.getMetadataLanguage()); } if (includeOwners){ - bld.add("ownerArray", getOwnersFromDvObject(ds)); + bld.add("isPartOf", getOwnersFromDvObject(ds)); } return bld; } @@ -402,10 +414,10 @@ public static JsonObjectBuilder json(FileDetailsHolder ds) { } public static JsonObjectBuilder json(DatasetVersion dsv, boolean includeFiles) { - return json(dsv, null, includeFiles); + return json(dsv, null, includeFiles, false); } - public static JsonObjectBuilder json(DatasetVersion dsv, List anonymizedFieldTypeNamesList, boolean includeFiles) { + public static JsonObjectBuilder json(DatasetVersion dsv, List anonymizedFieldTypeNamesList, boolean includeFiles, boolean includeOwners) { /* return json(dsv, null, includeFiles, null); } public static JsonObjectBuilder json(DatasetVersion dsv, List anonymizedFieldTypeNamesList, boolean includeFiles, Long numberOfFiles) {*/ @@ -452,7 +464,10 @@ public static JsonObjectBuilder json(DatasetVersion dsv, List anonymized bld.add("metadataBlocks", (anonymizedFieldTypeNamesList != null) ? jsonByBlocks(dsv.getDatasetFields(), anonymizedFieldTypeNamesList) : jsonByBlocks(dsv.getDatasetFields()) - ); + ); + if(includeOwners){ + bld.add("isPartOf", getOwnersFromDvObject(dataset)); + } if (includeFiles) { bld.add("files", jsonFileMetadatas(dsv.getFileMetadatas())); } @@ -762,7 +777,7 @@ public static JsonObjectBuilder json(DataFile df, FileMetadata fileMetadata, boo : null); } if (includeOwners){ - builder.add("ownerArray", getOwnersFromDvObject(df)); + builder.add("isPartOf", getOwnersFromDvObject(df, fileMetadata.getDatasetVersion())); } return builder; } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index f4e70e03d45..51fe52b5866 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -1889,7 +1889,7 @@ public void testDeleteDatasetWhileFileIngesting() { } @Test - public void testGetIncludeOwnerArray() { + public void testGetDatasetOwners() { Response createUser = UtilIT.createRandomUser(); createUser.then().assertThat() @@ -1913,7 +1913,7 @@ public void testGetIncludeOwnerArray() { Response getDatasetWithOwners = UtilIT.getDatasetWithOwners(persistentId, apiToken, true); getDatasetWithOwners.prettyPrint(); - getDatasetWithOwners.then().assertThat().body("data.ownerArray[0].identifier", equalTo(dataverseAlias)); + getDatasetWithOwners.then().assertThat().body("data.isPartOf.identifier", equalTo(dataverseAlias)); Response destroyDatasetResponse = UtilIT.destroyDataset(datasetId, apiToken); assertEquals(200, destroyDatasetResponse.getStatusCode()); @@ -1922,12 +1922,8 @@ public void testGetIncludeOwnerArray() { assertEquals(200, deleteDataverseResponse.getStatusCode()); Response deleteUserResponse = UtilIT.deleteUser(username); - assertEquals(200, deleteUserResponse.getStatusCode()); - + assertEquals(200, deleteUserResponse.getStatusCode()); } - - - /** * In order for this test to pass you must have the Data Capture Module ( 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 e41793a10d5..3330d11435a 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java @@ -170,7 +170,7 @@ public void testGetDataverseOwners() throws FileNotFoundException { Response getWithOwners = UtilIT.getDataverseWithOwners(level1a, apiToken, true); getWithOwners.prettyPrint(); - getWithOwners.then().assertThat().body("data.ownerArray[0].identifier", equalTo(first)); + getWithOwners.then().assertThat().body("data.isPartOf.identifier", equalTo(first)); } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java index d69a3ac885c..fd72f22a140 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java @@ -1504,7 +1504,7 @@ public void testGetFileOwners() { .body("data.dataFile.filesize", equalTo(8361)) .statusCode(OK.getStatusCode()); - getFileDataResponse.then().assertThat().body("data.dataFile.ownerArray[0].identifier", equalTo(datasetPid)); + getFileDataResponse.then().assertThat().body("data.dataFile.isPartOf.identifier", equalTo(datasetPid)); // ------------------------- // Publish dataverse and dataset From c9cccaac56121bdfcc8f4bc2038fdf5de0b04794 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Thu, 22 Feb 2024 07:38:45 +0100 Subject: [PATCH 379/689] docs(ct): remove configuration trigger step in IntelliJ Depending on the attachment configuration, the run configuration waits in blocking mode for more output from the container logs. The application would never be deployed, as the wait is indefinite. We need to run the compose step and the deploy step on their own. One appears in the services tab, the other in the run tab. --- .../source/container/dev-usage.rst | 6 +++--- .../img/intellij-compose-add-run-payara.png | Bin 14908 -> 0 bytes .../container/img/intellij-compose-run.png | Bin 0 -> 3080 bytes .../img/intellij-compose-sort-run-payara.png | Bin 9725 -> 0 bytes 4 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 doc/sphinx-guides/source/container/img/intellij-compose-add-run-payara.png create mode 100644 doc/sphinx-guides/source/container/img/intellij-compose-run.png delete mode 100644 doc/sphinx-guides/source/container/img/intellij-compose-sort-run-payara.png diff --git a/doc/sphinx-guides/source/container/dev-usage.rst b/doc/sphinx-guides/source/container/dev-usage.rst index aef262f30cf..d37b9f4763f 100644 --- a/doc/sphinx-guides/source/container/dev-usage.rst +++ b/doc/sphinx-guides/source/container/dev-usage.rst @@ -260,13 +260,13 @@ Hotswapping methods requires using JDWP (Debug Mode), but does not allow switchi .. image:: img/intellij-compose-add-new-config.png Give your configuration a meaningful name, select the compose file to use (in this case the default one), add the environment variable ``SKIP_DEPLOY=1``, and optionally select the services to start. + You might also want to change other options like attaching to containers to view the logs within the "Services" tab. .. image:: img/intellij-compose-setup.png - Now add this as dependent run configuration in your Payara Run Configuration you created before, in correct order: + Now run the configuration to prepare for deployment. - .. image:: img/intellij-compose-add-run-payara.png - .. image:: img/intellij-compose-sort-run-payara.png + .. image:: img/intellij-compose-run.png Note: the Admin Console can be reached at http://localhost:4848 or https://localhost:4949 diff --git a/doc/sphinx-guides/source/container/img/intellij-compose-add-run-payara.png b/doc/sphinx-guides/source/container/img/intellij-compose-add-run-payara.png deleted file mode 100644 index 52a301f7ed58ff6f05ccd9ad99f5293d0e4143e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14908 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 O571E6QK?dX6Zs$WY6r&v literal 0 HcmV?d00001 diff --git a/doc/sphinx-guides/source/container/img/intellij-compose-sort-run-payara.png b/doc/sphinx-guides/source/container/img/intellij-compose-sort-run-payara.png deleted file mode 100644 index 4efa1b0e925935eb58cb76654d6e89099047d959..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9725 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 CdnSkg From f3611a012773033e020c83230e3124df6351e195 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Thu, 22 Feb 2024 07:40:29 +0100 Subject: [PATCH 380/689] chore(ct): move IntelliJ scripts to Docker folder This is very much Docker specific now and should be located within that folder. --- {scripts => docker/util}/intellij/cpwebapp.sh | 0 {scripts => docker/util}/intellij/watchers.xml | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {scripts => docker/util}/intellij/cpwebapp.sh (100%) rename {scripts => docker/util}/intellij/watchers.xml (100%) diff --git a/scripts/intellij/cpwebapp.sh b/docker/util/intellij/cpwebapp.sh similarity index 100% rename from scripts/intellij/cpwebapp.sh rename to docker/util/intellij/cpwebapp.sh diff --git a/scripts/intellij/watchers.xml b/docker/util/intellij/watchers.xml similarity index 100% rename from scripts/intellij/watchers.xml rename to docker/util/intellij/watchers.xml From 67ee8b7835bcdc0907083389bfa9d2fe1256680e Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Thu, 22 Feb 2024 07:55:38 +0100 Subject: [PATCH 381/689] docs(ct): add README for IntelliJ auto-copy save trigger --- docker/util/intellij/README.md | 13 +++++++++++++ docker/util/intellij/cpwebapp.sh | 11 ----------- 2 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 docker/util/intellij/README.md diff --git a/docker/util/intellij/README.md b/docker/util/intellij/README.md new file mode 100644 index 00000000000..281d0e50ea6 --- /dev/null +++ b/docker/util/intellij/README.md @@ -0,0 +1,13 @@ +# IntelliJ Auto-Copy of Webapp Files + +When deploying the webapp via Payara Tools, you can use this tool to immediately copy changes to non-code files into the running deployment, instantly seeing changes in your browser. + +Note: as this relies on using a Bash shell script, it is pretty much limited to Mac and Linux. +Feel free to extend and provide a PowerShell equivalent! + +1. Install the [File Watcher plugin](https://plugins.jetbrains.com/plugin/7177-file-watchers) +2. Import the [watchers.xml](./watchers.xml) file at *File > Settings > Tools > File Watchers* +3. Once you have the deployment running (see Container Guides), editing files at `src/main/webapp` will be copied into the deployment after saving the edited file. + +Alternatively, you can add an External tool and trigger via menu or shortcut to do the copying manually: +https://www.jetbrains.com/help/idea/configuring-third-party-tools.html diff --git a/docker/util/intellij/cpwebapp.sh b/docker/util/intellij/cpwebapp.sh index a823f8871ce..0a59463f5aa 100755 --- a/docker/util/intellij/cpwebapp.sh +++ b/docker/util/intellij/cpwebapp.sh @@ -2,17 +2,6 @@ # # cpwebapp # -# Usage: -# -# Add a File watcher by importing watchers.xml into IntelliJ IDEA, and let it do the copying whenever you save a -# file under webapp. -# -# https://www.jetbrains.com/help/idea/settings-tools-file-watchers.html -# -# Alternatively, you can add an External tool and trigger via menu or shortcut to do the copying manually: -# -# https://www.jetbrains.com/help/idea/configuring-third-party-tools.html -# set -eu From fa61267a1de69527a504e846f5e97d7713bd166d Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Thu, 22 Feb 2024 07:56:22 +0100 Subject: [PATCH 382/689] fix(ct): properly quote var in expression Thank you, shellcheck! --- docker/util/intellij/cpwebapp.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/util/intellij/cpwebapp.sh b/docker/util/intellij/cpwebapp.sh index 0a59463f5aa..2d08fb1a873 100755 --- a/docker/util/intellij/cpwebapp.sh +++ b/docker/util/intellij/cpwebapp.sh @@ -7,7 +7,7 @@ set -eu PROJECT_DIR="$1" FILE_TO_COPY="$2" -RELATIVE_PATH="${FILE_TO_COPY#$PROJECT_DIR/}" +RELATIVE_PATH="${FILE_TO_COPY#"${PROJECT_DIR}/"}" # Check if RELATIVE_PATH starts with 'src/main/webapp', otherwise ignore if [[ "$RELATIVE_PATH" == "src/main/webapp"* ]]; then From d20785a97b392ce493dec84357973412d070d0b7 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Thu, 22 Feb 2024 07:56:46 +0100 Subject: [PATCH 383/689] fix(ct): correct path in watchers.xml to shell script location --- docker/util/intellij/watchers.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/util/intellij/watchers.xml b/docker/util/intellij/watchers.xml index e118fea558f..4ccee125ec2 100644 --- a/docker/util/intellij/watchers.xml +++ b/docker/util/intellij/watchers.xml @@ -12,7 +12,7 @@

    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 WN7MtqTsj1xKqS9VV&j&*ng0eP(x>YH literal 0 HcmV?d00001 diff --git a/doc/sphinx-guides/source/developers/classic-dev-env.rst b/doc/sphinx-guides/source/developers/classic-dev-env.rst index d7b7f281634..015ba43644d 100755 --- a/doc/sphinx-guides/source/developers/classic-dev-env.rst +++ b/doc/sphinx-guides/source/developers/classic-dev-env.rst @@ -88,6 +88,8 @@ On Mac, run this command: On Linux, install ``jq`` from your package manager or download a binary from https://stedolan.github.io/jq/ +.. _install-payara-dev: + Install Payara ~~~~~~~~~~~~~~ From efbdb72d8d8d985dc1efa4141bd14041e397c242 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 13 Feb 2024 22:18:01 -0500 Subject: [PATCH 318/689] US English spelling for consistency #9590 --- doc/sphinx-guides/source/container/dev-usage.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/sphinx-guides/source/container/dev-usage.rst b/doc/sphinx-guides/source/container/dev-usage.rst index 85b1b3e5f05..3e7dd374036 100644 --- a/doc/sphinx-guides/source/container/dev-usage.rst +++ b/doc/sphinx-guides/source/container/dev-usage.rst @@ -231,7 +231,7 @@ When opting for builtin features or Payara tools, please follow these steps: .. image:: img/intellij-payara-config-startup.png - You might want to tweak the hot deploy behaviour in the "Server" tab now. + You might want to tweak the hot deploy behavior in the "Server" tab now. "Update action" can be found in the run window (see below). "Frame deactivation" means switching from IntelliJ window to something else, e.g. your browser. *Note: static resources like properties, XHTML etc will only update when redeploying!* @@ -271,7 +271,7 @@ When opting for builtin features or Payara tools, please follow these steps: .. image:: img/intellij-payara-run-toolbar.png Watch the WAR build and the deployment unfold. - Note the "Update" action button (see config to change its behaviour). + Note the "Update" action button (see config to change its behavior). .. image:: img/intellij-payara-run-output.png From 6666857ef8d425bb46fd83569f6b3f1b1d620dc2 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 13 Feb 2024 22:22:12 -0500 Subject: [PATCH 319/689] switch away from hard-coded numbers in lists #9590 --- doc/sphinx-guides/source/container/dev-usage.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/sphinx-guides/source/container/dev-usage.rst b/doc/sphinx-guides/source/container/dev-usage.rst index 3e7dd374036..d2bf820d89a 100644 --- a/doc/sphinx-guides/source/container/dev-usage.rst +++ b/doc/sphinx-guides/source/container/dev-usage.rst @@ -154,21 +154,21 @@ IDE-triggered re-deployments You have at least two options: -1. Use builtin features of IDEs or plugins for different IDEs by Payara to ease the burden of redeploying an application during development to a running Payara application server. +#. Use builtin features of IDEs or plugins for different IDEs by Payara to ease the burden of redeploying an application during development to a running Payara application server. Their guides contain `documentation on Payara IDE plugins `_. -2. Use a paid product like `JRebel `_. +#. Use a paid product like `JRebel `_. The main difference between the first and the second option is support for hot deploys of non-class files plus limitations in what the JVM HotswapAgent can do for you. Find more `details in a blog article by JRebel `_. When opting for builtin features or Payara tools, please follow these steps: -1. | Download the Payara appserver to your machine, unzip and note the location for later. +#. | Download the Payara appserver to your machine, unzip and note the location for later. | - See :ref:`payara` for which version or run the following command | ``mvn help:evaluate -Dexpression=payara.version -q -DforceStdout`` | - To download, see :ref:`payara` or try `Maven Central `_. -2. Install Payara tools plugin in your IDE: +#. Install Payara tools plugin in your IDE: .. tabs:: .. group-tab:: Netbeans @@ -182,7 +182,7 @@ When opting for builtin features or Payara tools, please follow these steps: .. image:: img/intellij-payara-plugin-install.png -3. Configure a connection to the application server: +#. Configure a connection to the application server: .. tabs:: .. group-tab:: Netbeans @@ -238,13 +238,13 @@ When opting for builtin features or Payara tools, please follow these steps: .. image:: img/intellij-payara-config-server-behaviour.png -4. | Start all the containers. Follow the cheat sheet above, but take care to skip application deployment: +#. | Start all the containers. Follow the cheat sheet above, but take care to skip application deployment: | - When using the Maven commands, append ``-Dapp.deploy.skip``. For example: | ``mvn -Pct docker:run -Dapp.deploy.skip`` | - When using Docker Compose, prepend the command with ``SKIP_DEPLOY=1``. For example: | ``SKIP_DEPLOY=1 docker compose -f docker-compose-dev.yml up`` | - Note: the Admin Console can be reached at http://localhost:4848 or https://localhost:4949 -5. To deploy the application to the running server, use the configured tools to deploy. +#. To deploy the application to the running server, use the configured tools to deploy. Using the "Run" configuration only deploys and enables redeploys, while running "Debug" enables hot swapping of classes via JDWP. .. tabs:: From 0c0067e5e21775c6ea8a8635cb16599493708930 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 13 Feb 2024 22:56:16 -0500 Subject: [PATCH 320/689] various doc tweaks #9590 --- .../source/container/dev-usage.rst | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/doc/sphinx-guides/source/container/dev-usage.rst b/doc/sphinx-guides/source/container/dev-usage.rst index d2bf820d89a..6dbd0276cb3 100644 --- a/doc/sphinx-guides/source/container/dev-usage.rst +++ b/doc/sphinx-guides/source/container/dev-usage.rst @@ -144,29 +144,30 @@ Alternatives: Redeploying ----------- -Rebuild and Running Images -^^^^^^^^^^^^^^^^^^^^^^^^^^ +Rebuilding and Running Images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The safest way to redeploy code is to stop the running containers (with Ctrl-c if you started them in the foreground) and then build and run them again with ``mvn -Pct clean package docker:run``. +The safest and most reliable way to redeploy code is to stop the running containers (with Ctrl-c if you started them in the foreground) and then build and run them again with ``mvn -Pct clean package docker:run``. -IDE-triggered re-deployments -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +IDE-Triggered Redeployments +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Triggering redeployment using an IDE can greatly improve your feedback look when changing code. You have at least two options: -#. Use builtin features of IDEs or plugins for different IDEs by Payara to ease the burden of redeploying an application during development to a running Payara application server. - Their guides contain `documentation on Payara IDE plugins `_. +#. Use builtin features of IDEs or `IDE plugins from Payara `_. #. Use a paid product like `JRebel `_. -The main difference between the first and the second option is support for hot deploys of non-class files plus limitations in what the JVM HotswapAgent can do for you. -Find more `details in a blog article by JRebel `_. +The main differences between the first and the second options are support for hot deploys of non-class files and limitations in what the JVM HotswapAgent can do for you. +Find more details in a `blog article by JRebel `_. -When opting for builtin features or Payara tools, please follow these steps: +To make use of builtin features or Payara tools (option 1), please follow these steps: -#. | Download the Payara appserver to your machine, unzip and note the location for later. - | - See :ref:`payara` for which version or run the following command - | ``mvn help:evaluate -Dexpression=payara.version -q -DforceStdout`` - | - To download, see :ref:`payara` or try `Maven Central `_. +#. | Download the version of Payara shown in :ref:`install-payara-dev` and unzip it to a reasonable location such as ``/usr/local/payara6``. + | - Note that Payara can also be downloaded from `Maven Central `_. + | - Note that another way to check the expected version of Payara is to run this command: + | ``mvn help:evaluate -Dexpression=payara.version -q -DforceStdout`` #. Install Payara tools plugin in your IDE: @@ -182,16 +183,14 @@ When opting for builtin features or Payara tools, please follow these steps: .. image:: img/intellij-payara-plugin-install.png -#. Configure a connection to the application server: +#. Configure a connection to Payara: .. tabs:: .. group-tab:: Netbeans - Unzip Payara to ``/usr/local/payara6`` as explained in :ref:`install-payara-dev`. - - Launch Netbeans and click "Tools" and then "Servers". Click "Add Server" and select "Payara Server" and set the installation location to ``/usr/local/payara6``. Use the settings in the screenshot below. Most of the defaults are fine. + Launch Netbeans and click "Tools" and then "Servers". Click "Add Server" and select "Payara Server" and set the installation location to ``/usr/local/payara6`` (or wherever you unzipped Payara). Choose "Remote Domain". Use the settings in the screenshot below. Most of the defaults are fine. - Under "Common", the password should be "admin". Make sure "Enable Hot Deploy" is checked. + Under "Common", the username and password should be "admin". Make sure "Enable Hot Deploy" is checked. .. image:: img/netbeans-servers-common.png @@ -203,7 +202,7 @@ When opting for builtin features or Payara tools, please follow these steps: .. image:: img/netbeans-compile.png - Under "Run", select "Payara Server" under "Server" and make sure "Deploy on Save" is checked. + Under "Run", under "Server", select "Payara Server". Make sure "Deploy on Save" is checked. .. image:: img/netbeans-run.png @@ -265,6 +264,12 @@ When opting for builtin features or Payara tools, please follow these steps: Check to make sure the change is live by visiting, for example, http://localhost:8080/api/info/version + See below for a `video `_ demonstrating the steps above but please note that the ports used have changed and now that we have the concept of "skip deploy" the undeployment step shown is no longer necessary. + + .. raw:: html + + + .. group-tab:: IntelliJ Choose "Run" or "Debug" in the toolbar. From b2d5ea8381e15cc861e04b7a33a20a10b20762e0 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 13 Feb 2024 23:03:05 -0500 Subject: [PATCH 321/689] add release note #9590 --- doc/release-notes/9590-faster-redeploy.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 doc/release-notes/9590-faster-redeploy.md diff --git a/doc/release-notes/9590-faster-redeploy.md b/doc/release-notes/9590-faster-redeploy.md new file mode 100644 index 00000000000..caaa688bf58 --- /dev/null +++ b/doc/release-notes/9590-faster-redeploy.md @@ -0,0 +1,3 @@ +In the Container Guide, documentation for developers on how to quickly redeploy code has been improved for IntelliJ and Netbeans is now covered. + +Also in the context of containers, a new option to skip deployment has been added and the war file is now consistently named "dataverse.war" rather than having a version in the filename, such as "dataverse-6.1.war". This predictability makes tooling easier. From fc8aac32fa7f1b5d53cdc8f4ca1127b5493f2885 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 14 Feb 2024 11:50:29 +0000 Subject: [PATCH 322/689] Fixed: GetLatestAccessibleFileMetadataCommand --- .../GetLatestAccessibleFileMetadataCommand.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java index 980563a5489..a2022adbc27 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java @@ -2,6 +2,7 @@ import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.FileMetadata; +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; @@ -19,16 +20,20 @@ public GetLatestAccessibleFileMetadataCommand(DataverseRequest aRequest, DataFil @Override public FileMetadata execute(CommandContext ctxt) throws CommandException { - FileMetadata fileMetadata = ctxt.engine().submit( - new GetLatestPublishedFileMetadataCommand(getRequest(), dataFile) - ); + FileMetadata fileMetadata = null; - if (fileMetadata == null) { + if (ctxt.permissions().requestOn(getRequest(), dataFile.getOwner()).has(Permission.ViewUnpublishedDataset)) { fileMetadata = ctxt.engine().submit( new GetDraftFileMetadataIfAvailableCommand(getRequest(), dataFile) ); } + if (fileMetadata == null) { + fileMetadata = ctxt.engine().submit( + new GetLatestPublishedFileMetadataCommand(getRequest(), dataFile) + ); + } + return fileMetadata; } } From 153b9ae0f04283386c6f7a70920285aece25a990 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 14 Feb 2024 12:00:23 +0000 Subject: [PATCH 323/689] Added: FilesIT testGetFileInfo test cases --- .../edu/harvard/iq/dataverse/api/FilesIT.java | 45 ++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java index d84b0ed77ac..9af457c35c4 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java @@ -11,8 +11,7 @@ import org.junit.jupiter.api.BeforeAll; import io.restassured.path.json.JsonPath; -import static edu.harvard.iq.dataverse.api.ApiConstants.DS_VERSION_DRAFT; -import static edu.harvard.iq.dataverse.api.ApiConstants.DS_VERSION_LATEST_PUBLISHED; +import static edu.harvard.iq.dataverse.api.ApiConstants.*; import static io.restassured.path.json.JsonPath.with; import io.restassured.path.xml.XmlPath; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; @@ -1450,9 +1449,9 @@ public void testGetFileInfo() { .statusCode(NOT_FOUND.getStatusCode()); // Update the file metadata - String newFileName = "trees_2.png"; + String newFileNameFirstUpdate = "trees_2.png"; JsonObjectBuilder updateFileMetadata = Json.createObjectBuilder() - .add("label", newFileName); + .add("label", newFileNameFirstUpdate); Response updateFileMetadataResponse = UtilIT.updateFileMetadata(dataFileId, updateFileMetadata.build().toString(), superUserApiToken); updateFileMetadataResponse.then().statusCode(OK.getStatusCode()); @@ -1471,12 +1470,46 @@ public void testGetFileInfo() { publishDatasetResp.then().assertThat() .statusCode(OK.getStatusCode()); + // Update the file metadata once again + String newFileNameSecondUpdate = "trees_3.png"; + updateFileMetadata = Json.createObjectBuilder() + .add("label", newFileNameSecondUpdate); + updateFileMetadataResponse = UtilIT.updateFileMetadata(dataFileId, updateFileMetadata.build().toString(), superUserApiToken); + updateFileMetadataResponse.then().statusCode(OK.getStatusCode()); + // Regular user should get to see latest published file data getFileDataResponse = UtilIT.getFileData(dataFileId, apiTokenRegular, DS_VERSION_LATEST_PUBLISHED); getFileDataResponse.then().assertThat() .statusCode(OK.getStatusCode()) - .body("data.label", equalTo(newFileName)); - // TODO + .body("data.label", equalTo(newFileNameFirstUpdate)); + + // Regular user should get to see latest published file data if latest is requested + getFileDataResponse = UtilIT.getFileData(dataFileId, apiTokenRegular, DS_VERSION_LATEST); + getFileDataResponse.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.label", equalTo(newFileNameFirstUpdate)); + + // Superuser should get to see draft file data if latest is requested + getFileDataResponse = UtilIT.getFileData(dataFileId, superUserApiToken, DS_VERSION_LATEST); + getFileDataResponse.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.label", equalTo(newFileNameSecondUpdate)); + + // Publish dataset once again + publishDatasetResp = UtilIT.publishDatasetViaNativeApi(datasetId, "major", superUserApiToken); + publishDatasetResp.then().assertThat() + .statusCode(OK.getStatusCode()); + + // Regular user should get to see file data by specific version number + getFileDataResponse = UtilIT.getFileData(dataFileId, apiTokenRegular, "2.0"); + getFileDataResponse.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.label", equalTo(newFileNameFirstUpdate)); + + getFileDataResponse = UtilIT.getFileData(dataFileId, apiTokenRegular, "3.0"); + getFileDataResponse.then().assertThat() + .statusCode(OK.getStatusCode()) + .body("data.label", equalTo(newFileNameSecondUpdate)); // Cleanup Response destroyDatasetResponse = UtilIT.destroyDataset(datasetId, superUserApiToken); From 2a30f328b91faade4a0827a826d51095a1788053 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 14 Feb 2024 14:05:51 +0000 Subject: [PATCH 324/689] Stash: includeDeaccessioned support on get file info endpoint wip --- ...etLatestAccessibleFileMetadataCommand.java | 6 ++-- ...GetLatestPublishedFileMetadataCommand.java | 4 ++- ...edFileMetadataByDatasetVersionCommand.java | 33 +++++++++++-------- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java index a2022adbc27..fa80b75c593 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestAccessibleFileMetadataCommand.java @@ -12,10 +12,12 @@ @RequiredPermissions({}) public class GetLatestAccessibleFileMetadataCommand extends AbstractCommand { private final DataFile dataFile; + private final boolean includeDeaccessioned; - public GetLatestAccessibleFileMetadataCommand(DataverseRequest aRequest, DataFile dataFile) { + public GetLatestAccessibleFileMetadataCommand(DataverseRequest aRequest, DataFile dataFile, boolean includeDeaccessioned) { super(aRequest, dataFile); this.dataFile = dataFile; + this.includeDeaccessioned = includeDeaccessioned; } @Override @@ -30,7 +32,7 @@ public FileMetadata execute(CommandContext ctxt) throws CommandException { if (fileMetadata == null) { fileMetadata = ctxt.engine().submit( - new GetLatestPublishedFileMetadataCommand(getRequest(), dataFile) + new GetLatestPublishedFileMetadataCommand(getRequest(), dataFile, includeDeaccessioned) ); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedFileMetadataCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedFileMetadataCommand.java index 147a0fdce76..4056d145917 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedFileMetadataCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetLatestPublishedFileMetadataCommand.java @@ -11,10 +11,12 @@ @RequiredPermissions({}) public class GetLatestPublishedFileMetadataCommand extends AbstractCommand { private final DataFile dataFile; + private final boolean includeDeaccessioned; - public GetLatestPublishedFileMetadataCommand(DataverseRequest aRequest, DataFile dataFile) { + public GetLatestPublishedFileMetadataCommand(DataverseRequest aRequest, DataFile dataFile, boolean includeDeaccessioned) { super(aRequest, dataFile); this.dataFile = dataFile; + this.includeDeaccessioned = includeDeaccessioned; } @Override diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetSpecificPublishedFileMetadataByDatasetVersionCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetSpecificPublishedFileMetadataByDatasetVersionCommand.java index 84a51f6b31d..82350d3bd95 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetSpecificPublishedFileMetadataByDatasetVersionCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GetSpecificPublishedFileMetadataByDatasetVersionCommand.java @@ -1,43 +1,48 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.FileMetadata; +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 java.util.List; - @RequiredPermissions({}) public class GetSpecificPublishedFileMetadataByDatasetVersionCommand extends AbstractCommand { private final long majorVersion; private final long minorVersion; private final DataFile dataFile; + private final boolean includeDeaccessioned; - public GetSpecificPublishedFileMetadataByDatasetVersionCommand(DataverseRequest aRequest, DataFile dataFile, long majorVersion, long minorVersion) { + public GetSpecificPublishedFileMetadataByDatasetVersionCommand(DataverseRequest aRequest, DataFile dataFile, long majorVersion, long minorVersion, boolean includeDeaccessioned) { super(aRequest, dataFile); this.dataFile = dataFile; this.majorVersion = majorVersion; this.minorVersion = minorVersion; + this.includeDeaccessioned = includeDeaccessioned; } @Override public FileMetadata execute(CommandContext ctxt) throws CommandException { - List fileMetadatas = dataFile.getFileMetadatas(); - - for (FileMetadata fileMetadata : fileMetadatas) { - DatasetVersion datasetVersion = fileMetadata.getDatasetVersion(); + return dataFile.getFileMetadatas().stream() + .filter(fileMetadata -> isRequestedVersionFileMetadata(fileMetadata, ctxt)) + .findFirst() + .orElse(null); + } - if (datasetVersion.isPublished() && - datasetVersion.getVersionNumber().equals(majorVersion) && - datasetVersion.getMinorVersionNumber().equals(minorVersion)) { - return fileMetadata; - } - } + private boolean isRequestedVersionFileMetadata(FileMetadata fileMetadata, CommandContext ctxt) { + DatasetVersion datasetVersion = fileMetadata.getDatasetVersion(); + Dataset ownerDataset = dataFile.getOwner(); + return (datasetVersion.isReleased() || isDatasetVersionDeaccessionedAndAccessible(datasetVersion, ownerDataset, ctxt)) + && datasetVersion.getVersionNumber().equals(majorVersion) + && datasetVersion.getMinorVersionNumber().equals(minorVersion); + } - return null; + private boolean isDatasetVersionDeaccessionedAndAccessible(DatasetVersion datasetVersion, Dataset ownerDataset, CommandContext ctxt) { + return includeDeaccessioned && datasetVersion.isDeaccessioned() && ctxt.permissions().requestOn(getRequest(), ownerDataset).has(Permission.EditDataset); } } From b5aeb258d025036c2da808d775e8d5bbf15add38 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 14 Feb 2024 14:06:20 +0000 Subject: [PATCH 325/689] Stash: includeDeaccessioned support on get file info endpoint wip (2) --- .../edu/harvard/iq/dataverse/api/Files.java | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index 4116bf18973..55d65bae96b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -486,23 +486,37 @@ public Response updateFileMetadata(@Context ContainerRequestContext crc, @FormDa @GET @AuthRequired @Path("{id}") - public Response getFileData(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @Context UriInfo uriInfo, @Context HttpHeaders headers) { - return response( req -> getFileDataResponse(req, fileIdOrPersistentId, uriInfo, headers, DS_VERSION_LATEST), getRequestUser(crc)); + public Response getFileData(@Context ContainerRequestContext crc, + @PathParam("id") String fileIdOrPersistentId, + @QueryParam("includeDeaccessioned") boolean includeDeaccessioned, + @Context UriInfo uriInfo, + @Context HttpHeaders headers) { + return response( req -> getFileDataResponse(req, fileIdOrPersistentId, DS_VERSION_LATEST, includeDeaccessioned, uriInfo, headers), getRequestUser(crc)); } @GET @AuthRequired @Path("{id}/versions/{datasetVersionId}") - public Response getFileData(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @PathParam("datasetVersionId") String datasetVersionId, @Context UriInfo uriInfo, @Context HttpHeaders headers) { - return response( req -> getFileDataResponse(req, fileIdOrPersistentId, uriInfo, headers, datasetVersionId), getRequestUser(crc)); + public Response getFileData(@Context ContainerRequestContext crc, + @PathParam("id") String fileIdOrPersistentId, + @PathParam("datasetVersionId") String datasetVersionId, + @QueryParam("includeDeaccessioned") boolean includeDeaccessioned, + @Context UriInfo uriInfo, + @Context HttpHeaders headers) { + return response( req -> getFileDataResponse(req, fileIdOrPersistentId, datasetVersionId, includeDeaccessioned, uriInfo, headers), getRequestUser(crc)); } - private Response getFileDataResponse(final DataverseRequest req, String fileIdOrPersistentId, UriInfo uriInfo, HttpHeaders headers, String datasetVersionId) throws WrappedResponse { + private Response getFileDataResponse(final DataverseRequest req, + String fileIdOrPersistentId, + String datasetVersionId, + boolean includeDeaccessioned, + UriInfo uriInfo, + HttpHeaders headers) throws WrappedResponse { final DataFile dataFile = execCommand(new GetDataFileCommand(req, findDataFileOrDie(fileIdOrPersistentId))); FileMetadata fileMetadata = execCommand(handleVersion(datasetVersionId, new Datasets.DsVersionHandler<>() { @Override public Command handleLatest() { - return new GetLatestAccessibleFileMetadataCommand(req, dataFile); + return new GetLatestAccessibleFileMetadataCommand(req, dataFile, includeDeaccessioned); } @Override @@ -512,12 +526,12 @@ public Command handleDraft() { @Override public Command handleSpecific(long major, long minor) { - return new GetSpecificPublishedFileMetadataByDatasetVersionCommand(req, dataFile, major, minor); + return new GetSpecificPublishedFileMetadataByDatasetVersionCommand(req, dataFile, major, minor, includeDeaccessioned); } @Override public Command handleLatestPublished() { - return new GetLatestPublishedFileMetadataCommand(req, dataFile); + return new GetLatestPublishedFileMetadataCommand(req, dataFile, includeDeaccessioned); } })); From 48e71ec74a49ad6145f93f38c07b175533642813 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 14 Feb 2024 14:40:48 +0000 Subject: [PATCH 326/689] Refactor: DataFile getLatestFileMetadata and getLatestPublishedFileMetadata methods --- .../edu/harvard/iq/dataverse/DataFile.java | 68 +++++++++---------- 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataFile.java b/src/main/java/edu/harvard/iq/dataverse/DataFile.java index 3d8086b142b..8f2e0d4261e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataFile.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataFile.java @@ -549,57 +549,51 @@ public void setDescription(String description) { public FileMetadata getFileMetadata() { return getLatestFileMetadata(); } - + public FileMetadata getLatestFileMetadata() { - FileMetadata fmd = null; + FileMetadata resultFileMetadata = null; - // for newly added or harvested, just return the one fmd if (fileMetadatas.size() == 1) { return fileMetadatas.get(0); } - + for (FileMetadata fileMetadata : fileMetadatas) { - // if it finds a draft, return it if (fileMetadata.getDatasetVersion().getVersionState().equals(VersionState.DRAFT)) { return fileMetadata; - } - - // otherwise return the one with the latest version number - // duplicate logic in getLatestPublishedFileMetadata() - if (fmd == null || fileMetadata.getDatasetVersion().getVersionNumber().compareTo( fmd.getDatasetVersion().getVersionNumber() ) > 0 ) { - fmd = fileMetadata; - } else if ((fileMetadata.getDatasetVersion().getVersionNumber().compareTo( fmd.getDatasetVersion().getVersionNumber())==0 )&& - ( fileMetadata.getDatasetVersion().getMinorVersionNumber().compareTo( fmd.getDatasetVersion().getMinorVersionNumber()) > 0 ) ) { - fmd = fileMetadata; } + resultFileMetadata = getTheNewerFileMetadata(resultFileMetadata, fileMetadata); } - return fmd; + + return resultFileMetadata; } - -// //Returns null if no published version + public FileMetadata getLatestPublishedFileMetadata() throws UnsupportedOperationException { - FileMetadata fmd = null; - - for (FileMetadata fileMetadata : fileMetadatas) { - // if it finds a draft, skip - if (fileMetadata.getDatasetVersion().getVersionState().equals(VersionState.DRAFT)) { - continue; - } - - // otherwise return the one with the latest version number - // duplicate logic in getLatestFileMetadata() - if (fmd == null || fileMetadata.getDatasetVersion().getVersionNumber().compareTo( fmd.getDatasetVersion().getVersionNumber() ) > 0 ) { - fmd = fileMetadata; - } else if ((fileMetadata.getDatasetVersion().getVersionNumber().compareTo( fmd.getDatasetVersion().getVersionNumber())==0 )&& - ( fileMetadata.getDatasetVersion().getMinorVersionNumber().compareTo( fmd.getDatasetVersion().getMinorVersionNumber()) > 0 ) ) { - fmd = fileMetadata; - } - } - if(fmd == null) { + FileMetadata resultFileMetadata = fileMetadatas.stream() + .filter(metadata -> !metadata.getDatasetVersion().getVersionState().equals(VersionState.DRAFT)) + .reduce(null, this::getTheNewerFileMetadata); + + if (resultFileMetadata == null) { throw new UnsupportedOperationException("No published metadata version for DataFile " + this.getId()); } - return fmd; + return resultFileMetadata; + } + + private FileMetadata getTheNewerFileMetadata(FileMetadata currentFileMetadata, FileMetadata newFileMetadata) { + if (currentFileMetadata == null) { + return newFileMetadata; + } + + DatasetVersion currentVersion = currentFileMetadata.getDatasetVersion(); + DatasetVersion newVersion = newFileMetadata.getDatasetVersion(); + + if (newVersion.getVersionNumber().compareTo(currentVersion.getVersionNumber()) > 0 || + (newVersion.getVersionNumber().compareTo(currentVersion.getVersionNumber()) == 0 && + newVersion.getMinorVersionNumber().compareTo(currentVersion.getMinorVersionNumber()) > 0)) { + return newFileMetadata; + } + + return currentFileMetadata; } /** @@ -610,7 +604,7 @@ public long getFilesize() { if (this.filesize == null) { // -1 means "unknown" return -1; - } + } return this.filesize; } From 227fe53ba707e24d89c3e045337cf5e3860a9820 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 14 Feb 2024 15:57:47 +0000 Subject: [PATCH 327/689] Refactor: using DatasetVersion.compareByVersion in getTheNewerFileMetadata --- .../edu/harvard/iq/dataverse/DataFile.java | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataFile.java b/src/main/java/edu/harvard/iq/dataverse/DataFile.java index 8f2e0d4261e..818cade1eef 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataFile.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataFile.java @@ -579,21 +579,19 @@ public FileMetadata getLatestPublishedFileMetadata() throws UnsupportedOperation return resultFileMetadata; } - private FileMetadata getTheNewerFileMetadata(FileMetadata currentFileMetadata, FileMetadata newFileMetadata) { - if (currentFileMetadata == null) { - return newFileMetadata; + private FileMetadata getTheNewerFileMetadata(FileMetadata current, FileMetadata candidate) { + if (current == null) { + return candidate; } - DatasetVersion currentVersion = currentFileMetadata.getDatasetVersion(); - DatasetVersion newVersion = newFileMetadata.getDatasetVersion(); + DatasetVersion currentVersion = current.getDatasetVersion(); + DatasetVersion candidateVersion = candidate.getDatasetVersion(); - if (newVersion.getVersionNumber().compareTo(currentVersion.getVersionNumber()) > 0 || - (newVersion.getVersionNumber().compareTo(currentVersion.getVersionNumber()) == 0 && - newVersion.getMinorVersionNumber().compareTo(currentVersion.getMinorVersionNumber()) > 0)) { - return newFileMetadata; + if (DatasetVersion.compareByVersion.compare(candidateVersion, currentVersion) > 0) { + return candidate; } - return currentFileMetadata; + return current; } /** From 1dc4825cb1aa2f204958782e238ad77ac4e231b6 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Feb 2024 13:19:13 -0500 Subject: [PATCH 328/689] update perms --- .../edu/harvard/iq/dataverse/FilePage.java | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/FilePage.java b/src/main/java/edu/harvard/iq/dataverse/FilePage.java index 4e5843964e7..37798f1cd3c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/FilePage.java +++ b/src/main/java/edu/harvard/iq/dataverse/FilePage.java @@ -484,7 +484,7 @@ public String restrictFile(boolean restricted) throws CommandException{ public String ingestFile() throws CommandException{ User u = session.getUser(); - if(!u.isAuthenticated() || !(permissionService.permissionsFor(u, file).contains(Permission.PublishDataset))) { + if(!u.isAuthenticated() || !u.isSuperuser()) { //Shouldn't happen (choice not displayed for users who don't have the right permission), but check anyway logger.warning("User: " + u.getIdentifier() + " tried to ingest a file"); JH.addMessage(FacesMessage.SEVERITY_WARN, BundleUtil.getStringFromBundle("file.ingest.cantIngestFileWarning")); @@ -544,23 +544,29 @@ public String ingestFile() throws CommandException{ } public String uningestFile() throws CommandException { - + if (!file.isTabularData()) { - if(file.isIngestProblem()) { - User u = session.getUser(); - if(!u.isAuthenticated() || !(permissionService.permissionsFor(u, file).contains(Permission.PublishDataset))) { - logger.warning("User: " + u.getIdentifier() + " tried to uningest a file"); - //Shouldn't happen (choice not displayed for users who don't have the right permission), but check anyway - JH.addMessage(FacesMessage.SEVERITY_WARN, BundleUtil.getStringFromBundle("file.ingest.cantUningestFileWarning")); - return null; - } - file.setIngestDone(); - file.setIngestReport(null); + //Ingest never succeeded, either there was a failure or this is not a tabular data file + User u = session.getUser(); + if (!u.isAuthenticated() || !u.isSuperuser()) { + logger.warning("User: " + u.getIdentifier() + " tried to uningest a file"); + // Shouldn't happen (choice not displayed for users who don't have the right + // permission), but check anyway + JH.addMessage(FacesMessage.SEVERITY_WARN, + BundleUtil.getStringFromBundle("file.ingest.cantUningestFileWarning")); + return null; + } + if (file.isIngestProblem()) { + file.setIngestDone(); + file.setIngestReport(null); } else { - JH.addMessage(FacesMessage.SEVERITY_WARN, BundleUtil.getStringFromBundle("file.ingest.cantUningestFileWarning")); - return null; + //Shouldn't happen - got called when there is no tabular data or an ingest problem + JH.addMessage(FacesMessage.SEVERITY_WARN, + BundleUtil.getStringFromBundle("file.ingest.cantUningestFileWarning")); + return null; } } else { + //Uningest command does it's own check for isSuperuser commandEngine.submit(new UningestFileCommand(dvRequestService.getDataverseRequest(), file)); Long dataFileId = file.getId(); file = datafileService.find(dataFileId); @@ -580,12 +586,11 @@ public String uningestFile() throws CommandException { } } save(); - //Refresh filemetadata with file title, etc. + // Refresh filemetadata with file title, etc. init(); JH.addMessage(FacesMessage.SEVERITY_INFO, BundleUtil.getStringFromBundle("file.uningest.complete")); return returnToDraftVersion(); - } - + } private List filesToBeDeleted = new ArrayList<>(); From f15122615aea9109ed90b2ff5c4e6a4965f8efcf Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Feb 2024 13:19:23 -0500 Subject: [PATCH 329/689] add bundle strings --- src/main/java/propertyFiles/Bundle.properties | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index f1c8381816c..42c844e532e 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -2203,6 +2203,15 @@ ingest.csv.lineMismatch=Mismatch between line counts in first and final passes!, ingest.csv.recordMismatch=Reading mismatch, line {0} of the Data file: {1} delimited values expected, {2} found. ingest.csv.nullStream=Stream can't be null. +file.ingest=Ingest +file.uningest=Uningest +file.ingest.alreadyIngestedWarning=This file has already been ingested +file.ingest.ingestInProgressWarning=Ingestion of this file is already in progress +file.ingest.cantIngestFileWarning=Ingest not supported for this file type +file.ingest.ingestQueued=Ingestion has been requested +file.ingest.cantUningestFileWarning=This file cannot be uningested +file.uningest.complete=Uningestion of this file has been completed + # editdatafile.xhtml # editFilesFragment.xhtml From 14b280cf39dee856dfa9a1a6e97f1a7392418a02 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Feb 2024 13:19:33 -0500 Subject: [PATCH 330/689] doc updates --- doc/sphinx-guides/source/api/native-api.rst | 4 ++++ .../source/user/tabulardataingest/ingestprocess.rst | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index dbe769e2fd1..8cfa5deb96c 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -2854,6 +2854,8 @@ The fully expanded example above (without environment variables) looks like this curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X PUT -d true "https://demo.dataverse.org/api/files/:persistentId/restrict?persistentId=doi:10.5072/FK2/AAA000" +.. _file-uningest: + Uningest a File ~~~~~~~~~~~~~~~ @@ -2891,6 +2893,8 @@ The fully expanded example above (without environment variables) looks like this curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X POST "https://demo.dataverse.org/api/files/:persistentId/uningest?persistentId=doi:10.5072/FK2/AAA000" +.. _file-reingest: + Reingest a File ~~~~~~~~~~~~~~~ diff --git a/doc/sphinx-guides/source/user/tabulardataingest/ingestprocess.rst b/doc/sphinx-guides/source/user/tabulardataingest/ingestprocess.rst index 9e82ff12b9b..ac5fb5af4ec 100644 --- a/doc/sphinx-guides/source/user/tabulardataingest/ingestprocess.rst +++ b/doc/sphinx-guides/source/user/tabulardataingest/ingestprocess.rst @@ -66,11 +66,11 @@ This is non-fatal. The Dataverse software will not produce a .tab version of the who can see the draft version of the dataset containing the file that will indicate why ingest failed. When the file is published as part of the dataset, there will be no indication that ingest was attempted and failed. -If the warning message is a concern, the Dataverse software includes both an API call (see the Files section of the :doc:`/api/native-api` guide) +If the warning message is a concern, the Dataverse software includes both an API call (see :ref:`file-uningest` in the :doc:`/api/native-api` guide) and an Edit/Uningest menu option displayed on the file page, that allow a file to be Uningested. These are only available to superusers. Uningest will remove the warning. Uningest can also be done for a file that was successfully ingested. This will remove the .tab version of the file that was generated. If a file is a tabular format but was never ingested, .e.g. due to the ingest file size limit being lower in the past, or if ingest had failed, -e.g. in a prior Dataverse version, an reingest API (see the Files section of the :doc:`/api/native-api` guide) and a file page Edit/Reingest option +e.g. in a prior Dataverse version, an reingest API (see :ref:`file-reingest` in the :doc:`/api/native-api` guide) and a file page Edit/Reingest option in the user interface allow ingest to be tried again. As with Uningest, this fucntionality is only available to superusers. From 5dffe36c793fa25b9ee8199fda2104fb26f92b9a Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Feb 2024 13:21:09 -0500 Subject: [PATCH 331/689] Apply suggestions from code review Co-authored-by: Philip Durbin --- .../source/user/tabulardataingest/ingestprocess.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/sphinx-guides/source/user/tabulardataingest/ingestprocess.rst b/doc/sphinx-guides/source/user/tabulardataingest/ingestprocess.rst index ac5fb5af4ec..4dce441de4a 100644 --- a/doc/sphinx-guides/source/user/tabulardataingest/ingestprocess.rst +++ b/doc/sphinx-guides/source/user/tabulardataingest/ingestprocess.rst @@ -61,13 +61,13 @@ Uningest and Reingest ===================== Ingest will only work for files whose content can be interpreted as a table. -Multi-sheets spreadsheets and CSV files with different number of entries per row are two examples where ingest will fail. +Multi-sheet spreadsheets and CSV files with a different number of entries per row are two examples where ingest will fail. This is non-fatal. The Dataverse software will not produce a .tab version of the file and will show a warning to users who can see the draft version of the dataset containing the file that will indicate why ingest failed. When the file is published as part of the dataset, there will be no indication that ingest was attempted and failed. If the warning message is a concern, the Dataverse software includes both an API call (see :ref:`file-uningest` in the :doc:`/api/native-api` guide) -and an Edit/Uningest menu option displayed on the file page, that allow a file to be Uningested. These are only available to superusers. +and an Edit/Uningest menu option displayed on the file page, that allow a file to be uningested. These are only available to superusers. Uningest will remove the warning. Uningest can also be done for a file that was successfully ingested. This will remove the .tab version of the file that was generated. From a828fd1ba8cc6d6c01c818ad1a24db7bcbd624e9 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Wed, 14 Feb 2024 14:17:32 -0500 Subject: [PATCH 332/689] #10286 add integration tests --- .../edu/harvard/iq/dataverse/api/Files.java | 1 - .../iq/dataverse/util/json/JsonPrinter.java | 3 +- .../iq/dataverse/api/DataversesIT.java | 28 ++++++++ .../edu/harvard/iq/dataverse/api/FilesIT.java | 69 +++++++++++++++++++ .../edu/harvard/iq/dataverse/api/UtilIT.java | 16 +++++ 5 files changed, 115 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Files.java b/src/main/java/edu/harvard/iq/dataverse/api/Files.java index 155d8953d15..4fd66d45e47 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Files.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Files.java @@ -514,7 +514,6 @@ public Response getFileDataDraft(@Context ContainerRequestContext crc, @PathPara @Path("{id}") public Response getFileData(@Context ContainerRequestContext crc, @PathParam("id") String fileIdOrPersistentId, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response, @QueryParam("returnOwners") Boolean returnOwners) throws WrappedResponse, Exception { Boolean includeOwners = returnOwners == null ? false : returnOwners; - System.out.print("includeOwners: " + includeOwners); return getFileDataResponse(getRequestUser(crc), fileIdOrPersistentId, uriInfo, headers, response, false, includeOwners); } 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 6f750eaddac..d64f77b3526 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 @@ -335,7 +335,8 @@ public static JsonArrayBuilder getOwnersFromDvObject(DvObject dvObject) { } if (dvo.isInstanceofDataset() || dvo.isInstanceofDataFile() ){ if (dvo.getIdentifier() != null){ - ownerObject.add("identifier", dvo.getIdentifier()); + Dataset ds = (Dataset) dvo; + ownerObject.add("identifier", ds.getGlobalId().asString()); } else { ownerObject.add("identifier", dvo.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 78ece6ecc42..e41793a10d5 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java @@ -145,6 +145,34 @@ public void testMinimalDataverse() throws FileNotFoundException { deleteDataverse.prettyPrint(); deleteDataverse.then().assertThat().statusCode(OK.getStatusCode()); } + + + @Test + public void testGetDataverseOwners() throws FileNotFoundException { + Response createUser = UtilIT.createRandomUser(); + createUser.prettyPrint(); + String username = UtilIT.getUsernameFromResponse(createUser); + String apiToken = UtilIT.getApiTokenFromResponse(createUser); + Response createDataverse1Response = UtilIT.createRandomDataverse(apiToken); + + createDataverse1Response.prettyPrint(); + createDataverse1Response.then().assertThat().statusCode(CREATED.getStatusCode()); + + String first = UtilIT.getAliasFromResponse(createDataverse1Response); + + Response getWithOwnersFirst = UtilIT.getDataverseWithOwners(first, apiToken, true); + getWithOwnersFirst.prettyPrint(); + + Response createLevel1a = UtilIT.createSubDataverse(UtilIT.getRandomDvAlias() + "-level1a", null, apiToken, first); + createLevel1a.prettyPrint(); + String level1a = UtilIT.getAliasFromResponse(createLevel1a); + + Response getWithOwners = UtilIT.getDataverseWithOwners(level1a, apiToken, true); + getWithOwners.prettyPrint(); + + getWithOwners.then().assertThat().body("data.ownerArray[0].identifier", equalTo(first)); + + } /** * A regular user can create a Dataverse Collection and access its diff --git a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java index cfc6f9335b3..fe9985115e5 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java @@ -1465,6 +1465,75 @@ public void testGetFileInfo() { assertEquals(200, deleteUserResponse.getStatusCode()); } + @Test + public void testGetFileOwners() { + Response createUser = UtilIT.createRandomUser(); + String username = UtilIT.getUsernameFromResponse(createUser); + String apiToken = UtilIT.getApiTokenFromResponse(createUser); + Response makeSuperUser = UtilIT.makeSuperUser(username); + String dataverseAlias = createDataverseGetAlias(apiToken); + + + Response createDatasetResponse = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, apiToken); + createDatasetResponse.prettyPrint(); + Integer datasetId = JsonPath.from(createDatasetResponse.body().asString()).getInt("data.id"); + + String datasetPid = UtilIT.getDatasetPersistentIdFromResponse(createDatasetResponse); + + createUser = UtilIT.createRandomUser(); + String apiTokenRegular = UtilIT.getApiTokenFromResponse(createUser); + + msg("Add a non-tabular file"); + String pathToFile = "scripts/search/data/binary/trees.png"; + Response addResponse = UtilIT.uploadFileViaNative(datasetId.toString(), pathToFile, apiToken); + + String dataFileId = addResponse.getBody().jsonPath().getString("data.files[0].dataFile.id"); + msgt("datafile id: " + dataFileId); + + addResponse.prettyPrint(); + + Response getFileDataResponse = UtilIT.getFileWithOwners(dataFileId, apiToken, true); + + getFileDataResponse.prettyPrint(); + getFileDataResponse.then().assertThat() + .body("data.label", equalTo("trees.png")) + .body("data.dataFile.filename", equalTo("trees.png")) + .body("data.dataFile.contentType", equalTo("image/png")) + .body("data.dataFile.filesize", equalTo(8361)) + .statusCode(OK.getStatusCode()); + + getFileDataResponse.then().assertThat().body("data.dataFile.ownerArray[0].identifier", equalTo(datasetPid)); + + // ------------------------- + // Publish dataverse and dataset + // ------------------------- + msg("Publish dataverse and dataset"); + Response publishDataversetResp = UtilIT.publishDataverseViaSword(dataverseAlias, apiToken); + publishDataversetResp.then().assertThat() + .statusCode(OK.getStatusCode()); + + Response publishDatasetResp = UtilIT.publishDatasetViaNativeApi(datasetId, "major", apiToken); + publishDatasetResp.then().assertThat() + .statusCode(OK.getStatusCode()); + //regular user should get to see file data + getFileDataResponse = UtilIT.getFileData(dataFileId, apiTokenRegular); + getFileDataResponse.then().assertThat() + .statusCode(OK.getStatusCode()); + + //cleanup + /* + Response destroyDatasetResponse = UtilIT.destroyDataset(datasetId, apiToken); + assertEquals(200, destroyDatasetResponse.getStatusCode()); + + Response deleteDataverseResponse = UtilIT.deleteDataverse(dataverseAlias, apiToken); + assertEquals(200, deleteDataverseResponse.getStatusCode()); + + Response deleteUserResponse = UtilIT.deleteUser(username); + assertEquals(200, deleteUserResponse.getStatusCode()); + */ + + } + @Test public void testValidateDDI_issue6027() throws InterruptedException { msgt("testValidateDDI_issue6027"); 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 0598bb80ea6..0847aea1d37 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -1485,6 +1485,22 @@ static Response getDatasetWithOwners(String persistentId, String apiToken, bool + persistentId + (returnOwners ? "&returnOwners=true" : "")); } + + static Response getFileWithOwners(String datafileId, String apiToken, boolean returnOwners) { + return given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .get("/api/files/" + + datafileId + + (returnOwners ? "/?returnOwners=true" : "")); + } + + static Response getDataverseWithOwners(String alias, String apiToken, boolean returnOwners) { + return given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .get("/api/dataverses/" + + alias + + (returnOwners ? "/?returnOwners=true" : "")); + } static Response getMetadataBlockFromDatasetVersion(String persistentId, String versionNumber, String metadataBlock, String apiToken) { return given() From 923f02ef4a9aaff8c55f0f6ebc2dbfecad717246 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Wed, 14 Feb 2024 14:57:17 -0500 Subject: [PATCH 333/689] #10286 delete test data --- .../edu/harvard/iq/dataverse/api/DatasetsIT.java | 15 ++++++++++++++- .../edu/harvard/iq/dataverse/api/FilesIT.java | 4 ++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index 3703a0d39c3..f4e70e03d45 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -1913,8 +1913,21 @@ public void testGetIncludeOwnerArray() { Response getDatasetWithOwners = UtilIT.getDatasetWithOwners(persistentId, apiToken, true); getDatasetWithOwners.prettyPrint(); - getDatasetWithOwners.then().assertThat().body("data.ownerArray[0].identifier", equalTo(dataverseAlias)); + getDatasetWithOwners.then().assertThat().body("data.ownerArray[0].identifier", equalTo(dataverseAlias)); + + Response destroyDatasetResponse = UtilIT.destroyDataset(datasetId, apiToken); + assertEquals(200, destroyDatasetResponse.getStatusCode()); + + Response deleteDataverseResponse = UtilIT.deleteDataverse(dataverseAlias, apiToken); + assertEquals(200, deleteDataverseResponse.getStatusCode()); + + Response deleteUserResponse = UtilIT.deleteUser(username); + assertEquals(200, deleteUserResponse.getStatusCode()); + } + + + /** * In order for this test to pass you must have the Data Capture Module ( diff --git a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java index fe9985115e5..7ac43bbae94 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java @@ -1521,7 +1521,7 @@ public void testGetFileOwners() { .statusCode(OK.getStatusCode()); //cleanup - /* + Response destroyDatasetResponse = UtilIT.destroyDataset(datasetId, apiToken); assertEquals(200, destroyDatasetResponse.getStatusCode()); @@ -1530,7 +1530,7 @@ public void testGetFileOwners() { Response deleteUserResponse = UtilIT.deleteUser(username); assertEquals(200, deleteUserResponse.getStatusCode()); - */ + } From 70db48f7b51c1f789674fed74ba39d6c6bed80c4 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Feb 2024 16:21:20 -0500 Subject: [PATCH 334/689] change to require publish to uningest for a problem --- .../edu/harvard/iq/dataverse/FilePage.java | 20 ++++++++++--------- .../webapp/file-edit-button-fragment.xhtml | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/FilePage.java b/src/main/java/edu/harvard/iq/dataverse/FilePage.java index 37798f1cd3c..909a616a4a3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/FilePage.java +++ b/src/main/java/edu/harvard/iq/dataverse/FilePage.java @@ -547,16 +547,17 @@ public String uningestFile() throws CommandException { if (!file.isTabularData()) { //Ingest never succeeded, either there was a failure or this is not a tabular data file - User u = session.getUser(); - if (!u.isAuthenticated() || !u.isSuperuser()) { - logger.warning("User: " + u.getIdentifier() + " tried to uningest a file"); - // Shouldn't happen (choice not displayed for users who don't have the right - // permission), but check anyway - JH.addMessage(FacesMessage.SEVERITY_WARN, - BundleUtil.getStringFromBundle("file.ingest.cantUningestFileWarning")); - return null; - } if (file.isIngestProblem()) { + //We allow anyone who can publish to uningest in order to clear a problem + User u = session.getUser(); + if (!u.isAuthenticated() || !(permissionService.permissionsFor(u, file).contains(Permission.PublishDataset))) { + logger.warning("User: " + u.getIdentifier() + " tried to uningest a file"); + // Shouldn't happen (choice not displayed for users who don't have the right + // permission), but check anyway + JH.addMessage(FacesMessage.SEVERITY_WARN, + BundleUtil.getStringFromBundle("file.ingest.cantUningestFileWarning")); + return null; + } file.setIngestDone(); file.setIngestReport(null); } else { @@ -566,6 +567,7 @@ public String uningestFile() throws CommandException { return null; } } else { + //Superuser required to uningest after a success //Uningest command does it's own check for isSuperuser commandEngine.submit(new UningestFileCommand(dvRequestService.getDataverseRequest(), file)); Long dataFileId = file.getId(); diff --git a/src/main/webapp/file-edit-button-fragment.xhtml b/src/main/webapp/file-edit-button-fragment.xhtml index e08de716cda..fd455521c98 100644 --- a/src/main/webapp/file-edit-button-fragment.xhtml +++ b/src/main/webapp/file-edit-button-fragment.xhtml @@ -79,7 +79,7 @@ - +

    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

    -
    +
    diff --git a/src/main/webapp/dataverse.xhtml b/src/main/webapp/dataverse.xhtml index 41e2807c4fd..7f70f28e194 100644 --- a/src/main/webapp/dataverse.xhtml +++ b/src/main/webapp/dataverse.xhtml @@ -283,6 +283,19 @@
    +
    + + #{bundle.pidProviderOption} + + +
    + + + + +
    +
    diff --git a/src/test/java/edu/harvard/iq/dataverse/GlobalIdTest.java b/src/test/java/edu/harvard/iq/dataverse/GlobalIdTest.java index 394f08c6e93..7065e9689e1 100644 --- a/src/test/java/edu/harvard/iq/dataverse/GlobalIdTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/GlobalIdTest.java @@ -4,6 +4,8 @@ import org.junit.jupiter.api.Test; import edu.harvard.iq.dataverse.pidproviders.PidUtil; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; +import edu.harvard.iq.dataverse.pidproviders.handle.HandlePidProvider; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -19,7 +21,7 @@ public class GlobalIdTest { @Test public void testValidDOI() { System.out.println("testValidDOI"); - GlobalId instance = new GlobalId(DOIServiceBean.DOI_PROTOCOL,"10.5072","FK2/BYM3IW", "/", DOIServiceBean.DOI_RESOLVER_URL, null); + GlobalId instance = new GlobalId(AbstractDOIProvider.DOI_PROTOCOL,"10.5072","FK2/BYM3IW", "/", AbstractDOIProvider.DOI_RESOLVER_URL, null); assertEquals("doi", instance.getProtocol()); assertEquals("10.5072", instance.getAuthority()); @@ -30,7 +32,7 @@ public void testValidDOI() { @Test public void testValidHandle() { System.out.println("testValidDOI"); - GlobalId instance = new GlobalId(HandlenetServiceBean.HDL_PROTOCOL, "1902.1","111012", "/", HandlenetServiceBean.HDL_RESOLVER_URL, null); + GlobalId instance = new GlobalId(HandlePidProvider.HDL_PROTOCOL, "1902.1","111012", "/", HandlePidProvider.HDL_RESOLVER_URL, null); assertEquals("hdl", instance.getProtocol()); assertEquals("1902.1", instance.getAuthority()); @@ -57,7 +59,7 @@ public void testInject() { System.out.println("testInject (weak test)"); // String badProtocol = "hdl:'Select value from datasetfieldvalue';/ha"; - GlobalId instance = PidUtil.parseAsGlobalID(HandlenetServiceBean.HDL_PROTOCOL, "'Select value from datasetfieldvalue';", "ha"); + GlobalId instance = PidUtil.parseAsGlobalID(HandlePidProvider.HDL_PROTOCOL, "'Select value from datasetfieldvalue';", "ha"); assertNull(instance); //exception.expect(IllegalArgumentException.class); diff --git a/src/test/java/edu/harvard/iq/dataverse/PersistentIdentifierServiceBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/PersistentIdentifierServiceBeanTest.java deleted file mode 100644 index 542d00d0d78..00000000000 --- a/src/test/java/edu/harvard/iq/dataverse/PersistentIdentifierServiceBeanTest.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * 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; - -import edu.harvard.iq.dataverse.engine.TestCommandContext; -import edu.harvard.iq.dataverse.engine.command.CommandContext; -import edu.harvard.iq.dataverse.pidproviders.FakePidProviderServiceBean; -import edu.harvard.iq.dataverse.pidproviders.PermaLinkPidProviderServiceBean; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; -import org.mockito.junit.jupiter.MockitoExtension; - - -import static org.junit.jupiter.api.Assertions.*; - -/** - * - * @author michael - */ -@ExtendWith(MockitoExtension.class) -public class PersistentIdentifierServiceBeanTest { - - @Mock - private SettingsServiceBean settingsServiceBean; - - @InjectMocks - DOIEZIdServiceBean ezidServiceBean = new DOIEZIdServiceBean(); - @InjectMocks - DOIDataCiteServiceBean dataCiteServiceBean = new DOIDataCiteServiceBean(); - @InjectMocks - FakePidProviderServiceBean fakePidProviderServiceBean = new FakePidProviderServiceBean(); - HandlenetServiceBean hdlServiceBean = new HandlenetServiceBean(); - PermaLinkPidProviderServiceBean permaLinkServiceBean = new PermaLinkPidProviderServiceBean(); - - CommandContext ctxt; - - @BeforeEach - public void setup() { - MockitoAnnotations.initMocks(this); - ctxt = new TestCommandContext(){ - @Override - public HandlenetServiceBean handleNet() { - return hdlServiceBean; - } - - @Override - public DOIDataCiteServiceBean doiDataCite() { - return dataCiteServiceBean; - } - - @Override - public DOIEZIdServiceBean doiEZId() { - return ezidServiceBean; - } - - @Override - public FakePidProviderServiceBean fakePidProvider() { - return fakePidProviderServiceBean; - } - - @Override - public PermaLinkPidProviderServiceBean permaLinkProvider() { - return permaLinkServiceBean; - } - - }; - } - - /** - * Test of getBean method, of class PersistentIdentifierServiceBean. - */ - @Test - public void testGetBean_String_CommandContext_OK() { - ctxt.settings().setValueForKey( SettingsServiceBean.Key.DoiProvider, "EZID"); - Mockito.when(settingsServiceBean.getValueForKey(SettingsServiceBean.Key.DoiProvider, "")).thenReturn("EZID"); - - assertEquals(ezidServiceBean, - GlobalIdServiceBean.getBean("doi", ctxt)); - - ctxt.settings().setValueForKey( SettingsServiceBean.Key.DoiProvider, "DataCite"); - Mockito.when(settingsServiceBean.getValueForKey(SettingsServiceBean.Key.DoiProvider, "")).thenReturn("DataCite"); - - assertEquals(dataCiteServiceBean, - GlobalIdServiceBean.getBean("doi", ctxt)); - - ctxt.settings().setValueForKey(SettingsServiceBean.Key.DoiProvider, "FAKE"); - Mockito.when(settingsServiceBean.getValueForKey(SettingsServiceBean.Key.DoiProvider, "")).thenReturn("FAKE"); - - assertEquals(fakePidProviderServiceBean, - GlobalIdServiceBean.getBean("doi", ctxt)); - - assertEquals(hdlServiceBean, - GlobalIdServiceBean.getBean("hdl", ctxt)); - - assertEquals(permaLinkServiceBean, - GlobalIdServiceBean.getBean("perma", ctxt)); - } - - @Test - public void testGetBean_String_CommandContext_BAD() { - ctxt.settings().setValueForKey( SettingsServiceBean.Key.DoiProvider, "non-existent-provider"); - assertNull(GlobalIdServiceBean.getBean("doi", ctxt)); - - - assertNull(GlobalIdServiceBean.getBean("non-existent-protocol", ctxt)); - } - - /** - * Test of getBean method, of class PersistentIdentifierServiceBean. - */ - @Test - public void testGetBean_CommandContext() { - ctxt.settings().setValueForKey( SettingsServiceBean.Key.Protocol, "doi"); - ctxt.settings().setValueForKey( SettingsServiceBean.Key.DoiProvider, "EZID"); - Mockito.when(settingsServiceBean.getValueForKey(SettingsServiceBean.Key.DoiProvider, "")).thenReturn("EZID"); - - assertEquals(ezidServiceBean, - GlobalIdServiceBean.getBean("doi", ctxt)); - - ctxt.settings().setValueForKey( SettingsServiceBean.Key.Protocol, "hdl"); - assertEquals(hdlServiceBean, - GlobalIdServiceBean.getBean("hdl", ctxt)); - - ctxt.settings().setValueForKey( SettingsServiceBean.Key.Protocol, "perma"); - assertEquals(permaLinkServiceBean, - GlobalIdServiceBean.getBean("perma", ctxt)); - } - - -} diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index 304b0bd0438..c3036deb122 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -1217,6 +1217,10 @@ public void testExcludeEmail() { } + @Disabled + /*The identifier generation style is no longer a global, dynamically changeable setting. To make this test work after PR #10234, + * will require configuring a PidProvider that uses this style and creating a collection/dataset that uses that provider. + */ @Test public void testStoredProcGeneratedAsIdentifierGenerationStyle() { // Please note that this test only works if the stored procedure diff --git a/src/test/java/edu/harvard/iq/dataverse/dataaccess/GlobusOverlayAccessIOTest.java b/src/test/java/edu/harvard/iq/dataverse/dataaccess/GlobusOverlayAccessIOTest.java index ad980aa28cd..d173f65757f 100644 --- a/src/test/java/edu/harvard/iq/dataverse/dataaccess/GlobusOverlayAccessIOTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/dataaccess/GlobusOverlayAccessIOTest.java @@ -3,11 +3,11 @@ */ package edu.harvard.iq.dataverse.dataaccess; -import edu.harvard.iq.dataverse.DOIServiceBean; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.GlobalId; import edu.harvard.iq.dataverse.mocks.MocksFactory; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -94,8 +94,8 @@ public static void tearDown() { void testGlobusOverlayIdentifiers() throws IOException { dataset = MocksFactory.makeDataset(); - dataset.setGlobalId(new GlobalId(DOIServiceBean.DOI_PROTOCOL, authority, identifier, "/", - DOIServiceBean.DOI_RESOLVER_URL, null)); + dataset.setGlobalId(new GlobalId(AbstractDOIProvider.DOI_PROTOCOL, authority, identifier, "/", + AbstractDOIProvider.DOI_RESOLVER_URL, null)); mDatafile = MocksFactory.makeDataFile(); mDatafile.setOwner(dataset); mDatafile.setStorageIdentifier("globusm://" + baseStoreId1); diff --git a/src/test/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIOTest.java b/src/test/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIOTest.java index 1c371881ba6..2c0e0a5c6b7 100644 --- a/src/test/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIOTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIOTest.java @@ -4,11 +4,11 @@ */ package edu.harvard.iq.dataverse.dataaccess; -import edu.harvard.iq.dataverse.DOIServiceBean; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.GlobalId; import edu.harvard.iq.dataverse.mocks.MocksFactory; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; import edu.harvard.iq.dataverse.util.UrlSignerUtil; import org.junit.jupiter.api.AfterEach; @@ -50,7 +50,7 @@ public void setUp() { System.setProperty("dataverse.files.file.label", "default"); datafile = MocksFactory.makeDataFile(); dataset = MocksFactory.makeDataset(); - dataset.setGlobalId(new GlobalId(DOIServiceBean.DOI_PROTOCOL, authority, identifier, "/", DOIServiceBean.DOI_RESOLVER_URL, null)); + dataset.setGlobalId(new GlobalId(AbstractDOIProvider.DOI_PROTOCOL, authority, identifier, "/", AbstractDOIProvider.DOI_RESOLVER_URL, null)); datafile.setOwner(dataset); datafile.setStorageIdentifier("test://" + baseStoreId + "//" + logoPath); 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 a80adb33b8d..255125189ae 100644 --- a/src/test/java/edu/harvard/iq/dataverse/engine/TestCommandContext.java +++ b/src/test/java/edu/harvard/iq/dataverse/engine/TestCommandContext.java @@ -11,8 +11,7 @@ import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.ingest.IngestServiceBean; -import edu.harvard.iq.dataverse.pidproviders.FakePidProviderServiceBean; -import edu.harvard.iq.dataverse.pidproviders.PermaLinkPidProviderServiceBean; +import edu.harvard.iq.dataverse.pidproviders.PidProviderFactoryBean; import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean; import edu.harvard.iq.dataverse.search.IndexBatchServiceBean; import edu.harvard.iq.dataverse.search.IndexServiceBean; @@ -122,27 +121,7 @@ public DataverseFieldTypeInputLevelServiceBean fieldTypeInputLevels() { } @Override - public DOIEZIdServiceBean doiEZId() { - return null; - } - - @Override - public DOIDataCiteServiceBean doiDataCite() { - return null; - } - - @Override - public FakePidProviderServiceBean fakePidProvider() { - return null; - } - - @Override - public HandlenetServiceBean handleNet() { - return null; - } - - @Override - public PermaLinkPidProviderServiceBean permaLinkProvider() { + public PidProviderFactoryBean pidProviderFactory() { return null; } diff --git a/src/test/java/edu/harvard/iq/dataverse/export/OpenAireExportUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/export/OpenAireExportUtilTest.java index 4fc84f7e72d..8ebdeea6243 100644 --- a/src/test/java/edu/harvard/iq/dataverse/export/OpenAireExportUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/export/OpenAireExportUtilTest.java @@ -7,12 +7,13 @@ import com.google.gson.Gson; -import edu.harvard.iq.dataverse.DOIServiceBean; import edu.harvard.iq.dataverse.GlobalId; -import edu.harvard.iq.dataverse.HandlenetServiceBean; import edu.harvard.iq.dataverse.api.dto.DatasetDTO; import edu.harvard.iq.dataverse.api.dto.DatasetVersionDTO; import edu.harvard.iq.dataverse.export.openaire.OpenAireExportUtil; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; +import edu.harvard.iq.dataverse.pidproviders.handle.HandlePidProvider; + import java.io.IOException; import java.io.StringWriter; import java.nio.charset.StandardCharsets; @@ -56,7 +57,7 @@ public void testWriteIdentifierElementDoi() throws XMLStreamException { String persistentAgency = "doi"; String persistentAuthority = "10.123"; String persistentId = "123"; - GlobalId globalId = new GlobalId(persistentAgency, persistentAuthority, persistentId, null, DOIServiceBean.DOI_RESOLVER_URL, null); + GlobalId globalId = new GlobalId(persistentAgency, persistentAuthority, persistentId, null, AbstractDOIProvider.DOI_RESOLVER_URL, null); // when OpenAireExportUtil.writeIdentifierElement(xmlWriter, globalId.asURL(), null); @@ -76,7 +77,7 @@ public void testWriteIdentifierElementHandle() throws XMLStreamException { String persistentAgency = "hdl"; String persistentAuthority = "1902.1"; String persistentId = "111012"; - GlobalId globalId = new GlobalId(persistentAgency, persistentAuthority, persistentId, null, HandlenetServiceBean.HDL_RESOLVER_URL, null); + GlobalId globalId = new GlobalId(persistentAgency, persistentAuthority, persistentId, null, HandlePidProvider.HDL_RESOLVER_URL, null); // when OpenAireExportUtil.writeIdentifierElement(xmlWriter, globalId.asURL(), null); diff --git a/src/test/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandlerTest.java b/src/test/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandlerTest.java index 6f0132e2bc9..639a7c542c4 100644 --- a/src/test/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandlerTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandlerTest.java @@ -1,6 +1,5 @@ package edu.harvard.iq.dataverse.externaltools; -import edu.harvard.iq.dataverse.DOIServiceBean; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.DataFileServiceBean; import edu.harvard.iq.dataverse.Dataset; @@ -9,6 +8,7 @@ import edu.harvard.iq.dataverse.GlobalId; import edu.harvard.iq.dataverse.authorization.users.ApiToken; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.util.URLTokenUtil; import edu.harvard.iq.dataverse.util.json.JsonUtil; @@ -267,7 +267,7 @@ public void testDatasetConfigureTool() { .build().toString()); var dataset = new Dataset(); - dataset.setGlobalId(new GlobalId(DOIServiceBean.DOI_PROTOCOL, "10.5072", "ABC123", null, DOIServiceBean.DOI_RESOLVER_URL, null)); + dataset.setGlobalId(new GlobalId(AbstractDOIProvider.DOI_PROTOCOL, "10.5072", "ABC123", null, AbstractDOIProvider.DOI_RESOLVER_URL, null)); ApiToken nullApiToken = null; String nullLocaleCode = "en"; var externalToolHandler = new ExternalToolHandler(externalTool, dataset, nullApiToken, nullLocaleCode); diff --git a/src/test/java/edu/harvard/iq/dataverse/externaltools/ExternalToolServiceBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/externaltools/ExternalToolServiceBeanTest.java index 4f5af8b97b0..bb39aecfa79 100644 --- a/src/test/java/edu/harvard/iq/dataverse/externaltools/ExternalToolServiceBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/externaltools/ExternalToolServiceBeanTest.java @@ -1,6 +1,5 @@ package edu.harvard.iq.dataverse.externaltools; -import edu.harvard.iq.dataverse.DOIServiceBean; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.DataFileServiceBean; import edu.harvard.iq.dataverse.DataTable; @@ -9,6 +8,7 @@ import edu.harvard.iq.dataverse.FileMetadata; import edu.harvard.iq.dataverse.GlobalId; import edu.harvard.iq.dataverse.authorization.users.ApiToken; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; import edu.harvard.iq.dataverse.util.URLTokenUtil; import java.util.ArrayList; @@ -144,7 +144,7 @@ public void testParseAddFileToolFilePid() { assertEquals("explorer", externalTool.getToolName()); DataFile dataFile = new DataFile(); dataFile.setId(42l); - dataFile.setGlobalId(new GlobalId(DOIServiceBean.DOI_PROTOCOL,"10.5072","FK2/RMQT6J/G9F1A1", "/", DOIServiceBean.DOI_RESOLVER_URL, null)); + dataFile.setGlobalId(new GlobalId(AbstractDOIProvider.DOI_PROTOCOL,"10.5072","FK2/RMQT6J/G9F1A1", "/", AbstractDOIProvider.DOI_RESOLVER_URL, null)); FileMetadata fmd = new FileMetadata(); fmd.setId(2L); DatasetVersion dv = new DatasetVersion(); diff --git a/src/test/java/edu/harvard/iq/dataverse/globus/GlobusUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/globus/GlobusUtilTest.java index 56f8731b9c8..095e798f229 100644 --- a/src/test/java/edu/harvard/iq/dataverse/globus/GlobusUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/globus/GlobusUtilTest.java @@ -13,7 +13,6 @@ import org.mockito.Mock; import org.mockito.Mockito; -import edu.harvard.iq.dataverse.DOIServiceBean; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.GlobalId; @@ -21,6 +20,7 @@ import edu.harvard.iq.dataverse.dataaccess.DataAccess; import edu.harvard.iq.dataverse.dataaccess.GlobusAccessibleStore; import edu.harvard.iq.dataverse.mocks.MocksFactory; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; import edu.harvard.iq.dataverse.util.json.JsonUtil; import jakarta.json.JsonObject; @@ -52,8 +52,8 @@ public void setUp() { "d7c42580-6538-4605-9ad8-116a61982644/hdc1"); dataset = MocksFactory.makeDataset(); - dataset.setGlobalId(new GlobalId(DOIServiceBean.DOI_PROTOCOL, authority, identifier, "/", - DOIServiceBean.DOI_RESOLVER_URL, null)); + dataset.setGlobalId(new GlobalId(AbstractDOIProvider.DOI_PROTOCOL, authority, identifier, "/", + AbstractDOIProvider.DOI_RESOLVER_URL, null)); mDatafile = MocksFactory.makeDataFile(); mDatafile.setOwner(dataset); mDatafile.setStorageIdentifier("globusm://" + baseStoreId1); diff --git a/src/test/java/edu/harvard/iq/dataverse/pidproviders/PidUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/pidproviders/PidUtilTest.java index dabc7f68fce..dc226d2e85b 100644 --- a/src/test/java/edu/harvard/iq/dataverse/pidproviders/PidUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/pidproviders/PidUtilTest.java @@ -1,18 +1,43 @@ package edu.harvard.iq.dataverse.pidproviders; -import edu.harvard.iq.dataverse.DOIServiceBean; +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.GlobalId; -import edu.harvard.iq.dataverse.GlobalIdServiceBean; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; +import edu.harvard.iq.dataverse.pidproviders.doi.UnmanagedDOIProvider; +import edu.harvard.iq.dataverse.pidproviders.doi.datacite.DataCiteDOIProvider; +import edu.harvard.iq.dataverse.pidproviders.doi.datacite.DataCiteProviderFactory; +import edu.harvard.iq.dataverse.pidproviders.doi.ezid.EZIdDOIProvider; +import edu.harvard.iq.dataverse.pidproviders.doi.ezid.EZIdProviderFactory; +import edu.harvard.iq.dataverse.pidproviders.doi.fake.FakeDOIProvider; +import edu.harvard.iq.dataverse.pidproviders.doi.fake.FakeProviderFactory; +import edu.harvard.iq.dataverse.pidproviders.handle.HandlePidProvider; +import edu.harvard.iq.dataverse.pidproviders.handle.HandleProviderFactory; +import edu.harvard.iq.dataverse.pidproviders.handle.UnmanagedHandlePidProvider; +import edu.harvard.iq.dataverse.pidproviders.perma.PermaLinkPidProvider; +import edu.harvard.iq.dataverse.pidproviders.perma.PermaLinkProviderFactory; +import edu.harvard.iq.dataverse.pidproviders.perma.UnmanagedPermaLinkPidProvider; +import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.SystemConfig; import edu.harvard.iq.dataverse.util.json.JsonUtil; +import edu.harvard.iq.dataverse.util.testing.JvmSetting; +import edu.harvard.iq.dataverse.util.testing.LocalJvmSettings; + import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import jakarta.json.Json; import jakarta.json.JsonObjectBuilder; import jakarta.ws.rs.NotFoundException; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; @@ -24,32 +49,128 @@ import static org.junit.jupiter.api.Assertions.*; -/** - * Useful for testing but requires DataCite credentials, etc. - */ + @ExtendWith(MockitoExtension.class) +@LocalJvmSettings +//Perma 1 +@JvmSetting(key = JvmSettings.PID_PROVIDER_LABEL, value = "perma 1", varArgs = "perma1") +@JvmSetting(key = JvmSettings.PID_PROVIDER_TYPE, value = PermaLinkPidProvider.TYPE, varArgs = "perma1") +@JvmSetting(key = JvmSettings.PID_PROVIDER_AUTHORITY, value = "DANSLINK", varArgs = "perma1") +@JvmSetting(key = JvmSettings.PID_PROVIDER_SHOULDER, value = "QE", varArgs = "perma1") +@JvmSetting(key = JvmSettings.PERMALINK_SEPARATOR, value = "-", varArgs = "perma1") +@JvmSetting(key = JvmSettings.PID_PROVIDER_EXCLUDED_LIST, value = "perma:DANSLINKQE123456, perma:bad, perma:LINKIT123456", varArgs ="perma1") + +//Perma 2 +@JvmSetting(key = JvmSettings.PID_PROVIDER_LABEL, value = "perma 2", varArgs = "perma2") +@JvmSetting(key = JvmSettings.PID_PROVIDER_TYPE, value = PermaLinkPidProvider.TYPE, varArgs = "perma2") +@JvmSetting(key = JvmSettings.PID_PROVIDER_AUTHORITY, value = "DANSLINK", varArgs = "perma2") +@JvmSetting(key = JvmSettings.PID_PROVIDER_SHOULDER, value = "QE", varArgs = "perma2") +@JvmSetting(key = JvmSettings.PID_PROVIDER_MANAGED_LIST, value = "perma:LINKIT/FK2ABCDEF", varArgs ="perma2") +@JvmSetting(key = JvmSettings.PERMALINK_SEPARATOR, value = "/", varArgs = "perma2") +@JvmSetting(key = JvmSettings.PERMALINK_BASE_URL, value = "https://example.org/123", varArgs = "perma2") +// Datacite 1 +@JvmSetting(key = JvmSettings.PID_PROVIDER_LABEL, value = "dataCite 1", varArgs = "dc1") +@JvmSetting(key = JvmSettings.PID_PROVIDER_TYPE, value = DataCiteDOIProvider.TYPE, varArgs = "dc1") +@JvmSetting(key = JvmSettings.PID_PROVIDER_AUTHORITY, value = "10.5073", varArgs = "dc1") +@JvmSetting(key = JvmSettings.PID_PROVIDER_SHOULDER, value = "FK2", varArgs = "dc1") +@JvmSetting(key = JvmSettings.PID_PROVIDER_EXCLUDED_LIST, value = "doi:10.5073/FK2123456", varArgs ="dc1") +@JvmSetting(key = JvmSettings.DATACITE_MDS_API_URL, value = "https://mds.test.datacite.org/", varArgs = "dc1") +@JvmSetting(key = JvmSettings.DATACITE_REST_API_URL, value = "https://api.test.datacite.org", varArgs ="dc1") +@JvmSetting(key = JvmSettings.DATACITE_USERNAME, value = "test", varArgs ="dc1") +@JvmSetting(key = JvmSettings.DATACITE_PASSWORD, value = "changeme", varArgs ="dc1") +//Datacite 2 +@JvmSetting(key = JvmSettings.PID_PROVIDER_LABEL, value = "dataCite 2", varArgs = "dc2") +@JvmSetting(key = JvmSettings.PID_PROVIDER_TYPE, value = DataCiteDOIProvider.TYPE, varArgs = "dc2") +@JvmSetting(key = JvmSettings.PID_PROVIDER_AUTHORITY, value = "10.5072", varArgs = "dc2") +@JvmSetting(key = JvmSettings.PID_PROVIDER_SHOULDER, value = "FK3", varArgs = "dc2") +@JvmSetting(key = JvmSettings.DATACITE_MDS_API_URL, value = "https://mds.test.datacite.org/", varArgs = "dc2") +@JvmSetting(key = JvmSettings.DATACITE_REST_API_URL, value = "https://api.test.datacite.org", varArgs ="dc2") +@JvmSetting(key = JvmSettings.DATACITE_USERNAME, value = "test2", varArgs ="dc2") +@JvmSetting(key = JvmSettings.DATACITE_PASSWORD, value = "changeme2", varArgs ="dc2") +//EZID 1 +@JvmSetting(key = JvmSettings.PID_PROVIDER_LABEL, value = "EZId 1", varArgs = "ez1") +@JvmSetting(key = JvmSettings.PID_PROVIDER_TYPE, value = EZIdDOIProvider.TYPE, varArgs = "ez1") +@JvmSetting(key = JvmSettings.PID_PROVIDER_AUTHORITY, value = "10.5072", varArgs = "ez1") +@JvmSetting(key = JvmSettings.PID_PROVIDER_SHOULDER, value = "FK2", varArgs = "ez1") +@JvmSetting(key = JvmSettings.EZID_API_URL, value = "https://ezid.cdlib.org/", varArgs = "ez1") +@JvmSetting(key = JvmSettings.EZID_USERNAME, value = "apitest", varArgs ="ez1") +@JvmSetting(key = JvmSettings.EZID_PASSWORD, value = "apitest", varArgs ="ez1") +//FAKE 1 +@JvmSetting(key = JvmSettings.PID_PROVIDER_LABEL, value = "FAKE 1", varArgs = "fake1") +@JvmSetting(key = JvmSettings.PID_PROVIDER_TYPE, value = FakeDOIProvider.TYPE, varArgs = "fake1") +@JvmSetting(key = JvmSettings.PID_PROVIDER_AUTHORITY, value = "10.5074", varArgs = "fake1") +@JvmSetting(key = JvmSettings.PID_PROVIDER_SHOULDER, value = "FK", varArgs = "fake1") +@JvmSetting(key = JvmSettings.PID_PROVIDER_MANAGED_LIST, value = "doi:10.5073/FK3ABCDEF", varArgs ="fake1") + +//HANDLE 1 +@JvmSetting(key = JvmSettings.PID_PROVIDER_LABEL, value = "HDL 1", varArgs = "hdl1") +@JvmSetting(key = JvmSettings.PID_PROVIDER_TYPE, value = HandlePidProvider.TYPE, varArgs = "hdl1") +@JvmSetting(key = JvmSettings.PID_PROVIDER_AUTHORITY, value = "20.500.1234", varArgs = "hdl1") +@JvmSetting(key = JvmSettings.PID_PROVIDER_SHOULDER, value = "", varArgs = "hdl1") +@JvmSetting(key = JvmSettings.PID_PROVIDER_MANAGED_LIST, value = "hdl:20.20.20/FK2ABCDEF", varArgs ="hdl1") +@JvmSetting(key = JvmSettings.HANDLENET_AUTH_HANDLE, value = "20.500.1234/ADMIN", varArgs ="hdl1") +@JvmSetting(key = JvmSettings.HANDLENET_INDEPENDENT_SERVICE, value = "true", varArgs ="hdl1") +@JvmSetting(key = JvmSettings.HANDLENET_INDEX, value = "1", varArgs ="hdl1") +@JvmSetting(key = JvmSettings.HANDLENET_KEY_PASSPHRASE, value = "passphrase", varArgs ="hdl1") +@JvmSetting(key = JvmSettings.HANDLENET_KEY_PATH, value = "/tmp/cred", varArgs ="hdl1") + +//List to instantiate +@JvmSetting(key = JvmSettings.PID_PROVIDERS, value = "perma1, perma2, dc1, dc2, ez1, fake1, hdl1") + public class PidUtilTest { + @Mock private SettingsServiceBean settingsServiceBean; - @InjectMocks - private PermaLinkPidProviderServiceBean p = new PermaLinkPidProviderServiceBean(); - + @BeforeAll + //FWIW @JvmSetting doesn't appear to work with @BeforeAll + public static void setUpClass() throws Exception { + + //This mimics the initial config in the PidProviderFactoryBean.loadProviderFactories method - could potentially be used to mock that bean at some point + Map pidProviderFactoryMap = new HashMap<>(); + pidProviderFactoryMap.put(PermaLinkPidProvider.TYPE, new PermaLinkProviderFactory()); + pidProviderFactoryMap.put(DataCiteDOIProvider.TYPE, new DataCiteProviderFactory()); + pidProviderFactoryMap.put(HandlePidProvider.TYPE, new HandleProviderFactory()); + pidProviderFactoryMap.put(FakeDOIProvider.TYPE, new FakeProviderFactory()); + pidProviderFactoryMap.put(EZIdDOIProvider.TYPE, new EZIdProviderFactory()); + + PidUtil.clearPidProviders(); + + //Read list of providers to add + List providers = Arrays.asList(JvmSettings.PID_PROVIDERS.lookup().split(",\\s")); + //Iterate through the list of providers and add them using the PidProviderFactory of the appropriate type + for (String providerId : providers) { + System.out.println("Loading provider: " + providerId); + String type = JvmSettings.PID_PROVIDER_TYPE.lookup(providerId); + PidProviderFactory factory = pidProviderFactoryMap.get(type); + PidUtil.addToProviderList(factory.createPidProvider(providerId)); + } + PidUtil.addAllToUnmanagedProviderList(Arrays.asList(new UnmanagedDOIProvider(), + new UnmanagedHandlePidProvider(), new UnmanagedPermaLinkPidProvider())); + } + + @AfterAll + public static void tearDownClass() throws Exception { + PidUtil.clearPidProviders(); + } + @BeforeEach public void initMocks() { MockitoAnnotations.initMocks(this); - Mockito.when(settingsServiceBean.getValueForKey(SettingsServiceBean.Key.Protocol)).thenReturn("perma"); - Mockito.when(settingsServiceBean.getValueForKey(SettingsServiceBean.Key.Authority)).thenReturn("DANSLINK"); - p.reInit(); +// Mockito.when(settingsServiceBean.getValueForKey(SettingsServiceBean.Key.Protocol)).thenReturn("perma"); +// Mockito.when(settingsServiceBean.getValueForKey(SettingsServiceBean.Key.Authority)).thenReturn("DANSLINK"); } + /** + * Useful for testing but requires DataCite credentials, etc. + */ @Disabled @Test public void testGetDoi() throws IOException { String username = System.getenv("DataCiteUsername"); String password = System.getenv("DataCitePassword"); String baseUrl = "https://api.test.datacite.org"; - GlobalId pid = new GlobalId(DOIServiceBean.DOI_PROTOCOL,"10.70122","QE5A-XN55", "/", DOIServiceBean.DOI_RESOLVER_URL, null); + GlobalId pid = new GlobalId(AbstractDOIProvider.DOI_PROTOCOL,"10.70122","QE5A-XN55", "/", AbstractDOIProvider.DOI_RESOLVER_URL, null); try { JsonObjectBuilder result = PidUtil.queryDoi(pid, baseUrl, username, password); String out = JsonUtil.prettyPrint(result.build()); @@ -58,23 +179,291 @@ public void testGetDoi() throws IOException { System.out.println("ex: " + ex); } } + @Test - public void testGetPermaLink() throws IOException { - List list = new ArrayList(); + public void testFactories() throws IOException { + PidProvider p = PidUtil.getPidProvider("perma1"); + assertEquals("perma 1", p.getLabel()); + assertEquals(PermaLinkPidProvider.PERMA_PROTOCOL, p.getProtocol()); + assertEquals("DANSLINK", p.getAuthority()); + assertEquals("QE", p.getShoulder()); + assertEquals("-", p.getSeparator()); + assertTrue(p.getUrlPrefix().startsWith(SystemConfig.getDataverseSiteUrlStatic())); + p = PidUtil.getPidProvider("perma2"); + assertTrue(p.getUrlPrefix().startsWith("https://example.org/123")); + p = PidUtil.getPidProvider("dc2"); + assertEquals("FK3", p.getShoulder()); + } + + @Test + public void testPermaLinkParsing() throws IOException { + //Verify that we can parse a valid perma link associated with perma1 + String pid1String = "perma:DANSLINK-QE-5A-XN55"; + GlobalId pid2 = PidUtil.parseAsGlobalID(pid1String); + assertEquals(pid1String, pid2.asString()); + //Check that it was parsed by perma1 and that the URL is correct, etc + assertEquals("perma1", pid2.getProviderId()); + assertEquals(SystemConfig.getDataverseSiteUrlStatic() + "/citation?persistentId=" + pid1String, pid2.asURL()); + assertEquals("DANSLINK", pid2.getAuthority()); + assertEquals(PermaLinkPidProvider.PERMA_PROTOCOL, pid2.getProtocol()); + + //Verify that parsing the URL form works + GlobalId pid3 = PidUtil.parseAsGlobalID(pid2.asURL()); + assertEquals(pid1String, pid3.asString()); + assertEquals("perma1", pid3.getProviderId()); - list.add(p); - PidUtil.addAllToProviderList(list); - GlobalId pid = new GlobalId(PermaLinkPidProviderServiceBean.PERMA_PROTOCOL,"DANSLINK","QE5A-XN55", "", p.getUrlPrefix(), PermaLinkPidProviderServiceBean.PERMA_PROVIDER_NAME); - System.out.println(pid.asString()); - System.out.println(pid.asURL()); + //Repeat the basics with a permalink associated with perma2 + String pid4String = "perma:DANSLINK/QE-5A-XN55"; + GlobalId pid5 = PidUtil.parseAsGlobalID(pid4String); + assertEquals("perma2", pid5.getProviderId()); + assertEquals(pid4String, pid5.asString()); + assertEquals("https://example.org/123/citation?persistentId=" + pid4String, pid5.asURL()); + + } + + @Test + public void testDOIParsing() throws IOException { - GlobalId pid2 = PidUtil.parseAsGlobalID(pid.asString()); - assertEquals(pid.asString(), pid2.asString()); - GlobalId pid3 = PidUtil.parseAsGlobalID(pid.asURL()); - assertEquals(pid.asString(), pid3.asString()); + String pid1String = "doi:10.5073/FK2ABCDEF"; + GlobalId pid2 = PidUtil.parseAsGlobalID(pid1String); + assertEquals(pid1String, pid2.asString()); + assertEquals("dc1", pid2.getProviderId()); + assertEquals("https://doi.org/" + pid2.getAuthority() + PidUtil.getPidProvider(pid2.getProviderId()).getSeparator() + pid2.getIdentifier(),pid2.asURL()); + assertEquals("10.5073", pid2.getAuthority()); + assertEquals(AbstractDOIProvider.DOI_PROTOCOL, pid2.getProtocol()); + GlobalId pid3 = PidUtil.parseAsGlobalID(pid2.asURL()); + assertEquals(pid1String, pid3.asString()); + assertEquals("dc1", pid3.getProviderId()); + + String pid4String = "doi:10.5072/FK3ABCDEF"; + GlobalId pid4 = PidUtil.parseAsGlobalID(pid4String); + assertEquals(pid4String, pid4.asString()); + assertEquals("dc2", pid4.getProviderId()); + + String pid5String = "doi:10.5072/FK2ABCDEF"; + GlobalId pid5 = PidUtil.parseAsGlobalID(pid5String); + assertEquals(pid5String, pid5.asString()); + assertEquals("ez1", pid5.getProviderId()); + String pid6String = "doi:10.5074/FKABCDEF"; + GlobalId pid6 = PidUtil.parseAsGlobalID(pid6String); + assertEquals(pid6String, pid6.asString()); + assertEquals("fake1", pid6.getProviderId()); + + + } + + @Test + public void testHandleParsing() throws IOException { + + String pid1String = "hdl:20.500.1234/10052"; + GlobalId pid2 = PidUtil.parseAsGlobalID(pid1String); + assertEquals(pid1String, pid2.asString()); + assertEquals("hdl1", pid2.getProviderId()); + assertEquals("https://hdl.handle.net/" + pid2.getAuthority() + PidUtil.getPidProvider(pid2.getProviderId()).getSeparator() + pid2.getIdentifier(),pid2.asURL()); + assertEquals("20.500.1234", pid2.getAuthority()); + assertEquals(HandlePidProvider.HDL_PROTOCOL, pid2.getProtocol()); + GlobalId pid3 = PidUtil.parseAsGlobalID(pid2.asURL()); + assertEquals(pid1String, pid3.asString()); + assertEquals("hdl1", pid3.getProviderId()); } + @Test + public void testUnmanagedParsing() throws IOException { + // A handle managed not managed in the hdl1 provider + String pid1String = "hdl:20.500.3456/10052"; + GlobalId pid2 = PidUtil.parseAsGlobalID(pid1String); + assertEquals(pid1String, pid2.asString()); + //Only parsed by the unmanaged provider + assertEquals(UnmanagedHandlePidProvider.ID, pid2.getProviderId()); + assertEquals(HandlePidProvider.HDL_RESOLVER_URL + pid2.getAuthority() + PidUtil.getPidProvider(pid2.getProviderId()).getSeparator() + pid2.getIdentifier(),pid2.asURL()); + assertEquals("20.500.3456", pid2.getAuthority()); + assertEquals(HandlePidProvider.HDL_PROTOCOL, pid2.getProtocol()); + GlobalId pid3 = PidUtil.parseAsGlobalID(pid2.asURL()); + assertEquals(pid1String, pid3.asString()); + assertEquals(UnmanagedHandlePidProvider.ID, pid3.getProviderId()); + + //Same for DOIs + String pid5String = "doi:10.6083/FK2ABCDEF"; + GlobalId pid5 = PidUtil.parseAsGlobalID(pid5String); + assertEquals(pid5String, pid5.asString()); + assertEquals(UnmanagedDOIProvider.ID, pid5.getProviderId()); + + //And Permalinks + String pid6String = "perma:NOTDANSQEABCDEF"; + GlobalId pid6 = PidUtil.parseAsGlobalID(pid6String); + assertEquals(pid6String, pid6.asString()); + assertEquals(UnmanagedPermaLinkPidProvider.ID, pid6.getProviderId()); + + } + + @Test + public void testExcludedSetParsing() throws IOException { + + String pid1String = "doi:10.5073/FK2123456"; + GlobalId pid2 = PidUtil.parseAsGlobalID(pid1String); + assertEquals(pid1String, pid2.asString()); + assertEquals(UnmanagedDOIProvider.ID, pid2.getProviderId()); + assertEquals("https://doi.org/" + pid2.getAuthority() + PidUtil.getPidProvider(pid2.getProviderId()).getSeparator() + pid2.getIdentifier(),pid2.asURL()); + assertEquals("10.5073", pid2.getAuthority()); + assertEquals(AbstractDOIProvider.DOI_PROTOCOL, pid2.getProtocol()); + GlobalId pid3 = PidUtil.parseAsGlobalID(pid2.asURL()); + assertEquals(pid1String, pid3.asString()); + assertEquals(UnmanagedDOIProvider.ID, pid3.getProviderId()); + + String pid4String = "perma:bad"; + GlobalId pid4 = PidUtil.parseAsGlobalID(pid4String); + assertEquals(pid4String, pid4.asString()); + assertEquals(UnmanagedPermaLinkPidProvider.ID, pid4.getProviderId()); + + String pid5String = "perma:DANSLINKQE123456"; + GlobalId pid5 = PidUtil.parseAsGlobalID(pid5String); + assertEquals(pid5String, pid5.asString()); + assertEquals(UnmanagedPermaLinkPidProvider.ID, pid5.getProviderId()); + + String pid6String = "perma:LINKIT123456"; + GlobalId pid6 = PidUtil.parseAsGlobalID(pid6String); + assertEquals(pid6String, pid6.asString()); + assertEquals(UnmanagedPermaLinkPidProvider.ID, pid6.getProviderId()); + + + } + + @Test + public void testManagedSetParsing() throws IOException { + + String pid1String = "doi:10.5073/FK3ABCDEF"; + GlobalId pid2 = PidUtil.parseAsGlobalID(pid1String); + assertEquals(pid1String, pid2.asString()); + assertEquals("fake1", pid2.getProviderId()); + assertEquals("https://doi.org/" + pid2.getAuthority() + PidUtil.getPidProvider(pid2.getProviderId()).getSeparator() + pid2.getIdentifier(),pid2.asURL()); + assertEquals("10.5073", pid2.getAuthority()); + assertEquals(AbstractDOIProvider.DOI_PROTOCOL, pid2.getProtocol()); + GlobalId pid3 = PidUtil.parseAsGlobalID(pid2.asURL()); + assertEquals(pid1String, pid3.asString()); + assertEquals("fake1", pid3.getProviderId()); + assertFalse(PidUtil.getPidProvider(pid3.getProviderId()).canCreatePidsLike(pid3)); + + String pid4String = "hdl:20.20.20/FK2ABCDEF"; + GlobalId pid4 = PidUtil.parseAsGlobalID(pid4String); + assertEquals(pid4String, pid4.asString()); + assertEquals("hdl1", pid4.getProviderId()); + assertFalse(PidUtil.getPidProvider(pid4.getProviderId()).canCreatePidsLike(pid4)); + + String pid5String = "perma:LINKIT/FK2ABCDEF"; + GlobalId pid5 = PidUtil.parseAsGlobalID(pid5String); + assertEquals(pid5String, pid5.asString()); + assertEquals("perma2", pid5.getProviderId()); + assertFalse(PidUtil.getPidProvider(pid5.getProviderId()).canCreatePidsLike(pid5)); + } + + @Test + public void testFindingPidGenerators() throws IOException { + + Dataset dataset1 = new Dataset(); + Dataverse dataverse1 = new Dataverse(); + dataset1.setOwner(dataverse1); + String pidGeneratorSpecs = Json.createObjectBuilder().add("protocol", AbstractDOIProvider.DOI_PROTOCOL).add("authority","10.5072").add("shoulder", "FK2").build().toString(); + //Set a PID generator on the parent + dataverse1.setPidGeneratorSpecs(pidGeneratorSpecs); + assertEquals(pidGeneratorSpecs, dataverse1.getPidGeneratorSpecs()); + //Verify that the parent's PID generator is the effective one + assertEquals("ez1", dataverse1.getEffectivePidGenerator().getId()); + assertEquals("ez1", dataset1.getEffectivePidGenerator().getId()); + //Change dataset to have a provider and verify that it is used instead of any effective one + dataset1.setAuthority("10.5073"); + dataset1.setProtocol(AbstractDOIProvider.DOI_PROTOCOL); + dataset1.setIdentifier("FK2ABCDEF"); + //Reset to get rid of cached @transient value + dataset1.setPidGenerator(null); + assertEquals("dc1", dataset1.getGlobalId().getProviderId()); + assertEquals("dc1", dataset1.getEffectivePidGenerator().getId()); + assertTrue(PidUtil.getPidProvider(dataset1.getEffectivePidGenerator().getId()).canCreatePidsLike(dataset1.getGlobalId())); + + dataset1.setPidGenerator(null); + //Now set identifier so that the provider has this one in it's managed list (and therefore we can't mint new PIDs in the same auth/shoulder) and therefore we get the effective pid generator + dataset1.setIdentifier("FK3ABCDEF"); + assertEquals("fake1", dataset1.getGlobalId().getProviderId()); + assertEquals("ez1", dataset1.getEffectivePidGenerator().getId()); + + + + } + + @Test + @JvmSetting(key = JvmSettings.LEGACY_DATACITE_MDS_API_URL, value = "https://mds.test.datacite.org/") + @JvmSetting(key = JvmSettings.LEGACY_DATACITE_REST_API_URL, value = "https://api.test.datacite.org") + @JvmSetting(key = JvmSettings.LEGACY_DATACITE_USERNAME, value = "test2") + @JvmSetting(key = JvmSettings.LEGACY_DATACITE_PASSWORD, value = "changeme2") + public void testLegacyConfig() throws IOException { + MockitoAnnotations.openMocks(this); + Mockito.when(settingsServiceBean.getValueForKey(SettingsServiceBean.Key.DoiProvider)).thenReturn("DataCite"); + Mockito.when(settingsServiceBean.getValueForKey(SettingsServiceBean.Key.Shoulder)).thenReturn("FK2"); + + Mockito.when(settingsServiceBean.getValueForKey(SettingsServiceBean.Key.Protocol)).thenReturn("doi"); + Mockito.when(settingsServiceBean.getValueForKey(SettingsServiceBean.Key.Authority)).thenReturn("10.5075"); + + + + String protocol = settingsServiceBean.getValueForKey(SettingsServiceBean.Key.Protocol); + String authority = settingsServiceBean.getValueForKey(SettingsServiceBean.Key.Authority); + String shoulder = settingsServiceBean.getValueForKey(SettingsServiceBean.Key.Shoulder); + String provider = settingsServiceBean.getValueForKey(SettingsServiceBean.Key.DoiProvider); + + if (protocol != null && authority != null && shoulder != null && provider != null) { + // This line is different than in PidProviderFactoryBean because here we've + // already added the unmanaged providers, so we can't look for null + if (!PidUtil.getPidProvider(protocol, authority, shoulder).canManagePID()) { + PidProvider legacy = null; + // Try to add a legacy provider + String identifierGenerationStyle = settingsServiceBean + .getValueForKey(SettingsServiceBean.Key.IdentifierGenerationStyle, "random"); + String dataFilePidFormat = settingsServiceBean.getValueForKey(SettingsServiceBean.Key.DataFilePIDFormat, + "DEPENDENT"); + switch (provider) { + case "EZID": + /* + * String baseUrl = JvmSettings.PID_EZID_BASE_URL.lookup(String.class); String + * username = JvmSettings.PID_EZID_USERNAME.lookup(String.class); String + * password = JvmSettings.PID_EZID_PASSWORD.lookup(String.class); + * legacy = new EZIdDOIProvider("legacy", "legacy", authority, + * shoulder, identifierGenerationStyle, dataFilePidFormat, "", "", baseUrl, + * username, password); + */ + break; + case "DataCite": + String mdsUrl = JvmSettings.LEGACY_DATACITE_MDS_API_URL.lookup(String.class); + String restUrl = JvmSettings.LEGACY_DATACITE_REST_API_URL.lookup(String.class); + String dcUsername = JvmSettings.LEGACY_DATACITE_USERNAME.lookup(String.class); + String dcPassword = JvmSettings.LEGACY_DATACITE_PASSWORD.lookup(String.class); + if (mdsUrl != null && restUrl != null && dcUsername != null && dcPassword != null) { + legacy = new DataCiteDOIProvider("legacy", "legacy", authority, shoulder, + identifierGenerationStyle, dataFilePidFormat, "", "", mdsUrl, restUrl, dcUsername, + dcPassword); + } + break; + case "FAKE": + System.out.println("Legacy FAKE found"); + legacy = new FakeDOIProvider("legacy", "legacy", authority, shoulder, + identifierGenerationStyle, dataFilePidFormat, "", ""); + break; + } + if (legacy != null) { + // Not testing parts that require this bean + legacy.setPidProviderServiceBean(null); + PidUtil.addToProviderList(legacy); + } + } else { + System.out.println("Legacy PID provider settings found - ignored since a provider for the same protocol, authority, shoulder has been registered"); + } + + } + + String pid1String = "doi:10.5075/FK2ABCDEF"; + GlobalId pid2 = PidUtil.parseAsGlobalID(pid1String); + assertEquals(pid1String, pid2.asString()); + assertEquals("legacy", pid2.getProviderId()); + } } diff --git a/src/test/java/edu/harvard/iq/dataverse/pidproviders/doi/datacite/DataCiteProviderTest.java b/src/test/java/edu/harvard/iq/dataverse/pidproviders/doi/datacite/DataCiteProviderTest.java new file mode 100644 index 00000000000..572fc722272 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/pidproviders/doi/datacite/DataCiteProviderTest.java @@ -0,0 +1,187 @@ +package edu.harvard.iq.dataverse.pidproviders.doi.datacite; + +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetField; +import edu.harvard.iq.dataverse.DatasetFieldConstant; +import edu.harvard.iq.dataverse.DatasetFieldType; +import edu.harvard.iq.dataverse.DatasetVersion; +import edu.harvard.iq.dataverse.DatasetVersion.VersionState; +import edu.harvard.iq.dataverse.DataverseServiceBean; +import edu.harvard.iq.dataverse.GlobalId; +import edu.harvard.iq.dataverse.branding.BrandingUtil; +import edu.harvard.iq.dataverse.pidproviders.PidProviderFactoryBean; +import edu.harvard.iq.dataverse.pidproviders.PidUtil; +import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.SystemConfig; +import edu.harvard.iq.dataverse.util.testing.JvmSetting; +import edu.harvard.iq.dataverse.util.testing.LocalJvmSettings; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; + +@ExtendWith(MockitoExtension.class) +@LocalJvmSettings +@JvmSetting(key = JvmSettings.SITE_URL, value = "https://example.com") + +public class DataCiteProviderTest { + + static DataverseServiceBean dataverseSvc; + static SettingsServiceBean settingsSvc; + static PidProviderFactoryBean pidService; + static final String DEFAULT_NAME = "LibraScholar"; + + @BeforeAll + public static void setupMocks() { + dataverseSvc = Mockito.mock(DataverseServiceBean.class); + settingsSvc = Mockito.mock(SettingsServiceBean.class); + BrandingUtil.injectServices(dataverseSvc, settingsSvc); + + // initial values (needed here for other tests where this method is reused!) + Mockito.when(settingsSvc.getValueForKey(SettingsServiceBean.Key.InstallationName)).thenReturn(DEFAULT_NAME); + Mockito.when(dataverseSvc.getRootDataverseName()).thenReturn(DEFAULT_NAME); + + pidService = Mockito.mock(PidProviderFactoryBean.class); + Mockito.when(pidService.isGlobalIdLocallyUnique(any(GlobalId.class))).thenReturn(true); + Mockito.when(pidService.getProducer()).thenReturn("RootDataverse"); + + } + + /** + * Useful for testing but requires DataCite credentials, etc. + * + * To run the test: + * export DataCiteUsername=test2 + * export DataCitePassword=changeme2 + * export DataCiteAuthority=10.5072 + * export DataCiteShoulder=FK2 + * + * then run mvn test -Dtest=DataCiteProviderTest + * + * For each run of the test, one test DOI will be created and will remain in the registered state, as visible on Fabrica at doi.test.datacite.org + * (two DOIs are created, but one is deleted after being created in the draft state and never made findable.) + */ + @Test + @Disabled + public void testDoiLifecycle() throws IOException { + String username = System.getenv("DataCiteUsername"); + String password = System.getenv("DataCitePassword"); + String authority = System.getenv("DataCiteAuthority"); + String shoulder = System.getenv("DataCiteShoulder"); + DataCiteDOIProvider provider = new DataCiteDOIProvider("test", "test", authority, shoulder, "randomString", + SystemConfig.DataFilePIDFormat.DEPENDENT.toString(), "", "", "https://mds.test.datacite.org", + "https://api.test.datacite.org", username, password); + + provider.setPidProviderServiceBean(pidService); + + PidUtil.addToProviderList(provider); + + Dataset d = new Dataset(); + DatasetVersion dv = new DatasetVersion(); + DatasetFieldType primitiveDSFType = new DatasetFieldType(DatasetFieldConstant.title, + DatasetFieldType.FieldType.TEXT, false); + DatasetField testDatasetField = new DatasetField(); + + dv.setVersionState(VersionState.DRAFT); + + testDatasetField.setDatasetVersion(dv); + testDatasetField.setDatasetFieldType(primitiveDSFType); + testDatasetField.setSingleValue("First Title"); + List fields = new ArrayList<>(); + fields.add(testDatasetField); + dv.setDatasetFields(fields); + ArrayList dsvs = new ArrayList<>(); + dsvs.add(0, dv); + d.setVersions(dsvs); + + assertEquals(d.getCurrentName(), "First Title"); + + provider.generatePid(d); + assertEquals(d.getProtocol(), "doi"); + assertEquals(d.getAuthority(), authority); + assertTrue(d.getIdentifier().startsWith(shoulder)); + d.getGlobalId(); + + try { + provider.createIdentifier(d); + d.setIdentifierRegistered(true); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + assertEquals(DataCiteDOIProvider.DRAFT, provider.getPidStatus(d)); + Map mdMap = provider.getIdentifierMetadata(d); + assertEquals("First Title", mdMap.get("datacite.title")); + + testDatasetField.setSingleValue("Second Title"); + + //Modify called for a draft dataset shouldn't update DataCite (given current code) + try { + provider.modifyIdentifierTargetURL(d); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + //Verify the title hasn't changed + mdMap = provider.getIdentifierMetadata(d); + assertEquals("First Title", mdMap.get("datacite.title")); + //Check our local status + assertEquals(DataCiteDOIProvider.DRAFT, provider.getPidStatus(d)); + //Now delete the identifier + provider.deleteIdentifier(d); + //Causes a 404 and a caught exception that prints a stack trace. + mdMap = provider.getIdentifierMetadata(d); + // And verify the record is gone (no title, should be no entries at all) + assertEquals(null, mdMap.get("datacite.title")); + + //Now recreate and publicize in one step + assertTrue(provider.publicizeIdentifier(d)); + d.getLatestVersion().setVersionState(VersionState.RELEASED); + + //Verify the title hasn't changed + mdMap = provider.getIdentifierMetadata(d); + assertEquals("Second Title", mdMap.get("datacite.title")); + //Check our local status + assertEquals(DataCiteDOIProvider.FINDABLE, provider.getPidStatus(d)); + + //Verify that modify does update a published/findable record + testDatasetField.setSingleValue("Third Title"); + + try { + provider.modifyIdentifierTargetURL(d); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + mdMap = provider.getIdentifierMetadata(d); + assertEquals("Third Title", mdMap.get("datacite.title")); + + //Now delete the identifier . Once it's been findable, this should just flip the record to registered + //Not sure that can be easily verified in the test, but it will be visible in Fabrica + provider.deleteIdentifier(d); + d.getLatestVersion().setVersionState(VersionState.DEACCESSIONED); + + mdMap = provider.getIdentifierMetadata(d); + assertEquals("This item has been removed from publication", mdMap.get("datacite.title")); + + //Check our local status - just uses the version state + assertEquals(DataCiteDOIProvider.REGISTERED, provider.getPidStatus(d)); + + // provider.registerWhenPublished() + } + +} diff --git a/src/test/java/edu/harvard/iq/dataverse/search/IndexServiceBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/search/IndexServiceBeanTest.java index adf48e05f09..92b06e5936f 100644 --- a/src/test/java/edu/harvard/iq/dataverse/search/IndexServiceBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/search/IndexServiceBeanTest.java @@ -4,6 +4,7 @@ import edu.harvard.iq.dataverse.Dataverse.DataverseType; import edu.harvard.iq.dataverse.branding.BrandingUtil; import edu.harvard.iq.dataverse.mocks.MocksFactory; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.SystemConfig; @@ -134,7 +135,7 @@ private DatasetField constructBoundingBoxValue(String datasetFieldTypeName, Stri private IndexableDataset createIndexableDataset() { final Dataset dataset = MocksFactory.makeDataset(); - dataset.setGlobalId(new GlobalId(DOIServiceBean.DOI_PROTOCOL,"10.666", "FAKE/fake", "/", DOIServiceBean.DOI_RESOLVER_URL, null)); + dataset.setGlobalId(new GlobalId(AbstractDOIProvider.DOI_PROTOCOL,"10.666", "FAKE/fake", "/", AbstractDOIProvider.DOI_RESOLVER_URL, null)); final DatasetVersion datasetVersion = dataset.getCreateVersion(null); DatasetField field = createCVVField("language", "English", false); datasetVersion.getDatasetFields().add(field); diff --git a/src/test/java/edu/harvard/iq/dataverse/settings/JvmSettingsTest.java b/src/test/java/edu/harvard/iq/dataverse/settings/JvmSettingsTest.java index 6b03f20fc41..f4494b7116e 100644 --- a/src/test/java/edu/harvard/iq/dataverse/settings/JvmSettingsTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/settings/JvmSettingsTest.java @@ -17,22 +17,15 @@ void lookupSetting() { } @Test - @SystemProperty(key = "doi.username", value = "test") - void lookupSettingViaAlias() { - assertEquals("test", JvmSettings.DATACITE_USERNAME.lookup()); + @SystemProperty(key = "dataverse.pid.datacite.datacite.username", value = "test") + void lookupPidProviderSetting() { + assertEquals("test", JvmSettings.DATACITE_USERNAME.lookup("datacite")); } @Test - @SystemProperty(key = "doi.baseurlstring", value = "test") + @SystemProperty(key = "dataverse.ingest.rserve.port", value = "1234") void lookupSettingViaAliasWithDefaultInMPCFile() { - assertEquals("test", JvmSettings.DATACITE_MDS_API_URL.lookup()); - } - - @Test - @SystemProperty(key = "doi.dataciterestapiurlstring", value = "foo") - @SystemProperty(key = "doi.mdcbaseurlstring", value = "bar") - void lookupSettingViaAliasWithDefaultInMPCFileAndTwoAliases() { - assertEquals("foo", JvmSettings.DATACITE_REST_API_URL.lookup()); + assertEquals("1234", JvmSettings.RSERVE_PORT.lookup()); } } \ No newline at end of file diff --git a/src/test/java/edu/harvard/iq/dataverse/sitemap/SiteMapUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/sitemap/SiteMapUtilTest.java index 41032ffa811..310bec72c2e 100644 --- a/src/test/java/edu/harvard/iq/dataverse/sitemap/SiteMapUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/sitemap/SiteMapUtilTest.java @@ -1,11 +1,11 @@ package edu.harvard.iq.dataverse.sitemap; -import edu.harvard.iq.dataverse.DOIServiceBean; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.GlobalId; import edu.harvard.iq.dataverse.harvest.client.HarvestingClient; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; import edu.harvard.iq.dataverse.util.xml.XmlPrinter; import edu.harvard.iq.dataverse.util.xml.XmlValidator; import java.io.File; @@ -66,14 +66,14 @@ void testUpdateSiteMap() throws IOException, ParseException, SAXException { List datasets = new ArrayList<>(); Dataset published = new Dataset(); - published.setGlobalId(new GlobalId(DOIServiceBean.DOI_PROTOCOL, "10.666", "FAKE/published1", null, DOIServiceBean.DOI_RESOLVER_URL, null)); + published.setGlobalId(new GlobalId(AbstractDOIProvider.DOI_PROTOCOL, "10.666", "FAKE/published1", null, AbstractDOIProvider.DOI_RESOLVER_URL, null)); String publishedPid = published.getGlobalId().asString(); published.setPublicationDate(new Timestamp(new Date().getTime())); published.setModificationTime(new Timestamp(new Date().getTime())); datasets.add(published); Dataset unpublished = new Dataset(); - unpublished.setGlobalId(new GlobalId(DOIServiceBean.DOI_PROTOCOL, "10.666", "FAKE/unpublished1", null, DOIServiceBean.DOI_RESOLVER_URL, null)); + unpublished.setGlobalId(new GlobalId(AbstractDOIProvider.DOI_PROTOCOL, "10.666", "FAKE/unpublished1", null, AbstractDOIProvider.DOI_RESOLVER_URL, null)); String unpublishedPid = unpublished.getGlobalId().asString(); Timestamp nullPublicationDateToIndicateNotPublished = null; @@ -81,14 +81,14 @@ void testUpdateSiteMap() throws IOException, ParseException, SAXException { datasets.add(unpublished); Dataset harvested = new Dataset(); - harvested.setGlobalId(new GlobalId(DOIServiceBean.DOI_PROTOCOL, "10.666", "FAKE/harvested1", null, DOIServiceBean.DOI_RESOLVER_URL, null)); + harvested.setGlobalId(new GlobalId(AbstractDOIProvider.DOI_PROTOCOL, "10.666", "FAKE/harvested1", null, AbstractDOIProvider.DOI_RESOLVER_URL, null)); String harvestedPid = harvested.getGlobalId().asString(); harvested.setPublicationDate(new Timestamp(new Date().getTime())); harvested.setHarvestedFrom(new HarvestingClient()); datasets.add(harvested); Dataset deaccessioned = new Dataset(); - deaccessioned.setGlobalId(new GlobalId(DOIServiceBean.DOI_PROTOCOL, "10.666", "FAKE/deaccessioned1", null, DOIServiceBean.DOI_RESOLVER_URL, null)); + deaccessioned.setGlobalId(new GlobalId(AbstractDOIProvider.DOI_PROTOCOL, "10.666", "FAKE/deaccessioned1", null, AbstractDOIProvider.DOI_RESOLVER_URL, null)); String deaccessionedPid = deaccessioned.getGlobalId().asString(); deaccessioned.setPublicationDate(new Timestamp(new Date().getTime())); diff --git a/src/test/java/edu/harvard/iq/dataverse/util/UrlTokenUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/util/UrlTokenUtilTest.java index d70a108e7c6..15905c2971b 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/UrlTokenUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/UrlTokenUtilTest.java @@ -1,12 +1,12 @@ package edu.harvard.iq.dataverse.util; -import edu.harvard.iq.dataverse.DOIServiceBean; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.FileMetadata; import edu.harvard.iq.dataverse.GlobalId; import edu.harvard.iq.dataverse.authorization.users.ApiToken; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.util.testing.JvmSetting; import edu.harvard.iq.dataverse.util.testing.LocalJvmSettings; @@ -32,7 +32,7 @@ void testGetToolUrlWithOptionalQueryParameters() { DatasetVersion dv = new DatasetVersion(); Dataset ds = new Dataset(); ds.setId(50L); - ds.setGlobalId(new GlobalId(DOIServiceBean.DOI_PROTOCOL,"10.5072","FK2ABCDEF",null, DOIServiceBean.DOI_RESOLVER_URL, null)); + ds.setGlobalId(new GlobalId(AbstractDOIProvider.DOI_PROTOCOL,"10.5072","FK2ABCDEF",null, AbstractDOIProvider.DOI_RESOLVER_URL, null)); dv.setDataset(ds); fmd.setDatasetVersion(dv); List fmdl = new ArrayList<>(); From 4716c7ae18e89d9a3fe602e411953b23c981d5b3 Mon Sep 17 00:00:00 2001 From: luddaniel <83018819+luddaniel@users.noreply.github.com> Date: Wed, 6 Mar 2024 15:38:47 +0100 Subject: [PATCH 444/689] Returning to author now requires a reason that is sent by email to the author (#10137) * #3702 - Returning to author now requires a commented reason that is sent by email to the author * Update src/main/java/propertyFiles/Bundle.properties Co-authored-by: Philip Durbin * #3702 - Increased maxlength of message to 2000 + Added email contacts of collection in return to author email * #3702 - Added test on null or empty comment for ReturnDatasetToAuthorCommand * #3702 fixed Unit Tests errors * #3702 - Code commentary to be removed * #3702 - Adding release note * #3702 - Adding last Bundle.properties corrections * #3702 - Updated release note and guide --------- Co-authored-by: Philip Durbin --- doc/release-notes/3702-return-to-author.md | 4 +++ doc/sphinx-guides/source/api/native-api.rst | 3 +- .../edu/harvard/iq/dataverse/DatasetPage.java | 13 ++++++-- .../harvard/iq/dataverse/MailServiceBean.java | 26 ++++++++++------ .../harvard/iq/dataverse/api/Datasets.java | 3 +- .../iq/dataverse/api/Notifications.java | 1 - .../impl/ReturnDatasetToAuthorCommand.java | 5 +++ src/main/java/propertyFiles/Bundle.properties | 13 +++++--- src/main/webapp/dataset.xhtml | 31 ++++++++++--------- src/main/webapp/dataverseuser.xhtml | 3 -- .../ReturnDatasetToAuthorCommandTest.java | 31 +++++++++---------- 11 files changed, 78 insertions(+), 55 deletions(-) create mode 100644 doc/release-notes/3702-return-to-author.md diff --git a/doc/release-notes/3702-return-to-author.md b/doc/release-notes/3702-return-to-author.md new file mode 100644 index 00000000000..aa7dd9feaef --- /dev/null +++ b/doc/release-notes/3702-return-to-author.md @@ -0,0 +1,4 @@ +### Return to author + +Popup for returning to author now requires a reason that will be sent by email to the author. +Please note that you can still type a creative and meaningful comment such as "The author would like to modify his dataset", "Files are missing", "Nothing to report" or "A curation report with comments and suggestions/instructions will follow in another email" that suits your situation. \ No newline at end of file diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 7f048f96eb9..70d73ae3c98 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -2126,7 +2126,8 @@ The fully expanded example above (without environment variables) looks like this curl -H "X-Dataverse-key: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X POST "https://demo.dataverse.org/api/datasets/:persistentId/returnToAuthor?persistentId=doi:10.5072/FK2/J8SJZB" -H "Content-type: application/json" -d @reason-for-return.json -The review process can sometimes resemble a tennis match, with the authors submitting and resubmitting the dataset over and over until the curators are satisfied. Each time the curators send a "reason for return" via API, that reason is persisted into the database, stored at the dataset version level. +The review process can sometimes resemble a tennis match, with the authors submitting and resubmitting the dataset over and over until the curators are satisfied. Each time the curators send a "reason for return" via API, that reason is sent by email and is persisted into the database, stored at the dataset version level. +The reason is required, please note that you can still type a creative and meaningful comment such as "The author would like to modify his dataset", "Files are missing", "Nothing to report" or "A curation report with comments and suggestions/instructions will follow in another email" that suits your situation. The :ref:`send-feedback` API call may be useful as a way to move the conversation to email. However, note that these emails go to contacts (versus authors) and there is no database record of the email contents. (:ref:`dataverse.mail.cc-support-on-contact-email` will send a copy of these emails to the support email address which would provide a record.) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index 88b1f4f49bc..0641039e433 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -709,6 +709,16 @@ public void setNumberOfFilesToShow(Long numberOfFilesToShow) { this.numberOfFilesToShow = numberOfFilesToShow; } + private String returnReason = ""; + + public String getReturnReason() { + return returnReason; + } + + public void setReturnReason(String returnReason) { + this.returnReason = returnReason; + } + public void showAll(){ setNumberOfFilesToShow(new Long(fileMetadatasSearch.size())); } @@ -2653,8 +2663,7 @@ public void edit(EditMode editMode) { public String sendBackToContributor() { try { - //FIXME - Get Return Comment from sendBackToContributor popup - Command cmd = new ReturnDatasetToAuthorCommand(dvRequestService.getDataverseRequest(), dataset, ""); + Command cmd = new ReturnDatasetToAuthorCommand(dvRequestService.getDataverseRequest(), dataset, returnReason); dataset = commandEngine.submit(cmd); JsfHelper.addSuccessMessage(BundleUtil.getStringFromBundle("dataset.reject.success")); } catch (CommandException ex) { diff --git a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java index 72fc6ee6d64..4b591d240bd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java @@ -466,18 +466,24 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio case RETURNEDDS: version = (DatasetVersion) targetObject; pattern = BundleUtil.getStringFromBundle("notification.email.wasReturnedByReviewer"); - String optionalReturnReason = ""; - /* - FIXME - Setting up to add single comment when design completed - optionalReturnReason = "."; - if (comment != null && !comment.isEmpty()) { - optionalReturnReason = ".\n\n" + BundleUtil.getStringFromBundle("wasReturnedReason") + "\n\n" + comment; - } - */ + String[] paramArrayReturnedDataset = {version.getDataset().getDisplayName(), getDatasetDraftLink(version.getDataset()), - version.getDataset().getOwner().getDisplayName(), getDataverseLink(version.getDataset().getOwner()), optionalReturnReason}; + version.getDataset().getOwner().getDisplayName(), getDataverseLink(version.getDataset().getOwner())}; messageText += MessageFormat.format(pattern, paramArrayReturnedDataset); + + if (comment != null && !comment.isEmpty()) { + messageText += "\n\n" + MessageFormat.format(BundleUtil.getStringFromBundle("notification.email.wasReturnedByReviewerReason"), comment); + } + + Dataverse d = (Dataverse) version.getDataset().getOwner(); + List contactEmailList = new ArrayList(); + for (DataverseContact dc : d.getDataverseContacts()) { + contactEmailList.add(dc.getContactEmail()); + } + if (!contactEmailList.isEmpty()) { + String contactEmails = String.join(", ", contactEmailList); + messageText += "\n\n" + MessageFormat.format(BundleUtil.getStringFromBundle("notification.email.wasReturnedByReviewer.collectionContacts"), contactEmails); + } return messageText; case WORKFLOW_SUCCESS: diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index e312d6ec15b..ad66fb468f4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2145,9 +2145,8 @@ public Response returnToAuthor(@Context ContainerRequestContext crc, @PathParam( Dataset dataset = findDatasetOrDie(idSupplied); String reasonForReturn = null; reasonForReturn = json.getString("reasonForReturn"); - // TODO: Once we add a box for the curator to type into, pass the reason for return to the ReturnDatasetToAuthorCommand and delete this check and call to setReturnReason on the API side. if (reasonForReturn == null || reasonForReturn.isEmpty()) { - return error(Response.Status.BAD_REQUEST, "You must enter a reason for returning a dataset to the author(s)."); + return error(Response.Status.BAD_REQUEST, BundleUtil.getStringFromBundle("dataset.reject.datasetNotInReview")); } AuthenticatedUser authenticatedUser = getRequestAuthenticatedUserOrDie(crc); Dataset updatedDataset = execCommand(new ReturnDatasetToAuthorCommand(createDataverseRequest(authenticatedUser), dataset, reasonForReturn )); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Notifications.java b/src/main/java/edu/harvard/iq/dataverse/api/Notifications.java index 37c894d3071..df172f36973 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Notifications.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Notifications.java @@ -55,7 +55,6 @@ public Response getAllNotificationsForUser(@Context ContainerRequestContext crc) notificationObjectBuilder.add("id", notification.getId()); notificationObjectBuilder.add("type", type.toString()); /* FIXME - Re-add reasons for return if/when they are added to the notifications page. - if (Type.RETURNEDDS.equals(type) || Type.SUBMITTEDDS.equals(type)) { JsonArrayBuilder reasons = getReasonsForReturn(notification); for (JsonValue reason : reasons.build()) { diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ReturnDatasetToAuthorCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ReturnDatasetToAuthorCommand.java index caf37ad4de1..f3b33f82524 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ReturnDatasetToAuthorCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/ReturnDatasetToAuthorCommand.java @@ -25,6 +25,11 @@ public class ReturnDatasetToAuthorCommand extends AbstractDatasetCommand
    You may contact us for support at {0}.

    Thank you,
    {1} notification.email.assignRole=You are now {0} for the {1} "{2}" (view at {3} ). @@ -1476,7 +1478,7 @@ dataset.submit.failure.inReview=You cannot submit this dataset for review becaus dataset.status.failure.notallowed=Status update failed - label not allowed dataset.status.failure.disabled=Status labeling disabled for this dataset dataset.status.failure.isReleased=Latest version of dataset is already released. Status can only be set on draft versions -dataset.rejectMessage=Return this dataset to contributor for modification. +dataset.rejectMessage=Return this dataset to contributor for modification. The reason for return entered below will be sent by email to the author. dataset.rejectMessage.label=Return to Author Reason dataset.rejectWatermark=Please enter a reason for returning this dataset to its author(s). dataset.reject.enterReason.error=Reason for return to author is required. @@ -1484,6 +1486,7 @@ dataset.reject.success=This dataset has been sent back to the contributor. dataset.reject.failure=Dataset Submission Return Failed - {0} dataset.reject.datasetNull=Cannot return the dataset to the author(s) because it is null. dataset.reject.datasetNotInReview=This dataset cannot be return to the author(s) because the latest version is not In Review. The author(s) needs to click Submit for Review first. +dataset.reject.commentNull=You must enter a reason for returning a dataset to the author(s). dataset.publish.tip=Are you sure you want to publish this dataset? Once you do so it must remain published. dataset.publish.terms.tip=This version of the dataset will be published with the following terms: dataset.publish.terms.help.tip=To change the terms for this version, click the Cancel button and go to the Terms tab for this dataset. diff --git a/src/main/webapp/dataset.xhtml b/src/main/webapp/dataset.xhtml index 4bb1ec869f6..34c6d3dcbea 100644 --- a/src/main/webapp/dataset.xhtml +++ b/src/main/webapp/dataset.xhtml @@ -1843,27 +1843,30 @@

    #{bundle['dataset.rejectMessage']}

    - - - - -

    - -

    - -
    + + +

    + +

    + + +
    - + + +
    - + diff --git a/src/main/webapp/dataverseuser.xhtml b/src/main/webapp/dataverseuser.xhtml index 2426cf980d3..9ed8b5209b6 100644 --- a/src/main/webapp/dataverseuser.xhtml +++ b/src/main/webapp/dataverseuser.xhtml @@ -178,9 +178,6 @@ #{DataverseUserPage.getRequestorEmail(item)} - - #{DataverseUserPage.getReasonForReturn(item.theObject)} - diff --git a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/ReturnDatasetToAuthorCommandTest.java b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/ReturnDatasetToAuthorCommandTest.java index 23cc4547bc4..fc52abecaf2 100644 --- a/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/ReturnDatasetToAuthorCommandTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/engine/command/impl/ReturnDatasetToAuthorCommandTest.java @@ -155,7 +155,7 @@ public void testReleasedDataset() { String actual = null; Dataset updatedDataset = null; try { - updatedDataset = testEngine.submit(new ReturnDatasetToAuthorCommand(dataverseRequest, dataset, "")); + updatedDataset = testEngine.submit(new ReturnDatasetToAuthorCommand(dataverseRequest, dataset, "Update Your Files, Dummy")); } catch (CommandException ex) { actual = ex.getMessage(); } @@ -171,36 +171,33 @@ public void testNotInReviewDataset() { String actual = null; Dataset updatedDataset = null; try { - updatedDataset = testEngine.submit(new ReturnDatasetToAuthorCommand(dataverseRequest, dataset, "")); + updatedDataset = testEngine.submit(new ReturnDatasetToAuthorCommand(dataverseRequest, dataset, "Update Your Files, Dummy")); } catch (CommandException ex) { actual = ex.getMessage(); } assertEquals(expected, actual); } - /* - FIXME - Empty Comments won't be allowed in future @Test - public void testEmptyComments(){ - - dataset.setIdentifier("DUMMY"); + public void testEmptyOrNullComment(){ dataset.getLatestVersion().setVersionState(DatasetVersion.VersionState.DRAFT); - dataset.getLatestVersion().setInReview(true); - dataset.getLatestVersion().setReturnReason(null); + Dataset updatedDataset = null; String expected = "You must enter a reason for returning a dataset to the author(s)."; String actual = null; - Dataset updatedDataset = null; try { - - updatedDataset = testEngine.submit(new ReturnDatasetToAuthorCommand(dataverseRequest, dataset)); - } catch (CommandException ex) { + testEngine.submit( new AddLockCommand(dataverseRequest, dataset, + new DatasetLock(DatasetLock.Reason.InReview, dataverseRequest.getAuthenticatedUser()))); + + assertThrowsExactly(IllegalArgumentException.class, + () -> new ReturnDatasetToAuthorCommand(dataverseRequest, dataset, null), expected); + assertThrowsExactly(IllegalArgumentException.class, + () -> new ReturnDatasetToAuthorCommand(dataverseRequest, dataset, ""), expected); + updatedDataset = testEngine.submit(new ReturnDatasetToAuthorCommand(dataverseRequest, dataset, "")); + } catch (IllegalArgumentException | CommandException ex) { actual = ex.getMessage(); } - assertEquals(expected, actual); - - + assertEquals(expected, actual); } - */ @Test public void testAllGood() { From 3417751a376a04a84432b92e5dfd579e51e06344 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Wed, 6 Mar 2024 10:27:31 -0500 Subject: [PATCH 445/689] flyway script bump --- ...id-providers.sql => V6.1.0.5__3623-multiple-pid-providers.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V6.1.0.4__3623-multiple-pid-providers.sql => V6.1.0.5__3623-multiple-pid-providers.sql} (100%) diff --git a/src/main/resources/db/migration/V6.1.0.4__3623-multiple-pid-providers.sql b/src/main/resources/db/migration/V6.1.0.5__3623-multiple-pid-providers.sql similarity index 100% rename from src/main/resources/db/migration/V6.1.0.4__3623-multiple-pid-providers.sql rename to src/main/resources/db/migration/V6.1.0.5__3623-multiple-pid-providers.sql From 38d54bc8f3147e83b254ae72f7cbeaab219ee9c6 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 6 Mar 2024 11:23:00 -0600 Subject: [PATCH 446/689] no-op update to pom.xml to force Docker image build As described in .github/workflows/maven_unit_test.yml we need to touch Java files or pom.xml for this action (which pushes Docker images) to run. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index aaa2b49eaae..8b2850e1df9 100644 --- a/pom.xml +++ b/pom.xml @@ -57,7 +57,7 @@ In case the dependency is both transitive and direct (e. g. some common lib for logging), manage the version above and add the direct dependency here WITHOUT version tag, too. --> - + + + redis.clients + jedis + 5.1.0 + + org.junit.jupiter diff --git a/scripts/installer/default.config b/scripts/installer/default.config index 8647cd02416..2a29a1d5270 100644 --- a/scripts/installer/default.config +++ b/scripts/installer/default.config @@ -32,3 +32,9 @@ DOI_USERNAME = dataciteuser DOI_PASSWORD = datacitepassword DOI_BASEURL = https://mds.test.datacite.org DOI_DATACITERESTAPIURL = https://api.test.datacite.org + +[redis] +REDIS_HOST = redis +REDIS_PORT = 6379 +REDIS_USER = default +REDIS_PASSWORD = redis_secret diff --git a/scripts/installer/install.py b/scripts/installer/install.py index 99316efb83b..6d6003607bd 100644 --- a/scripts/installer/install.py +++ b/scripts/installer/install.py @@ -100,7 +100,8 @@ "database", "rserve", "system", - "doi"] + "doi", + "redis"] # read pre-defined defaults: diff --git a/scripts/installer/installAppServer.py b/scripts/installer/installAppServer.py index 698f5ba9a58..faa5bf42341 100644 --- a/scripts/installer/installAppServer.py +++ b/scripts/installer/installAppServer.py @@ -30,6 +30,11 @@ def runAsadminScript(config): os.environ['DOI_PASSWORD'] = config.get('doi','DOI_PASSWORD') os.environ['DOI_DATACITERESTAPIURL'] = config.get('doi','DOI_DATACITERESTAPIURL') + os.environ['REDIS_HOST'] = config.get('redis','REDIS_HOST') + os.environ['REDIS_PORT'] = config.get('redis','REDIS_PORT') + os.environ['REDIS_USER'] = config.get('redis','REDIS_USER') + os.environ['REDIS_PASS'] = config.get('redis','REDIS_PASSWORD') + mailServerEntry = config.get('system','MAIL_SERVER') try: diff --git a/scripts/installer/interactive.config b/scripts/installer/interactive.config index ef8110c554f..9e0fafaa8b4 100644 --- a/scripts/installer/interactive.config +++ b/scripts/installer/interactive.config @@ -24,6 +24,10 @@ DOI_USERNAME = Datacite username DOI_PASSWORD = Datacite password DOI_BASEURL = Datacite URL DOI_DATACITERESTAPIURL = Datacite REST API URL +REDIS_HOST = Redis Server +REDIS_PORT = Redis Server Port +REDIS_USER = Redis User Name +REDIS_PASSWORD = Redis User Password [comments] HOST_DNS_ADDRESS = :(enter numeric IP address, if FQDN is unavailable) GLASSFISH_USER = :This user will be running the App. Server (Payara) service on your system.\n - If this is a dev. environment, this should be your own username; \n - In production, we suggest you create the account "dataverse", or use any other unprivileged user account\n: @@ -46,3 +50,7 @@ DOI_USERNAME = DataCite or EZID username. Only necessary for publishing / mintin DOI_PASSWORD = DataCite or EZID account password. DOI_BASEURL = DataCite or EZID URL. Probably https://mds.datacite.org DOI_DATACITERESTAPIURL = DataCite REST API URL (Make Data Count, /pids API). Probably https://api.datacite.org +REDIS_HOST = +REDIS_PORT = +REDIS_USER = +REDIS_PASSWORD = diff --git a/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java b/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java index 3793b6eeeb4..8636172b731 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.cache.CacheFactoryBean; import edu.harvard.iq.dataverse.engine.DataverseEngine; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean; @@ -16,6 +17,7 @@ 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.PermissionException; +import edu.harvard.iq.dataverse.engine.command.exception.RateLimitCommandException; import edu.harvard.iq.dataverse.ingest.IngestServiceBean; import edu.harvard.iq.dataverse.pidproviders.PidProviderFactoryBean; import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean; @@ -176,7 +178,9 @@ public class EjbDataverseEngine { @EJB EjbDataverseEngineInner innerEngine; - + + @EJB + CacheFactoryBean cacheFactory; @Resource EJBContext ejbCtxt; @@ -202,7 +206,11 @@ public R submit(Command aCommand) throws CommandException { try { logRec.setUserIdentifier( aCommand.getRequest().getUser().getIdentifier() ); - + // Check for rate limit exceeded. Must be done before anything else to prevent unnecessary processing. + if (!cacheFactory.checkRate(aCommand.getRequest().getUser(), aCommand.getClass().getSimpleName())) { + throw new RateLimitCommandException(BundleUtil.getStringFromBundle("command.exception.user.ratelimited", Arrays.asList(aCommand.getClass().getSimpleName())), aCommand); + } + // Check permissions - or throw an exception Map> requiredMap = aCommand.getRequiredPermissions(); if (requiredMap == null) { diff --git a/src/main/java/edu/harvard/iq/dataverse/UserServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/UserServiceBean.java index 93892376edc..50680b67cee 100644 --- a/src/main/java/edu/harvard/iq/dataverse/UserServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/UserServiceBean.java @@ -147,6 +147,8 @@ private AuthenticatedUser createAuthenticatedUserForView (Object[] dbRowValues, user.setMutedEmails(Type.tokenizeToSet((String) dbRowValues[15])); user.setMutedNotifications(Type.tokenizeToSet((String) dbRowValues[15])); + user.setRateLimitTier(Integer.valueOf((int)dbRowValues[16])); + user.setRoles(roles); return user; } @@ -419,7 +421,7 @@ private List getUserListCore(String searchTerm, qstr += " u.createdtime, u.lastlogintime, u.lastapiusetime, "; qstr += " prov.id, prov.factoryalias, "; qstr += " u.deactivated, u.deactivatedtime, "; - qstr += " u.mutedEmails, u.mutedNotifications "; + qstr += " u.mutedEmails, u.mutedNotifications, u.rateLimitTier "; qstr += " FROM authenticateduser u,"; qstr += " authenticateduserlookup prov_lookup,"; qstr += " authenticationproviderrow prov"; 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 60e0b79662b..44629d5dd76 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -20,6 +20,7 @@ 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.locality.StorageSiteServiceBean; @@ -421,7 +422,7 @@ public Command handleLatestPublished() { })); return dsv; } - + protected DataFile findDataFileOrDie(String id) throws WrappedResponse { DataFile datafile; if (id.equals(PERSISTENT_ID_KEY)) { @@ -575,6 +576,8 @@ protected T execCommand( Command cmd ) throws WrappedResponse { try { return engineSvc.submit(cmd); + } catch (RateLimitCommandException ex) { + throw new WrappedResponse(rateLimited(ex.getMessage())); } catch (IllegalCommandException ex) { //for 8859 for api calls that try to update datasets with TOA out of compliance if (ex.getMessage().toLowerCase().contains("terms of use")){ @@ -776,11 +779,12 @@ protected Response notFound( String msg ) { protected Response badRequest( String msg ) { return error( Status.BAD_REQUEST, msg ); } - + protected Response forbidden( String msg ) { return error( Status.FORBIDDEN, msg ); } - + protected Response rateLimited( String msg ) { return error( Status.TOO_MANY_REQUESTS, msg ); } + protected Response conflict( String msg ) { return error( Status.CONFLICT, msg ); } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java b/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java index b307c655798..ff884926a1f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java @@ -146,6 +146,9 @@ public class AuthenticatedUser implements User, Serializable { @Transient private Set mutedNotificationsSet = new HashSet<>(); + @Column(nullable=true) + private Integer rateLimitTier; + @PrePersist void prePersist() { mutedNotifications = Type.toStringValue(mutedNotificationsSet); @@ -397,6 +400,13 @@ public void setDeactivatedTime(Timestamp deactivatedTime) { this.deactivatedTime = deactivatedTime; } + public Integer getRateLimitTier() { + return rateLimitTier; + } + public void setRateLimitTier(Integer rateLimitTier) { + this.rateLimitTier = rateLimitTier; + } + @OneToOne(mappedBy = "authenticatedUser") private AuthenticatedUserLookup authenticatedUserLookup; diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java new file mode 100644 index 00000000000..83ba7a418e4 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java @@ -0,0 +1,60 @@ +package edu.harvard.iq.dataverse.cache; + +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.authorization.users.GuestUser; +import edu.harvard.iq.dataverse.authorization.users.User; +import edu.harvard.iq.dataverse.util.SystemConfig; +import jakarta.annotation.PostConstruct; +import jakarta.ejb.EJB; +import jakarta.ejb.Stateless; +import jakarta.inject.Named; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +import java.util.logging.Logger; + +@Stateless +@Named +public class CacheFactoryBean implements java.io.Serializable { + private static final Logger logger = Logger.getLogger(CacheFactoryBean.class.getCanonicalName()); + private static JedisPool jedisPool = null; + @EJB + SystemConfig systemConfig; + + @PostConstruct + public void init() { + logger.info("CacheFactoryBean.init Redis Host:Port " + systemConfig.getRedisBaseHost() + ":" + systemConfig.getRedisBasePort()); + jedisPool = new JedisPool(new JedisPoolConfig(), systemConfig.getRedisBaseHost(), Integer.valueOf(systemConfig.getRedisBasePort()), + systemConfig.getRedisUser(), systemConfig.getRedisPassword()); + } + @Override + protected void finalize() throws Throwable { + if (jedisPool != null) { + jedisPool.close(); + } + super.finalize(); + } + + /** + * Check if user can make this call or if they are rate limited + * @param user + * @param action + * @return true if user is superuser or rate not limited + */ + public boolean checkRate(User user, String action) { + if (user != null && user.isSuperuser()) { + return true; + }; + StringBuffer id = new StringBuffer(); + id.append(user != null ? user.getIdentifier() : GuestUser.get().getIdentifier()); + if (action != null) { + id.append(":").append(action); + } + + // get the capacity, i.e. calls per hour, from config + int capacity = (user instanceof AuthenticatedUser) ? + RateLimitUtil.getCapacityByTier(systemConfig, ((AuthenticatedUser) user).getRateLimitTier()) : + RateLimitUtil.getCapacityByTier(systemConfig, 0); + return (!RateLimitUtil.rateLimited(jedisPool, id.toString(), capacity)); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitSetting.java b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitSetting.java new file mode 100644 index 00000000000..14a4439bb56 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitSetting.java @@ -0,0 +1,53 @@ +package edu.harvard.iq.dataverse.cache; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class RateLimitSetting { + + @JsonProperty("tier") + private int tier; + @JsonProperty("limitPerHour") + private int limitPerHour = RateLimitUtil.NO_LIMIT; + @JsonProperty("actions") + private List rateLimitActions = new ArrayList<>(); + + private int defaultLimitPerHour; + + public RateLimitSetting() {} + + @JsonProperty("tier") + public void setTier(int tier) { + this.tier = tier; + } + @JsonProperty("tier") + public int getTier() { + return this.tier; + } + @JsonProperty("limitPerHour") + public void setLimitPerHour(int limitPerHour) { + this.limitPerHour = limitPerHour; + } + @JsonProperty("limitPerHour") + public int getLimitPerHour() { + return this.limitPerHour; + } + @JsonProperty("actions") + public void setRateLimitActions(List rateLimitActions) { + this.rateLimitActions = rateLimitActions; + } + @JsonProperty("actions") + public List getRateLimitActions() { + return this.rateLimitActions; + } + public void setDefaultLimit(int defaultLimitPerHour) { + this.defaultLimitPerHour = defaultLimitPerHour; + } + public int getDefaultLimitPerHour() { + return this.defaultLimitPerHour; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java new file mode 100644 index 00000000000..b97773e0312 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java @@ -0,0 +1,124 @@ +package edu.harvard.iq.dataverse.cache; + +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.SystemConfig; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; + +import java.io.StringReader; +import java.util.*; +import java.util.logging.Logger; + +import static java.lang.Math.max; +import static java.lang.Math.min; + +public class RateLimitUtil { + private static final Logger logger = Logger.getLogger(RateLimitUtil.class.getCanonicalName()); + protected static final List rateLimits = new ArrayList<>(); + protected static final Map rateLimitMap = new HashMap<>(); + public static final int NO_LIMIT = -1; + + public static int getCapacityByTier(SystemConfig systemConfig, Integer tier) { + return systemConfig.getIntFromCSVStringOrDefault(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers, tier, NO_LIMIT); + } + + public static boolean rateLimited(final JedisPool jedisPool, final String key, int capacityPerHour) { + if (capacityPerHour == NO_LIMIT) { + return false; + } + Jedis jedis; + try { + jedis = jedisPool.getResource(); + } catch (Exception e) { + // We can't rate limit if Redis is not reachable + logger.severe("RateLimitUtil.rateLimited jedisPool.getResource() " + e.getMessage()); + return false; + } + + long currentTime = System.currentTimeMillis() / 60000L; // convert to minutes + int tokensPerMinute = (int)Math.ceil(capacityPerHour / 60.0); + + // Get the last time this bucket was added to + final String keyLastUpdate = String.format("%s:last_update",key); + long lastUpdate = longFromKey(jedis, keyLastUpdate); + long deltaTime = currentTime - lastUpdate; + // Get the current number of tokens in the bucket + long tokens = longFromKey(jedis, key); + long tokensToAdd = (long) (deltaTime * tokensPerMinute); + + if (tokensToAdd > 0) { // Don't update timestamp if we aren't adding any tokens to the bucket + tokens = min(capacityPerHour, tokens + tokensToAdd); + jedis.set(keyLastUpdate, String.valueOf(currentTime)); + } + + // Update with any added tokens and decrement 1 token for this call if not rate limited (0 tokens) + jedis.set(key, String.valueOf(max(0, tokens-1))); + jedisPool.returnResource(jedis); + return tokens < 1; + } + + public static int getCapacityByTierAndAction(SystemConfig systemConfig, Integer tier, String action) { + if (rateLimits.isEmpty()) { + init(systemConfig); + } + + return rateLimitMap.containsKey(getMapKey(tier,action)) ? rateLimitMap.get(getMapKey(tier,action)) : + rateLimitMap.containsKey(getMapKey(tier)) ? rateLimitMap.get(getMapKey(tier)) : + getCapacityByTier(systemConfig, tier); + } + + private static void init(SystemConfig systemConfig) { + getRateLimitsFromJson(systemConfig); + /* Convert the List of Rate Limit Settings containing a list of Actions to a fast lookup Map where the key is: + for default if no action defined: "{tier}:" and the value is the default limit for the tier + for each action: "{tier}:{action}" and the value is the limit defined in the setting + */ + rateLimits.forEach(r -> { + r.setDefaultLimit(getCapacityByTier(systemConfig, r.getTier())); + rateLimitMap.put(getMapKey(r.getTier()), r.getDefaultLimitPerHour()); + r.getRateLimitActions().forEach(a -> rateLimitMap.put(getMapKey(r.getTier(), a), r.getLimitPerHour())); + }); + } + + private static void getRateLimitsFromJson(SystemConfig systemConfig) { + ObjectMapper mapper = new ObjectMapper(); + String setting = systemConfig.getRateLimitsJson(); + if (!setting.isEmpty()) { + try { + JsonReader jr = Json.createReader(new StringReader(setting)); + JsonObject obj= jr.readObject(); + JsonArray lst = obj.getJsonArray("rateLimits"); + + rateLimits.addAll(mapper.readValue(lst.toString(), + mapper.getTypeFactory().constructCollectionType(List.class, RateLimitSetting.class))); + } catch (Exception e) { + logger.warning("Unable to parse Rate Limit Json" + ": " + e.getLocalizedMessage()); + rateLimits.add(new RateLimitSetting()); // add a default entry to prevent re-initialization + e.printStackTrace(); + } + } + } + + private static String getMapKey(Integer tier) { + return getMapKey(tier, null); + } + + private static String getMapKey(Integer tier, String action) { + StringBuffer key = new StringBuffer(); + key.append(tier).append(":"); + if (action != null) { + key.append(action); + } + return key.toString(); + } + + private static long longFromKey(Jedis r, String key) { + String l = r.get(key); + return l != null ? Long.parseLong(l) : 0L; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/exception/RateLimitCommandException.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/exception/RateLimitCommandException.java new file mode 100644 index 00000000000..99a665b31ac --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/exception/RateLimitCommandException.java @@ -0,0 +1,16 @@ +package edu.harvard.iq.dataverse.engine.command.exception; + +import edu.harvard.iq.dataverse.engine.command.Command; + +/** + * An exception raised when a command cannot be executed, due to the + * issuing user being rate limited. + * + * @author + */ +public class RateLimitCommandException extends CommandException { + + public RateLimitCommandException(String message, Command aCommand) { + super(message, aCommand); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java index b05c88c0be2..2d1667f0cc5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java @@ -239,6 +239,10 @@ public enum Key { CVocConf, + // Default calls per hour for each tier. csv format (30,60,...) + RateLimitingDefaultCapacityTiers, + // json defined list of capacities by tier and action list. See RateLimitSetting.java + RateLimitingCapacityByTierAndAction, /** * A link to an installation of https://github.com/IQSS/miniverse or * some other metrics app. diff --git a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java index 3f1ec3dd7eb..0f4537ddb99 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java @@ -1146,11 +1146,50 @@ public Long getTestStorageQuotaLimit() { return settingsService.getValueForKeyAsLong(SettingsServiceBean.Key.StorageQuotaSizeInBytes); } /** - * Should we store tab-delimited files produced during ingest *with* the - * variable name header line included? + * Should we store tab-delimited files produced during ingest *with* the + * variable name header line included? * @return boolean - defaults to false. */ public boolean isStoringIngestedFilesWithHeaders() { return settingsService.isTrueForKey(SettingsServiceBean.Key.StoreIngestedTabularFilesWithVarHeaders, false); } + + /* + RateLimitUtil will parse the json to create a List + */ + public String getRateLimitsJson() { + return settingsService.getValueForKey(SettingsServiceBean.Key.RateLimitingCapacityByTierAndAction, ""); + } + + public Integer getIntFromCSVStringOrDefault(final SettingsServiceBean.Key settingKey, final Integer index, final Integer defaultValue) { + Integer value = defaultValue; + if (settingKey != null && !settingKey.equals("")) { + String csv = settingsService.getValueForKey(settingKey, ""); + try { + int[] values = Arrays.stream(csv.split(",")).mapToInt(Integer::parseInt).toArray(); + value = index > values.length ? defaultValue : Integer.valueOf(values[index]); + } catch (NumberFormatException nfe) { + logger.warning(nfe.getMessage()); + } + } + + return value; + } + + public String getRedisBaseHost() { + String saneDefault = "redis"; + return System.getProperty("DATAVERSE_REDIS_HOST",saneDefault); + } + public String getRedisBasePort() { + String saneDefault = "6379"; + return System.getProperty("DATAVERSE_REDIS_PORT",saneDefault); + } + public String getRedisUser() { + String saneDefault = "default"; + return System.getProperty("DATAVERSE_REDIS_USER",saneDefault); + } + public String getRedisPassword() { + String saneDefault = "redis_secret"; + return System.getProperty("DATAVERSE_REDIS_PASSWORD",saneDefault); + } } diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 17dd0933f55..1b9ffd53e55 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -2629,6 +2629,7 @@ pid.allowedCharacters=^[A-Za-z0-9._/:\\-]* command.exception.only.superusers={1} can only be called by superusers. command.exception.user.deactivated={0} failed: User account has been deactivated. command.exception.user.deleted={0} failed: User account has been deleted. +command.exception.user.ratelimited={0} failed: Rate limited due to too many requests. #Admin-API admin.api.auth.mustBeSuperUser=Forbidden. You must be a superuser. diff --git a/src/main/resources/db/migration/V6.1.0.1__9356-add-rate-limiting.sql b/src/main/resources/db/migration/V6.1.0.1__9356-add-rate-limiting.sql new file mode 100644 index 00000000000..ae30fd96bfd --- /dev/null +++ b/src/main/resources/db/migration/V6.1.0.1__9356-add-rate-limiting.sql @@ -0,0 +1 @@ +ALTER TABLE authenticateduser ADD COLUMN IF NOT EXISTS ratelimittier int DEFAULT 1; \ No newline at end of file diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java new file mode 100644 index 00000000000..fa27ea6d4fd --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -0,0 +1,114 @@ +package edu.harvard.iq.dataverse.cache; + +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.authorization.users.GuestUser; +import edu.harvard.iq.dataverse.authorization.users.User; +import edu.harvard.iq.dataverse.util.SystemConfig; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.lenient; + +@ExtendWith(MockitoExtension.class) +public class CacheFactoryBeanTest { + + @Mock + SystemConfig systemConfig; + @InjectMocks + CacheFactoryBean cache = new CacheFactoryBean(); + AuthenticatedUser authUser = new AuthenticatedUser(); + String action; + + @BeforeEach + public void setup() { + lenient().doReturn("localhost").when(systemConfig).getRedisBaseHost(); + lenient().doReturn("6379").when(systemConfig).getRedisBasePort(); + lenient().doReturn("default").when(systemConfig).getRedisUser(); + lenient().doReturn("redis_secret").when(systemConfig).getRedisPassword(); + lenient().doReturn(30).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(0), anyInt()); + lenient().doReturn(60).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(1), anyInt()); + lenient().doReturn(120).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(2), anyInt()); + + cache.init(); + authUser.setRateLimitTier(1); // reset to default + action = "cmd-" + UUID.randomUUID(); + } + @Test + public void testGuestUserGettingRateLimited() throws InterruptedException { + User user = GuestUser.get(); + boolean rateLimited = false; + int cnt = 0; + for (; cnt <100; cnt++) { + rateLimited = !cache.checkRate(user, action); + if (rateLimited) { + break; + } + } + assertTrue(rateLimited && cnt > 1 && cnt <= 30); + } + + @Test + public void testAdminUserExemptFromGettingRateLimited() throws InterruptedException { + authUser.setSuperuser(true); + authUser.setUserIdentifier("admin"); + + boolean rateLimited = false; + int cnt = 0; + for (; cnt <100; cnt++) { + rateLimited = !cache.checkRate(authUser, action); + if (rateLimited) { + break; + } + } + assertTrue(!rateLimited && cnt >= 99); + } + + @Test + public void testAuthenticatedUserGettingRateLimited() throws InterruptedException { + authUser.setSuperuser(false); + authUser.setUserIdentifier("authUser"); + authUser.setRateLimitTier(2); // 120 cals per hour - 1 added token every 30 seconds + boolean limited = false; + int cnt; + for (cnt = 0; cnt <200; cnt++) { + limited = !cache.checkRate(authUser, action); + if (limited) { + break; + } + } + assertTrue(limited && cnt == 120); + + for (cnt = 0; cnt <60; cnt++) { + Thread.sleep(1000);// wait for bucket to be replenished (check each second for 1 minute max) + limited = !cache.checkRate(authUser, action); + if (!limited) { + break; + } + } + assertTrue(!limited && cnt > 15, "cnt:" + cnt); + } + + @Test + public void testAuthenticatedUserWithRateLimitingOff() throws InterruptedException { + lenient().doReturn(RateLimitUtil.NO_LIMIT).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(1), anyInt()); + authUser.setSuperuser(false); + authUser.setUserIdentifier("user1"); + boolean rateLimited = false; + int cnt = 0; + for (; cnt <100; cnt++) { + rateLimited = !cache.checkRate(authUser, action); + if (rateLimited) { + break; + } + } + assertTrue(!rateLimited && cnt > 99); + } +} diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/RateLimitUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/RateLimitUtilTest.java new file mode 100644 index 00000000000..d51fe7471e3 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/cache/RateLimitUtilTest.java @@ -0,0 +1,95 @@ +package edu.harvard.iq.dataverse.cache; + +import edu.harvard.iq.dataverse.util.SystemConfig; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.lenient; + +@ExtendWith(MockitoExtension.class) +public class RateLimitUtilTest { + + @Mock + SystemConfig systemConfig; + + static final String settingJson = "{\n" + + " \"rateLimits\":[\n" + + " {\n" + + " \"tier\": 0,\n" + + " \"limitPerHour\": 10,\n" + + " \"actions\": [\n" + + " \"GetLatestPublishedDatasetVersionCommand\",\n" + + " \"GetPrivateUrlCommand\",\n" + + " \"GetDatasetCommand\",\n" + + " \"GetLatestAccessibleDatasetVersionCommand\"\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"tier\": 0,\n" + + " \"limitPerHour\": 1,\n" + + " \"actions\": [\n" + + " \"CreateGuestbookResponseCommand\",\n" + + " \"UpdateDatasetVersionCommand\",\n" + + " \"DestroyDatasetCommand\",\n" + + " \"DeleteDataFileCommand\",\n" + + " \"FinalizeDatasetPublicationCommand\",\n" + + " \"PublishDatasetCommand\"\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"tier\": 1,\n" + + " \"limitPerHour\": 30,\n" + + " \"actions\": [\n" + + " \"CreateGuestbookResponseCommand\",\n" + + " \"GetLatestPublishedDatasetVersionCommand\",\n" + + " \"GetPrivateUrlCommand\",\n" + + " \"GetDatasetCommand\",\n" + + " \"GetLatestAccessibleDatasetVersionCommand\",\n" + + " \"UpdateDatasetVersionCommand\",\n" + + " \"DestroyDatasetCommand\",\n" + + " \"DeleteDataFileCommand\",\n" + + " \"FinalizeDatasetPublicationCommand\",\n" + + " \"PublishDatasetCommand\"\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + static final String settingJsonBad = "{\n"; + + @BeforeEach + public void setup() { + lenient().doReturn(100).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(0), anyInt()); + lenient().doReturn(200).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(1), anyInt()); + lenient().doReturn(RateLimitUtil.NO_LIMIT).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(2), anyInt()); + RateLimitUtil.rateLimitMap.clear(); + RateLimitUtil.rateLimits.clear(); + } + @Test + public void testConfig() { + lenient().doReturn(settingJson).when(systemConfig).getRateLimitsJson(); + assertEquals(100, RateLimitUtil.getCapacityByTier(systemConfig, 0)); + assertEquals(200, RateLimitUtil.getCapacityByTier(systemConfig, 1)); + assertEquals(1, RateLimitUtil.getCapacityByTierAndAction(systemConfig, 0, "DestroyDatasetCommand")); + assertEquals(100, RateLimitUtil.getCapacityByTierAndAction(systemConfig, 0, "Default Limit")); + + assertEquals(30, RateLimitUtil.getCapacityByTierAndAction(systemConfig, 1, "GetLatestAccessibleDatasetVersionCommand")); + assertEquals(200, RateLimitUtil.getCapacityByTierAndAction(systemConfig, 1, "Default Limit")); + + assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacityByTierAndAction(systemConfig, 2, "Default No Limit")); + } + @Test + public void testBadJson() { + lenient().doReturn(settingJsonBad).when(systemConfig).getRateLimitsJson(); + assertEquals(100, RateLimitUtil.getCapacityByTier(systemConfig, 0)); + assertEquals(200, RateLimitUtil.getCapacityByTier(systemConfig, 1)); + assertEquals(100, RateLimitUtil.getCapacityByTierAndAction(systemConfig, 0, "GetLatestAccessibleDatasetVersionCommand")); + assertEquals(200, RateLimitUtil.getCapacityByTierAndAction(systemConfig, 1, "GetLatestAccessibleDatasetVersionCommand")); + assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacityByTierAndAction(systemConfig, 2, "GetLatestAccessibleDatasetVersionCommand")); + } +} From c657eb0a6d3424b12375f46d0245d4940af016fe Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Fri, 5 Jan 2024 11:12:28 -0500 Subject: [PATCH 472/689] fixing tests --- .../java/edu/harvard/iq/dataverse/api/AbstractApiBean.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 44629d5dd76..b7305a24f69 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -783,7 +783,10 @@ protected Response badRequest( String msg ) { protected Response forbidden( String msg ) { return error( Status.FORBIDDEN, msg ); } - protected Response rateLimited( String msg ) { return error( Status.TOO_MANY_REQUESTS, msg ); } + + protected Response rateLimited( String msg ) { + return error( Status.TOO_MANY_REQUESTS, msg ); + } protected Response conflict( String msg ) { return error( Status.CONFLICT, msg ); From f5e00706cd400619845fff4ef2e1f992e69fe56f Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Fri, 5 Jan 2024 14:01:44 -0500 Subject: [PATCH 473/689] fixing tests --- pom.xml | 6 +++++ .../dataverse/cache/CacheFactoryBeanTest.java | 25 +++++++++++++++---- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index de7e12cbfa6..7ae274bc42e 100644 --- a/pom.xml +++ b/pom.xml @@ -660,6 +660,12 @@ 3.9.0 test + + ai.grakn + redis-mock + 0.1.3 + test + diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index fa27ea6d4fd..eabc9cd4c2c 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -1,9 +1,12 @@ package edu.harvard.iq.dataverse.cache; +import ai.grakn.redismock.RedisServer; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.util.SystemConfig; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -11,6 +14,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.io.IOException; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -26,13 +30,14 @@ public class CacheFactoryBeanTest { CacheFactoryBean cache = new CacheFactoryBean(); AuthenticatedUser authUser = new AuthenticatedUser(); String action; + static RedisServer mockRedisServer; @BeforeEach - public void setup() { - lenient().doReturn("localhost").when(systemConfig).getRedisBaseHost(); - lenient().doReturn("6379").when(systemConfig).getRedisBasePort(); - lenient().doReturn("default").when(systemConfig).getRedisUser(); - lenient().doReturn("redis_secret").when(systemConfig).getRedisPassword(); + public void setup() throws IOException { + lenient().doReturn(mockRedisServer.getHost()).when(systemConfig).getRedisBaseHost(); + lenient().doReturn(String.valueOf(mockRedisServer.getBindPort())).when(systemConfig).getRedisBasePort(); + lenient().doReturn(null).when(systemConfig).getRedisUser(); + lenient().doReturn(null).when(systemConfig).getRedisPassword(); lenient().doReturn(30).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(0), anyInt()); lenient().doReturn(60).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(1), anyInt()); lenient().doReturn(120).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(2), anyInt()); @@ -41,6 +46,16 @@ public void setup() { authUser.setRateLimitTier(1); // reset to default action = "cmd-" + UUID.randomUUID(); } + @BeforeAll + public static void init() throws IOException { + mockRedisServer = RedisServer.newRedisServer(); + mockRedisServer.start(); + } + @AfterAll + public static void cleanup() { + if (mockRedisServer != null) + mockRedisServer.stop(); + } @Test public void testGuestUserGettingRateLimited() throws InterruptedException { User user = GuestUser.get(); From 1b0a55496bb0570326e2eb5b340a566325282b72 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Fri, 5 Jan 2024 14:05:21 -0500 Subject: [PATCH 474/689] fixing tests --- .../edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index eabc9cd4c2c..f2d14afc488 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -108,7 +108,7 @@ public void testAuthenticatedUserGettingRateLimited() throws InterruptedExceptio break; } } - assertTrue(!limited && cnt > 15, "cnt:" + cnt); + assertTrue(!limited); } @Test From a53462736e95859b464e62985ac77d7c951d4700 Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Tue, 9 Jan 2024 11:26:31 -0500 Subject: [PATCH 475/689] Update doc/release-notes/9356-rate-limiting.md Co-authored-by: Philip Durbin --- doc/release-notes/9356-rate-limiting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/9356-rate-limiting.md b/doc/release-notes/9356-rate-limiting.md index 6b40ed7498c..970a5fc8218 100644 --- a/doc/release-notes/9356-rate-limiting.md +++ b/doc/release-notes/9356-rate-limiting.md @@ -1,4 +1,4 @@ -Rate limiting has been added to prevent users from over taxing the system either deliberately or by runaway automated processes. Rate limiting can be configured on a tier level with tier 0 being reserved for guest users and tiers 1-any for authenticated users. Administrator/Superuser accounts are exempt from rate limiting. +Rate limiting has been added to prevent users from over taxing the system either deliberately or by runaway automated processes. Rate limiting can be configured on a tier level with tier 0 being reserved for guest users and tiers 1-any for authenticated users. Superuser accounts are exempt from rate limiting. Rate limits can be imposed on command APIs by configuring the tier, the command, and the hourly limit in the database. Two database settings configure the rate limiting. RateLimitingDefaultCapacityTiers is a comma separated list of default values for each tier. In the following example, the default for tier 0 (guest users) is set to 10,000 calls per command per hour and tier 1 (authenticated users) is set to 20,000 calls per command per hour. Tiers not specified in this setting will default to -1 (No Limit). From c80f74aceb502b68fdb681b177f31c478d1c1a0c Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 9 Jan 2024 11:48:10 -0500 Subject: [PATCH 476/689] fixing review comments --- src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java index 0f4537ddb99..dc9dbab097c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java @@ -1166,7 +1166,7 @@ public Integer getIntFromCSVStringOrDefault(final SettingsServiceBean.Key settin if (settingKey != null && !settingKey.equals("")) { String csv = settingsService.getValueForKey(settingKey, ""); try { - int[] values = Arrays.stream(csv.split(",")).mapToInt(Integer::parseInt).toArray(); + int[] values = csv.isEmpty() ? new int[0] : Arrays.stream(csv.split(",")).mapToInt(Integer::parseInt).toArray(); value = index > values.length ? defaultValue : Integer.valueOf(values[index]); } catch (NumberFormatException nfe) { logger.warning(nfe.getMessage()); From 12c15b58776b897fd398a70a6144ef2b343dcf0b Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 9 Jan 2024 17:31:24 -0500 Subject: [PATCH 477/689] review comment fixes --- doc/release-notes/9356-rate-limiting.md | 9 ++- .../source/installation/config.rst | 71 +++++++++++++++++++ ...l => V6.1.0.2__9356-add-rate-limiting.sql} | 0 3 files changed, 77 insertions(+), 3 deletions(-) rename src/main/resources/db/migration/{V6.1.0.1__9356-add-rate-limiting.sql => V6.1.0.2__9356-add-rate-limiting.sql} (100%) diff --git a/doc/release-notes/9356-rate-limiting.md b/doc/release-notes/9356-rate-limiting.md index 970a5fc8218..c89a87f83bd 100644 --- a/doc/release-notes/9356-rate-limiting.md +++ b/doc/release-notes/9356-rate-limiting.md @@ -1,6 +1,9 @@ -Rate limiting has been added to prevent users from over taxing the system either deliberately or by runaway automated processes. Rate limiting can be configured on a tier level with tier 0 being reserved for guest users and tiers 1-any for authenticated users. Superuser accounts are exempt from rate limiting. +Rate Limiting using Redis Server +The option to Rate limit has been added to prevent users from over taxing the system either deliberately or by runaway automated processes. Rate limiting can be configured on a tier level with tier 0 being reserved for guest users and tiers 1-any for authenticated users. Superuser accounts are exempt from rate limiting. Rate limits can be imposed on command APIs by configuring the tier, the command, and the hourly limit in the database. Two database settings configure the rate limiting. +Note: If either of these settings exist in the database rate limiting will be enabled. If neither setting exists rate limiting is disabled. + RateLimitingDefaultCapacityTiers is a comma separated list of default values for each tier. In the following example, the default for tier 0 (guest users) is set to 10,000 calls per command per hour and tier 1 (authenticated users) is set to 20,000 calls per command per hour. Tiers not specified in this setting will default to -1 (No Limit). curl http://localhost:8080/api/admin/settings/:RateLimitingDefaultCapacityTiers -X PUT -d '10000,20000' @@ -8,5 +11,5 @@ RateLimitingCapacityByTierAndAction is a Json object specifying the rate by tier In the following example, calls made by a guest user (tier 0) for API GetLatestPublishedDatasetVersionCommand is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '{"rateLimits":[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]}' -Rate Limiting cache is handled by a Redis server. The following system setting are used to configure access to the server: -DATAVERSE_REDIS_HOST; DATAVERSE_REDIS_POST; DATAVERSE_REDIS_USER; DATAVERSE_REDIS_PASSWORD. \ No newline at end of file +Rate Limiting cache is handled by a Redis server. The following environment variables are used to configure access to the server: +DATAVERSE_REDIS_HOST; DATAVERSE_REDIS_PORT; DATAVERSE_REDIS_USER; DATAVERSE_REDIS_PASSWORD. \ No newline at end of file diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 2baa2827250..79df6e76b28 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1373,6 +1373,77 @@ Before being moved there, on your machine, large file uploads via API will cause RAM and/or swap usage bursts. You might want to point this to a different location, restrict maximum size of it, and monitor for stale uploads. +.. _redis-cache-rate-limiting: + +Configure Your Dataverse Installation to use Redis for rate limiting +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Rate limiting has been added to prevent users from over taxing the system either deliberately or by runaway automated processes. +Rate limiting can be configured on a tier level with tier 0 being reserved for guest users and tiers 1-any for authenticated users. +Superuser accounts are exempt from rate limiting. +Rate limits can be imposed on command APIs by configuring the tier, the command, and the hourly limit in the database. +Two database settings configure the rate limiting. +Note: If either of these settings exist in the database rate limiting will be enabled. If neither setting exists rate limiting is disabled. + +- RateLimitingDefaultCapacityTiers is the number of calls allowed per hour if the specific command is not configured. The values represent the number of calls per hour per user for tiers 0,1,... + A value of -1 can be used to signify no rate limit. Also, by default, a tier not defined would receive a default of no limit. + ``curl http://localhost:8080/api/admin/settings/:RateLimitingDefaultCapacityTiers -X PUT -d '10000,20000'`` + +- RateLimitingCapacityByTierAndAction is a Json object specifying the rate by tier and a list of actions (commands). This allows for more control over the rate limit of individual API command calls. + In the following example, calls made by a guest user (tier 0) for API GetLatestPublishedDatasetVersionCommand is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. + curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '{"rateLimits":[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]}' + +.. code-block:: json + { + "rateLimits": [ + { + "tier": 0, + "limitPerHour": 10, + "actions": [ + "GetLatestPublishedDatasetVersionCommand", + "GetPrivateUrlCommand", + "GetDatasetCommand", + "GetLatestAccessibleDatasetVersionCommand" + ] + }, + { + "tier": 0, + "limitPerHour": 1, + "actions": [ + "CreateGuestbookResponseCommand", + "UpdateDatasetVersionCommand", + "DestroyDatasetCommand", + "DeleteDataFileCommand", + "FinalizeDatasetPublicationCommand", + "PublishDatasetCommand" + ] + }, + { + "tier": 1, + "limitPerHour": 30, + "actions": [ + "CreateGuestbookResponseCommand", + "GetLatestPublishedDatasetVersionCommand", + "GetPrivateUrlCommand", + "GetDatasetCommand", + "GetLatestAccessibleDatasetVersionCommand", + "UpdateDatasetVersionCommand", + "DestroyDatasetCommand", + "DeleteDataFileCommand", + "FinalizeDatasetPublicationCommand", + "PublishDatasetCommand" + ] + } + ] + } + +- Redis server configuration is handled through environment variables. The following environment variables are used to configure access to the server: + DATAVERSE_REDIS_HOST; DATAVERSE_REDIS_PORT; DATAVERSE_REDIS_USER; DATAVERSE_REDIS_PASSWORD. + Defaults for docker testing: + DATAVERSE_REDIS_HOST: "redis" + DATAVERSE_REDIS_PORT: "6379" + DATAVERSE_REDIS_USER: "default" + DATAVERSE_REDIS_PASSWORD: "redis_secret" .. _Branding Your Installation: diff --git a/src/main/resources/db/migration/V6.1.0.1__9356-add-rate-limiting.sql b/src/main/resources/db/migration/V6.1.0.2__9356-add-rate-limiting.sql similarity index 100% rename from src/main/resources/db/migration/V6.1.0.1__9356-add-rate-limiting.sql rename to src/main/resources/db/migration/V6.1.0.2__9356-add-rate-limiting.sql From a178929fb1964be56d9e27d83ed1663f246bb100 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 9 Jan 2024 17:38:23 -0500 Subject: [PATCH 478/689] review comment fixes --- .../source/installation/config.rst | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 79df6e76b28..46265160ed6 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1393,50 +1393,6 @@ Note: If either of these settings exist in the database rate limiting will be en In the following example, calls made by a guest user (tier 0) for API GetLatestPublishedDatasetVersionCommand is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '{"rateLimits":[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]}' -.. code-block:: json - { - "rateLimits": [ - { - "tier": 0, - "limitPerHour": 10, - "actions": [ - "GetLatestPublishedDatasetVersionCommand", - "GetPrivateUrlCommand", - "GetDatasetCommand", - "GetLatestAccessibleDatasetVersionCommand" - ] - }, - { - "tier": 0, - "limitPerHour": 1, - "actions": [ - "CreateGuestbookResponseCommand", - "UpdateDatasetVersionCommand", - "DestroyDatasetCommand", - "DeleteDataFileCommand", - "FinalizeDatasetPublicationCommand", - "PublishDatasetCommand" - ] - }, - { - "tier": 1, - "limitPerHour": 30, - "actions": [ - "CreateGuestbookResponseCommand", - "GetLatestPublishedDatasetVersionCommand", - "GetPrivateUrlCommand", - "GetDatasetCommand", - "GetLatestAccessibleDatasetVersionCommand", - "UpdateDatasetVersionCommand", - "DestroyDatasetCommand", - "DeleteDataFileCommand", - "FinalizeDatasetPublicationCommand", - "PublishDatasetCommand" - ] - } - ] - } - - Redis server configuration is handled through environment variables. The following environment variables are used to configure access to the server: DATAVERSE_REDIS_HOST; DATAVERSE_REDIS_PORT; DATAVERSE_REDIS_USER; DATAVERSE_REDIS_PASSWORD. Defaults for docker testing: From 4684384ed67bd60352bde3c359230fd96d8c4123 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 9 Jan 2024 17:41:43 -0500 Subject: [PATCH 479/689] review comment fixes --- doc/sphinx-guides/source/installation/config.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 46265160ed6..130af770a46 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1396,10 +1396,10 @@ Note: If either of these settings exist in the database rate limiting will be en - Redis server configuration is handled through environment variables. The following environment variables are used to configure access to the server: DATAVERSE_REDIS_HOST; DATAVERSE_REDIS_PORT; DATAVERSE_REDIS_USER; DATAVERSE_REDIS_PASSWORD. Defaults for docker testing: - DATAVERSE_REDIS_HOST: "redis" - DATAVERSE_REDIS_PORT: "6379" - DATAVERSE_REDIS_USER: "default" - DATAVERSE_REDIS_PASSWORD: "redis_secret" + DATAVERSE_REDIS_HOST: "redis" + DATAVERSE_REDIS_PORT: "6379" + DATAVERSE_REDIS_USER: "default" + DATAVERSE_REDIS_PASSWORD: "redis_secret" .. _Branding Your Installation: From 1e44206bb8c8edc46c874b815bb3074bb588b142 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Thu, 11 Jan 2024 11:55:10 -0500 Subject: [PATCH 480/689] fixes to get DatasetsIT to pass --- .../edu/harvard/iq/dataverse/UserServiceBean.java | 2 +- .../authorization/users/AuthenticatedUser.java | 8 ++++---- .../harvard/iq/dataverse/cache/RateLimitUtil.java | 2 +- .../edu/harvard/iq/dataverse/util/SystemConfig.java | 13 ++++++++----- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/UserServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/UserServiceBean.java index 50680b67cee..47aebb78a35 100644 --- a/src/main/java/edu/harvard/iq/dataverse/UserServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/UserServiceBean.java @@ -147,7 +147,7 @@ private AuthenticatedUser createAuthenticatedUserForView (Object[] dbRowValues, user.setMutedEmails(Type.tokenizeToSet((String) dbRowValues[15])); user.setMutedNotifications(Type.tokenizeToSet((String) dbRowValues[15])); - user.setRateLimitTier(Integer.valueOf((int)dbRowValues[16])); + user.setRateLimitTier((int)dbRowValues[16]); user.setRoles(roles); return user; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java b/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java index ff884926a1f..0ed036afc6b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java @@ -146,8 +146,8 @@ public class AuthenticatedUser implements User, Serializable { @Transient private Set mutedNotificationsSet = new HashSet<>(); - @Column(nullable=true) - private Integer rateLimitTier; + @Column + private int rateLimitTier; @PrePersist void prePersist() { @@ -400,10 +400,10 @@ public void setDeactivatedTime(Timestamp deactivatedTime) { this.deactivatedTime = deactivatedTime; } - public Integer getRateLimitTier() { + public int getRateLimitTier() { return rateLimitTier; } - public void setRateLimitTier(Integer rateLimitTier) { + public void setRateLimitTier(int rateLimitTier) { this.rateLimitTier = rateLimitTier; } diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java index b97773e0312..afc0b323da0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java @@ -23,7 +23,7 @@ public class RateLimitUtil { protected static final Map rateLimitMap = new HashMap<>(); public static final int NO_LIMIT = -1; - public static int getCapacityByTier(SystemConfig systemConfig, Integer tier) { + public static int getCapacityByTier(SystemConfig systemConfig, int tier) { return systemConfig.getIntFromCSVStringOrDefault(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers, tier, NO_LIMIT); } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java index dc9dbab097c..37eec5a1e80 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java @@ -1161,18 +1161,21 @@ public String getRateLimitsJson() { return settingsService.getValueForKey(SettingsServiceBean.Key.RateLimitingCapacityByTierAndAction, ""); } - public Integer getIntFromCSVStringOrDefault(final SettingsServiceBean.Key settingKey, final Integer index, final Integer defaultValue) { - Integer value = defaultValue; + public int getIntFromCSVStringOrDefault(final SettingsServiceBean.Key settingKey, int index, int defaultValue) { + int value = defaultValue; if (settingKey != null && !settingKey.equals("")) { String csv = settingsService.getValueForKey(settingKey, ""); try { - int[] values = csv.isEmpty() ? new int[0] : Arrays.stream(csv.split(",")).mapToInt(Integer::parseInt).toArray(); - value = index > values.length ? defaultValue : Integer.valueOf(values[index]); + if (!csv.isEmpty()) { + int[] values = Arrays.stream(csv.split(",")).mapToInt(Integer::parseInt).toArray(); + if (index < values.length) { + value = values[index]; + } + } } catch (NumberFormatException nfe) { logger.warning(nfe.getMessage()); } } - return value; } From 77074cc45de9e5ec8e5cdb3de404ab312bb358a7 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Thu, 18 Jan 2024 11:32:31 -0500 Subject: [PATCH 481/689] fix mock for redis tests --- .../edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index f2d14afc488..579da3f97a7 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -41,6 +41,7 @@ public void setup() throws IOException { lenient().doReturn(30).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(0), anyInt()); lenient().doReturn(60).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(1), anyInt()); lenient().doReturn(120).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(2), anyInt()); + lenient().doReturn("").when(systemConfig).getRateLimitsJson(); cache.init(); authUser.setRateLimitTier(1); // reset to default From 4cdba95b80f3492c024e9ab649fd7f7c35a224e2 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Thu, 18 Jan 2024 12:54:08 -0500 Subject: [PATCH 482/689] fix mock for redis tests --- .../dataverse/cache/CacheFactoryBeanTest.java | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index 579da3f97a7..769b7ce5859 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -31,6 +31,48 @@ public class CacheFactoryBeanTest { AuthenticatedUser authUser = new AuthenticatedUser(); String action; static RedisServer mockRedisServer; + static final String settingJson = "{\n" + + " \"rateLimits\":[\n" + + " {\n" + + " \"tier\": 0,\n" + + " \"limitPerHour\": 10,\n" + + " \"actions\": [\n" + + " \"GetLatestPublishedDatasetVersionCommand\",\n" + + " \"GetPrivateUrlCommand\",\n" + + " \"GetDatasetCommand\",\n" + + " \"GetLatestAccessibleDatasetVersionCommand\"\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"tier\": 0,\n" + + " \"limitPerHour\": 1,\n" + + " \"actions\": [\n" + + " \"CreateGuestbookResponseCommand\",\n" + + " \"UpdateDatasetVersionCommand\",\n" + + " \"DestroyDatasetCommand\",\n" + + " \"DeleteDataFileCommand\",\n" + + " \"FinalizeDatasetPublicationCommand\",\n" + + " \"PublishDatasetCommand\"\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"tier\": 1,\n" + + " \"limitPerHour\": 30,\n" + + " \"actions\": [\n" + + " \"CreateGuestbookResponseCommand\",\n" + + " \"GetLatestPublishedDatasetVersionCommand\",\n" + + " \"GetPrivateUrlCommand\",\n" + + " \"GetDatasetCommand\",\n" + + " \"GetLatestAccessibleDatasetVersionCommand\",\n" + + " \"UpdateDatasetVersionCommand\",\n" + + " \"DestroyDatasetCommand\",\n" + + " \"DeleteDataFileCommand\",\n" + + " \"FinalizeDatasetPublicationCommand\",\n" + + " \"PublishDatasetCommand\"\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; @BeforeEach public void setup() throws IOException { @@ -41,7 +83,7 @@ public void setup() throws IOException { lenient().doReturn(30).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(0), anyInt()); lenient().doReturn(60).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(1), anyInt()); lenient().doReturn(120).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(2), anyInt()); - lenient().doReturn("").when(systemConfig).getRateLimitsJson(); + lenient().doReturn(settingJson).when(systemConfig).getRateLimitsJson(); cache.init(); authUser.setRateLimitTier(1); // reset to default From 1b4f613c46fa1e51d9d50f32717fceb99979346a Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Wed, 24 Jan 2024 09:19:49 -0500 Subject: [PATCH 483/689] Update doc/sphinx-guides/source/installation/config.rst Co-authored-by: Philip Durbin --- doc/sphinx-guides/source/installation/config.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 130af770a46..ab22451a210 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1375,8 +1375,8 @@ Before being moved there, .. _redis-cache-rate-limiting: -Configure Your Dataverse Installation to use Redis for rate limiting -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Configure Your Dataverse Installation to use Redis for Rate Limiting +-------------------------------------------------------------------- Rate limiting has been added to prevent users from over taxing the system either deliberately or by runaway automated processes. Rate limiting can be configured on a tier level with tier 0 being reserved for guest users and tiers 1-any for authenticated users. From 2b603a60db0595adb6f4fc6fcda270f15a9215eb Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Wed, 24 Jan 2024 09:18:33 -0500 Subject: [PATCH 484/689] fixes from comments --- src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java | 2 +- .../edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java index afc0b323da0..ee76342dc17 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java @@ -41,7 +41,7 @@ public static boolean rateLimited(final JedisPool jedisPool, final String key, i } long currentTime = System.currentTimeMillis() / 60000L; // convert to minutes - int tokensPerMinute = (int)Math.ceil(capacityPerHour / 60.0); + double tokensPerMinute = (capacityPerHour / 60.0); // Get the last time this bucket was added to final String keyLastUpdate = String.format("%s:last_update",key); diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index 769b7ce5859..d6be3dcf831 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -76,7 +76,7 @@ public class CacheFactoryBeanTest { @BeforeEach public void setup() throws IOException { - lenient().doReturn(mockRedisServer.getHost()).when(systemConfig).getRedisBaseHost(); + lenient().doReturn("127.0.0.1").when(systemConfig).getRedisBaseHost(); lenient().doReturn(String.valueOf(mockRedisServer.getBindPort())).when(systemConfig).getRedisBasePort(); lenient().doReturn(null).when(systemConfig).getRedisUser(); lenient().doReturn(null).when(systemConfig).getRedisPassword(); From 13e301148c1bf7fe392c3a422daee3438025144b Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Wed, 24 Jan 2024 14:47:14 -0500 Subject: [PATCH 485/689] Update doc/release-notes/9356-rate-limiting.md Co-authored-by: Oliver Bertuch --- doc/release-notes/9356-rate-limiting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/9356-rate-limiting.md b/doc/release-notes/9356-rate-limiting.md index c89a87f83bd..75c47adeb4d 100644 --- a/doc/release-notes/9356-rate-limiting.md +++ b/doc/release-notes/9356-rate-limiting.md @@ -1,4 +1,4 @@ -Rate Limiting using Redis Server +## Rate Limiting using Redis Cache The option to Rate limit has been added to prevent users from over taxing the system either deliberately or by runaway automated processes. Rate limiting can be configured on a tier level with tier 0 being reserved for guest users and tiers 1-any for authenticated users. Superuser accounts are exempt from rate limiting. Rate limits can be imposed on command APIs by configuring the tier, the command, and the hourly limit in the database. Two database settings configure the rate limiting. From 0467f4c23176305c991c96286254fe00ae4f747e Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Wed, 24 Jan 2024 14:47:47 -0500 Subject: [PATCH 486/689] Update doc/release-notes/9356-rate-limiting.md Co-authored-by: Oliver Bertuch --- doc/release-notes/9356-rate-limiting.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/release-notes/9356-rate-limiting.md b/doc/release-notes/9356-rate-limiting.md index 75c47adeb4d..028ff442520 100644 --- a/doc/release-notes/9356-rate-limiting.md +++ b/doc/release-notes/9356-rate-limiting.md @@ -1,5 +1,7 @@ ## Rate Limiting using Redis Cache -The option to Rate limit has been added to prevent users from over taxing the system either deliberately or by runaway automated processes. Rate limiting can be configured on a tier level with tier 0 being reserved for guest users and tiers 1-any for authenticated users. Superuser accounts are exempt from rate limiting. +The option to rate limit has been added to prevent users from over taxing the system either deliberately or by runaway automated processes. +Rate limiting can be configured on a tier level with tier 0 being reserved for guest users and tiers 1-any for authenticated users. +Superuser accounts are exempt from rate limiting. Rate limits can be imposed on command APIs by configuring the tier, the command, and the hourly limit in the database. Two database settings configure the rate limiting. Note: If either of these settings exist in the database rate limiting will be enabled. If neither setting exists rate limiting is disabled. From 5253de8d777be30fdb1975af75330194eb49feb3 Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Wed, 24 Jan 2024 14:48:09 -0500 Subject: [PATCH 487/689] Update doc/release-notes/9356-rate-limiting.md Co-authored-by: Oliver Bertuch --- doc/release-notes/9356-rate-limiting.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/release-notes/9356-rate-limiting.md b/doc/release-notes/9356-rate-limiting.md index 028ff442520..5732a72fbef 100644 --- a/doc/release-notes/9356-rate-limiting.md +++ b/doc/release-notes/9356-rate-limiting.md @@ -4,7 +4,8 @@ Rate limiting can be configured on a tier level with tier 0 being reserved for g Superuser accounts are exempt from rate limiting. Rate limits can be imposed on command APIs by configuring the tier, the command, and the hourly limit in the database. Two database settings configure the rate limiting. -Note: If either of these settings exist in the database rate limiting will be enabled. If neither setting exists rate limiting is disabled. +Note: If either of these settings exist in the database rate limiting will be enabled. +If neither setting exists rate limiting is disabled. RateLimitingDefaultCapacityTiers is a comma separated list of default values for each tier. In the following example, the default for tier 0 (guest users) is set to 10,000 calls per command per hour and tier 1 (authenticated users) is set to 20,000 calls per command per hour. Tiers not specified in this setting will default to -1 (No Limit). curl http://localhost:8080/api/admin/settings/:RateLimitingDefaultCapacityTiers -X PUT -d '10000,20000' From 13fdd8837cf6c589df81250340c153407e7fc40c Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Wed, 24 Jan 2024 14:48:38 -0500 Subject: [PATCH 488/689] Update doc/release-notes/9356-rate-limiting.md Co-authored-by: Oliver Bertuch --- doc/release-notes/9356-rate-limiting.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/release-notes/9356-rate-limiting.md b/doc/release-notes/9356-rate-limiting.md index 5732a72fbef..d593bdacbbf 100644 --- a/doc/release-notes/9356-rate-limiting.md +++ b/doc/release-notes/9356-rate-limiting.md @@ -7,7 +7,9 @@ Two database settings configure the rate limiting. Note: If either of these settings exist in the database rate limiting will be enabled. If neither setting exists rate limiting is disabled. -RateLimitingDefaultCapacityTiers is a comma separated list of default values for each tier. In the following example, the default for tier 0 (guest users) is set to 10,000 calls per command per hour and tier 1 (authenticated users) is set to 20,000 calls per command per hour. Tiers not specified in this setting will default to -1 (No Limit). +`RateLimitingDefaultCapacityTiers` is a comma separated list of default values for each tier. +In the following example, the default for tier `0` (guest users) is set to 10,000 calls per command per hour and tier `1` (authenticated users) is set to 20,000 calls per command per hour. +Tiers not specified in this setting will default to `-1` (No Limit). curl http://localhost:8080/api/admin/settings/:RateLimitingDefaultCapacityTiers -X PUT -d '10000,20000' RateLimitingCapacityByTierAndAction is a Json object specifying the rate by tier and a list of actions (commands). This allows for more control over the rate limit of individual API command calls. From dd30c7b96f00b8aec5eb8a82cedb1594db530852 Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Wed, 24 Jan 2024 14:48:57 -0500 Subject: [PATCH 489/689] Update doc/release-notes/9356-rate-limiting.md Co-authored-by: Oliver Bertuch --- doc/release-notes/9356-rate-limiting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/9356-rate-limiting.md b/doc/release-notes/9356-rate-limiting.md index d593bdacbbf..6e060117db4 100644 --- a/doc/release-notes/9356-rate-limiting.md +++ b/doc/release-notes/9356-rate-limiting.md @@ -10,7 +10,7 @@ If neither setting exists rate limiting is disabled. `RateLimitingDefaultCapacityTiers` is a comma separated list of default values for each tier. In the following example, the default for tier `0` (guest users) is set to 10,000 calls per command per hour and tier `1` (authenticated users) is set to 20,000 calls per command per hour. Tiers not specified in this setting will default to `-1` (No Limit). -curl http://localhost:8080/api/admin/settings/:RateLimitingDefaultCapacityTiers -X PUT -d '10000,20000' +`curl http://localhost:8080/api/admin/settings/:RateLimitingDefaultCapacityTiers -X PUT -d '10000,20000'` RateLimitingCapacityByTierAndAction is a Json object specifying the rate by tier and a list of actions (commands). This allows for more control over the rate limit of individual API command calls. In the following example, calls made by a guest user (tier 0) for API GetLatestPublishedDatasetVersionCommand is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. From 23606a066dd07b4e565bc8a49ca1e66389aeaa31 Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Wed, 24 Jan 2024 14:49:14 -0500 Subject: [PATCH 490/689] Update doc/release-notes/9356-rate-limiting.md Co-authored-by: Oliver Bertuch --- doc/release-notes/9356-rate-limiting.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/release-notes/9356-rate-limiting.md b/doc/release-notes/9356-rate-limiting.md index 6e060117db4..d6653de2d12 100644 --- a/doc/release-notes/9356-rate-limiting.md +++ b/doc/release-notes/9356-rate-limiting.md @@ -12,7 +12,8 @@ In the following example, the default for tier `0` (guest users) is set to 10,00 Tiers not specified in this setting will default to `-1` (No Limit). `curl http://localhost:8080/api/admin/settings/:RateLimitingDefaultCapacityTiers -X PUT -d '10000,20000'` -RateLimitingCapacityByTierAndAction is a Json object specifying the rate by tier and a list of actions (commands). This allows for more control over the rate limit of individual API command calls. +`RateLimitingCapacityByTierAndAction` is a JSON object specifying the rate by tier and a list of actions (commands). +This allows for more control over the rate limit of individual API command calls. In the following example, calls made by a guest user (tier 0) for API GetLatestPublishedDatasetVersionCommand is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '{"rateLimits":[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]}' From 1bd25560146d4da58a405d981cc2cab509926350 Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Wed, 24 Jan 2024 14:49:36 -0500 Subject: [PATCH 491/689] Update doc/release-notes/9356-rate-limiting.md Co-authored-by: Oliver Bertuch --- doc/release-notes/9356-rate-limiting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/9356-rate-limiting.md b/doc/release-notes/9356-rate-limiting.md index d6653de2d12..427e2e846b7 100644 --- a/doc/release-notes/9356-rate-limiting.md +++ b/doc/release-notes/9356-rate-limiting.md @@ -14,7 +14,7 @@ Tiers not specified in this setting will default to `-1` (No Limit). `RateLimitingCapacityByTierAndAction` is a JSON object specifying the rate by tier and a list of actions (commands). This allows for more control over the rate limit of individual API command calls. -In the following example, calls made by a guest user (tier 0) for API GetLatestPublishedDatasetVersionCommand is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. +In the following example, calls made by a guest user (tier 0) for API `GetLatestPublishedDatasetVersionCommand` is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '{"rateLimits":[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]}' Rate Limiting cache is handled by a Redis server. The following environment variables are used to configure access to the server: From c04db0ab42801c035d45091e6f5cc24acc9f48ac Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Wed, 24 Jan 2024 14:49:53 -0500 Subject: [PATCH 492/689] Update doc/release-notes/9356-rate-limiting.md Co-authored-by: Oliver Bertuch --- doc/release-notes/9356-rate-limiting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/9356-rate-limiting.md b/doc/release-notes/9356-rate-limiting.md index 427e2e846b7..49fbfc2621c 100644 --- a/doc/release-notes/9356-rate-limiting.md +++ b/doc/release-notes/9356-rate-limiting.md @@ -15,7 +15,7 @@ Tiers not specified in this setting will default to `-1` (No Limit). `RateLimitingCapacityByTierAndAction` is a JSON object specifying the rate by tier and a list of actions (commands). This allows for more control over the rate limit of individual API command calls. In the following example, calls made by a guest user (tier 0) for API `GetLatestPublishedDatasetVersionCommand` is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. -curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '{"rateLimits":[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]}' +`curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '{"rateLimits":[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]}'` Rate Limiting cache is handled by a Redis server. The following environment variables are used to configure access to the server: DATAVERSE_REDIS_HOST; DATAVERSE_REDIS_PORT; DATAVERSE_REDIS_USER; DATAVERSE_REDIS_PASSWORD. \ No newline at end of file From 3dfc2a04ddfee83720355b60e8c5d6347fb424b7 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Wed, 24 Jan 2024 14:55:59 -0500 Subject: [PATCH 493/689] adding changes per pr comments --- .../iq/dataverse/cache/CacheFactoryBean.java | 8 ++--- .../iq/dataverse/cache/RateLimitSetting.java | 32 +++++++++---------- .../iq/dataverse/cache/RateLimitUtil.java | 22 ++++++------- 3 files changed, 29 insertions(+), 33 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java index 83ba7a418e4..8e163d21dfe 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java @@ -6,15 +6,15 @@ import edu.harvard.iq.dataverse.util.SystemConfig; import jakarta.annotation.PostConstruct; import jakarta.ejb.EJB; -import jakarta.ejb.Stateless; -import jakarta.inject.Named; +import jakarta.ejb.Singleton; +import jakarta.ejb.Startup; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; import java.util.logging.Logger; -@Stateless -@Named +@Singleton +@Startup public class CacheFactoryBean implements java.io.Serializable { private static final Logger logger = Logger.getLogger(CacheFactoryBean.class.getCanonicalName()); private static JedisPool jedisPool = null; diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitSetting.java b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitSetting.java index 14a4439bb56..752f9860127 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitSetting.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitSetting.java @@ -1,48 +1,46 @@ package edu.harvard.iq.dataverse.cache; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.json.bind.annotation.JsonbProperty; import java.util.ArrayList; import java.util.List; -@JsonInclude(JsonInclude.Include.NON_NULL) public class RateLimitSetting { - @JsonProperty("tier") + @JsonbProperty("tier") private int tier; - @JsonProperty("limitPerHour") + @JsonbProperty("limitPerHour") private int limitPerHour = RateLimitUtil.NO_LIMIT; - @JsonProperty("actions") - private List rateLimitActions = new ArrayList<>(); + @JsonbProperty("actions") + private List actions = new ArrayList<>(); private int defaultLimitPerHour; public RateLimitSetting() {} - @JsonProperty("tier") + @JsonbProperty("tier") public void setTier(int tier) { this.tier = tier; } - @JsonProperty("tier") + @JsonbProperty("tier") public int getTier() { return this.tier; } - @JsonProperty("limitPerHour") + @JsonbProperty("limitPerHour") public void setLimitPerHour(int limitPerHour) { this.limitPerHour = limitPerHour; } - @JsonProperty("limitPerHour") + @JsonbProperty("limitPerHour") public int getLimitPerHour() { return this.limitPerHour; } - @JsonProperty("actions") - public void setRateLimitActions(List rateLimitActions) { - this.rateLimitActions = rateLimitActions; + @JsonbProperty("actions") + public void setActions(List actions) { + this.actions = actions; } - @JsonProperty("actions") - public List getRateLimitActions() { - return this.rateLimitActions; + @JsonbProperty("actions") + public List getActions() { + return this.actions; } public void setDefaultLimit(int defaultLimitPerHour) { this.defaultLimitPerHour = defaultLimitPerHour; diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java index ee76342dc17..0bde961fa82 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java @@ -1,17 +1,16 @@ package edu.harvard.iq.dataverse.cache; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.SystemConfig; -import jakarta.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonObject; -import jakarta.json.JsonReader; +import jakarta.json.*; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import java.io.StringReader; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Logger; import static java.lang.Math.max; @@ -19,8 +18,9 @@ public class RateLimitUtil { private static final Logger logger = Logger.getLogger(RateLimitUtil.class.getCanonicalName()); - protected static final List rateLimits = new ArrayList<>(); - protected static final Map rateLimitMap = new HashMap<>(); + protected static final List rateLimits = new CopyOnWriteArrayList<>(); + protected static final Map rateLimitMap = new ConcurrentHashMap<>(); + private static final Gson gson = new Gson(); public static final int NO_LIMIT = -1; public static int getCapacityByTier(SystemConfig systemConfig, int tier) { @@ -81,21 +81,19 @@ private static void init(SystemConfig systemConfig) { rateLimits.forEach(r -> { r.setDefaultLimit(getCapacityByTier(systemConfig, r.getTier())); rateLimitMap.put(getMapKey(r.getTier()), r.getDefaultLimitPerHour()); - r.getRateLimitActions().forEach(a -> rateLimitMap.put(getMapKey(r.getTier(), a), r.getLimitPerHour())); + r.getActions().forEach(a -> rateLimitMap.put(getMapKey(r.getTier(), a), r.getLimitPerHour())); }); } private static void getRateLimitsFromJson(SystemConfig systemConfig) { - ObjectMapper mapper = new ObjectMapper(); String setting = systemConfig.getRateLimitsJson(); if (!setting.isEmpty()) { try { JsonReader jr = Json.createReader(new StringReader(setting)); JsonObject obj= jr.readObject(); JsonArray lst = obj.getJsonArray("rateLimits"); - - rateLimits.addAll(mapper.readValue(lst.toString(), - mapper.getTypeFactory().constructCollectionType(List.class, RateLimitSetting.class))); + rateLimits.addAll(gson.fromJson(String.valueOf(lst), + new ArrayList() {}.getClass().getGenericSuperclass())); } catch (Exception e) { logger.warning("Unable to parse Rate Limit Json" + ": " + e.getLocalizedMessage()); rateLimits.add(new RateLimitSetting()); // add a default entry to prevent re-initialization From 727cccf3c121118bc02977c3c7c681ee0df85ab8 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Fri, 26 Jan 2024 13:29:03 -0500 Subject: [PATCH 494/689] remove redis and replace with jcache hazelcast --- doc/release-notes/9356-rate-limiting.md | 5 +-- .../source/installation/config.rst | 14 ++----- docker-compose-dev.yml | 17 -------- pom.xml | 24 ++++++----- scripts/installer/default.config | 6 --- scripts/installer/install.py | 3 +- scripts/installer/installAppServer.py | 6 --- scripts/installer/interactive.config | 8 ---- .../iq/dataverse/cache/CacheFactoryBean.java | 30 +++++++++----- .../iq/dataverse/cache/RateLimitUtil.java | 38 ++++++------------ .../iq/dataverse/util/SystemConfig.java | 17 -------- .../dataverse/cache/CacheFactoryBeanTest.java | 40 +++++-------------- 12 files changed, 59 insertions(+), 149 deletions(-) diff --git a/doc/release-notes/9356-rate-limiting.md b/doc/release-notes/9356-rate-limiting.md index 49fbfc2621c..d7b9d2defcf 100644 --- a/doc/release-notes/9356-rate-limiting.md +++ b/doc/release-notes/9356-rate-limiting.md @@ -1,4 +1,4 @@ -## Rate Limiting using Redis Cache +## Rate Limiting using JCache (with Hazelcast as a provider) The option to rate limit has been added to prevent users from over taxing the system either deliberately or by runaway automated processes. Rate limiting can be configured on a tier level with tier 0 being reserved for guest users and tiers 1-any for authenticated users. Superuser accounts are exempt from rate limiting. @@ -16,6 +16,3 @@ Tiers not specified in this setting will default to `-1` (No Limit). This allows for more control over the rate limit of individual API command calls. In the following example, calls made by a guest user (tier 0) for API `GetLatestPublishedDatasetVersionCommand` is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. `curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '{"rateLimits":[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]}'` - -Rate Limiting cache is handled by a Redis server. The following environment variables are used to configure access to the server: -DATAVERSE_REDIS_HOST; DATAVERSE_REDIS_PORT; DATAVERSE_REDIS_USER; DATAVERSE_REDIS_PASSWORD. \ No newline at end of file diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index ab22451a210..c60953c66f5 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1373,10 +1373,10 @@ Before being moved there, on your machine, large file uploads via API will cause RAM and/or swap usage bursts. You might want to point this to a different location, restrict maximum size of it, and monitor for stale uploads. -.. _redis-cache-rate-limiting: +.. _cache-rate-limiting: -Configure Your Dataverse Installation to use Redis for Rate Limiting --------------------------------------------------------------------- +Configure Your Dataverse Installation to use JCache (with Hazelcast as a provider) for Rate Limiting +---------------------------------------------------------------------------------------------------- Rate limiting has been added to prevent users from over taxing the system either deliberately or by runaway automated processes. Rate limiting can be configured on a tier level with tier 0 being reserved for guest users and tiers 1-any for authenticated users. @@ -1393,14 +1393,6 @@ Note: If either of these settings exist in the database rate limiting will be en In the following example, calls made by a guest user (tier 0) for API GetLatestPublishedDatasetVersionCommand is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '{"rateLimits":[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]}' -- Redis server configuration is handled through environment variables. The following environment variables are used to configure access to the server: - DATAVERSE_REDIS_HOST; DATAVERSE_REDIS_PORT; DATAVERSE_REDIS_USER; DATAVERSE_REDIS_PASSWORD. - Defaults for docker testing: - DATAVERSE_REDIS_HOST: "redis" - DATAVERSE_REDIS_PORT: "6379" - DATAVERSE_REDIS_USER: "default" - DATAVERSE_REDIS_PASSWORD: "redis_secret" - .. _Branding Your Installation: Branding Your Installation diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index fcb13609c94..b4a7a510839 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -12,10 +12,6 @@ services: DATAVERSE_DB_HOST: postgres DATAVERSE_DB_PASSWORD: secret DATAVERSE_DB_USER: ${DATAVERSE_DB_USER} - DATAVERSE_REDIS_HOST: "redis" - DATAVERSE_REDIS_PORT: "6379" - DATAVERSE_REDIS_USER: "default" - DATAVERSE_REDIS_PASSWORD: "redis_secret" ENABLE_JDWP: "1" ENABLE_RELOAD: "1" SKIP_DEPLOY: "${SKIP_DEPLOY}" @@ -69,7 +65,6 @@ services: - dev_postgres - dev_solr - dev_dv_initializer - - redis_dev volumes: - ./docker-dev-volumes/app/data:/dv - ./docker-dev-volumes/app/secrets:/secrets @@ -237,18 +232,6 @@ services: MINIO_ROOT_USER: 4cc355_k3y MINIO_ROOT_PASSWORD: s3cr3t_4cc355_k3y command: server /data - - dev_redis: - container_name: "redis_dev" - hostname: "redis" - image: redis/redis-stack:latest - restart: always - ports: - - "6379:6379" - networks: - - dataverse - command: ["redis-server","--bind","redis","--port","6379","--requirepass","redis_secret" ] - networks: dataverse: driver: bridge diff --git a/pom.xml b/pom.xml index 7ae274bc42e..4a2bc13dbc7 100644 --- a/pom.xml +++ b/pom.xml @@ -542,13 +542,21 @@ dataverse-spi 2.0.0 - - redis.clients - jedis - 5.1.0 + javax.cache + cache-api + 1.1.1 + + + com.hazelcast + hazelcast + 5.3.6 + + + xerces + xercesImpl + 2.11.0 - org.junit.jupiter @@ -660,12 +668,6 @@ 3.9.0 test - - ai.grakn - redis-mock - 0.1.3 - test - diff --git a/scripts/installer/default.config b/scripts/installer/default.config index 2a29a1d5270..8647cd02416 100644 --- a/scripts/installer/default.config +++ b/scripts/installer/default.config @@ -32,9 +32,3 @@ DOI_USERNAME = dataciteuser DOI_PASSWORD = datacitepassword DOI_BASEURL = https://mds.test.datacite.org DOI_DATACITERESTAPIURL = https://api.test.datacite.org - -[redis] -REDIS_HOST = redis -REDIS_PORT = 6379 -REDIS_USER = default -REDIS_PASSWORD = redis_secret diff --git a/scripts/installer/install.py b/scripts/installer/install.py index 6d6003607bd..99316efb83b 100644 --- a/scripts/installer/install.py +++ b/scripts/installer/install.py @@ -100,8 +100,7 @@ "database", "rserve", "system", - "doi", - "redis"] + "doi"] # read pre-defined defaults: diff --git a/scripts/installer/installAppServer.py b/scripts/installer/installAppServer.py index faa5bf42341..03abc03b05e 100644 --- a/scripts/installer/installAppServer.py +++ b/scripts/installer/installAppServer.py @@ -29,12 +29,6 @@ def runAsadminScript(config): os.environ['DOI_USERNAME'] = config.get('doi','DOI_USERNAME') os.environ['DOI_PASSWORD'] = config.get('doi','DOI_PASSWORD') os.environ['DOI_DATACITERESTAPIURL'] = config.get('doi','DOI_DATACITERESTAPIURL') - - os.environ['REDIS_HOST'] = config.get('redis','REDIS_HOST') - os.environ['REDIS_PORT'] = config.get('redis','REDIS_PORT') - os.environ['REDIS_USER'] = config.get('redis','REDIS_USER') - os.environ['REDIS_PASS'] = config.get('redis','REDIS_PASSWORD') - mailServerEntry = config.get('system','MAIL_SERVER') try: diff --git a/scripts/installer/interactive.config b/scripts/installer/interactive.config index 9e0fafaa8b4..ef8110c554f 100644 --- a/scripts/installer/interactive.config +++ b/scripts/installer/interactive.config @@ -24,10 +24,6 @@ DOI_USERNAME = Datacite username DOI_PASSWORD = Datacite password DOI_BASEURL = Datacite URL DOI_DATACITERESTAPIURL = Datacite REST API URL -REDIS_HOST = Redis Server -REDIS_PORT = Redis Server Port -REDIS_USER = Redis User Name -REDIS_PASSWORD = Redis User Password [comments] HOST_DNS_ADDRESS = :(enter numeric IP address, if FQDN is unavailable) GLASSFISH_USER = :This user will be running the App. Server (Payara) service on your system.\n - If this is a dev. environment, this should be your own username; \n - In production, we suggest you create the account "dataverse", or use any other unprivileged user account\n: @@ -50,7 +46,3 @@ DOI_USERNAME = DataCite or EZID username. Only necessary for publishing / mintin DOI_PASSWORD = DataCite or EZID account password. DOI_BASEURL = DataCite or EZID URL. Probably https://mds.datacite.org DOI_DATACITERESTAPIURL = DataCite REST API URL (Make Data Count, /pids API). Probably https://api.datacite.org -REDIS_HOST = -REDIS_PORT = -REDIS_USER = -REDIS_PASSWORD = diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java index 8e163d21dfe..25bc20ec03d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java @@ -1,5 +1,8 @@ package edu.harvard.iq.dataverse.cache; +import com.hazelcast.config.Config; +import com.hazelcast.core.Hazelcast; +import com.hazelcast.core.HazelcastInstance; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.User; @@ -8,29 +11,34 @@ import jakarta.ejb.EJB; import jakarta.ejb.Singleton; import jakarta.ejb.Startup; -import redis.clients.jedis.JedisPool; -import redis.clients.jedis.JedisPoolConfig; import java.util.logging.Logger; +import java.util.Map; @Singleton @Startup public class CacheFactoryBean implements java.io.Serializable { private static final Logger logger = Logger.getLogger(CacheFactoryBean.class.getCanonicalName()); - private static JedisPool jedisPool = null; + private static HazelcastInstance hazelcastInstance = null; + private static Map rateLimitCache; @EJB SystemConfig systemConfig; + public final static String RATE_LIMIT_CACHE = "rateLimitCache"; + @PostConstruct public void init() { - logger.info("CacheFactoryBean.init Redis Host:Port " + systemConfig.getRedisBaseHost() + ":" + systemConfig.getRedisBasePort()); - jedisPool = new JedisPool(new JedisPoolConfig(), systemConfig.getRedisBaseHost(), Integer.valueOf(systemConfig.getRedisBasePort()), - systemConfig.getRedisUser(), systemConfig.getRedisPassword()); + if (hazelcastInstance == null) { + Config hazelcastConfig = new Config(); + hazelcastConfig.setClusterName("dataverse"); + hazelcastInstance = Hazelcast.newHazelcastInstance(hazelcastConfig); + rateLimitCache = hazelcastInstance.getMap(RATE_LIMIT_CACHE); + } } @Override protected void finalize() throws Throwable { - if (jedisPool != null) { - jedisPool.close(); + if (hazelcastInstance != null) { + hazelcastInstance.shutdown(); } super.finalize(); } @@ -53,8 +61,8 @@ public boolean checkRate(User user, String action) { // get the capacity, i.e. calls per hour, from config int capacity = (user instanceof AuthenticatedUser) ? - RateLimitUtil.getCapacityByTier(systemConfig, ((AuthenticatedUser) user).getRateLimitTier()) : - RateLimitUtil.getCapacityByTier(systemConfig, 0); - return (!RateLimitUtil.rateLimited(jedisPool, id.toString(), capacity)); + RateLimitUtil.getCapacityByTierAndAction(systemConfig, ((AuthenticatedUser) user).getRateLimitTier(), action) : + RateLimitUtil.getCapacityByTierAndAction(systemConfig, 0, action); + return (!RateLimitUtil.rateLimited(rateLimitCache, id.toString(), capacity)); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java index 0bde961fa82..0688e4536ee 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java @@ -3,9 +3,10 @@ import com.google.gson.Gson; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.SystemConfig; -import jakarta.json.*; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisPool; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; import java.io.StringReader; import java.util.*; @@ -23,42 +24,29 @@ public class RateLimitUtil { private static final Gson gson = new Gson(); public static final int NO_LIMIT = -1; - public static int getCapacityByTier(SystemConfig systemConfig, int tier) { + protected static int getCapacityByTier(SystemConfig systemConfig, int tier) { return systemConfig.getIntFromCSVStringOrDefault(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers, tier, NO_LIMIT); } - public static boolean rateLimited(final JedisPool jedisPool, final String key, int capacityPerHour) { + public static boolean rateLimited(final Map cache, final String key, int capacityPerHour) { if (capacityPerHour == NO_LIMIT) { return false; } - Jedis jedis; - try { - jedis = jedisPool.getResource(); - } catch (Exception e) { - // We can't rate limit if Redis is not reachable - logger.severe("RateLimitUtil.rateLimited jedisPool.getResource() " + e.getMessage()); - return false; - } - long currentTime = System.currentTimeMillis() / 60000L; // convert to minutes double tokensPerMinute = (capacityPerHour / 60.0); - // Get the last time this bucket was added to final String keyLastUpdate = String.format("%s:last_update",key); - long lastUpdate = longFromKey(jedis, keyLastUpdate); + long lastUpdate = longFromKey(cache, keyLastUpdate); long deltaTime = currentTime - lastUpdate; // Get the current number of tokens in the bucket - long tokens = longFromKey(jedis, key); + long tokens = longFromKey(cache, key); long tokensToAdd = (long) (deltaTime * tokensPerMinute); - if (tokensToAdd > 0) { // Don't update timestamp if we aren't adding any tokens to the bucket tokens = min(capacityPerHour, tokens + tokensToAdd); - jedis.set(keyLastUpdate, String.valueOf(currentTime)); + cache.put(keyLastUpdate, String.valueOf(currentTime)); } - // Update with any added tokens and decrement 1 token for this call if not rate limited (0 tokens) - jedis.set(key, String.valueOf(max(0, tokens-1))); - jedisPool.returnResource(jedis); + cache.put(key, String.valueOf(max(0, tokens-1))); return tokens < 1; } @@ -115,8 +103,8 @@ private static String getMapKey(Integer tier, String action) { return key.toString(); } - private static long longFromKey(Jedis r, String key) { - String l = r.get(key); - return l != null ? Long.parseLong(l) : 0L; + private static long longFromKey(Map cache, String key) { + Object l = cache.get(key); + return l != null ? Long.parseLong(String.valueOf(l)) : 0L; } } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java index 37eec5a1e80..9f4bd7c2e62 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java @@ -1178,21 +1178,4 @@ public int getIntFromCSVStringOrDefault(final SettingsServiceBean.Key settingKey } return value; } - - public String getRedisBaseHost() { - String saneDefault = "redis"; - return System.getProperty("DATAVERSE_REDIS_HOST",saneDefault); - } - public String getRedisBasePort() { - String saneDefault = "6379"; - return System.getProperty("DATAVERSE_REDIS_PORT",saneDefault); - } - public String getRedisUser() { - String saneDefault = "default"; - return System.getProperty("DATAVERSE_REDIS_USER",saneDefault); - } - public String getRedisPassword() { - String saneDefault = "redis_secret"; - return System.getProperty("DATAVERSE_REDIS_PASSWORD",saneDefault); - } } diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index d6be3dcf831..6241674dd7a 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -1,12 +1,9 @@ package edu.harvard.iq.dataverse.cache; -import ai.grakn.redismock.RedisServer; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.util.SystemConfig; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -27,10 +24,9 @@ public class CacheFactoryBeanTest { @Mock SystemConfig systemConfig; @InjectMocks - CacheFactoryBean cache = new CacheFactoryBean(); + static CacheFactoryBean cache = new CacheFactoryBean(); AuthenticatedUser authUser = new AuthenticatedUser(); String action; - static RedisServer mockRedisServer; static final String settingJson = "{\n" + " \"rateLimits\":[\n" + " {\n" + @@ -76,29 +72,17 @@ public class CacheFactoryBeanTest { @BeforeEach public void setup() throws IOException { - lenient().doReturn("127.0.0.1").when(systemConfig).getRedisBaseHost(); - lenient().doReturn(String.valueOf(mockRedisServer.getBindPort())).when(systemConfig).getRedisBasePort(); - lenient().doReturn(null).when(systemConfig).getRedisUser(); - lenient().doReturn(null).when(systemConfig).getRedisPassword(); lenient().doReturn(30).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(0), anyInt()); lenient().doReturn(60).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(1), anyInt()); lenient().doReturn(120).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(2), anyInt()); + lenient().doReturn(RateLimitUtil.NO_LIMIT).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(3), anyInt()); lenient().doReturn(settingJson).when(systemConfig).getRateLimitsJson(); cache.init(); authUser.setRateLimitTier(1); // reset to default action = "cmd-" + UUID.randomUUID(); } - @BeforeAll - public static void init() throws IOException { - mockRedisServer = RedisServer.newRedisServer(); - mockRedisServer.start(); - } - @AfterAll - public static void cleanup() { - if (mockRedisServer != null) - mockRedisServer.stop(); - } + @Test public void testGuestUserGettingRateLimited() throws InterruptedException { User user = GuestUser.get(); @@ -152,21 +136,15 @@ public void testAuthenticatedUserGettingRateLimited() throws InterruptedExceptio } } assertTrue(!limited); - } - @Test - public void testAuthenticatedUserWithRateLimitingOff() throws InterruptedException { - lenient().doReturn(RateLimitUtil.NO_LIMIT).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(1), anyInt()); - authUser.setSuperuser(false); - authUser.setUserIdentifier("user1"); - boolean rateLimited = false; - int cnt = 0; - for (; cnt <100; cnt++) { - rateLimited = !cache.checkRate(authUser, action); - if (rateLimited) { + // Now change the user's tier so it is no longer limited + authUser.setRateLimitTier(3); // tier 3 = no limit + for (cnt = 0; cnt <200; cnt++) { + limited = !cache.checkRate(authUser, action); + if (limited) { break; } } - assertTrue(!rateLimited && cnt > 99); + assertTrue(!limited && cnt == 200); } } From 58ea032a2bc76d06e496e3e5ca0239146febc00a Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Fri, 26 Jan 2024 16:47:05 -0500 Subject: [PATCH 495/689] adding cache tests --- .../iq/dataverse/cache/CacheFactoryBean.java | 32 +++++++++++++++++++ .../dataverse/cache/CacheFactoryBeanTest.java | 10 ++++++ 2 files changed, 42 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java index 25bc20ec03d..43c79b8c7b8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java @@ -65,4 +65,36 @@ public boolean checkRate(User user, String action) { RateLimitUtil.getCapacityByTierAndAction(systemConfig, 0, action); return (!RateLimitUtil.rateLimited(rateLimitCache, id.toString(), capacity)); } + + public long getCacheSize(String cacheName) { + long cacheSize = 0; + switch (cacheName) { + case RATE_LIMIT_CACHE: + cacheSize = rateLimitCache.size(); + break; + default: + break; + } + return cacheSize; + } + public Object getCacheValue(String cacheName, String key) { + Object cacheValue = null; + switch (cacheName) { + case RATE_LIMIT_CACHE: + cacheValue = rateLimitCache.containsKey(key) ? rateLimitCache.get(key) : ""; + break; + default: + break; + } + return cacheValue; + } + public void setCacheValue(String cacheName, String key, Object value) { + switch (cacheName) { + case RATE_LIMIT_CACHE: + rateLimitCache.put(key, (String) value); + break; + default: + break; + } + } } diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index 6241674dd7a..f65da27deb6 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -81,6 +81,16 @@ public void setup() throws IOException { cache.init(); authUser.setRateLimitTier(1); // reset to default action = "cmd-" + UUID.randomUUID(); + + // testing cache implementation and code coverage + final String cacheKey = "CacheTestKey" + UUID.randomUUID(); + final String cacheValue = "CacheTestValue" + UUID.randomUUID(); + long cacheSize = cache.getCacheSize(cache.RATE_LIMIT_CACHE); + System.out.println("Cache Size : " + cacheSize); + cache.setCacheValue(cache.RATE_LIMIT_CACHE, cacheKey,cacheValue); + assertTrue(cache.getCacheSize(cache.RATE_LIMIT_CACHE) > cacheSize); + Object cacheValueObj = cache.getCacheValue(cache.RATE_LIMIT_CACHE, cacheKey); + assertTrue(cacheValueObj != null && cacheValue.equalsIgnoreCase((String) cacheValueObj)); } @Test From f7f96646f23f7ba4fdba1f18372efa523bc716d1 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Mon, 29 Jan 2024 09:50:46 -0500 Subject: [PATCH 496/689] fixing unit tests --- .../iq/dataverse/cache/CacheFactoryBean.java | 9 +++---- .../iq/dataverse/cache/RateLimitUtil.java | 13 ++++++++-- .../dataverse/cache/CacheFactoryBeanTest.java | 24 +++++++++---------- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java index 43c79b8c7b8..d39c4686bfe 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java @@ -53,17 +53,14 @@ public boolean checkRate(User user, String action) { if (user != null && user.isSuperuser()) { return true; }; - StringBuffer id = new StringBuffer(); - id.append(user != null ? user.getIdentifier() : GuestUser.get().getIdentifier()); - if (action != null) { - id.append(":").append(action); - } + + String cacheKey = RateLimitUtil.generateCacheKey(user, action); // get the capacity, i.e. calls per hour, from config int capacity = (user instanceof AuthenticatedUser) ? RateLimitUtil.getCapacityByTierAndAction(systemConfig, ((AuthenticatedUser) user).getRateLimitTier(), action) : RateLimitUtil.getCapacityByTierAndAction(systemConfig, 0, action); - return (!RateLimitUtil.rateLimited(rateLimitCache, id.toString(), capacity)); + return (!RateLimitUtil.rateLimited(rateLimitCache, cacheKey, capacity)); } public long getCacheSize(String cacheName) { diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java index 0688e4536ee..c60f2bb8e0e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java @@ -1,6 +1,8 @@ package edu.harvard.iq.dataverse.cache; import com.google.gson.Gson; +import edu.harvard.iq.dataverse.authorization.users.GuestUser; +import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.SystemConfig; import jakarta.json.Json; @@ -28,6 +30,14 @@ protected static int getCapacityByTier(SystemConfig systemConfig, int tier) { return systemConfig.getIntFromCSVStringOrDefault(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers, tier, NO_LIMIT); } + public static String generateCacheKey(final User user, final String action) { + StringBuffer id = new StringBuffer(); + id.append(user != null ? user.getIdentifier() : GuestUser.get().getIdentifier()); + if (action != null) { + id.append(":").append(action); + } + return id.toString(); + } public static boolean rateLimited(final Map cache, final String key, int capacityPerHour) { if (capacityPerHour == NO_LIMIT) { return false; @@ -83,9 +93,8 @@ private static void getRateLimitsFromJson(SystemConfig systemConfig) { rateLimits.addAll(gson.fromJson(String.valueOf(lst), new ArrayList() {}.getClass().getGenericSuperclass())); } catch (Exception e) { - logger.warning("Unable to parse Rate Limit Json" + ": " + e.getLocalizedMessage()); + logger.warning("Unable to parse Rate Limit Json: " + e.getLocalizedMessage() + " Json:(" + setting + ")"); rateLimits.add(new RateLimitSetting()); // add a default entry to prevent re-initialization - e.printStackTrace(); } } } diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index f65da27deb6..df57948980d 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -104,7 +104,7 @@ public void testGuestUserGettingRateLimited() throws InterruptedException { break; } } - assertTrue(rateLimited && cnt > 1 && cnt <= 30); + assertTrue(rateLimited && cnt > 1 && cnt <= 30, "rateLimited:"+rateLimited + " cnt:"+cnt); } @Test @@ -120,7 +120,7 @@ public void testAdminUserExemptFromGettingRateLimited() throws InterruptedExcept break; } } - assertTrue(!rateLimited && cnt >= 99); + assertTrue(!rateLimited && cnt >= 99, "rateLimited:"+rateLimited + " cnt:"+cnt); } @Test @@ -128,33 +128,33 @@ public void testAuthenticatedUserGettingRateLimited() throws InterruptedExceptio authUser.setSuperuser(false); authUser.setUserIdentifier("authUser"); authUser.setRateLimitTier(2); // 120 cals per hour - 1 added token every 30 seconds - boolean limited = false; + boolean rateLimited = false; int cnt; for (cnt = 0; cnt <200; cnt++) { - limited = !cache.checkRate(authUser, action); - if (limited) { + rateLimited = !cache.checkRate(authUser, action); + if (rateLimited) { break; } } - assertTrue(limited && cnt == 120); + assertTrue(rateLimited && cnt == 120, "rateLimited:"+rateLimited + " cnt:"+cnt); for (cnt = 0; cnt <60; cnt++) { Thread.sleep(1000);// wait for bucket to be replenished (check each second for 1 minute max) - limited = !cache.checkRate(authUser, action); - if (!limited) { + rateLimited = !cache.checkRate(authUser, action); + if (!rateLimited) { break; } } - assertTrue(!limited); + assertTrue(!rateLimited, "rateLimited:"+rateLimited + " cnt:"+cnt); // Now change the user's tier so it is no longer limited authUser.setRateLimitTier(3); // tier 3 = no limit for (cnt = 0; cnt <200; cnt++) { - limited = !cache.checkRate(authUser, action); - if (limited) { + rateLimited = !cache.checkRate(authUser, action); + if (rateLimited) { break; } } - assertTrue(!limited && cnt == 200); + assertTrue(!rateLimited && cnt == 200, "rateLimited:"+rateLimited + " cnt:"+cnt); } } From dbb774b0b146d5c7ea4f3aea8ad5108e49b63ab0 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Mon, 29 Jan 2024 10:39:12 -0500 Subject: [PATCH 497/689] fixing unit tests --- .../iq/dataverse/cache/CacheFactoryBean.java | 18 ++++++---------- .../iq/dataverse/cache/RateLimitUtil.java | 17 ++++++++++++--- .../dataverse/cache/CacheFactoryBeanTest.java | 10 ++++----- .../iq/dataverse/cache/RateLimitUtilTest.java | 21 +++++++++++++++++++ 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java index d39c4686bfe..a1caa0379e0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java @@ -3,8 +3,6 @@ import com.hazelcast.config.Config; import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; -import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; -import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.util.SystemConfig; import jakarta.annotation.PostConstruct; @@ -50,17 +48,13 @@ protected void finalize() throws Throwable { * @return true if user is superuser or rate not limited */ public boolean checkRate(User user, String action) { - if (user != null && user.isSuperuser()) { + int capacity = RateLimitUtil.getCapacity(systemConfig, user, action); + if (capacity == RateLimitUtil.NO_LIMIT) { return true; - }; - - String cacheKey = RateLimitUtil.generateCacheKey(user, action); - - // get the capacity, i.e. calls per hour, from config - int capacity = (user instanceof AuthenticatedUser) ? - RateLimitUtil.getCapacityByTierAndAction(systemConfig, ((AuthenticatedUser) user).getRateLimitTier(), action) : - RateLimitUtil.getCapacityByTierAndAction(systemConfig, 0, action); - return (!RateLimitUtil.rateLimited(rateLimitCache, cacheKey, capacity)); + } else { + String cacheKey = RateLimitUtil.generateCacheKey(user, action); + return (!RateLimitUtil.rateLimited(rateLimitCache, cacheKey, capacity)); + } } public long getCacheSize(String cacheName) { diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java index c60f2bb8e0e..a5bff19599c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java @@ -1,10 +1,12 @@ package edu.harvard.iq.dataverse.cache; import com.google.gson.Gson; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.SystemConfig; +import jakarta.ejb.EJB; import jakarta.json.Json; import jakarta.json.JsonArray; import jakarta.json.JsonObject; @@ -30,7 +32,7 @@ protected static int getCapacityByTier(SystemConfig systemConfig, int tier) { return systemConfig.getIntFromCSVStringOrDefault(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers, tier, NO_LIMIT); } - public static String generateCacheKey(final User user, final String action) { + protected static String generateCacheKey(final User user, final String action) { StringBuffer id = new StringBuffer(); id.append(user != null ? user.getIdentifier() : GuestUser.get().getIdentifier()); if (action != null) { @@ -38,7 +40,16 @@ public static String generateCacheKey(final User user, final String action) { } return id.toString(); } - public static boolean rateLimited(final Map cache, final String key, int capacityPerHour) { + protected static int getCapacity(SystemConfig systemConfig, User user, String action) { + if (user != null && user.isSuperuser()) { + return NO_LIMIT; + }; + // get the capacity, i.e. calls per hour, from config + return (user instanceof AuthenticatedUser) ? + RateLimitUtil.getCapacityByTierAndAction(systemConfig, ((AuthenticatedUser) user).getRateLimitTier(), action) : + RateLimitUtil.getCapacityByTierAndAction(systemConfig, 0, action); + } + protected static boolean rateLimited(final Map cache, final String key, int capacityPerHour) { if (capacityPerHour == NO_LIMIT) { return false; } @@ -60,7 +71,7 @@ public static boolean rateLimited(final Map cache, final String return tokens < 1; } - public static int getCapacityByTierAndAction(SystemConfig systemConfig, Integer tier, String action) { + protected static int getCapacityByTierAndAction(SystemConfig systemConfig, Integer tier, String action) { if (rateLimits.isEmpty()) { init(systemConfig); } diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index df57948980d..e7b98d84908 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -26,7 +26,6 @@ public class CacheFactoryBeanTest { @InjectMocks static CacheFactoryBean cache = new CacheFactoryBean(); AuthenticatedUser authUser = new AuthenticatedUser(); - String action; static final String settingJson = "{\n" + " \"rateLimits\":[\n" + " {\n" + @@ -80,13 +79,11 @@ public void setup() throws IOException { cache.init(); authUser.setRateLimitTier(1); // reset to default - action = "cmd-" + UUID.randomUUID(); // testing cache implementation and code coverage final String cacheKey = "CacheTestKey" + UUID.randomUUID(); final String cacheValue = "CacheTestValue" + UUID.randomUUID(); long cacheSize = cache.getCacheSize(cache.RATE_LIMIT_CACHE); - System.out.println("Cache Size : " + cacheSize); cache.setCacheValue(cache.RATE_LIMIT_CACHE, cacheKey,cacheValue); assertTrue(cache.getCacheSize(cache.RATE_LIMIT_CACHE) > cacheSize); Object cacheValueObj = cache.getCacheValue(cache.RATE_LIMIT_CACHE, cacheKey); @@ -96,6 +93,7 @@ public void setup() throws IOException { @Test public void testGuestUserGettingRateLimited() throws InterruptedException { User user = GuestUser.get(); + String action = "cmd-" + UUID.randomUUID(); boolean rateLimited = false; int cnt = 0; for (; cnt <100; cnt++) { @@ -104,6 +102,7 @@ public void testGuestUserGettingRateLimited() throws InterruptedException { break; } } + assertTrue(cache.getCacheSize(cache.RATE_LIMIT_CACHE) > 0); assertTrue(rateLimited && cnt > 1 && cnt <= 30, "rateLimited:"+rateLimited + " cnt:"+cnt); } @@ -111,7 +110,7 @@ public void testGuestUserGettingRateLimited() throws InterruptedException { public void testAdminUserExemptFromGettingRateLimited() throws InterruptedException { authUser.setSuperuser(true); authUser.setUserIdentifier("admin"); - + String action = "cmd-" + UUID.randomUUID(); boolean rateLimited = false; int cnt = 0; for (; cnt <100; cnt++) { @@ -128,6 +127,7 @@ public void testAuthenticatedUserGettingRateLimited() throws InterruptedExceptio authUser.setSuperuser(false); authUser.setUserIdentifier("authUser"); authUser.setRateLimitTier(2); // 120 cals per hour - 1 added token every 30 seconds + String action = "cmd-" + UUID.randomUUID(); boolean rateLimited = false; int cnt; for (cnt = 0; cnt <200; cnt++) { @@ -147,7 +147,7 @@ public void testAuthenticatedUserGettingRateLimited() throws InterruptedExceptio } assertTrue(!rateLimited, "rateLimited:"+rateLimited + " cnt:"+cnt); - // Now change the user's tier so it is no longer limited + // Now change the user's tier, so it is no longer limited authUser.setRateLimitTier(3); // tier 3 = no limit for (cnt = 0; cnt <200; cnt++) { rateLimited = !cache.checkRate(authUser, action); diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/RateLimitUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/RateLimitUtilTest.java index d51fe7471e3..b2b7434cc3c 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/RateLimitUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/RateLimitUtilTest.java @@ -1,5 +1,8 @@ package edu.harvard.iq.dataverse.cache; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.authorization.users.GuestUser; +import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.util.SystemConfig; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -92,4 +95,22 @@ public void testBadJson() { assertEquals(200, RateLimitUtil.getCapacityByTierAndAction(systemConfig, 1, "GetLatestAccessibleDatasetVersionCommand")); assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacityByTierAndAction(systemConfig, 2, "GetLatestAccessibleDatasetVersionCommand")); } + + @Test + public void testGenerateCacheKey() { + User user = GuestUser.get(); + assertEquals(RateLimitUtil.generateCacheKey(user,"action1"), ":guest:action1"); + } + @Test + public void testGetCapacity() { + lenient().doReturn(settingJson).when(systemConfig).getRateLimitsJson(); + GuestUser guestUser = GuestUser.get(); + assertEquals(10, RateLimitUtil.getCapacity(systemConfig, guestUser, "GetPrivateUrlCommand")); + + AuthenticatedUser authUser = new AuthenticatedUser(); + authUser.setRateLimitTier(1); + assertEquals(30, RateLimitUtil.getCapacity(systemConfig, authUser, "GetPrivateUrlCommand")); + authUser.setSuperuser(true); + assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacity(systemConfig, authUser, "GetPrivateUrlCommand")); + } } From b489ec87d970a49dbb72a30f974609c81806824b Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Mon, 29 Jan 2024 11:03:53 -0500 Subject: [PATCH 498/689] fixing unit tests --- .../edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index e7b98d84908..488c4afdd19 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -89,7 +89,7 @@ public void setup() throws IOException { Object cacheValueObj = cache.getCacheValue(cache.RATE_LIMIT_CACHE, cacheKey); assertTrue(cacheValueObj != null && cacheValue.equalsIgnoreCase((String) cacheValueObj)); } - +/* @Test public void testGuestUserGettingRateLimited() throws InterruptedException { User user = GuestUser.get(); @@ -105,7 +105,7 @@ public void testGuestUserGettingRateLimited() throws InterruptedException { assertTrue(cache.getCacheSize(cache.RATE_LIMIT_CACHE) > 0); assertTrue(rateLimited && cnt > 1 && cnt <= 30, "rateLimited:"+rateLimited + " cnt:"+cnt); } - +*/ @Test public void testAdminUserExemptFromGettingRateLimited() throws InterruptedException { authUser.setSuperuser(true); From 700e7991226c25bf608c737825e393977a073df9 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Mon, 29 Jan 2024 11:37:36 -0500 Subject: [PATCH 499/689] fixing unit tests --- .../harvard/iq/dataverse/cache/CacheFactoryBeanTest.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index 488c4afdd19..88704840923 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -89,7 +89,7 @@ public void setup() throws IOException { Object cacheValueObj = cache.getCacheValue(cache.RATE_LIMIT_CACHE, cacheKey); assertTrue(cacheValueObj != null && cacheValue.equalsIgnoreCase((String) cacheValueObj)); } -/* + @Test public void testGuestUserGettingRateLimited() throws InterruptedException { User user = GuestUser.get(); @@ -102,10 +102,15 @@ public void testGuestUserGettingRateLimited() throws InterruptedException { break; } } + String key = RateLimitUtil.generateCacheKey(user,action); + String value = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, key)); + String keyLastUpdate = String.format("%s:last_update",key); + String lastUpdate = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, keyLastUpdate)); + System.out.println(">>> key/value/lastUpdate /" + key + "/" + value + "/" + lastUpdate); assertTrue(cache.getCacheSize(cache.RATE_LIMIT_CACHE) > 0); assertTrue(rateLimited && cnt > 1 && cnt <= 30, "rateLimited:"+rateLimited + " cnt:"+cnt); } -*/ + @Test public void testAdminUserExemptFromGettingRateLimited() throws InterruptedException { authUser.setSuperuser(true); From 5a7d3002dcecc60dddf44de730b7a1a3d8cd14a5 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Mon, 29 Jan 2024 12:05:21 -0500 Subject: [PATCH 500/689] fixing unit tests --- .../dataverse/cache/CacheFactoryBeanTest.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index 88704840923..1918c7b6743 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -94,6 +94,13 @@ public void setup() throws IOException { public void testGuestUserGettingRateLimited() throws InterruptedException { User user = GuestUser.get(); String action = "cmd-" + UUID.randomUUID(); + + String key = RateLimitUtil.generateCacheKey(user,action); + String value = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, key)); + String keyLastUpdate = String.format("%s:last_update",key); + String lastUpdate = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, keyLastUpdate)); + System.out.println(">>> key/value/lastUpdate /" + key + "/" + value + "/" + lastUpdate); + boolean rateLimited = false; int cnt = 0; for (; cnt <100; cnt++) { @@ -101,11 +108,15 @@ public void testGuestUserGettingRateLimited() throws InterruptedException { if (rateLimited) { break; } + if (cnt == 10) { + value = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, key)); + lastUpdate = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, keyLastUpdate)); + System.out.println(">>> key/value/lastUpdate /" + key + "/" + value + "/" + lastUpdate); + } } - String key = RateLimitUtil.generateCacheKey(user,action); - String value = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, key)); - String keyLastUpdate = String.format("%s:last_update",key); - String lastUpdate = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, keyLastUpdate)); + + value = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, key)); + lastUpdate = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, keyLastUpdate)); System.out.println(">>> key/value/lastUpdate /" + key + "/" + value + "/" + lastUpdate); assertTrue(cache.getCacheSize(cache.RATE_LIMIT_CACHE) > 0); assertTrue(rateLimited && cnt > 1 && cnt <= 30, "rateLimited:"+rateLimited + " cnt:"+cnt); From e2b5fe85991e035824748ba16fba547781e89999 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Mon, 29 Jan 2024 14:37:36 -0500 Subject: [PATCH 501/689] fixing unit tests --- .../iq/dataverse/cache/RateLimitUtil.java | 7 ++-- .../dataverse/cache/CacheFactoryBeanTest.java | 36 ++++++++++--------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java index a5bff19599c..73de0fe5528 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java @@ -6,7 +6,6 @@ import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.SystemConfig; -import jakarta.ejb.EJB; import jakarta.json.Json; import jakarta.json.JsonArray; import jakarta.json.JsonObject; @@ -96,7 +95,7 @@ private static void init(SystemConfig systemConfig) { private static void getRateLimitsFromJson(SystemConfig systemConfig) { String setting = systemConfig.getRateLimitsJson(); - if (!setting.isEmpty()) { + if (!setting.isEmpty() && rateLimits.isEmpty()) { try { JsonReader jr = Json.createReader(new StringReader(setting)); JsonObject obj= jr.readObject(); @@ -110,11 +109,11 @@ private static void getRateLimitsFromJson(SystemConfig systemConfig) { } } - private static String getMapKey(Integer tier) { + private static String getMapKey(int tier) { return getMapKey(tier, null); } - private static String getMapKey(Integer tier, String action) { + private static String getMapKey(int tier, String action) { StringBuffer key = new StringBuffer(); key.append(tier).append(":"); if (action != null) { diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index 1918c7b6743..e3d334d4623 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -2,7 +2,6 @@ import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; -import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.util.SystemConfig; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -10,15 +9,18 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import java.io.IOException; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.doReturn; @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) public class CacheFactoryBeanTest { @Mock @@ -26,6 +28,7 @@ public class CacheFactoryBeanTest { @InjectMocks static CacheFactoryBean cache = new CacheFactoryBean(); AuthenticatedUser authUser = new AuthenticatedUser(); + GuestUser guestUser = GuestUser.get(); static final String settingJson = "{\n" + " \"rateLimits\":[\n" + " {\n" + @@ -71,13 +74,13 @@ public class CacheFactoryBeanTest { @BeforeEach public void setup() throws IOException { - lenient().doReturn(30).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(0), anyInt()); - lenient().doReturn(60).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(1), anyInt()); - lenient().doReturn(120).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(2), anyInt()); - lenient().doReturn(RateLimitUtil.NO_LIMIT).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(3), anyInt()); - lenient().doReturn(settingJson).when(systemConfig).getRateLimitsJson(); + doReturn(30).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(0), anyInt()); + doReturn(60).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(1), anyInt()); + doReturn(120).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(2), anyInt()); + doReturn(RateLimitUtil.NO_LIMIT).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(3), anyInt()); + doReturn(settingJson).when(systemConfig).getRateLimitsJson(); - cache.init(); + cache.init(); // PostConstruct authUser.setRateLimitTier(1); // reset to default // testing cache implementation and code coverage @@ -91,39 +94,38 @@ public void setup() throws IOException { } @Test - public void testGuestUserGettingRateLimited() throws InterruptedException { - User user = GuestUser.get(); + public void testGuestUserGettingRateLimited() { String action = "cmd-" + UUID.randomUUID(); - String key = RateLimitUtil.generateCacheKey(user,action); + String key = RateLimitUtil.generateCacheKey(guestUser,action); String value = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, key)); String keyLastUpdate = String.format("%s:last_update",key); String lastUpdate = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, keyLastUpdate)); - System.out.println(">>> key/value/lastUpdate /" + key + "/" + value + "/" + lastUpdate); + System.out.println(">>> key|value|lastUpdate |" + key + "|" + value + "|" + lastUpdate); boolean rateLimited = false; int cnt = 0; for (; cnt <100; cnt++) { - rateLimited = !cache.checkRate(user, action); + rateLimited = !cache.checkRate(guestUser, action); if (rateLimited) { break; } - if (cnt == 10) { + if (cnt % 10 == 0) { value = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, key)); lastUpdate = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, keyLastUpdate)); - System.out.println(">>> key/value/lastUpdate /" + key + "/" + value + "/" + lastUpdate); + System.out.println(cnt + " key|value|lastUpdate |" + key + "|" + value + "|" + lastUpdate); } } value = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, key)); lastUpdate = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, keyLastUpdate)); - System.out.println(">>> key/value/lastUpdate /" + key + "/" + value + "/" + lastUpdate); + System.out.println(cnt + " key|value|lastUpdate |" + key + "|" + value + "|" + lastUpdate); assertTrue(cache.getCacheSize(cache.RATE_LIMIT_CACHE) > 0); assertTrue(rateLimited && cnt > 1 && cnt <= 30, "rateLimited:"+rateLimited + " cnt:"+cnt); } @Test - public void testAdminUserExemptFromGettingRateLimited() throws InterruptedException { + public void testAdminUserExemptFromGettingRateLimited() { authUser.setSuperuser(true); authUser.setUserIdentifier("admin"); String action = "cmd-" + UUID.randomUUID(); From 7fb8c8867ab36d6544639c279ba6c99a45b7703b Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Mon, 29 Jan 2024 15:05:45 -0500 Subject: [PATCH 502/689] fixing unit tests --- .../edu/harvard/iq/dataverse/cache/RateLimitUtil.java | 5 +++-- .../harvard/iq/dataverse/cache/CacheFactoryBeanTest.java | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java index 73de0fe5528..48b1b1be072 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java @@ -28,6 +28,7 @@ public class RateLimitUtil { public static final int NO_LIMIT = -1; protected static int getCapacityByTier(SystemConfig systemConfig, int tier) { + System.out.println("getIntFromCSVStringOrDefault: " +tier + " " + systemConfig.getIntFromCSVStringOrDefault(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers, tier, NO_LIMIT)); return systemConfig.getIntFromCSVStringOrDefault(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers, tier, NO_LIMIT); } @@ -45,8 +46,8 @@ protected static int getCapacity(SystemConfig systemConfig, User user, String ac }; // get the capacity, i.e. calls per hour, from config return (user instanceof AuthenticatedUser) ? - RateLimitUtil.getCapacityByTierAndAction(systemConfig, ((AuthenticatedUser) user).getRateLimitTier(), action) : - RateLimitUtil.getCapacityByTierAndAction(systemConfig, 0, action); + getCapacityByTierAndAction(systemConfig, ((AuthenticatedUser) user).getRateLimitTier(), action) : + getCapacityByTierAndAction(systemConfig, 0, action); } protected static boolean rateLimited(final Map cache, final String key, int capacityPerHour) { if (capacityPerHour == NO_LIMIT) { diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index e3d334d4623..15408605473 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -2,6 +2,7 @@ import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.SystemConfig; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -74,10 +75,10 @@ public class CacheFactoryBeanTest { @BeforeEach public void setup() throws IOException { - doReturn(30).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(0), anyInt()); - doReturn(60).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(1), anyInt()); - doReturn(120).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(2), anyInt()); - doReturn(RateLimitUtil.NO_LIMIT).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(3), anyInt()); + doReturn(30).when(systemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(0), eq(RateLimitUtil.NO_LIMIT)); + doReturn(60).when(systemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(1), eq(RateLimitUtil.NO_LIMIT)); + doReturn(120).when(systemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(2), eq(RateLimitUtil.NO_LIMIT)); + doReturn(RateLimitUtil.NO_LIMIT).when(systemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(3), eq(RateLimitUtil.NO_LIMIT)); doReturn(settingJson).when(systemConfig).getRateLimitsJson(); cache.init(); // PostConstruct From 0674105914b7f4d7faff1855c2583fddce0eb629 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Mon, 29 Jan 2024 16:00:08 -0500 Subject: [PATCH 503/689] fixing unit tests --- .../iq/dataverse/cache/CacheFactoryBean.java | 1 + .../dataverse/cache/CacheFactoryBeanTest.java | 62 ++++++++++--------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java index a1caa0379e0..f7b93b52c3e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java @@ -29,6 +29,7 @@ public void init() { if (hazelcastInstance == null) { Config hazelcastConfig = new Config(); hazelcastConfig.setClusterName("dataverse"); + hazelcastConfig.getJetConfig().setEnabled(true); hazelcastInstance = Hazelcast.newHazelcastInstance(hazelcastConfig); rateLimitCache = hazelcastInstance.getMap(RATE_LIMIT_CACHE); } diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index 15408605473..e968a6f9fad 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -7,8 +7,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; @@ -19,17 +17,17 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) public class CacheFactoryBeanTest { - @Mock - SystemConfig systemConfig; - @InjectMocks - static CacheFactoryBean cache = new CacheFactoryBean(); + private SystemConfig mockedSystemConfig; + static CacheFactoryBean cache = null; AuthenticatedUser authUser = new AuthenticatedUser(); GuestUser guestUser = GuestUser.get(); + String action; static final String settingJson = "{\n" + " \"rateLimits\":[\n" + " {\n" + @@ -75,29 +73,39 @@ public class CacheFactoryBeanTest { @BeforeEach public void setup() throws IOException { - doReturn(30).when(systemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(0), eq(RateLimitUtil.NO_LIMIT)); - doReturn(60).when(systemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(1), eq(RateLimitUtil.NO_LIMIT)); - doReturn(120).when(systemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(2), eq(RateLimitUtil.NO_LIMIT)); - doReturn(RateLimitUtil.NO_LIMIT).when(systemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(3), eq(RateLimitUtil.NO_LIMIT)); - doReturn(settingJson).when(systemConfig).getRateLimitsJson(); - - cache.init(); // PostConstruct - authUser.setRateLimitTier(1); // reset to default - - // testing cache implementation and code coverage - final String cacheKey = "CacheTestKey" + UUID.randomUUID(); - final String cacheValue = "CacheTestValue" + UUID.randomUUID(); - long cacheSize = cache.getCacheSize(cache.RATE_LIMIT_CACHE); - cache.setCacheValue(cache.RATE_LIMIT_CACHE, cacheKey,cacheValue); - assertTrue(cache.getCacheSize(cache.RATE_LIMIT_CACHE) > cacheSize); - Object cacheValueObj = cache.getCacheValue(cache.RATE_LIMIT_CACHE, cacheKey); - assertTrue(cacheValueObj != null && cacheValue.equalsIgnoreCase((String) cacheValueObj)); + // reuse cache and config for all tests + if (cache == null) { + mockedSystemConfig = mock(SystemConfig.class); + doReturn(30).when(mockedSystemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(0), eq(RateLimitUtil.NO_LIMIT)); + doReturn(60).when(mockedSystemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(1), eq(RateLimitUtil.NO_LIMIT)); + doReturn(120).when(mockedSystemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(2), eq(RateLimitUtil.NO_LIMIT)); + doReturn(RateLimitUtil.NO_LIMIT).when(mockedSystemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(3), eq(RateLimitUtil.NO_LIMIT)); + doReturn(settingJson).when(mockedSystemConfig).getRateLimitsJson(); + cache = new CacheFactoryBean(); + cache.systemConfig = mockedSystemConfig; + cache.init(); // PostConstruct - start Hazelcast + + // testing cache implementation and code coverage + final String cacheKey = "CacheTestKey" + UUID.randomUUID(); + final String cacheValue = "CacheTestValue" + UUID.randomUUID(); + long cacheSize = cache.getCacheSize(cache.RATE_LIMIT_CACHE); + cache.setCacheValue(cache.RATE_LIMIT_CACHE, cacheKey,cacheValue); + assertTrue(cache.getCacheSize(cache.RATE_LIMIT_CACHE) > cacheSize); + Object cacheValueObj = cache.getCacheValue(cache.RATE_LIMIT_CACHE, cacheKey); + assertTrue(cacheValueObj != null && cacheValue.equalsIgnoreCase((String) cacheValueObj)); + } + + // reset to default auth user + authUser.setRateLimitTier(1); + authUser.setSuperuser(false); + authUser.setUserIdentifier("authUser"); + + // create a unique action for each test + action = "cmd-" + UUID.randomUUID(); } @Test public void testGuestUserGettingRateLimited() { - String action = "cmd-" + UUID.randomUUID(); - String key = RateLimitUtil.generateCacheKey(guestUser,action); String value = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, key)); String keyLastUpdate = String.format("%s:last_update",key); @@ -129,7 +137,6 @@ public void testGuestUserGettingRateLimited() { public void testAdminUserExemptFromGettingRateLimited() { authUser.setSuperuser(true); authUser.setUserIdentifier("admin"); - String action = "cmd-" + UUID.randomUUID(); boolean rateLimited = false; int cnt = 0; for (; cnt <100; cnt++) { @@ -143,10 +150,7 @@ public void testAdminUserExemptFromGettingRateLimited() { @Test public void testAuthenticatedUserGettingRateLimited() throws InterruptedException { - authUser.setSuperuser(false); - authUser.setUserIdentifier("authUser"); authUser.setRateLimitTier(2); // 120 cals per hour - 1 added token every 30 seconds - String action = "cmd-" + UUID.randomUUID(); boolean rateLimited = false; int cnt; for (cnt = 0; cnt <200; cnt++) { From a55ed93dd2136dd20921d5bafa78957974a253d8 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Mon, 29 Jan 2024 16:27:22 -0500 Subject: [PATCH 504/689] fixing unit tests --- .../iq/dataverse/cache/CacheFactoryBean.java | 4 +- .../iq/dataverse/cache/RateLimitUtil.java | 11 ++-- .../dataverse/cache/CacheFactoryBeanTest.java | 4 ++ .../iq/dataverse/cache/RateLimitUtilTest.java | 55 ++++++++++--------- 4 files changed, 40 insertions(+), 34 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java index f7b93b52c3e..71e009c7ef2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java @@ -18,7 +18,7 @@ public class CacheFactoryBean implements java.io.Serializable { private static final Logger logger = Logger.getLogger(CacheFactoryBean.class.getCanonicalName()); private static HazelcastInstance hazelcastInstance = null; - private static Map rateLimitCache; + protected static Map rateLimitCache; @EJB SystemConfig systemConfig; @@ -54,7 +54,7 @@ public boolean checkRate(User user, String action) { return true; } else { String cacheKey = RateLimitUtil.generateCacheKey(user, action); - return (!RateLimitUtil.rateLimited(rateLimitCache, cacheKey, capacity)); + return (!RateLimitUtil.rateLimited(cacheKey, capacity)); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java index 48b1b1be072..a1ccab65505 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java @@ -28,7 +28,6 @@ public class RateLimitUtil { public static final int NO_LIMIT = -1; protected static int getCapacityByTier(SystemConfig systemConfig, int tier) { - System.out.println("getIntFromCSVStringOrDefault: " +tier + " " + systemConfig.getIntFromCSVStringOrDefault(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers, tier, NO_LIMIT)); return systemConfig.getIntFromCSVStringOrDefault(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers, tier, NO_LIMIT); } @@ -49,7 +48,7 @@ protected static int getCapacity(SystemConfig systemConfig, User user, String ac getCapacityByTierAndAction(systemConfig, ((AuthenticatedUser) user).getRateLimitTier(), action) : getCapacityByTierAndAction(systemConfig, 0, action); } - protected static boolean rateLimited(final Map cache, final String key, int capacityPerHour) { + protected static boolean rateLimited(final String key, int capacityPerHour) { if (capacityPerHour == NO_LIMIT) { return false; } @@ -57,17 +56,17 @@ protected static boolean rateLimited(final Map cache, final Stri double tokensPerMinute = (capacityPerHour / 60.0); // Get the last time this bucket was added to final String keyLastUpdate = String.format("%s:last_update",key); - long lastUpdate = longFromKey(cache, keyLastUpdate); + long lastUpdate = longFromKey(CacheFactoryBean.rateLimitCache, keyLastUpdate); long deltaTime = currentTime - lastUpdate; // Get the current number of tokens in the bucket - long tokens = longFromKey(cache, key); + long tokens = longFromKey(CacheFactoryBean.rateLimitCache, key); long tokensToAdd = (long) (deltaTime * tokensPerMinute); if (tokensToAdd > 0) { // Don't update timestamp if we aren't adding any tokens to the bucket tokens = min(capacityPerHour, tokens + tokensToAdd); - cache.put(keyLastUpdate, String.valueOf(currentTime)); + CacheFactoryBean.rateLimitCache.put(keyLastUpdate, String.valueOf(currentTime)); } // Update with any added tokens and decrement 1 token for this call if not rate limited (0 tokens) - cache.put(key, String.valueOf(max(0, tokens-1))); + CacheFactoryBean.rateLimitCache.put(key, String.valueOf(max(0, tokens-1))); return tokens < 1; } diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index e968a6f9fad..5eb0305a60a 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -85,6 +85,10 @@ public void setup() throws IOException { cache.systemConfig = mockedSystemConfig; cache.init(); // PostConstruct - start Hazelcast + // clear the static data so it can be reloaded with the new mocked data + RateLimitUtil.rateLimitMap.clear(); + RateLimitUtil.rateLimits.clear(); + // testing cache implementation and code coverage final String cacheKey = "CacheTestKey" + UUID.randomUUID(); final String cacheValue = "CacheTestValue" + UUID.randomUUID(); diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/RateLimitUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/RateLimitUtilTest.java index b2b7434cc3c..a7825481ade 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/RateLimitUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/RateLimitUtilTest.java @@ -3,23 +3,24 @@ import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.User; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.SystemConfig; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.*; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) public class RateLimitUtilTest { - @Mock - SystemConfig systemConfig; + private SystemConfig mockedSystemConfig; static final String settingJson = "{\n" + " \"rateLimits\":[\n" + @@ -67,33 +68,35 @@ public class RateLimitUtilTest { @BeforeEach public void setup() { - lenient().doReturn(100).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(0), anyInt()); - lenient().doReturn(200).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(1), anyInt()); - lenient().doReturn(RateLimitUtil.NO_LIMIT).when(systemConfig).getIntFromCSVStringOrDefault(any(),eq(2), anyInt()); + mockedSystemConfig = mock(SystemConfig.class); + doReturn(100).when(mockedSystemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(0), eq(RateLimitUtil.NO_LIMIT)); + doReturn(200).when(mockedSystemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(1), eq(RateLimitUtil.NO_LIMIT)); + doReturn(RateLimitUtil.NO_LIMIT).when(mockedSystemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(2), eq(RateLimitUtil.NO_LIMIT)); + // clear the static data so it can be reloaded with the new mocked data RateLimitUtil.rateLimitMap.clear(); RateLimitUtil.rateLimits.clear(); } @Test public void testConfig() { - lenient().doReturn(settingJson).when(systemConfig).getRateLimitsJson(); - assertEquals(100, RateLimitUtil.getCapacityByTier(systemConfig, 0)); - assertEquals(200, RateLimitUtil.getCapacityByTier(systemConfig, 1)); - assertEquals(1, RateLimitUtil.getCapacityByTierAndAction(systemConfig, 0, "DestroyDatasetCommand")); - assertEquals(100, RateLimitUtil.getCapacityByTierAndAction(systemConfig, 0, "Default Limit")); + doReturn(settingJson).when(mockedSystemConfig).getRateLimitsJson(); + assertEquals(100, RateLimitUtil.getCapacityByTier(mockedSystemConfig, 0)); + assertEquals(200, RateLimitUtil.getCapacityByTier(mockedSystemConfig, 1)); + assertEquals(1, RateLimitUtil.getCapacityByTierAndAction(mockedSystemConfig, 0, "DestroyDatasetCommand")); + assertEquals(100, RateLimitUtil.getCapacityByTierAndAction(mockedSystemConfig, 0, "Default Limit")); - assertEquals(30, RateLimitUtil.getCapacityByTierAndAction(systemConfig, 1, "GetLatestAccessibleDatasetVersionCommand")); - assertEquals(200, RateLimitUtil.getCapacityByTierAndAction(systemConfig, 1, "Default Limit")); + assertEquals(30, RateLimitUtil.getCapacityByTierAndAction(mockedSystemConfig, 1, "GetLatestAccessibleDatasetVersionCommand")); + assertEquals(200, RateLimitUtil.getCapacityByTierAndAction(mockedSystemConfig, 1, "Default Limit")); - assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacityByTierAndAction(systemConfig, 2, "Default No Limit")); + assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacityByTierAndAction(mockedSystemConfig, 2, "Default No Limit")); } @Test public void testBadJson() { - lenient().doReturn(settingJsonBad).when(systemConfig).getRateLimitsJson(); - assertEquals(100, RateLimitUtil.getCapacityByTier(systemConfig, 0)); - assertEquals(200, RateLimitUtil.getCapacityByTier(systemConfig, 1)); - assertEquals(100, RateLimitUtil.getCapacityByTierAndAction(systemConfig, 0, "GetLatestAccessibleDatasetVersionCommand")); - assertEquals(200, RateLimitUtil.getCapacityByTierAndAction(systemConfig, 1, "GetLatestAccessibleDatasetVersionCommand")); - assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacityByTierAndAction(systemConfig, 2, "GetLatestAccessibleDatasetVersionCommand")); + doReturn(settingJsonBad).when(mockedSystemConfig).getRateLimitsJson(); + assertEquals(100, RateLimitUtil.getCapacityByTier(mockedSystemConfig, 0)); + assertEquals(200, RateLimitUtil.getCapacityByTier(mockedSystemConfig, 1)); + assertEquals(100, RateLimitUtil.getCapacityByTierAndAction(mockedSystemConfig, 0, "GetLatestAccessibleDatasetVersionCommand")); + assertEquals(200, RateLimitUtil.getCapacityByTierAndAction(mockedSystemConfig, 1, "GetLatestAccessibleDatasetVersionCommand")); + assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacityByTierAndAction(mockedSystemConfig, 2, "GetLatestAccessibleDatasetVersionCommand")); } @Test @@ -103,14 +106,14 @@ public void testGenerateCacheKey() { } @Test public void testGetCapacity() { - lenient().doReturn(settingJson).when(systemConfig).getRateLimitsJson(); + doReturn(settingJson).when(mockedSystemConfig).getRateLimitsJson(); GuestUser guestUser = GuestUser.get(); - assertEquals(10, RateLimitUtil.getCapacity(systemConfig, guestUser, "GetPrivateUrlCommand")); + assertEquals(10, RateLimitUtil.getCapacity(mockedSystemConfig, guestUser, "GetPrivateUrlCommand")); AuthenticatedUser authUser = new AuthenticatedUser(); authUser.setRateLimitTier(1); - assertEquals(30, RateLimitUtil.getCapacity(systemConfig, authUser, "GetPrivateUrlCommand")); + assertEquals(30, RateLimitUtil.getCapacity(mockedSystemConfig, authUser, "GetPrivateUrlCommand")); authUser.setSuperuser(true); - assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacity(systemConfig, authUser, "GetPrivateUrlCommand")); + assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacity(mockedSystemConfig, authUser, "GetPrivateUrlCommand")); } } From c7b5969e6545777391fadf251b1ec709cd79eaf5 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Mon, 29 Jan 2024 16:42:21 -0500 Subject: [PATCH 505/689] fixing unit tests --- .../iq/dataverse/cache/CacheFactoryBeanTest.java | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index 5eb0305a60a..63c3f9e8bb8 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -110,12 +110,6 @@ public void setup() throws IOException { @Test public void testGuestUserGettingRateLimited() { - String key = RateLimitUtil.generateCacheKey(guestUser,action); - String value = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, key)); - String keyLastUpdate = String.format("%s:last_update",key); - String lastUpdate = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, keyLastUpdate)); - System.out.println(">>> key|value|lastUpdate |" + key + "|" + value + "|" + lastUpdate); - boolean rateLimited = false; int cnt = 0; for (; cnt <100; cnt++) { @@ -123,16 +117,7 @@ public void testGuestUserGettingRateLimited() { if (rateLimited) { break; } - if (cnt % 10 == 0) { - value = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, key)); - lastUpdate = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, keyLastUpdate)); - System.out.println(cnt + " key|value|lastUpdate |" + key + "|" + value + "|" + lastUpdate); - } } - - value = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, key)); - lastUpdate = String.valueOf(cache.getCacheValue(cache.RATE_LIMIT_CACHE, keyLastUpdate)); - System.out.println(cnt + " key|value|lastUpdate |" + key + "|" + value + "|" + lastUpdate); assertTrue(cache.getCacheSize(cache.RATE_LIMIT_CACHE) > 0); assertTrue(rateLimited && cnt > 1 && cnt <= 30, "rateLimited:"+rateLimited + " cnt:"+cnt); } From a27c7851e76282da3b88c1acf51989c5e5216be4 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 30 Jan 2024 11:54:14 -0500 Subject: [PATCH 506/689] fixing unit tests --- .../iq/dataverse/cache/CacheFactoryBean.java | 25 ++++++++--- .../iq/dataverse/cache/RateLimitUtil.java | 10 ++--- .../dataverse/cache/CacheFactoryBeanTest.java | 41 +++++++++++++++++++ 3 files changed, 66 insertions(+), 10 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java index 71e009c7ef2..213ba429bdf 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java @@ -6,6 +6,7 @@ import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.util.SystemConfig; import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import jakarta.ejb.EJB; import jakarta.ejb.Singleton; import jakarta.ejb.Startup; @@ -17,8 +18,8 @@ @Startup public class CacheFactoryBean implements java.io.Serializable { private static final Logger logger = Logger.getLogger(CacheFactoryBean.class.getCanonicalName()); - private static HazelcastInstance hazelcastInstance = null; - protected static Map rateLimitCache; + private HazelcastInstance hazelcastInstance = null; + private Map rateLimitCache; @EJB SystemConfig systemConfig; @@ -34,11 +35,16 @@ public void init() { rateLimitCache = hazelcastInstance.getMap(RATE_LIMIT_CACHE); } } - @Override - protected void finalize() throws Throwable { + @PreDestroy + protected void cleanup() { if (hazelcastInstance != null) { hazelcastInstance.shutdown(); + hazelcastInstance = null; } + } + @Override + protected void finalize() throws Throwable { + cleanup(); super.finalize(); } @@ -54,7 +60,7 @@ public boolean checkRate(User user, String action) { return true; } else { String cacheKey = RateLimitUtil.generateCacheKey(user, action); - return (!RateLimitUtil.rateLimited(cacheKey, capacity)); + return (!RateLimitUtil.rateLimited(rateLimitCache, cacheKey, capacity)); } } @@ -89,4 +95,13 @@ public void setCacheValue(String cacheName, String key, Object value) { break; } } + public void clearCache(String cacheName) { + switch (cacheName) { + case RATE_LIMIT_CACHE: + rateLimitCache.clear(); + break; + default: + break; + } + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java index a1ccab65505..1e676adfe03 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java @@ -48,7 +48,7 @@ protected static int getCapacity(SystemConfig systemConfig, User user, String ac getCapacityByTierAndAction(systemConfig, ((AuthenticatedUser) user).getRateLimitTier(), action) : getCapacityByTierAndAction(systemConfig, 0, action); } - protected static boolean rateLimited(final String key, int capacityPerHour) { + protected static boolean rateLimited(final Map rateLimitCache, final String key, int capacityPerHour) { if (capacityPerHour == NO_LIMIT) { return false; } @@ -56,17 +56,17 @@ protected static boolean rateLimited(final String key, int capacityPerHour) { double tokensPerMinute = (capacityPerHour / 60.0); // Get the last time this bucket was added to final String keyLastUpdate = String.format("%s:last_update",key); - long lastUpdate = longFromKey(CacheFactoryBean.rateLimitCache, keyLastUpdate); + long lastUpdate = longFromKey(rateLimitCache, keyLastUpdate); long deltaTime = currentTime - lastUpdate; // Get the current number of tokens in the bucket - long tokens = longFromKey(CacheFactoryBean.rateLimitCache, key); + long tokens = longFromKey(rateLimitCache, key); long tokensToAdd = (long) (deltaTime * tokensPerMinute); if (tokensToAdd > 0) { // Don't update timestamp if we aren't adding any tokens to the bucket tokens = min(capacityPerHour, tokens + tokensToAdd); - CacheFactoryBean.rateLimitCache.put(keyLastUpdate, String.valueOf(currentTime)); + rateLimitCache.put(keyLastUpdate, String.valueOf(currentTime)); } // Update with any added tokens and decrement 1 token for this call if not rate limited (0 tokens) - CacheFactoryBean.rateLimitCache.put(key, String.valueOf(max(0, tokens-1))); + rateLimitCache.put(key, String.valueOf(max(0, tokens-1))); return tokens < 1; } diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index 63c3f9e8bb8..a4d955dc64c 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -4,6 +4,7 @@ import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.SystemConfig; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -108,6 +109,13 @@ public void setup() throws IOException { action = "cmd-" + UUID.randomUUID(); } + @AfterAll + public static void cleanup() { + if (cache != null) { + cache.cleanup(); // PreDestroy - shutdown Hazelcast + cache = null; + } + } @Test public void testGuestUserGettingRateLimited() { boolean rateLimited = false; @@ -169,4 +177,37 @@ public void testAuthenticatedUserGettingRateLimited() throws InterruptedExceptio } assertTrue(!rateLimited && cnt == 200, "rateLimited:"+rateLimited + " cnt:"+cnt); } + + @Test + public void testCluster() { + //make sure at least 1 entry is in the original cache + cache.checkRate(authUser, action); + + // create a second cache to test cluster + CacheFactoryBean cache2 = new CacheFactoryBean(); + cache2.systemConfig = mockedSystemConfig; + cache2.init(); // PostConstruct - start Hazelcast + + // check to see if the new cache synced with the existing cache + long s1 = cache.getCacheSize(CacheFactoryBean.RATE_LIMIT_CACHE); + long s2 = cache2.getCacheSize(CacheFactoryBean.RATE_LIMIT_CACHE); + assertTrue(s1 > 0 && s1 == s2); + + String key = "key1"; + String value = "value1"; + // verify that both caches stay in sync + cache.setCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key, value); + assertTrue(value.equals(cache2.getCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key))); + // clearing one cache also clears the other cache in the cluster + cache2.clearCache(CacheFactoryBean.RATE_LIMIT_CACHE); + assertTrue(String.valueOf(cache.getCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key)).isEmpty()); + + // verify no issue dropping one node from cluster + cache2.setCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key, value); + assertTrue(value.equals(cache2.getCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key))); + assertTrue(value.equals(cache.getCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key))); + cache2.cleanup(); // remove cache2 + assertTrue(value.equals(cache.getCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key))); + + } } From ecca881731c4796c7379db45e90fb868767c1058 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 30 Jan 2024 12:07:03 -0500 Subject: [PATCH 507/689] fixing unit tests --- .../edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index a4d955dc64c..6815d8b872b 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -191,7 +191,7 @@ public void testCluster() { // check to see if the new cache synced with the existing cache long s1 = cache.getCacheSize(CacheFactoryBean.RATE_LIMIT_CACHE); long s2 = cache2.getCacheSize(CacheFactoryBean.RATE_LIMIT_CACHE); - assertTrue(s1 > 0 && s1 == s2); + assertTrue(s1 > 0 && s1 == s2, "Size1:" + s1 + " Size2:" + s2 ); String key = "key1"; String value = "value1"; From 11a37e39b10e4bf1a4ce1a09427c2d77ff9136db Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 30 Jan 2024 13:06:38 -0500 Subject: [PATCH 508/689] fixing unit tests --- .../iq/dataverse/cache/CacheFactoryBean.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java index 213ba429bdf..94fe6cd2a90 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java @@ -28,10 +28,18 @@ public class CacheFactoryBean implements java.io.Serializable { @PostConstruct public void init() { if (hazelcastInstance == null) { - Config hazelcastConfig = new Config(); - hazelcastConfig.setClusterName("dataverse"); - hazelcastConfig.getJetConfig().setEnabled(true); - hazelcastInstance = Hazelcast.newHazelcastInstance(hazelcastConfig); + Config config = new Config(); + config.setClusterName("dataverse"); + config.getJetConfig().setEnabled(true); + + config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(true); + config.getNetworkConfig().getJoin().getAwsConfig().setEnabled(false); + config.getNetworkConfig().getJoin().getAzureConfig().setEnabled(false); + // .setProperty("tag-key", "my-ec2-instance-tag-key") + // .setProperty("tag-value", "my-ec2-instance-tag-value"); + + + hazelcastInstance = Hazelcast.newHazelcastInstance(config); rateLimitCache = hazelcastInstance.getMap(RATE_LIMIT_CACHE); } } From c84ae145945dedb368dfa7fbfdad318c0a176ea0 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 30 Jan 2024 13:23:11 -0500 Subject: [PATCH 509/689] fixing unit tests --- docker-compose-dev.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index b4a7a510839..0c29813f03b 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -59,6 +59,8 @@ services: - "4949:4848" # HTTPS (Payara Admin Console) - "9009:9009" # JDWP - "8686:8686" # JMX + - "5701:5701" # Hazelcast + - "5702:5702" # Hazelcast networks: - dataverse depends_on: From 4f8a39c3f98868c7e07e0eff78c62dec05d4cfd7 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 30 Jan 2024 15:52:57 -0500 Subject: [PATCH 510/689] fixing unit tests --- .../edu/harvard/iq/dataverse/cache/CacheFactoryBean.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java index 94fe6cd2a90..a3bcc1ae64a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java @@ -28,17 +28,18 @@ public class CacheFactoryBean implements java.io.Serializable { @PostConstruct public void init() { if (hazelcastInstance == null) { + // TODO: move config to a file (yml) Config config = new Config(); config.setClusterName("dataverse"); config.getJetConfig().setEnabled(true); - - config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(true); + config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(false); + config.getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(true); + config.getNetworkConfig().getJoin().getTcpIpConfig().addMember("localhost:5701"); + config.getNetworkConfig().getJoin().getTcpIpConfig().addMember("localhost:5702"); config.getNetworkConfig().getJoin().getAwsConfig().setEnabled(false); config.getNetworkConfig().getJoin().getAzureConfig().setEnabled(false); // .setProperty("tag-key", "my-ec2-instance-tag-key") // .setProperty("tag-value", "my-ec2-instance-tag-value"); - - hazelcastInstance = Hazelcast.newHazelcastInstance(config); rateLimitCache = hazelcastInstance.getMap(RATE_LIMIT_CACHE); } From 3d0e4383ba8664426ed86fa66c14560d01d72a9b Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Wed, 31 Jan 2024 11:58:45 -0500 Subject: [PATCH 511/689] fix test hazelcast config --- scripts/installer/as-setup.sh | 4 ++ .../iq/dataverse/cache/CacheFactoryBean.java | 52 +++++++++++++------ 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/scripts/installer/as-setup.sh b/scripts/installer/as-setup.sh index c89bcb4ff4d..3eb81f553e7 100755 --- a/scripts/installer/as-setup.sh +++ b/scripts/installer/as-setup.sh @@ -128,6 +128,10 @@ function preliminary_setup() # so we can front with apache httpd ( ProxyPass / ajp://localhost:8009/ ) ./asadmin $ASADMIN_OPTS create-network-listener --protocol http-listener-1 --listenerport 8009 --jkenabled true jk-connector + + # set up rate limiting using hazelcast in TcpIp discovery mode + ./asadmin $ASADMIN_OPTS create-jvm-options "\-Ddataverse.hazelcast.join=TcpIp" + ./asadmin $ASADMIN_OPTS create-jvm-options "\-Ddataverse.hazelcast.members=localhost:5701,localhost:5702" } function final_setup(){ diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java index a3bcc1ae64a..b7ec7f6736c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java @@ -11,6 +11,7 @@ import jakarta.ejb.Singleton; import jakarta.ejb.Startup; +import java.util.Arrays; import java.util.logging.Logger; import java.util.Map; @@ -22,25 +23,14 @@ public class CacheFactoryBean implements java.io.Serializable { private Map rateLimitCache; @EJB SystemConfig systemConfig; - public final static String RATE_LIMIT_CACHE = "rateLimitCache"; - + public enum JoinVia { + Multicast, TcpIp, AWS, Azure; + } @PostConstruct public void init() { if (hazelcastInstance == null) { - // TODO: move config to a file (yml) - Config config = new Config(); - config.setClusterName("dataverse"); - config.getJetConfig().setEnabled(true); - config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(false); - config.getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(true); - config.getNetworkConfig().getJoin().getTcpIpConfig().addMember("localhost:5701"); - config.getNetworkConfig().getJoin().getTcpIpConfig().addMember("localhost:5702"); - config.getNetworkConfig().getJoin().getAwsConfig().setEnabled(false); - config.getNetworkConfig().getJoin().getAzureConfig().setEnabled(false); - // .setProperty("tag-key", "my-ec2-instance-tag-key") - // .setProperty("tag-value", "my-ec2-instance-tag-value"); - hazelcastInstance = Hazelcast.newHazelcastInstance(config); + hazelcastInstance = Hazelcast.newHazelcastInstance(getConfig()); rateLimitCache = hazelcastInstance.getMap(RATE_LIMIT_CACHE); } } @@ -113,4 +103,36 @@ public void clearCache(String cacheName) { break; } } + + private Config getConfig() { + JoinVia joinVia; + try { + String join = System.getProperty("dataverse.hazelcast.join", "Multicast"); + joinVia = JoinVia.valueOf(join); + } catch (IllegalArgumentException e) { + logger.warning("dataverse.hazelcast.join must be one of " + JoinVia.values() + ". Defaulting to Multicast"); + joinVia = JoinVia.Multicast; + } + Config config = new Config(); + config.setClusterName("dataverse"); + config.getJetConfig().setEnabled(true); + if (joinVia == JoinVia.TcpIp) { + config.getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(true); + String members = System.getProperty("dataverse.hazelcast.members", ""); + logger.info("dataverse.hazelcast.members: " + members); + try { + Arrays.stream(members.split(",")).forEach(m -> + config.getNetworkConfig().getJoin().getTcpIpConfig().addMember(m)); + } catch (IllegalArgumentException e) { + logger.warning("dataverse.hazelcast.members must contain at least 1 'host:port' entry, Defaulting to Multicast"); + joinVia = JoinVia.Multicast; + } + } + logger.info("dataverse.hazelcast.join:" + joinVia); + config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(joinVia == JoinVia.Multicast); + config.getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(joinVia == JoinVia.TcpIp); + config.getNetworkConfig().getJoin().getAwsConfig().setEnabled(joinVia == JoinVia.AWS); + config.getNetworkConfig().getJoin().getAzureConfig().setEnabled(joinVia == JoinVia.Azure); + return config; + } } From 176adbc778976e3606db32830dd4e40c23091c53 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Wed, 31 Jan 2024 14:07:16 -0500 Subject: [PATCH 512/689] fix test hazelcast config --- .../harvard/iq/dataverse/cache/CacheFactoryBeanTest.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index 6815d8b872b..9034d8c00b4 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -5,6 +5,7 @@ import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.SystemConfig; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -29,6 +30,7 @@ public class CacheFactoryBeanTest { AuthenticatedUser authUser = new AuthenticatedUser(); GuestUser guestUser = GuestUser.get(); String action; + static final String staticHazelcastSystemProperties = "dataverse.hazelcast."; static final String settingJson = "{\n" + " \"rateLimits\":[\n" + " {\n" + @@ -72,8 +74,13 @@ public class CacheFactoryBeanTest { " ]\n" + "}"; + @BeforeAll + public static void setup() { + System.setProperty(staticHazelcastSystemProperties + "join", "TcpIp"); + System.setProperty(staticHazelcastSystemProperties + "members", "localhost:5701,localhost:5702"); + } @BeforeEach - public void setup() throws IOException { + public void init() throws IOException { // reuse cache and config for all tests if (cache == null) { mockedSystemConfig = mock(SystemConfig.class); From 403dc084cec57157a347eb4a83aca78d5eeab95a Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Wed, 31 Jan 2024 15:08:05 -0500 Subject: [PATCH 513/689] fix test hazelcast config --- doc/release-notes/9356-rate-limiting.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/release-notes/9356-rate-limiting.md b/doc/release-notes/9356-rate-limiting.md index d7b9d2defcf..8bae4b59de4 100644 --- a/doc/release-notes/9356-rate-limiting.md +++ b/doc/release-notes/9356-rate-limiting.md @@ -16,3 +16,10 @@ Tiers not specified in this setting will default to `-1` (No Limit). This allows for more control over the rate limit of individual API command calls. In the following example, calls made by a guest user (tier 0) for API `GetLatestPublishedDatasetVersionCommand` is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. `curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '{"rateLimits":[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]}'` + +JVM properties to configure Hazelcast to work as a cluster. +By default, Hazelcast uses Multicast to discover cluster members see https://docs.hazelcast.com/imdg/4.2/clusters/discovery-mechanisms +Valid join types: Multicast or TcpIp +Members can be listed in a CSV field of 'host:port' for each dataverse app instance +-Ddataverse.hazelcast.join=TcpIp +-Ddataverse.hazelcast.members=localhost:5701,localhost:5702 \ No newline at end of file From 9e43b25d67ab0726b055b4bc816e4844b0da9fa0 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Wed, 31 Jan 2024 15:58:19 -0500 Subject: [PATCH 514/689] fix test hazelcast config --- .../java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java index 1e676adfe03..446a8b4b712 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java @@ -22,8 +22,8 @@ public class RateLimitUtil { private static final Logger logger = Logger.getLogger(RateLimitUtil.class.getCanonicalName()); - protected static final List rateLimits = new CopyOnWriteArrayList<>(); - protected static final Map rateLimitMap = new ConcurrentHashMap<>(); + static final List rateLimits = new CopyOnWriteArrayList<>(); + static final Map rateLimitMap = new ConcurrentHashMap<>(); private static final Gson gson = new Gson(); public static final int NO_LIMIT = -1; From 0771fae20d7d394fa3b2b1a03b350d925127c883 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Thu, 1 Feb 2024 11:50:45 -0500 Subject: [PATCH 515/689] fixing more review comments --- .../iq/dataverse/cache/RateLimitUtil.java | 43 +++++++++++-------- .../iq/dataverse/util/SystemConfig.java | 19 +------- .../dataverse/cache/CacheFactoryBeanTest.java | 8 +--- .../iq/dataverse/cache/RateLimitUtilTest.java | 18 +++++--- 4 files changed, 41 insertions(+), 47 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java index 446a8b4b712..b710138865f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java @@ -4,7 +4,6 @@ import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.User; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.SystemConfig; import jakarta.json.Json; import jakarta.json.JsonArray; @@ -27,11 +26,7 @@ public class RateLimitUtil { private static final Gson gson = new Gson(); public static final int NO_LIMIT = -1; - protected static int getCapacityByTier(SystemConfig systemConfig, int tier) { - return systemConfig.getIntFromCSVStringOrDefault(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers, tier, NO_LIMIT); - } - - protected static String generateCacheKey(final User user, final String action) { + static String generateCacheKey(final User user, final String action) { StringBuffer id = new StringBuffer(); id.append(user != null ? user.getIdentifier() : GuestUser.get().getIdentifier()); if (action != null) { @@ -39,7 +34,7 @@ protected static String generateCacheKey(final User user, final String action) { } return id.toString(); } - protected static int getCapacity(SystemConfig systemConfig, User user, String action) { + static int getCapacity(SystemConfig systemConfig, User user, String action) { if (user != null && user.isSuperuser()) { return NO_LIMIT; }; @@ -48,7 +43,7 @@ protected static int getCapacity(SystemConfig systemConfig, User user, String ac getCapacityByTierAndAction(systemConfig, ((AuthenticatedUser) user).getRateLimitTier(), action) : getCapacityByTierAndAction(systemConfig, 0, action); } - protected static boolean rateLimited(final Map rateLimitCache, final String key, int capacityPerHour) { + static boolean rateLimited(final Map rateLimitCache, final String key, int capacityPerHour) { if (capacityPerHour == NO_LIMIT) { return false; } @@ -70,7 +65,7 @@ protected static boolean rateLimited(final Map rateLimitCache, final String key, return tokens < 1; } - protected static int getCapacityByTierAndAction(SystemConfig systemConfig, Integer tier, String action) { + static int getCapacityByTierAndAction(SystemConfig systemConfig, Integer tier, String action) { if (rateLimits.isEmpty()) { init(systemConfig); } @@ -79,8 +74,22 @@ protected static int getCapacityByTierAndAction(SystemConfig systemConfig, Integ rateLimitMap.containsKey(getMapKey(tier)) ? rateLimitMap.get(getMapKey(tier)) : getCapacityByTier(systemConfig, tier); } - - private static void init(SystemConfig systemConfig) { + static int getCapacityByTier(SystemConfig systemConfig, int tier) { + int value = NO_LIMIT; + String csvString = systemConfig.getRateLimitingDefaultCapacityTiers(); + try { + if (!csvString.isEmpty()) { + int[] values = Arrays.stream(csvString.split(",")).mapToInt(Integer::parseInt).toArray(); + if (tier < values.length) { + value = values[tier]; + } + } + } catch (NumberFormatException nfe) { + logger.warning(nfe.getMessage()); + } + return value; + } + static void init(SystemConfig systemConfig) { getRateLimitsFromJson(systemConfig); /* Convert the List of Rate Limit Settings containing a list of Actions to a fast lookup Map where the key is: for default if no action defined: "{tier}:" and the value is the default limit for the tier @@ -92,8 +101,7 @@ private static void init(SystemConfig systemConfig) { r.getActions().forEach(a -> rateLimitMap.put(getMapKey(r.getTier(), a), r.getLimitPerHour())); }); } - - private static void getRateLimitsFromJson(SystemConfig systemConfig) { + static void getRateLimitsFromJson(SystemConfig systemConfig) { String setting = systemConfig.getRateLimitsJson(); if (!setting.isEmpty() && rateLimits.isEmpty()) { try { @@ -108,12 +116,10 @@ private static void getRateLimitsFromJson(SystemConfig systemConfig) { } } } - - private static String getMapKey(int tier) { + static String getMapKey(int tier) { return getMapKey(tier, null); } - - private static String getMapKey(int tier, String action) { + static String getMapKey(int tier, String action) { StringBuffer key = new StringBuffer(); key.append(tier).append(":"); if (action != null) { @@ -121,8 +127,7 @@ private static String getMapKey(int tier, String action) { } return key.toString(); } - - private static long longFromKey(Map cache, String key) { + static long longFromKey(Map cache, String key) { Object l = cache.get(key); return l != null ? Long.parseLong(String.valueOf(l)) : 0L; } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java index 9f4bd7c2e62..b388e978808 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java @@ -1160,22 +1160,7 @@ public boolean isStoringIngestedFilesWithHeaders() { public String getRateLimitsJson() { return settingsService.getValueForKey(SettingsServiceBean.Key.RateLimitingCapacityByTierAndAction, ""); } - - public int getIntFromCSVStringOrDefault(final SettingsServiceBean.Key settingKey, int index, int defaultValue) { - int value = defaultValue; - if (settingKey != null && !settingKey.equals("")) { - String csv = settingsService.getValueForKey(settingKey, ""); - try { - if (!csv.isEmpty()) { - int[] values = Arrays.stream(csv.split(",")).mapToInt(Integer::parseInt).toArray(); - if (index < values.length) { - value = values[index]; - } - } - } catch (NumberFormatException nfe) { - logger.warning(nfe.getMessage()); - } - } - return value; + public String getRateLimitingDefaultCapacityTiers() { + return settingsService.getValueForKey(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers, ""); } } diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index 9034d8c00b4..1b4c7e973af 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -2,7 +2,6 @@ import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.SystemConfig; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -17,7 +16,6 @@ import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -31,6 +29,7 @@ public class CacheFactoryBeanTest { GuestUser guestUser = GuestUser.get(); String action; static final String staticHazelcastSystemProperties = "dataverse.hazelcast."; + static final String settingDefaultCapacity = "30,60,120"; static final String settingJson = "{\n" + " \"rateLimits\":[\n" + " {\n" + @@ -84,10 +83,7 @@ public void init() throws IOException { // reuse cache and config for all tests if (cache == null) { mockedSystemConfig = mock(SystemConfig.class); - doReturn(30).when(mockedSystemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(0), eq(RateLimitUtil.NO_LIMIT)); - doReturn(60).when(mockedSystemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(1), eq(RateLimitUtil.NO_LIMIT)); - doReturn(120).when(mockedSystemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(2), eq(RateLimitUtil.NO_LIMIT)); - doReturn(RateLimitUtil.NO_LIMIT).when(mockedSystemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(3), eq(RateLimitUtil.NO_LIMIT)); + doReturn(settingDefaultCapacity).when(mockedSystemConfig).getRateLimitingDefaultCapacityTiers(); doReturn(settingJson).when(mockedSystemConfig).getRateLimitsJson(); cache = new CacheFactoryBean(); cache.systemConfig = mockedSystemConfig; diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/RateLimitUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/RateLimitUtilTest.java index a7825481ade..033f9dbb67e 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/RateLimitUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/RateLimitUtilTest.java @@ -3,7 +3,6 @@ import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.User; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.SystemConfig; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -13,7 +12,6 @@ import org.mockito.quality.Strictness; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -69,9 +67,7 @@ public class RateLimitUtilTest { @BeforeEach public void setup() { mockedSystemConfig = mock(SystemConfig.class); - doReturn(100).when(mockedSystemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(0), eq(RateLimitUtil.NO_LIMIT)); - doReturn(200).when(mockedSystemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(1), eq(RateLimitUtil.NO_LIMIT)); - doReturn(RateLimitUtil.NO_LIMIT).when(mockedSystemConfig).getIntFromCSVStringOrDefault(eq(SettingsServiceBean.Key.RateLimitingDefaultCapacityTiers),eq(2), eq(RateLimitUtil.NO_LIMIT)); + doReturn("100,200").when(mockedSystemConfig).getRateLimitingDefaultCapacityTiers(); // clear the static data so it can be reloaded with the new mocked data RateLimitUtil.rateLimitMap.clear(); RateLimitUtil.rateLimits.clear(); @@ -115,5 +111,17 @@ public void testGetCapacity() { assertEquals(30, RateLimitUtil.getCapacity(mockedSystemConfig, authUser, "GetPrivateUrlCommand")); authUser.setSuperuser(true); assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacity(mockedSystemConfig, authUser, "GetPrivateUrlCommand")); + + // no setting means rate limiting is not on + doReturn("").when(mockedSystemConfig).getRateLimitsJson(); + doReturn("").when(mockedSystemConfig).getRateLimitingDefaultCapacityTiers(); + RateLimitUtil.rateLimitMap.clear(); + RateLimitUtil.rateLimits.clear(); + assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacity(mockedSystemConfig, guestUser, "GetPrivateUrlCommand")); + assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacity(mockedSystemConfig, guestUser, "xyz")); + assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacity(mockedSystemConfig, authUser, "GetPrivateUrlCommand")); + assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacity(mockedSystemConfig, authUser, "abc")); + authUser.setRateLimitTier(99); + assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacity(mockedSystemConfig, authUser, "def")); } } From 252337a8373aeff6e803de2f54ec430f42e2c912 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Thu, 1 Feb 2024 14:21:58 -0500 Subject: [PATCH 516/689] fix db rate limit tier column --- src/main/java/edu/harvard/iq/dataverse/UserServiceBean.java | 2 +- .../iq/dataverse/authorization/users/AuthenticatedUser.java | 5 ++++- .../db/migration/V6.1.0.2__9356-add-rate-limiting.sql | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/UserServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/UserServiceBean.java index 47aebb78a35..d63fcfa3e34 100644 --- a/src/main/java/edu/harvard/iq/dataverse/UserServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/UserServiceBean.java @@ -147,7 +147,7 @@ private AuthenticatedUser createAuthenticatedUserForView (Object[] dbRowValues, user.setMutedEmails(Type.tokenizeToSet((String) dbRowValues[15])); user.setMutedNotifications(Type.tokenizeToSet((String) dbRowValues[15])); - user.setRateLimitTier((int)dbRowValues[16]); + user.setRateLimitTier((int)dbRowValues[17]); user.setRoles(roles); return user; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java b/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java index 0ed036afc6b..6abcb350222 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java @@ -16,6 +16,8 @@ import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.json.JsonPrinter; import static edu.harvard.iq.dataverse.util.StringUtil.nonEmpty; +import static java.lang.Math.max; + import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; import java.io.Serializable; import java.sql.Timestamp; @@ -146,19 +148,20 @@ public class AuthenticatedUser implements User, Serializable { @Transient private Set mutedNotificationsSet = new HashSet<>(); - @Column private int rateLimitTier; @PrePersist void prePersist() { mutedNotifications = Type.toStringValue(mutedNotificationsSet); mutedEmails = Type.toStringValue(mutedEmailsSet); + rateLimitTier = max(1,rateLimitTier); // db column defaults to 1 (minimum value for a tier). } @PostLoad public void initialize() { mutedNotificationsSet = Type.tokenizeToSet(mutedNotifications); mutedEmailsSet = Type.tokenizeToSet(mutedEmails); + rateLimitTier = max(1,rateLimitTier); // db column defaults to 1 (minimum value for a tier). } /** diff --git a/src/main/resources/db/migration/V6.1.0.2__9356-add-rate-limiting.sql b/src/main/resources/db/migration/V6.1.0.2__9356-add-rate-limiting.sql index ae30fd96bfd..be370625b3f 100644 --- a/src/main/resources/db/migration/V6.1.0.2__9356-add-rate-limiting.sql +++ b/src/main/resources/db/migration/V6.1.0.2__9356-add-rate-limiting.sql @@ -1 +1,2 @@ -ALTER TABLE authenticateduser ADD COLUMN IF NOT EXISTS ratelimittier int DEFAULT 1; \ No newline at end of file +ALTER TABLE authenticateduser ADD COLUMN IF NOT EXISTS ratelimittier int DEFAULT 1; +UPDATE authenticateduser set ratelimittier = 1 WHERE ratelimittier = 0; \ No newline at end of file From cc70ba7f1886a7f9dc62470706e0e0df5ebf5fdd Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Thu, 1 Feb 2024 14:37:51 -0500 Subject: [PATCH 517/689] fix db rate limit tier column --- scripts/installer/installAppServer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/installer/installAppServer.py b/scripts/installer/installAppServer.py index 03abc03b05e..698f5ba9a58 100644 --- a/scripts/installer/installAppServer.py +++ b/scripts/installer/installAppServer.py @@ -29,6 +29,7 @@ def runAsadminScript(config): os.environ['DOI_USERNAME'] = config.get('doi','DOI_USERNAME') os.environ['DOI_PASSWORD'] = config.get('doi','DOI_PASSWORD') os.environ['DOI_DATACITERESTAPIURL'] = config.get('doi','DOI_DATACITERESTAPIURL') + mailServerEntry = config.get('system','MAIL_SERVER') try: From 794f0243f4c4bf100e90095c189df48c15bffb36 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Thu, 1 Feb 2024 14:40:58 -0500 Subject: [PATCH 518/689] fix db rate limit tier column --- .../resources/db/migration/V6.1.0.2__9356-add-rate-limiting.sql | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/resources/db/migration/V6.1.0.2__9356-add-rate-limiting.sql b/src/main/resources/db/migration/V6.1.0.2__9356-add-rate-limiting.sql index be370625b3f..470483e2bf4 100644 --- a/src/main/resources/db/migration/V6.1.0.2__9356-add-rate-limiting.sql +++ b/src/main/resources/db/migration/V6.1.0.2__9356-add-rate-limiting.sql @@ -1,2 +1 @@ ALTER TABLE authenticateduser ADD COLUMN IF NOT EXISTS ratelimittier int DEFAULT 1; -UPDATE authenticateduser set ratelimittier = 1 WHERE ratelimittier = 0; \ No newline at end of file From 605097c1dd49a9526a3183cee6c05db154d33378 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Fri, 2 Feb 2024 10:20:45 -0500 Subject: [PATCH 519/689] getting tests to pass on Jenkins --- doc/release-notes/9356-rate-limiting.md | 7 +++++-- pom.xml | 4 ++-- .../iq/dataverse/cache/CacheFactoryBean.java | 8 ++++---- .../dataverse/cache/CacheFactoryBeanTest.java | 19 +++++++++++++++---- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/doc/release-notes/9356-rate-limiting.md b/doc/release-notes/9356-rate-limiting.md index 8bae4b59de4..098b20a20aa 100644 --- a/doc/release-notes/9356-rate-limiting.md +++ b/doc/release-notes/9356-rate-limiting.md @@ -19,7 +19,10 @@ In the following example, calls made by a guest user (tier 0) for API `GetLatest JVM properties to configure Hazelcast to work as a cluster. By default, Hazelcast uses Multicast to discover cluster members see https://docs.hazelcast.com/imdg/4.2/clusters/discovery-mechanisms -Valid join types: Multicast or TcpIp -Members can be listed in a CSV field of 'host:port' for each dataverse app instance +and the cluster name defaults to 'dataverse' +Cluster name can be configured using +-Ddataverse.hazelcast.cluster=dataverse-test +Valid join types: Multicast, TcpIp, AWS, or Azure +TcpIp member IPs can be listed in a CSV field of 'host:port' for each dataverse app instance -Ddataverse.hazelcast.join=TcpIp -Ddataverse.hazelcast.members=localhost:5701,localhost:5702 \ No newline at end of file diff --git a/pom.xml b/pom.xml index 4a2bc13dbc7..a90f76e2034 100644 --- a/pom.xml +++ b/pom.xml @@ -549,8 +549,8 @@ com.hazelcast - hazelcast - 5.3.6 + hazelcast-all + 4.0.2 xerces diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java index b7ec7f6736c..d060b191c36 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java @@ -30,7 +30,7 @@ public enum JoinVia { @PostConstruct public void init() { if (hazelcastInstance == null) { - hazelcastInstance = Hazelcast.newHazelcastInstance(getConfig()); + hazelcastInstance = Hazelcast.newHazelcastInstance(getHazelcastConfig()); rateLimitCache = hazelcastInstance.getMap(RATE_LIMIT_CACHE); } } @@ -104,7 +104,7 @@ public void clearCache(String cacheName) { } } - private Config getConfig() { + private Config getHazelcastConfig() { JoinVia joinVia; try { String join = System.getProperty("dataverse.hazelcast.join", "Multicast"); @@ -114,8 +114,8 @@ private Config getConfig() { joinVia = JoinVia.Multicast; } Config config = new Config(); - config.setClusterName("dataverse"); - config.getJetConfig().setEnabled(true); + String clusterName = System.getProperty("dataverse.hazelcast.cluster", "dataverse"); + config.setClusterName(clusterName); if (joinVia == JoinVia.TcpIp) { config.getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(true); String members = System.getProperty("dataverse.hazelcast.members", ""); diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index 1b4c7e973af..41e4c556312 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -25,6 +25,8 @@ public class CacheFactoryBeanTest { private SystemConfig mockedSystemConfig; static CacheFactoryBean cache = null; + // Second instance for cluster testing + static CacheFactoryBean cache2 = null; AuthenticatedUser authUser = new AuthenticatedUser(); GuestUser guestUser = GuestUser.get(); String action; @@ -75,8 +77,14 @@ public class CacheFactoryBeanTest { @BeforeAll public static void setup() { - System.setProperty(staticHazelcastSystemProperties + "join", "TcpIp"); - System.setProperty(staticHazelcastSystemProperties + "members", "localhost:5701,localhost:5702"); + System.setProperty(staticHazelcastSystemProperties + "cluster", "dataverse-test"); + if (System.getenv("JENKINS_HOME") != null) { + System.setProperty(staticHazelcastSystemProperties + "join", "AWS"); + } else { + System.setProperty(staticHazelcastSystemProperties + "join", "Multicast"); + } + //System.setProperty(staticHazelcastSystemProperties + "join", "TcpIp"); + //System.setProperty(staticHazelcastSystemProperties + "members", "localhost:5701,localhost:5702"); } @BeforeEach public void init() throws IOException { @@ -118,6 +126,10 @@ public static void cleanup() { cache.cleanup(); // PreDestroy - shutdown Hazelcast cache = null; } + if (cache2 != null) { + cache2.cleanup(); // PreDestroy - shutdown Hazelcast + cache2 = null; + } } @Test public void testGuestUserGettingRateLimited() { @@ -187,7 +199,7 @@ public void testCluster() { cache.checkRate(authUser, action); // create a second cache to test cluster - CacheFactoryBean cache2 = new CacheFactoryBean(); + cache2 = new CacheFactoryBean(); cache2.systemConfig = mockedSystemConfig; cache2.init(); // PostConstruct - start Hazelcast @@ -211,6 +223,5 @@ public void testCluster() { assertTrue(value.equals(cache.getCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key))); cache2.cleanup(); // remove cache2 assertTrue(value.equals(cache.getCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key))); - } } From 879bc5cf4703b8ce4854a4dd1d43f47f268f3922 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Fri, 2 Feb 2024 11:22:33 -0500 Subject: [PATCH 520/689] testing in jenkins --- scripts/installer/as-setup.sh | 3 +-- .../harvard/iq/dataverse/cache/CacheFactoryBeanTest.java | 7 +++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/scripts/installer/as-setup.sh b/scripts/installer/as-setup.sh index 3eb81f553e7..94f088cc9df 100755 --- a/scripts/installer/as-setup.sh +++ b/scripts/installer/as-setup.sh @@ -130,8 +130,7 @@ function preliminary_setup() ./asadmin $ASADMIN_OPTS create-network-listener --protocol http-listener-1 --listenerport 8009 --jkenabled true jk-connector # set up rate limiting using hazelcast in TcpIp discovery mode - ./asadmin $ASADMIN_OPTS create-jvm-options "\-Ddataverse.hazelcast.join=TcpIp" - ./asadmin $ASADMIN_OPTS create-jvm-options "\-Ddataverse.hazelcast.members=localhost:5701,localhost:5702" + ./asadmin $ASADMIN_OPTS create-jvm-options "\-Ddataverse.hazelcast.join=Multicast" } function final_setup(){ diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index 41e4c556312..be967ec23cc 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -78,10 +78,9 @@ public class CacheFactoryBeanTest { @BeforeAll public static void setup() { System.setProperty(staticHazelcastSystemProperties + "cluster", "dataverse-test"); + System.setProperty(staticHazelcastSystemProperties + "join", "Multicast"); if (System.getenv("JENKINS_HOME") != null) { - System.setProperty(staticHazelcastSystemProperties + "join", "AWS"); - } else { - System.setProperty(staticHazelcastSystemProperties + "join", "Multicast"); + // System.setProperty(staticHazelcastSystemProperties + "join", "AWS"); } //System.setProperty(staticHazelcastSystemProperties + "join", "TcpIp"); //System.setProperty(staticHazelcastSystemProperties + "members", "localhost:5701,localhost:5702"); @@ -97,7 +96,7 @@ public void init() throws IOException { cache.systemConfig = mockedSystemConfig; cache.init(); // PostConstruct - start Hazelcast - // clear the static data so it can be reloaded with the new mocked data + // clear the static data, so it can be reloaded with the new mocked data RateLimitUtil.rateLimitMap.clear(); RateLimitUtil.rateLimits.clear(); From 27cce94b4fa502b67533bfdda3b7750fbbca9691 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Fri, 2 Feb 2024 13:35:24 -0500 Subject: [PATCH 521/689] use payara instance of hazelcast --- doc/release-notes/9356-rate-limiting.md | 10 +-- docker-compose-dev.yml | 2 - scripts/installer/as-setup.sh | 3 - .../iq/dataverse/cache/CacheFactoryBean.java | 63 +++---------------- .../dataverse/cache/CacheFactoryBeanTest.java | 35 +++++------ 5 files changed, 23 insertions(+), 90 deletions(-) diff --git a/doc/release-notes/9356-rate-limiting.md b/doc/release-notes/9356-rate-limiting.md index 098b20a20aa..3281e80beed 100644 --- a/doc/release-notes/9356-rate-limiting.md +++ b/doc/release-notes/9356-rate-limiting.md @@ -17,12 +17,4 @@ This allows for more control over the rate limit of individual API command calls In the following example, calls made by a guest user (tier 0) for API `GetLatestPublishedDatasetVersionCommand` is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. `curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '{"rateLimits":[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]}'` -JVM properties to configure Hazelcast to work as a cluster. -By default, Hazelcast uses Multicast to discover cluster members see https://docs.hazelcast.com/imdg/4.2/clusters/discovery-mechanisms -and the cluster name defaults to 'dataverse' -Cluster name can be configured using --Ddataverse.hazelcast.cluster=dataverse-test -Valid join types: Multicast, TcpIp, AWS, or Azure -TcpIp member IPs can be listed in a CSV field of 'host:port' for each dataverse app instance --Ddataverse.hazelcast.join=TcpIp --Ddataverse.hazelcast.members=localhost:5701,localhost:5702 \ No newline at end of file +Hazelcast is configured in Payara and should not need any changes for this feature \ No newline at end of file diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 0c29813f03b..b4a7a510839 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -59,8 +59,6 @@ services: - "4949:4848" # HTTPS (Payara Admin Console) - "9009:9009" # JDWP - "8686:8686" # JMX - - "5701:5701" # Hazelcast - - "5702:5702" # Hazelcast networks: - dataverse depends_on: diff --git a/scripts/installer/as-setup.sh b/scripts/installer/as-setup.sh index 94f088cc9df..c89bcb4ff4d 100755 --- a/scripts/installer/as-setup.sh +++ b/scripts/installer/as-setup.sh @@ -128,9 +128,6 @@ function preliminary_setup() # so we can front with apache httpd ( ProxyPass / ajp://localhost:8009/ ) ./asadmin $ASADMIN_OPTS create-network-listener --protocol http-listener-1 --listenerport 8009 --jkenabled true jk-connector - - # set up rate limiting using hazelcast in TcpIp discovery mode - ./asadmin $ASADMIN_OPTS create-jvm-options "\-Ddataverse.hazelcast.join=Multicast" } function final_setup(){ diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java index d060b191c36..d3837ea8c9e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java @@ -1,17 +1,14 @@ package edu.harvard.iq.dataverse.cache; -import com.hazelcast.config.Config; -import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.util.SystemConfig; import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import jakarta.ejb.EJB; import jakarta.ejb.Singleton; import jakarta.ejb.Startup; +import jakarta.inject.Inject; -import java.util.Arrays; import java.util.logging.Logger; import java.util.Map; @@ -19,32 +16,18 @@ @Startup public class CacheFactoryBean implements java.io.Serializable { private static final Logger logger = Logger.getLogger(CacheFactoryBean.class.getCanonicalName()); - private HazelcastInstance hazelcastInstance = null; private Map rateLimitCache; @EJB SystemConfig systemConfig; + @Inject + HazelcastInstance hzInstance; public final static String RATE_LIMIT_CACHE = "rateLimitCache"; - public enum JoinVia { - Multicast, TcpIp, AWS, Azure; - } + @PostConstruct public void init() { - if (hazelcastInstance == null) { - hazelcastInstance = Hazelcast.newHazelcastInstance(getHazelcastConfig()); - rateLimitCache = hazelcastInstance.getMap(RATE_LIMIT_CACHE); - } - } - @PreDestroy - protected void cleanup() { - if (hazelcastInstance != null) { - hazelcastInstance.shutdown(); - hazelcastInstance = null; - } - } - @Override - protected void finalize() throws Throwable { - cleanup(); - super.finalize(); + logger.info("Hazelcast member:" + hzInstance.getCluster().getLocalMember()); + rateLimitCache = hzInstance.getMap(RATE_LIMIT_CACHE); + logger.info("Rate Limit Cache Size: " + rateLimitCache.size()); } /** @@ -103,36 +86,4 @@ public void clearCache(String cacheName) { break; } } - - private Config getHazelcastConfig() { - JoinVia joinVia; - try { - String join = System.getProperty("dataverse.hazelcast.join", "Multicast"); - joinVia = JoinVia.valueOf(join); - } catch (IllegalArgumentException e) { - logger.warning("dataverse.hazelcast.join must be one of " + JoinVia.values() + ". Defaulting to Multicast"); - joinVia = JoinVia.Multicast; - } - Config config = new Config(); - String clusterName = System.getProperty("dataverse.hazelcast.cluster", "dataverse"); - config.setClusterName(clusterName); - if (joinVia == JoinVia.TcpIp) { - config.getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(true); - String members = System.getProperty("dataverse.hazelcast.members", ""); - logger.info("dataverse.hazelcast.members: " + members); - try { - Arrays.stream(members.split(",")).forEach(m -> - config.getNetworkConfig().getJoin().getTcpIpConfig().addMember(m)); - } catch (IllegalArgumentException e) { - logger.warning("dataverse.hazelcast.members must contain at least 1 'host:port' entry, Defaulting to Multicast"); - joinVia = JoinVia.Multicast; - } - } - logger.info("dataverse.hazelcast.join:" + joinVia); - config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(joinVia == JoinVia.Multicast); - config.getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(joinVia == JoinVia.TcpIp); - config.getNetworkConfig().getJoin().getAwsConfig().setEnabled(joinVia == JoinVia.AWS); - config.getNetworkConfig().getJoin().getAzureConfig().setEnabled(joinVia == JoinVia.Azure); - return config; - } } diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index be967ec23cc..5063269695d 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -1,10 +1,11 @@ package edu.harvard.iq.dataverse.cache; +import com.hazelcast.config.Config; +import com.hazelcast.core.*; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.util.SystemConfig; import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -75,16 +76,6 @@ public class CacheFactoryBeanTest { " ]\n" + "}"; - @BeforeAll - public static void setup() { - System.setProperty(staticHazelcastSystemProperties + "cluster", "dataverse-test"); - System.setProperty(staticHazelcastSystemProperties + "join", "Multicast"); - if (System.getenv("JENKINS_HOME") != null) { - // System.setProperty(staticHazelcastSystemProperties + "join", "AWS"); - } - //System.setProperty(staticHazelcastSystemProperties + "join", "TcpIp"); - //System.setProperty(staticHazelcastSystemProperties + "members", "localhost:5701,localhost:5702"); - } @BeforeEach public void init() throws IOException { // reuse cache and config for all tests @@ -94,7 +85,10 @@ public void init() throws IOException { doReturn(settingJson).when(mockedSystemConfig).getRateLimitsJson(); cache = new CacheFactoryBean(); cache.systemConfig = mockedSystemConfig; - cache.init(); // PostConstruct - start Hazelcast + if (cache.hzInstance == null) { + cache.hzInstance = Hazelcast.newHazelcastInstance(new Config()); + } + cache.init(); // PostConstruct - set up Hazelcast // clear the static data, so it can be reloaded with the new mocked data RateLimitUtil.rateLimitMap.clear(); @@ -121,13 +115,11 @@ public void init() throws IOException { @AfterAll public static void cleanup() { - if (cache != null) { - cache.cleanup(); // PreDestroy - shutdown Hazelcast - cache = null; + if (cache != null && cache.hzInstance != null) { + cache.hzInstance.shutdown(); } - if (cache2 != null) { - cache2.cleanup(); // PreDestroy - shutdown Hazelcast - cache2 = null; + if (cache2 != null && cache2.hzInstance != null) { + cache2.hzInstance.shutdown(); } } @Test @@ -200,7 +192,10 @@ public void testCluster() { // create a second cache to test cluster cache2 = new CacheFactoryBean(); cache2.systemConfig = mockedSystemConfig; - cache2.init(); // PostConstruct - start Hazelcast + if (cache2.hzInstance == null) { + cache2.hzInstance = Hazelcast.newHazelcastInstance(new Config()); + } + cache2.init(); // PostConstruct - set up Hazelcast // check to see if the new cache synced with the existing cache long s1 = cache.getCacheSize(CacheFactoryBean.RATE_LIMIT_CACHE); @@ -220,7 +215,7 @@ public void testCluster() { cache2.setCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key, value); assertTrue(value.equals(cache2.getCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key))); assertTrue(value.equals(cache.getCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key))); - cache2.cleanup(); // remove cache2 + cache2.hzInstance.shutdown(); // remove cache2 assertTrue(value.equals(cache.getCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key))); } } From 9784416fe7670b78911db534e592e32f8b42d692 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Fri, 2 Feb 2024 15:59:23 -0500 Subject: [PATCH 522/689] fixes for Jenkins --- .../dataverse/cache/CacheFactoryBeanTest.java | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index 5063269695d..73e521c810c 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -1,5 +1,6 @@ package edu.harvard.iq.dataverse.cache; +import com.hazelcast.cluster.Address; import com.hazelcast.config.Config; import com.hazelcast.core.*; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; @@ -78,7 +79,7 @@ public class CacheFactoryBeanTest { @BeforeEach public void init() throws IOException { - // reuse cache and config for all tests + // Reuse cache and config for all tests if (cache == null) { mockedSystemConfig = mock(SystemConfig.class); doReturn(settingDefaultCapacity).when(mockedSystemConfig).getRateLimitingDefaultCapacityTiers(); @@ -90,11 +91,11 @@ public void init() throws IOException { } cache.init(); // PostConstruct - set up Hazelcast - // clear the static data, so it can be reloaded with the new mocked data + // Clear the static data, so it can be reloaded with the new mocked data RateLimitUtil.rateLimitMap.clear(); RateLimitUtil.rateLimits.clear(); - // testing cache implementation and code coverage + // Testing cache implementation and code coverage final String cacheKey = "CacheTestKey" + UUID.randomUUID(); final String cacheValue = "CacheTestValue" + UUID.randomUUID(); long cacheSize = cache.getCacheSize(cache.RATE_LIMIT_CACHE); @@ -104,12 +105,12 @@ public void init() throws IOException { assertTrue(cacheValueObj != null && cacheValue.equalsIgnoreCase((String) cacheValueObj)); } - // reset to default auth user + // Reset to default auth user authUser.setRateLimitTier(1); authUser.setSuperuser(false); authUser.setUserIdentifier("authUser"); - // create a unique action for each test + // Create a unique action for each test action = "cmd-" + UUID.randomUUID(); } @@ -165,7 +166,7 @@ public void testAuthenticatedUserGettingRateLimited() throws InterruptedExceptio assertTrue(rateLimited && cnt == 120, "rateLimited:"+rateLimited + " cnt:"+cnt); for (cnt = 0; cnt <60; cnt++) { - Thread.sleep(1000);// wait for bucket to be replenished (check each second for 1 minute max) + Thread.sleep(1000);// Wait for bucket to be replenished (check each second for 1 minute max) rateLimited = !cache.checkRate(authUser, action); if (!rateLimited) { break; @@ -186,36 +187,45 @@ public void testAuthenticatedUserGettingRateLimited() throws InterruptedExceptio @Test public void testCluster() { - //make sure at least 1 entry is in the original cache + // Make sure at least 1 entry is in the original cache cache.checkRate(authUser, action); - // create a second cache to test cluster + // Create a second cache to test cluster cache2 = new CacheFactoryBean(); cache2.systemConfig = mockedSystemConfig; if (cache2.hzInstance == null) { cache2.hzInstance = Hazelcast.newHazelcastInstance(new Config()); + + // Needed for Jenkins to form cluster based on TcpIp since Multicast fails + Address m1 = cache.hzInstance.getCluster().getLocalMember().getAddress(); + Address m2 = cache2.hzInstance.getCluster().getLocalMember().getAddress(); + String members = String.format("%s:%d,%s:%d", m1.getHost(),m1.getPort(),m2.getHost(),m2.getPort()); + cache.hzInstance.getConfig().getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(true).addMember(members); + cache2.hzInstance.getConfig().getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(true).addMember(members); } cache2.init(); // PostConstruct - set up Hazelcast - // check to see if the new cache synced with the existing cache + // Check to see if the new cache synced with the existing cache long s1 = cache.getCacheSize(CacheFactoryBean.RATE_LIMIT_CACHE); long s2 = cache2.getCacheSize(CacheFactoryBean.RATE_LIMIT_CACHE); assertTrue(s1 > 0 && s1 == s2, "Size1:" + s1 + " Size2:" + s2 ); String key = "key1"; String value = "value1"; - // verify that both caches stay in sync + // Verify that both caches stay in sync cache.setCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key, value); assertTrue(value.equals(cache2.getCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key))); - // clearing one cache also clears the other cache in the cluster + // Clearing one cache also clears the other cache in the cluster cache2.clearCache(CacheFactoryBean.RATE_LIMIT_CACHE); assertTrue(String.valueOf(cache.getCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key)).isEmpty()); - // verify no issue dropping one node from cluster + // Verify no issue dropping one node from cluster cache2.setCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key, value); assertTrue(value.equals(cache2.getCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key))); assertTrue(value.equals(cache.getCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key))); - cache2.hzInstance.shutdown(); // remove cache2 + // Shut down hazelcast on cache2 and make sure data is still available in original cache + cache2.hzInstance.shutdown(); + cache2 = null; assertTrue(value.equals(cache.getCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key))); } } From 21b095176a9fbd5f15f632198934a760db950706 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Fri, 2 Feb 2024 16:03:32 -0500 Subject: [PATCH 523/689] fixes for Jenkins --- docker-compose-dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index b4a7a510839..ae0aa2bdf76 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -232,6 +232,7 @@ services: MINIO_ROOT_USER: 4cc355_k3y MINIO_ROOT_PASSWORD: s3cr3t_4cc355_k3y command: server /data + networks: dataverse: driver: bridge From 465c5d5901318da19638223bb035de49b9d6b99b Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Fri, 2 Feb 2024 17:04:05 -0500 Subject: [PATCH 524/689] fixes for Jenkins --- .../dataverse/cache/CacheFactoryBeanTest.java | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index 73e521c810c..96a9b58315f 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.util.UUID; +import java.util.logging.Logger; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.doReturn; @@ -24,7 +25,7 @@ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) public class CacheFactoryBeanTest { - + private static final Logger logger = Logger.getLogger(CacheFactoryBeanTest.class.getCanonicalName()); private SystemConfig mockedSystemConfig; static CacheFactoryBean cache = null; // Second instance for cluster testing @@ -87,7 +88,7 @@ public void init() throws IOException { cache = new CacheFactoryBean(); cache.systemConfig = mockedSystemConfig; if (cache.hzInstance == null) { - cache.hzInstance = Hazelcast.newHazelcastInstance(new Config()); + cache.hzInstance = Hazelcast.newHazelcastInstance(getConfig()); } cache.init(); // PostConstruct - set up Hazelcast @@ -194,14 +195,11 @@ public void testCluster() { cache2 = new CacheFactoryBean(); cache2.systemConfig = mockedSystemConfig; if (cache2.hzInstance == null) { - cache2.hzInstance = Hazelcast.newHazelcastInstance(new Config()); - // Needed for Jenkins to form cluster based on TcpIp since Multicast fails - Address m1 = cache.hzInstance.getCluster().getLocalMember().getAddress(); - Address m2 = cache2.hzInstance.getCluster().getLocalMember().getAddress(); - String members = String.format("%s:%d,%s:%d", m1.getHost(),m1.getPort(),m2.getHost(),m2.getPort()); - cache.hzInstance.getConfig().getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(true).addMember(members); - cache2.hzInstance.getConfig().getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(true).addMember(members); + Address initialCache = cache.hzInstance.getCluster().getLocalMember().getAddress(); + String members = String.format("%s:%d", initialCache.getHost(),initialCache.getPort()); + logger.info("Switching to TcpIp mode with members: " + members); + cache2.hzInstance = Hazelcast.newHazelcastInstance(getConfig(members)); } cache2.init(); // PostConstruct - set up Hazelcast @@ -228,4 +226,21 @@ public void testCluster() { cache2 = null; assertTrue(value.equals(cache.getCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key))); } + + private Config getConfig() { + return getConfig(null); + } + private Config getConfig(String members) { + Config config = new Config(); + config.getNetworkConfig().getJoin().getAutoDetectionConfig().setEnabled(false); + config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(false); + config.getNetworkConfig().getJoin().getAwsConfig().setEnabled(false); + config.getNetworkConfig().getJoin().getAzureConfig().setEnabled(false); + config.getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(true); + if (members != null) { + config.getNetworkConfig().getJoin().getAutoDetectionConfig().setEnabled(true); + config.getNetworkConfig().getJoin().getTcpIpConfig().addMember(members); + } + return config; + } } From e5fe18fc3c454df194b82cdeb97513f23feb18f4 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Fri, 2 Feb 2024 17:08:52 -0500 Subject: [PATCH 525/689] fixes for Jenkins --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index a90f76e2034..4a2bc13dbc7 100644 --- a/pom.xml +++ b/pom.xml @@ -549,8 +549,8 @@ com.hazelcast - hazelcast-all - 4.0.2 + hazelcast + 5.3.6 xerces From 15ef82eb4241248864e0e411a545b9388ea9f004 Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Mon, 5 Feb 2024 15:14:42 -0500 Subject: [PATCH 526/689] Update pom.xml Co-authored-by: Oliver Bertuch --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4a2bc13dbc7..e229b35fd0a 100644 --- a/pom.xml +++ b/pom.xml @@ -550,7 +550,7 @@ com.hazelcast hazelcast - 5.3.6 + provided xerces From 77cede2bb9848037a5ded7ed8f20635bec1fd935 Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Mon, 5 Feb 2024 15:18:24 -0500 Subject: [PATCH 527/689] Update src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java Co-authored-by: Oliver Bertuch --- .../edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index 96a9b58315f..fd05f216eb0 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -33,7 +33,6 @@ public class CacheFactoryBeanTest { AuthenticatedUser authUser = new AuthenticatedUser(); GuestUser guestUser = GuestUser.get(); String action; - static final String staticHazelcastSystemProperties = "dataverse.hazelcast."; static final String settingDefaultCapacity = "30,60,120"; static final String settingJson = "{\n" + " \"rateLimits\":[\n" + From 02cd0d03581443500366d2ff835e18e6f8d7c661 Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Mon, 5 Feb 2024 15:19:33 -0500 Subject: [PATCH 528/689] Update pom.xml Co-authored-by: Oliver Bertuch --- pom.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pom.xml b/pom.xml index e229b35fd0a..529d2fa35c3 100644 --- a/pom.xml +++ b/pom.xml @@ -542,11 +542,6 @@ dataverse-spi 2.0.0 - - javax.cache - cache-api - 1.1.1 - com.hazelcast hazelcast From 9b95e4db7f289087749e88c8760bc5f9e3cace49 Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Mon, 5 Feb 2024 15:20:07 -0500 Subject: [PATCH 529/689] Update src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java Co-authored-by: Oliver Bertuch --- .../java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java index d3837ea8c9e..4282a77b6af 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java @@ -16,6 +16,7 @@ @Startup public class CacheFactoryBean implements java.io.Serializable { private static final Logger logger = Logger.getLogger(CacheFactoryBean.class.getCanonicalName()); + // Retrieved from Hazelcast, implements ConcurrentMap and is threadsafe private Map rateLimitCache; @EJB SystemConfig systemConfig; From 52e714bd1b8ecf12a163d24535171d09bd33260a Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 6 Feb 2024 12:29:37 -0500 Subject: [PATCH 530/689] review comments re: JCache --- .../source/installation/config.rst | 4 +- pom.xml | 17 +- .../iq/dataverse/cache/CacheFactoryBean.java | 61 ++------ .../iq/dataverse/cache/RateLimitUtil.java | 9 +- .../dataverse/cache/CacheFactoryBeanTest.java | 147 ++++++++++++------ 5 files changed, 134 insertions(+), 104 deletions(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index c60953c66f5..98513024160 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1387,10 +1387,12 @@ Note: If either of these settings exist in the database rate limiting will be en - RateLimitingDefaultCapacityTiers is the number of calls allowed per hour if the specific command is not configured. The values represent the number of calls per hour per user for tiers 0,1,... A value of -1 can be used to signify no rate limit. Also, by default, a tier not defined would receive a default of no limit. - ``curl http://localhost:8080/api/admin/settings/:RateLimitingDefaultCapacityTiers -X PUT -d '10000,20000'`` +.. code-block:: bash + curl http://localhost:8080/api/admin/settings/:RateLimitingDefaultCapacityTiers -X PUT -d '10000,20000' - RateLimitingCapacityByTierAndAction is a Json object specifying the rate by tier and a list of actions (commands). This allows for more control over the rate limit of individual API command calls. In the following example, calls made by a guest user (tier 0) for API GetLatestPublishedDatasetVersionCommand is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. +.. code-block:: bash curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '{"rateLimits":[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]}' .. _Branding Your Installation: diff --git a/pom.xml b/pom.xml index 529d2fa35c3..0544c29fa15 100644 --- a/pom.xml +++ b/pom.xml @@ -543,14 +543,9 @@ 2.0.0 - com.hazelcast - hazelcast - provided - - - xerces - xercesImpl - 2.11.0 + javax.cache + cache-api + 1.1.1 @@ -663,6 +658,12 @@ 3.9.0 test + + com.hazelcast + hazelcast + 5.3.6 + test + diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java index 4282a77b6af..2c3eabd9c4e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java @@ -1,6 +1,5 @@ package edu.harvard.iq.dataverse.cache; -import com.hazelcast.core.HazelcastInstance; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.util.SystemConfig; import jakarta.annotation.PostConstruct; @@ -9,26 +8,33 @@ import jakarta.ejb.Startup; import jakarta.inject.Inject; +import javax.cache.Cache; +import javax.cache.CacheManager; +import javax.cache.configuration.CompleteConfiguration; +import javax.cache.configuration.MutableConfiguration; +import javax.cache.spi.CachingProvider; import java.util.logging.Logger; -import java.util.Map; @Singleton @Startup public class CacheFactoryBean implements java.io.Serializable { private static final Logger logger = Logger.getLogger(CacheFactoryBean.class.getCanonicalName()); // Retrieved from Hazelcast, implements ConcurrentMap and is threadsafe - private Map rateLimitCache; + Cache rateLimitCache; @EJB SystemConfig systemConfig; @Inject - HazelcastInstance hzInstance; + CacheManager manager; + @Inject + CachingProvider provider; public final static String RATE_LIMIT_CACHE = "rateLimitCache"; @PostConstruct public void init() { - logger.info("Hazelcast member:" + hzInstance.getCluster().getLocalMember()); - rateLimitCache = hzInstance.getMap(RATE_LIMIT_CACHE); - logger.info("Rate Limit Cache Size: " + rateLimitCache.size()); + CompleteConfiguration config = + new MutableConfiguration() + .setTypes( String.class, String.class ); + rateLimitCache = manager.createCache(RATE_LIMIT_CACHE, config); } /** @@ -46,45 +52,4 @@ public boolean checkRate(User user, String action) { return (!RateLimitUtil.rateLimited(rateLimitCache, cacheKey, capacity)); } } - - public long getCacheSize(String cacheName) { - long cacheSize = 0; - switch (cacheName) { - case RATE_LIMIT_CACHE: - cacheSize = rateLimitCache.size(); - break; - default: - break; - } - return cacheSize; - } - public Object getCacheValue(String cacheName, String key) { - Object cacheValue = null; - switch (cacheName) { - case RATE_LIMIT_CACHE: - cacheValue = rateLimitCache.containsKey(key) ? rateLimitCache.get(key) : ""; - break; - default: - break; - } - return cacheValue; - } - public void setCacheValue(String cacheName, String key, Object value) { - switch (cacheName) { - case RATE_LIMIT_CACHE: - rateLimitCache.put(key, (String) value); - break; - default: - break; - } - } - public void clearCache(String cacheName) { - switch (cacheName) { - case RATE_LIMIT_CACHE: - rateLimitCache.clear(); - break; - default: - break; - } - } } diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java index b710138865f..6d4c8352ce1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java @@ -10,6 +10,7 @@ import jakarta.json.JsonObject; import jakarta.json.JsonReader; +import javax.cache.Cache; import java.io.StringReader; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -43,7 +44,7 @@ static int getCapacity(SystemConfig systemConfig, User user, String action) { getCapacityByTierAndAction(systemConfig, ((AuthenticatedUser) user).getRateLimitTier(), action) : getCapacityByTierAndAction(systemConfig, 0, action); } - static boolean rateLimited(final Map rateLimitCache, final String key, int capacityPerHour) { + static boolean rateLimited(final Cache rateLimitCache, final String key, int capacityPerHour) { if (capacityPerHour == NO_LIMIT) { return false; } @@ -95,6 +96,7 @@ static void init(SystemConfig systemConfig) { for default if no action defined: "{tier}:" and the value is the default limit for the tier for each action: "{tier}:{action}" and the value is the limit defined in the setting */ + rateLimitMap.clear(); rateLimits.forEach(r -> { r.setDefaultLimit(getCapacityByTier(systemConfig, r.getTier())); rateLimitMap.put(getMapKey(r.getTier()), r.getDefaultLimitPerHour()); @@ -103,7 +105,8 @@ static void init(SystemConfig systemConfig) { } static void getRateLimitsFromJson(SystemConfig systemConfig) { String setting = systemConfig.getRateLimitsJson(); - if (!setting.isEmpty() && rateLimits.isEmpty()) { + rateLimits.clear(); + if (!setting.isEmpty()) { try { JsonReader jr = Json.createReader(new StringReader(setting)); JsonObject obj= jr.readObject(); @@ -127,7 +130,7 @@ static String getMapKey(int tier, String action) { } return key.toString(); } - static long longFromKey(Map cache, String key) { + static long longFromKey(Cache cache, String key) { Object l = cache.get(key); return l != null ? Long.parseLong(String.valueOf(l)) : 0L; } diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index fd05f216eb0..36e0c42e3ed 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -2,7 +2,9 @@ import com.hazelcast.cluster.Address; import com.hazelcast.config.Config; -import com.hazelcast.core.*; +import com.hazelcast.core.Hazelcast; +import com.hazelcast.core.HazelcastInstance; +import com.hazelcast.map.IMap; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.util.SystemConfig; @@ -14,7 +16,18 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import javax.cache.Cache; +import javax.cache.CacheManager; +import javax.cache.configuration.CacheEntryListenerConfiguration; +import javax.cache.configuration.Configuration; +import javax.cache.integration.CompletionListener; +import javax.cache.processor.EntryProcessor; +import javax.cache.processor.EntryProcessorException; +import javax.cache.processor.EntryProcessorResult; import java.io.IOException; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.logging.Logger; @@ -28,8 +41,7 @@ public class CacheFactoryBeanTest { private static final Logger logger = Logger.getLogger(CacheFactoryBeanTest.class.getCanonicalName()); private SystemConfig mockedSystemConfig; static CacheFactoryBean cache = null; - // Second instance for cluster testing - static CacheFactoryBean cache2 = null; + AuthenticatedUser authUser = new AuthenticatedUser(); GuestUser guestUser = GuestUser.get(); String action; @@ -86,23 +98,13 @@ public void init() throws IOException { doReturn(settingJson).when(mockedSystemConfig).getRateLimitsJson(); cache = new CacheFactoryBean(); cache.systemConfig = mockedSystemConfig; - if (cache.hzInstance == null) { - cache.hzInstance = Hazelcast.newHazelcastInstance(getConfig()); + if (cache.rateLimitCache == null) { + cache.rateLimitCache = new TestCache(getConfig()); } - cache.init(); // PostConstruct - set up Hazelcast // Clear the static data, so it can be reloaded with the new mocked data RateLimitUtil.rateLimitMap.clear(); RateLimitUtil.rateLimits.clear(); - - // Testing cache implementation and code coverage - final String cacheKey = "CacheTestKey" + UUID.randomUUID(); - final String cacheValue = "CacheTestValue" + UUID.randomUUID(); - long cacheSize = cache.getCacheSize(cache.RATE_LIMIT_CACHE); - cache.setCacheValue(cache.RATE_LIMIT_CACHE, cacheKey,cacheValue); - assertTrue(cache.getCacheSize(cache.RATE_LIMIT_CACHE) > cacheSize); - Object cacheValueObj = cache.getCacheValue(cache.RATE_LIMIT_CACHE, cacheKey); - assertTrue(cacheValueObj != null && cacheValue.equalsIgnoreCase((String) cacheValueObj)); } // Reset to default auth user @@ -116,12 +118,7 @@ public void init() throws IOException { @AfterAll public static void cleanup() { - if (cache != null && cache.hzInstance != null) { - cache.hzInstance.shutdown(); - } - if (cache2 != null && cache2.hzInstance != null) { - cache2.hzInstance.shutdown(); - } + Hazelcast.shutdownAll(); } @Test public void testGuestUserGettingRateLimited() { @@ -133,7 +130,8 @@ public void testGuestUserGettingRateLimited() { break; } } - assertTrue(cache.getCacheSize(cache.RATE_LIMIT_CACHE) > 0); + String key = RateLimitUtil.generateCacheKey(guestUser, action); + assertTrue(cache.rateLimitCache.containsKey(key)); assertTrue(rateLimited && cnt > 1 && cnt <= 30, "rateLimited:"+rateLimited + " cnt:"+cnt); } @@ -189,41 +187,34 @@ public void testAuthenticatedUserGettingRateLimited() throws InterruptedExceptio public void testCluster() { // Make sure at least 1 entry is in the original cache cache.checkRate(authUser, action); + String key = RateLimitUtil.generateCacheKey(authUser, action); // Create a second cache to test cluster - cache2 = new CacheFactoryBean(); + CacheFactoryBean cache2 = new CacheFactoryBean(); cache2.systemConfig = mockedSystemConfig; - if (cache2.hzInstance == null) { - // Needed for Jenkins to form cluster based on TcpIp since Multicast fails - Address initialCache = cache.hzInstance.getCluster().getLocalMember().getAddress(); - String members = String.format("%s:%d", initialCache.getHost(),initialCache.getPort()); - logger.info("Switching to TcpIp mode with members: " + members); - cache2.hzInstance = Hazelcast.newHazelcastInstance(getConfig(members)); - } - cache2.init(); // PostConstruct - set up Hazelcast + // join cluster with original Hazelcast instance + cache2.rateLimitCache = new TestCache(getConfig(cache.rateLimitCache.get("memberAddress"))); // Check to see if the new cache synced with the existing cache - long s1 = cache.getCacheSize(CacheFactoryBean.RATE_LIMIT_CACHE); - long s2 = cache2.getCacheSize(CacheFactoryBean.RATE_LIMIT_CACHE); - assertTrue(s1 > 0 && s1 == s2, "Size1:" + s1 + " Size2:" + s2 ); + assertTrue(cache.rateLimitCache.get(key).equals(cache2.rateLimitCache.get(key))); - String key = "key1"; + key = "key1"; String value = "value1"; // Verify that both caches stay in sync - cache.setCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key, value); - assertTrue(value.equals(cache2.getCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key))); + cache.rateLimitCache.put(key, value); + assertTrue(value.equals(cache2.rateLimitCache.get(key))); // Clearing one cache also clears the other cache in the cluster - cache2.clearCache(CacheFactoryBean.RATE_LIMIT_CACHE); - assertTrue(String.valueOf(cache.getCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key)).isEmpty()); + cache2.rateLimitCache.clear(); + assertTrue(cache.rateLimitCache.get(key) == null); // Verify no issue dropping one node from cluster - cache2.setCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key, value); - assertTrue(value.equals(cache2.getCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key))); - assertTrue(value.equals(cache.getCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key))); + cache2.rateLimitCache.put(key, value); + assertTrue(value.equals(cache2.rateLimitCache.get(key))); + assertTrue(value.equals(cache.rateLimitCache.get(key))); // Shut down hazelcast on cache2 and make sure data is still available in original cache - cache2.hzInstance.shutdown(); + cache2.rateLimitCache.close(); cache2 = null; - assertTrue(value.equals(cache.getCacheValue(CacheFactoryBean.RATE_LIMIT_CACHE, key))); + assertTrue(value.equals(cache.rateLimitCache.get(key))); } private Config getConfig() { @@ -242,4 +233,72 @@ private Config getConfig(String members) { } return config; } + + // convert Hazelcast IMap to JCache Cache + private class TestCache implements Cache{ + HazelcastInstance hzInstance; + IMap cache; + TestCache(Config config) { + hzInstance = Hazelcast.newHazelcastInstance(config); + cache = hzInstance.getMap("test"); + Address address = hzInstance.getCluster().getLocalMember().getAddress(); + cache.put("memberAddress", String.format("%s:%d", address.getHost(), address.getPort())); + } + @Override + public String get(String s) {return cache.get(s);} + @Override + public Map getAll(Set set) {return null;} + @Override + public boolean containsKey(String s) {return get(s) != null;} + @Override + public void loadAll(Set set, boolean b, CompletionListener completionListener) {} + @Override + public void put(String s, String s2) {cache.put(s,s2);} + @Override + public String getAndPut(String s, String s2) {return null;} + @Override + public void putAll(Map map) {} + @Override + public boolean putIfAbsent(String s, String s2) {return false;} + @Override + public boolean remove(String s) {return false;} + @Override + public boolean remove(String s, String s2) {return false;} + @Override + public String getAndRemove(String s) {return null;} + @Override + public boolean replace(String s, String s2, String v1) {return false;} + @Override + public boolean replace(String s, String s2) {return false;} + @Override + public String getAndReplace(String s, String s2) {return null;} + @Override + public void removeAll(Set set) {} + @Override + public void removeAll() {} + @Override + public void clear() {cache.clear();} + @Override + public > C getConfiguration(Class aClass) {return null;} + @Override + public T invoke(String s, EntryProcessor entryProcessor, Object... objects) throws EntryProcessorException {return null;} + @Override + public Map> invokeAll(Set set, EntryProcessor entryProcessor, Object... objects) {return null;} + @Override + public String getName() {return null;} + @Override + public CacheManager getCacheManager() {return null;} + @Override + public void close() {hzInstance.shutdown();} + @Override + public boolean isClosed() {return false;} + @Override + public T unwrap(Class aClass) {return null;} + @Override + public void registerCacheEntryListener(CacheEntryListenerConfiguration cacheEntryListenerConfiguration) {} + @Override + public void deregisterCacheEntryListener(CacheEntryListenerConfiguration cacheEntryListenerConfiguration) {} + @Override + public Iterator> iterator() {return null;} + } } From 669d273b6c8f25e5c53727635b695028f5eaef49 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 6 Feb 2024 12:39:38 -0500 Subject: [PATCH 531/689] review comments re: JCache --- .../source/installation/config.rst | 4 + .../dataverse/cache/CacheFactoryBeanTest.java | 112 +++++++++++++----- 2 files changed, 88 insertions(+), 28 deletions(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 98513024160..41411b5dfee 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1387,12 +1387,16 @@ Note: If either of these settings exist in the database rate limiting will be en - RateLimitingDefaultCapacityTiers is the number of calls allowed per hour if the specific command is not configured. The values represent the number of calls per hour per user for tiers 0,1,... A value of -1 can be used to signify no rate limit. Also, by default, a tier not defined would receive a default of no limit. + .. code-block:: bash + curl http://localhost:8080/api/admin/settings/:RateLimitingDefaultCapacityTiers -X PUT -d '10000,20000' - RateLimitingCapacityByTierAndAction is a Json object specifying the rate by tier and a list of actions (commands). This allows for more control over the rate limit of individual API command calls. In the following example, calls made by a guest user (tier 0) for API GetLatestPublishedDatasetVersionCommand is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. + .. code-block:: bash + curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '{"rateLimits":[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]}' .. _Branding Your Installation: diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java index 36e0c42e3ed..59027913dee 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java @@ -245,60 +245,116 @@ private class TestCache implements Cache{ cache.put("memberAddress", String.format("%s:%d", address.getHost(), address.getPort())); } @Override - public String get(String s) {return cache.get(s);} + public String get(String s) { + return cache.get(s); + } @Override - public Map getAll(Set set) {return null;} + public Map getAll(Set set) { + return null; + } @Override - public boolean containsKey(String s) {return get(s) != null;} + public boolean containsKey(String s) { + return get(s) != null; + } @Override - public void loadAll(Set set, boolean b, CompletionListener completionListener) {} + public void loadAll(Set set, boolean b, CompletionListener completionListener) { + + } @Override - public void put(String s, String s2) {cache.put(s,s2);} + public void put(String s, String s2) { + cache.put(s,s2); + } @Override - public String getAndPut(String s, String s2) {return null;} + public String getAndPut(String s, String s2) { + return null; + } @Override - public void putAll(Map map) {} + public void putAll(Map map) { + + } @Override - public boolean putIfAbsent(String s, String s2) {return false;} + public boolean putIfAbsent(String s, String s2) { + return false; + } @Override - public boolean remove(String s) {return false;} + public boolean remove(String s) { + return false; + } @Override - public boolean remove(String s, String s2) {return false;} + public boolean remove(String s, String s2) { + return false; + } @Override - public String getAndRemove(String s) {return null;} + public String getAndRemove(String s) { + return null; + } @Override - public boolean replace(String s, String s2, String v1) {return false;} + public boolean replace(String s, String s2, String v1) { + return false; + } @Override - public boolean replace(String s, String s2) {return false;} + public boolean replace(String s, String s2) { + return false; + } @Override - public String getAndReplace(String s, String s2) {return null;} + public String getAndReplace(String s, String s2) { + return null; + } @Override - public void removeAll(Set set) {} + public void removeAll(Set set) { + + } @Override - public void removeAll() {} + public void removeAll() { + + } @Override - public void clear() {cache.clear();} + public void clear() { + cache.clear(); + } @Override - public > C getConfiguration(Class aClass) {return null;} + public > C getConfiguration(Class aClass) { + return null; + } @Override - public T invoke(String s, EntryProcessor entryProcessor, Object... objects) throws EntryProcessorException {return null;} + public T invoke(String s, EntryProcessor entryProcessor, Object... objects) throws EntryProcessorException { + return null; + } @Override - public Map> invokeAll(Set set, EntryProcessor entryProcessor, Object... objects) {return null;} + public Map> invokeAll(Set set, EntryProcessor entryProcessor, Object... objects) { + return null; + } @Override - public String getName() {return null;} + public String getName() { + return null; + } @Override - public CacheManager getCacheManager() {return null;} + public CacheManager getCacheManager() { + return null; + } @Override - public void close() {hzInstance.shutdown();} + public void close() { + hzInstance.shutdown(); + } @Override - public boolean isClosed() {return false;} + public boolean isClosed() { + return false; + } @Override - public T unwrap(Class aClass) {return null;} + public T unwrap(Class aClass) { + return null; + } @Override - public void registerCacheEntryListener(CacheEntryListenerConfiguration cacheEntryListenerConfiguration) {} + public void registerCacheEntryListener(CacheEntryListenerConfiguration cacheEntryListenerConfiguration) { + + } @Override - public void deregisterCacheEntryListener(CacheEntryListenerConfiguration cacheEntryListenerConfiguration) {} + public void deregisterCacheEntryListener(CacheEntryListenerConfiguration cacheEntryListenerConfiguration) { + + } @Override - public Iterator> iterator() {return null;} + public Iterator> iterator() { + return null; + } } } From 9800fc16bf6827b08ad9b040e07f8166328be0a6 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 6 Feb 2024 15:13:15 -0500 Subject: [PATCH 532/689] doc change --- doc/release-notes/9356-rate-limiting.md | 2 +- doc/sphinx-guides/source/installation/config.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/release-notes/9356-rate-limiting.md b/doc/release-notes/9356-rate-limiting.md index 3281e80beed..b05fa5e2131 100644 --- a/doc/release-notes/9356-rate-limiting.md +++ b/doc/release-notes/9356-rate-limiting.md @@ -1,4 +1,4 @@ -## Rate Limiting using JCache (with Hazelcast as a provider) +## Rate Limiting using JCache (with Hazelcast as provided by Payara) The option to rate limit has been added to prevent users from over taxing the system either deliberately or by runaway automated processes. Rate limiting can be configured on a tier level with tier 0 being reserved for guest users and tiers 1-any for authenticated users. Superuser accounts are exempt from rate limiting. diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 41411b5dfee..2022987cae2 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1375,8 +1375,8 @@ Before being moved there, .. _cache-rate-limiting: -Configure Your Dataverse Installation to use JCache (with Hazelcast as a provider) for Rate Limiting ----------------------------------------------------------------------------------------------------- +Configure Your Dataverse Installation to use JCache (with Hazelcast as provided by Payara) for Rate Limiting +------------------------------------------------------------------------------------------------------------ Rate limiting has been added to prevent users from over taxing the system either deliberately or by runaway automated processes. Rate limiting can be configured on a tier level with tier 0 being reserved for guest users and tiers 1-any for authenticated users. From ae0ec5a3f8a697c0969c4e641a920fd54823f742 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Wed, 7 Feb 2024 15:46:07 -0500 Subject: [PATCH 533/689] fix bad merge --- src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java index b388e978808..3f2f36ea36a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java @@ -1154,8 +1154,8 @@ public boolean isStoringIngestedFilesWithHeaders() { return settingsService.isTrueForKey(SettingsServiceBean.Key.StoreIngestedTabularFilesWithVarHeaders, false); } - /* - RateLimitUtil will parse the json to create a List + /** + * RateLimitUtil will parse the json to create a List */ public String getRateLimitsJson() { return settingsService.getValueForKey(SettingsServiceBean.Key.RateLimitingCapacityByTierAndAction, ""); From 5e507a020224af856ea505e5da97b7f6d7e3285a Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Wed, 7 Feb 2024 15:53:52 -0500 Subject: [PATCH 534/689] moving cache to util/cache --- src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java | 2 +- .../iq/dataverse/{ => util}/cache/CacheFactoryBean.java | 2 +- .../iq/dataverse/{ => util}/cache/RateLimitSetting.java | 2 +- .../harvard/iq/dataverse/{ => util}/cache/RateLimitUtil.java | 2 +- .../iq/dataverse/{ => util}/cache/CacheFactoryBeanTest.java | 2 +- .../iq/dataverse/{ => util}/cache/RateLimitUtilTest.java | 3 ++- 6 files changed, 7 insertions(+), 6 deletions(-) rename src/main/java/edu/harvard/iq/dataverse/{ => util}/cache/CacheFactoryBean.java (97%) rename src/main/java/edu/harvard/iq/dataverse/{ => util}/cache/RateLimitSetting.java (96%) rename src/main/java/edu/harvard/iq/dataverse/{ => util}/cache/RateLimitUtil.java (99%) rename src/test/java/edu/harvard/iq/dataverse/{ => util}/cache/CacheFactoryBeanTest.java (99%) rename src/test/java/edu/harvard/iq/dataverse/{ => util}/cache/RateLimitUtilTest.java (98%) diff --git a/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java b/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java index 8636172b731..553e2d7497e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java +++ b/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java @@ -4,7 +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.cache.CacheFactoryBean; +import edu.harvard.iq.dataverse.util.cache.CacheFactoryBean; import edu.harvard.iq.dataverse.engine.DataverseEngine; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean; diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java b/src/main/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBean.java similarity index 97% rename from src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java rename to src/main/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBean.java index 2c3eabd9c4e..384391e200b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/CacheFactoryBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBean.java @@ -1,4 +1,4 @@ -package edu.harvard.iq.dataverse.cache; +package edu.harvard.iq.dataverse.util.cache; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.util.SystemConfig; diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitSetting.java b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitSetting.java similarity index 96% rename from src/main/java/edu/harvard/iq/dataverse/cache/RateLimitSetting.java rename to src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitSetting.java index 752f9860127..cf9c9a5410e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitSetting.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitSetting.java @@ -1,4 +1,4 @@ -package edu.harvard.iq.dataverse.cache; +package edu.harvard.iq.dataverse.util.cache; import jakarta.json.bind.annotation.JsonbProperty; diff --git a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java similarity index 99% rename from src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java rename to src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java index 6d4c8352ce1..64c86b0f25f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/cache/RateLimitUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java @@ -1,4 +1,4 @@ -package edu.harvard.iq.dataverse.cache; +package edu.harvard.iq.dataverse.util.cache; import com.google.gson.Gson; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBeanTest.java similarity index 99% rename from src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java rename to src/test/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBeanTest.java index 59027913dee..b271ec42b82 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBeanTest.java @@ -1,4 +1,4 @@ -package edu.harvard.iq.dataverse.cache; +package edu.harvard.iq.dataverse.util.cache; import com.hazelcast.cluster.Address; import com.hazelcast.config.Config; diff --git a/src/test/java/edu/harvard/iq/dataverse/cache/RateLimitUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtilTest.java similarity index 98% rename from src/test/java/edu/harvard/iq/dataverse/cache/RateLimitUtilTest.java rename to src/test/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtilTest.java index 033f9dbb67e..23ba3673252 100644 --- a/src/test/java/edu/harvard/iq/dataverse/cache/RateLimitUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtilTest.java @@ -1,9 +1,10 @@ -package edu.harvard.iq.dataverse.cache; +package edu.harvard.iq.dataverse.util.cache; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.util.SystemConfig; +import edu.harvard.iq.dataverse.util.cache.RateLimitUtil; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; From 0774223c6e8982fd0c04629735db8b218605ed1e Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Wed, 7 Feb 2024 16:05:04 -0500 Subject: [PATCH 535/689] review comments fixed --- .../edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java index 64c86b0f25f..09057c13ab8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java @@ -12,7 +12,10 @@ import javax.cache.Cache; import java.io.StringReader; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Logger; From d5b1fb5617b096068fa10f11ea3112470bbadab3 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Fri, 9 Feb 2024 14:00:30 -0500 Subject: [PATCH 536/689] rename db script --- ...add-rate-limiting.sql => V6.1.0.3__9356-add-rate-limiting.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V6.1.0.2__9356-add-rate-limiting.sql => V6.1.0.3__9356-add-rate-limiting.sql} (100%) diff --git a/src/main/resources/db/migration/V6.1.0.2__9356-add-rate-limiting.sql b/src/main/resources/db/migration/V6.1.0.3__9356-add-rate-limiting.sql similarity index 100% rename from src/main/resources/db/migration/V6.1.0.2__9356-add-rate-limiting.sql rename to src/main/resources/db/migration/V6.1.0.3__9356-add-rate-limiting.sql From 54f1077196e4d1603a5cc538d7e4ec477c572dc5 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 20 Feb 2024 13:20:17 -0500 Subject: [PATCH 537/689] review comments --- .../examples/rate-limit-actions-setting.json | 42 +++++ .../source/installation/config.rst | 16 +- pom.xml | 2 - .../iq/dataverse/EjbDataverseEngine.java | 2 +- .../util/cache/CacheFactoryBean.java | 6 +- .../util/cache/CacheFactoryBeanTest.java | 140 ++++++---------- .../util/cache/RateLimitUtilTest.java | 155 ++++++++++-------- 7 files changed, 203 insertions(+), 160 deletions(-) create mode 100644 doc/sphinx-guides/source/_static/installation/files/examples/rate-limit-actions-setting.json diff --git a/doc/sphinx-guides/source/_static/installation/files/examples/rate-limit-actions-setting.json b/doc/sphinx-guides/source/_static/installation/files/examples/rate-limit-actions-setting.json new file mode 100644 index 00000000000..1086d0bd51f --- /dev/null +++ b/doc/sphinx-guides/source/_static/installation/files/examples/rate-limit-actions-setting.json @@ -0,0 +1,42 @@ +{ + "rateLimits": [ + { + "tier": 0, + "limitPerHour": 10, + "actions": [ + "GetLatestPublishedDatasetVersionCommand", + "GetPrivateUrlCommand", + "GetDatasetCommand", + "GetLatestAccessibleDatasetVersionCommand" + ] + }, + { + "tier": 0, + "limitPerHour": 1, + "actions": [ + "CreateGuestbookResponseCommand", + "UpdateDatasetVersionCommand", + "DestroyDatasetCommand", + "DeleteDataFileCommand", + "FinalizeDatasetPublicationCommand", + "PublishDatasetCommand" + ] + }, + { + "tier": 1, + "limitPerHour": 30, + "actions": [ + "CreateGuestbookResponseCommand", + "GetLatestPublishedDatasetVersionCommand", + "GetPrivateUrlCommand", + "GetDatasetCommand", + "GetLatestAccessibleDatasetVersionCommand", + "UpdateDatasetVersionCommand", + "DestroyDatasetCommand", + "DeleteDataFileCommand", + "FinalizeDatasetPublicationCommand", + "PublishDatasetCommand" + ] + } + ] +} \ No newline at end of file diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 2022987cae2..4f6d05d2639 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1394,7 +1394,7 @@ Note: If either of these settings exist in the database rate limiting will be en - RateLimitingCapacityByTierAndAction is a Json object specifying the rate by tier and a list of actions (commands). This allows for more control over the rate limit of individual API command calls. In the following example, calls made by a guest user (tier 0) for API GetLatestPublishedDatasetVersionCommand is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. - + :download:`rate-limit-actions.json ` .. code-block:: bash curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '{"rateLimits":[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]}' @@ -4521,3 +4521,17 @@ tab. files saved with these headers on S3 - since they no longer have to be generated and added to the streamed file on the fly. The setting is ``false`` by default, preserving the legacy behavior. + +:RateLimitingDefaultCapacityTiers ++++++++++++++++++++++++++++++++++ +Number of calls allowed per hour if the specific command is not configured. The values represent the number of calls per hour per user for tiers 0,1,... +A value of -1 can be used to signify no rate limit. Also, by default, a tier not defined would receive a default of no limit. + +:RateLimitingCapacityByTierAndAction +++++++++++++++++++++++++++++++++++++ +Json object specifying the rate by tier and a list of actions (commands). This allows for more control over the rate limit of individual API command calls. +In the following example, calls made by a guest user (tier 0) for API GetLatestPublishedDatasetVersionCommand is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. +{"rateLimits":[ +{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, +{"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, +{"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]} diff --git a/pom.xml b/pom.xml index 0544c29fa15..8c4c2b3c4b8 100644 --- a/pom.xml +++ b/pom.xml @@ -545,7 +545,6 @@ javax.cache cache-api - 1.1.1 @@ -661,7 +660,6 @@ com.hazelcast hazelcast - 5.3.6 test diff --git a/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java b/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java index 553e2d7497e..bb3fa475847 100644 --- a/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java +++ b/src/main/java/edu/harvard/iq/dataverse/EjbDataverseEngine.java @@ -207,7 +207,7 @@ public R submit(Command aCommand) throws CommandException { try { logRec.setUserIdentifier( aCommand.getRequest().getUser().getIdentifier() ); // Check for rate limit exceeded. Must be done before anything else to prevent unnecessary processing. - if (!cacheFactory.checkRate(aCommand.getRequest().getUser(), aCommand.getClass().getSimpleName())) { + if (!cacheFactory.checkRate(aCommand.getRequest().getUser(), aCommand)) { throw new RateLimitCommandException(BundleUtil.getStringFromBundle("command.exception.user.ratelimited", Arrays.asList(aCommand.getClass().getSimpleName())), aCommand); } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBean.java b/src/main/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBean.java index 384391e200b..c2781f3f4b8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBean.java @@ -1,6 +1,7 @@ package edu.harvard.iq.dataverse.util.cache; import edu.harvard.iq.dataverse.authorization.users.User; +import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.util.SystemConfig; import jakarta.annotation.PostConstruct; import jakarta.ejb.EJB; @@ -40,10 +41,11 @@ public void init() { /** * Check if user can make this call or if they are rate limited * @param user - * @param action + * @param command * @return true if user is superuser or rate not limited */ - public boolean checkRate(User user, String action) { + public boolean checkRate(User user, Command command) { + final String action = command.getClass().getSimpleName(); int capacity = RateLimitUtil.getCapacity(systemConfig, user, action); if (capacity == RateLimitUtil.NO_LIMIT) { return true; diff --git a/src/test/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBeanTest.java index b271ec42b82..e4162f20ce3 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBeanTest.java @@ -7,6 +7,10 @@ import com.hazelcast.map.IMap; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; +import edu.harvard.iq.dataverse.engine.command.Command; +import edu.harvard.iq.dataverse.engine.command.impl.ListDataverseContentCommand; +import edu.harvard.iq.dataverse.engine.command.impl.ListExplicitGroupsCommand; +import edu.harvard.iq.dataverse.engine.command.impl.ListFacetsCommand; import edu.harvard.iq.dataverse.util.SystemConfig; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; @@ -28,8 +32,6 @@ import java.util.Iterator; import java.util.Map; import java.util.Set; -import java.util.UUID; -import java.util.logging.Logger; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.doReturn; @@ -38,64 +40,64 @@ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) public class CacheFactoryBeanTest { - private static final Logger logger = Logger.getLogger(CacheFactoryBeanTest.class.getCanonicalName()); private SystemConfig mockedSystemConfig; static CacheFactoryBean cache = null; AuthenticatedUser authUser = new AuthenticatedUser(); GuestUser guestUser = GuestUser.get(); - String action; static final String settingDefaultCapacity = "30,60,120"; - static final String settingJson = "{\n" + - " \"rateLimits\":[\n" + - " {\n" + - " \"tier\": 0,\n" + - " \"limitPerHour\": 10,\n" + - " \"actions\": [\n" + - " \"GetLatestPublishedDatasetVersionCommand\",\n" + - " \"GetPrivateUrlCommand\",\n" + - " \"GetDatasetCommand\",\n" + - " \"GetLatestAccessibleDatasetVersionCommand\"\n" + - " ]\n" + - " },\n" + - " {\n" + - " \"tier\": 0,\n" + - " \"limitPerHour\": 1,\n" + - " \"actions\": [\n" + - " \"CreateGuestbookResponseCommand\",\n" + - " \"UpdateDatasetVersionCommand\",\n" + - " \"DestroyDatasetCommand\",\n" + - " \"DeleteDataFileCommand\",\n" + - " \"FinalizeDatasetPublicationCommand\",\n" + - " \"PublishDatasetCommand\"\n" + - " ]\n" + - " },\n" + - " {\n" + - " \"tier\": 1,\n" + - " \"limitPerHour\": 30,\n" + - " \"actions\": [\n" + - " \"CreateGuestbookResponseCommand\",\n" + - " \"GetLatestPublishedDatasetVersionCommand\",\n" + - " \"GetPrivateUrlCommand\",\n" + - " \"GetDatasetCommand\",\n" + - " \"GetLatestAccessibleDatasetVersionCommand\",\n" + - " \"UpdateDatasetVersionCommand\",\n" + - " \"DestroyDatasetCommand\",\n" + - " \"DeleteDataFileCommand\",\n" + - " \"FinalizeDatasetPublicationCommand\",\n" + - " \"PublishDatasetCommand\"\n" + - " ]\n" + - " }\n" + - " ]\n" + - "}"; - + public String getJsonSetting() { + return """ + { + "rateLimits": [ + { + "tier": 0, + "limitPerHour": 10, + "actions": [ + "GetLatestPublishedDatasetVersionCommand", + "GetPrivateUrlCommand", + "GetDatasetCommand", + "GetLatestAccessibleDatasetVersionCommand" + ] + }, + { + "tier": 0, + "limitPerHour": 1, + "actions": [ + "CreateGuestbookResponseCommand", + "UpdateDatasetVersionCommand", + "DestroyDatasetCommand", + "DeleteDataFileCommand", + "FinalizeDatasetPublicationCommand", + "PublishDatasetCommand" + ] + }, + { + "tier": 1, + "limitPerHour": 30, + "actions": [ + "CreateGuestbookResponseCommand", + "GetLatestPublishedDatasetVersionCommand", + "GetPrivateUrlCommand", + "GetDatasetCommand", + "GetLatestAccessibleDatasetVersionCommand", + "UpdateDatasetVersionCommand", + "DestroyDatasetCommand", + "DeleteDataFileCommand", + "FinalizeDatasetPublicationCommand", + "PublishDatasetCommand" + ] + } + ] + }"""; + } @BeforeEach public void init() throws IOException { // Reuse cache and config for all tests if (cache == null) { mockedSystemConfig = mock(SystemConfig.class); doReturn(settingDefaultCapacity).when(mockedSystemConfig).getRateLimitingDefaultCapacityTiers(); - doReturn(settingJson).when(mockedSystemConfig).getRateLimitsJson(); + doReturn(getJsonSetting()).when(mockedSystemConfig).getRateLimitsJson(); cache = new CacheFactoryBean(); cache.systemConfig = mockedSystemConfig; if (cache.rateLimitCache == null) { @@ -111,9 +113,6 @@ public void init() throws IOException { authUser.setRateLimitTier(1); authUser.setSuperuser(false); authUser.setUserIdentifier("authUser"); - - // Create a unique action for each test - action = "cmd-" + UUID.randomUUID(); } @AfterAll @@ -122,6 +121,7 @@ public static void cleanup() { } @Test public void testGuestUserGettingRateLimited() { + Command action = new ListDataverseContentCommand(null,null); boolean rateLimited = false; int cnt = 0; for (; cnt <100; cnt++) { @@ -130,13 +130,14 @@ public void testGuestUserGettingRateLimited() { break; } } - String key = RateLimitUtil.generateCacheKey(guestUser, action); + String key = RateLimitUtil.generateCacheKey(guestUser, action.getClass().getSimpleName()); assertTrue(cache.rateLimitCache.containsKey(key)); assertTrue(rateLimited && cnt > 1 && cnt <= 30, "rateLimited:"+rateLimited + " cnt:"+cnt); } @Test public void testAdminUserExemptFromGettingRateLimited() { + Command action = new ListExplicitGroupsCommand(null,null); authUser.setSuperuser(true); authUser.setUserIdentifier("admin"); boolean rateLimited = false; @@ -152,6 +153,7 @@ public void testAdminUserExemptFromGettingRateLimited() { @Test public void testAuthenticatedUserGettingRateLimited() throws InterruptedException { + Command action = new ListFacetsCommand(null,null); authUser.setRateLimitTier(2); // 120 cals per hour - 1 added token every 30 seconds boolean rateLimited = false; int cnt; @@ -183,40 +185,6 @@ public void testAuthenticatedUserGettingRateLimited() throws InterruptedExceptio assertTrue(!rateLimited && cnt == 200, "rateLimited:"+rateLimited + " cnt:"+cnt); } - @Test - public void testCluster() { - // Make sure at least 1 entry is in the original cache - cache.checkRate(authUser, action); - String key = RateLimitUtil.generateCacheKey(authUser, action); - - // Create a second cache to test cluster - CacheFactoryBean cache2 = new CacheFactoryBean(); - cache2.systemConfig = mockedSystemConfig; - // join cluster with original Hazelcast instance - cache2.rateLimitCache = new TestCache(getConfig(cache.rateLimitCache.get("memberAddress"))); - - // Check to see if the new cache synced with the existing cache - assertTrue(cache.rateLimitCache.get(key).equals(cache2.rateLimitCache.get(key))); - - key = "key1"; - String value = "value1"; - // Verify that both caches stay in sync - cache.rateLimitCache.put(key, value); - assertTrue(value.equals(cache2.rateLimitCache.get(key))); - // Clearing one cache also clears the other cache in the cluster - cache2.rateLimitCache.clear(); - assertTrue(cache.rateLimitCache.get(key) == null); - - // Verify no issue dropping one node from cluster - cache2.rateLimitCache.put(key, value); - assertTrue(value.equals(cache2.rateLimitCache.get(key))); - assertTrue(value.equals(cache.rateLimitCache.get(key))); - // Shut down hazelcast on cache2 and make sure data is still available in original cache - cache2.rateLimitCache.close(); - cache2 = null; - assertTrue(value.equals(cache.rateLimitCache.get(key))); - } - private Config getConfig() { return getConfig(null); } diff --git a/src/test/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtilTest.java index 23ba3673252..564b69c1402 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtilTest.java @@ -4,10 +4,12 @@ import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.util.SystemConfig; -import edu.harvard.iq.dataverse.util.cache.RateLimitUtil; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; @@ -19,81 +21,99 @@ @MockitoSettings(strictness = Strictness.LENIENT) public class RateLimitUtilTest { - private SystemConfig mockedSystemConfig; + static SystemConfig mockedSystemConfig = mock(SystemConfig.class); + static SystemConfig mockedSystemConfigBad = mock(SystemConfig.class); - static final String settingJson = "{\n" + - " \"rateLimits\":[\n" + - " {\n" + - " \"tier\": 0,\n" + - " \"limitPerHour\": 10,\n" + - " \"actions\": [\n" + - " \"GetLatestPublishedDatasetVersionCommand\",\n" + - " \"GetPrivateUrlCommand\",\n" + - " \"GetDatasetCommand\",\n" + - " \"GetLatestAccessibleDatasetVersionCommand\"\n" + - " ]\n" + - " },\n" + - " {\n" + - " \"tier\": 0,\n" + - " \"limitPerHour\": 1,\n" + - " \"actions\": [\n" + - " \"CreateGuestbookResponseCommand\",\n" + - " \"UpdateDatasetVersionCommand\",\n" + - " \"DestroyDatasetCommand\",\n" + - " \"DeleteDataFileCommand\",\n" + - " \"FinalizeDatasetPublicationCommand\",\n" + - " \"PublishDatasetCommand\"\n" + - " ]\n" + - " },\n" + - " {\n" + - " \"tier\": 1,\n" + - " \"limitPerHour\": 30,\n" + - " \"actions\": [\n" + - " \"CreateGuestbookResponseCommand\",\n" + - " \"GetLatestPublishedDatasetVersionCommand\",\n" + - " \"GetPrivateUrlCommand\",\n" + - " \"GetDatasetCommand\",\n" + - " \"GetLatestAccessibleDatasetVersionCommand\",\n" + - " \"UpdateDatasetVersionCommand\",\n" + - " \"DestroyDatasetCommand\",\n" + - " \"DeleteDataFileCommand\",\n" + - " \"FinalizeDatasetPublicationCommand\",\n" + - " \"PublishDatasetCommand\"\n" + - " ]\n" + - " }\n" + - " ]\n" + - "}"; + static String getJsonSetting() { + return """ + { + "rateLimits": [ + { + "tier": 0, + "limitPerHour": 10, + "actions": [ + "GetLatestPublishedDatasetVersionCommand", + "GetPrivateUrlCommand", + "GetDatasetCommand", + "GetLatestAccessibleDatasetVersionCommand" + ] + }, + { + "tier": 0, + "limitPerHour": 1, + "actions": [ + "CreateGuestbookResponseCommand", + "UpdateDatasetVersionCommand", + "DestroyDatasetCommand", + "DeleteDataFileCommand", + "FinalizeDatasetPublicationCommand", + "PublishDatasetCommand" + ] + }, + { + "tier": 1, + "limitPerHour": 30, + "actions": [ + "CreateGuestbookResponseCommand", + "GetLatestPublishedDatasetVersionCommand", + "GetPrivateUrlCommand", + "GetDatasetCommand", + "GetLatestAccessibleDatasetVersionCommand", + "UpdateDatasetVersionCommand", + "DestroyDatasetCommand", + "DeleteDataFileCommand", + "FinalizeDatasetPublicationCommand", + "PublishDatasetCommand" + ] + } + ] + }"""; + } static final String settingJsonBad = "{\n"; + @BeforeAll + public static void setUp() { + doReturn(settingJsonBad).when(mockedSystemConfigBad).getRateLimitsJson(); + doReturn("100,200").when(mockedSystemConfigBad).getRateLimitingDefaultCapacityTiers(); + } @BeforeEach - public void setup() { - mockedSystemConfig = mock(SystemConfig.class); + public void resetSettings() { + doReturn(getJsonSetting()).when(mockedSystemConfig).getRateLimitsJson(); doReturn("100,200").when(mockedSystemConfig).getRateLimitingDefaultCapacityTiers(); - // clear the static data so it can be reloaded with the new mocked data RateLimitUtil.rateLimitMap.clear(); RateLimitUtil.rateLimits.clear(); } - @Test - public void testConfig() { - doReturn(settingJson).when(mockedSystemConfig).getRateLimitsJson(); - assertEquals(100, RateLimitUtil.getCapacityByTier(mockedSystemConfig, 0)); - assertEquals(200, RateLimitUtil.getCapacityByTier(mockedSystemConfig, 1)); - assertEquals(1, RateLimitUtil.getCapacityByTierAndAction(mockedSystemConfig, 0, "DestroyDatasetCommand")); - assertEquals(100, RateLimitUtil.getCapacityByTierAndAction(mockedSystemConfig, 0, "Default Limit")); - - assertEquals(30, RateLimitUtil.getCapacityByTierAndAction(mockedSystemConfig, 1, "GetLatestAccessibleDatasetVersionCommand")); - assertEquals(200, RateLimitUtil.getCapacityByTierAndAction(mockedSystemConfig, 1, "Default Limit")); - - assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacityByTierAndAction(mockedSystemConfig, 2, "Default No Limit")); + @ParameterizedTest + @CsvSource(value = { + "100,0,", + "200,1,", + "1,0,DestroyDatasetCommand", + "100,0,Default Limit", + "30,1,DestroyDatasetCommand", + "200,1,Default Limit", + "-1,2,Default No Limit" + }) + void testConfig(int exp, int tier, String action) { + if (action == null) { + assertEquals(exp, RateLimitUtil.getCapacityByTier(mockedSystemConfig, tier)); + } else { + assertEquals(exp, RateLimitUtil.getCapacityByTierAndAction(mockedSystemConfig, tier, action)); + } } - @Test - public void testBadJson() { - doReturn(settingJsonBad).when(mockedSystemConfig).getRateLimitsJson(); - assertEquals(100, RateLimitUtil.getCapacityByTier(mockedSystemConfig, 0)); - assertEquals(200, RateLimitUtil.getCapacityByTier(mockedSystemConfig, 1)); - assertEquals(100, RateLimitUtil.getCapacityByTierAndAction(mockedSystemConfig, 0, "GetLatestAccessibleDatasetVersionCommand")); - assertEquals(200, RateLimitUtil.getCapacityByTierAndAction(mockedSystemConfig, 1, "GetLatestAccessibleDatasetVersionCommand")); - assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacityByTierAndAction(mockedSystemConfig, 2, "GetLatestAccessibleDatasetVersionCommand")); + @ParameterizedTest + @CsvSource(value = { + "100,0,", + "200,1,", + "100,0,GetLatestAccessibleDatasetVersionCommand", + "200,1,GetLatestAccessibleDatasetVersionCommand", + "-1,2,GetLatestAccessibleDatasetVersionCommand" + }) + void testBadJson(int exp, int tier, String action) { + if (action == null) { + assertEquals(exp, RateLimitUtil.getCapacityByTier(mockedSystemConfigBad, tier)); + } else { + assertEquals(exp, RateLimitUtil.getCapacityByTierAndAction(mockedSystemConfigBad, tier, action)); + } } @Test @@ -103,7 +123,6 @@ public void testGenerateCacheKey() { } @Test public void testGetCapacity() { - doReturn(settingJson).when(mockedSystemConfig).getRateLimitsJson(); GuestUser guestUser = GuestUser.get(); assertEquals(10, RateLimitUtil.getCapacity(mockedSystemConfig, guestUser, "GetPrivateUrlCommand")); From d2d3b4a129e9fc9817ef9191effe22ea43b7893f Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 20 Feb 2024 15:31:05 -0500 Subject: [PATCH 538/689] fixing config.rst --- doc/sphinx-guides/source/installation/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 4f6d05d2639..c035d75b53a 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1394,7 +1394,7 @@ Note: If either of these settings exist in the database rate limiting will be en - RateLimitingCapacityByTierAndAction is a Json object specifying the rate by tier and a list of actions (commands). This allows for more control over the rate limit of individual API command calls. In the following example, calls made by a guest user (tier 0) for API GetLatestPublishedDatasetVersionCommand is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. - :download:`rate-limit-actions.json ` +:download:`rate-limit-actions.json ` Example json for RateLimitingCapacityByTierAndAction .. code-block:: bash curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '{"rateLimits":[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]}' From 3102b056b21c40c4da164fb020a8fcfa662caad2 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 20 Feb 2024 15:34:05 -0500 Subject: [PATCH 539/689] fixing config.rst --- doc/sphinx-guides/source/installation/config.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index c035d75b53a..7d51e006a36 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1394,7 +1394,9 @@ Note: If either of these settings exist in the database rate limiting will be en - RateLimitingCapacityByTierAndAction is a Json object specifying the rate by tier and a list of actions (commands). This allows for more control over the rate limit of individual API command calls. In the following example, calls made by a guest user (tier 0) for API GetLatestPublishedDatasetVersionCommand is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. + :download:`rate-limit-actions.json ` Example json for RateLimitingCapacityByTierAndAction + .. code-block:: bash curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '{"rateLimits":[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]}' From 736c633b162562e5277d24dea746b47dc06bc653 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 20 Feb 2024 17:17:22 -0500 Subject: [PATCH 540/689] more review comments --- .../authorization/users/AuthenticatedUser.java | 6 ++---- .../iq/dataverse/util/cache/RateLimitSetting.java | 9 --------- .../iq/dataverse/util/cache/RateLimitUtil.java | 13 +++++-------- ...ing.sql => V6.1.0.4__9356-add-rate-limiting.sql} | 0 .../dataverse/util/cache/CacheFactoryBeanTest.java | 10 +++++++--- 5 files changed, 14 insertions(+), 24 deletions(-) rename src/main/resources/db/migration/{V6.1.0.3__9356-add-rate-limiting.sql => V6.1.0.4__9356-add-rate-limiting.sql} (100%) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java b/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java index 6abcb350222..50a1be7635f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java @@ -148,20 +148,18 @@ public class AuthenticatedUser implements User, Serializable { @Transient private Set mutedNotificationsSet = new HashSet<>(); - private int rateLimitTier; + private int rateLimitTier = 1; @PrePersist void prePersist() { mutedNotifications = Type.toStringValue(mutedNotificationsSet); mutedEmails = Type.toStringValue(mutedEmailsSet); - rateLimitTier = max(1,rateLimitTier); // db column defaults to 1 (minimum value for a tier). } @PostLoad public void initialize() { mutedNotificationsSet = Type.tokenizeToSet(mutedNotifications); mutedEmailsSet = Type.tokenizeToSet(mutedEmails); - rateLimitTier = max(1,rateLimitTier); // db column defaults to 1 (minimum value for a tier). } /** @@ -407,7 +405,7 @@ public int getRateLimitTier() { return rateLimitTier; } public void setRateLimitTier(int rateLimitTier) { - this.rateLimitTier = rateLimitTier; + this.rateLimitTier = max(1,rateLimitTier); } @OneToOne(mappedBy = "authenticatedUser") diff --git a/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitSetting.java b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitSetting.java index cf9c9a5410e..1f781f99a64 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitSetting.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitSetting.java @@ -7,38 +7,29 @@ public class RateLimitSetting { - @JsonbProperty("tier") private int tier; - @JsonbProperty("limitPerHour") private int limitPerHour = RateLimitUtil.NO_LIMIT; - @JsonbProperty("actions") private List actions = new ArrayList<>(); private int defaultLimitPerHour; public RateLimitSetting() {} - @JsonbProperty("tier") public void setTier(int tier) { this.tier = tier; } - @JsonbProperty("tier") public int getTier() { return this.tier; } - @JsonbProperty("limitPerHour") public void setLimitPerHour(int limitPerHour) { this.limitPerHour = limitPerHour; } - @JsonbProperty("limitPerHour") public int getLimitPerHour() { return this.limitPerHour; } - @JsonbProperty("actions") public void setActions(List actions) { this.actions = actions; } - @JsonbProperty("actions") public List getActions() { return this.actions; } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java index 09057c13ab8..35cc1a5e451 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java @@ -1,14 +1,12 @@ package edu.harvard.iq.dataverse.util.cache; -import com.google.gson.Gson; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.util.SystemConfig; -import jakarta.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonObject; -import jakarta.json.JsonReader; +import jakarta.json.*; +import jakarta.json.bind.JsonbBuilder; +import jakarta.json.bind.JsonbException; import javax.cache.Cache; import java.io.StringReader; @@ -27,7 +25,6 @@ public class RateLimitUtil { private static final Logger logger = Logger.getLogger(RateLimitUtil.class.getCanonicalName()); static final List rateLimits = new CopyOnWriteArrayList<>(); static final Map rateLimitMap = new ConcurrentHashMap<>(); - private static final Gson gson = new Gson(); public static final int NO_LIMIT = -1; static String generateCacheKey(final User user, final String action) { @@ -114,9 +111,9 @@ static void getRateLimitsFromJson(SystemConfig systemConfig) { JsonReader jr = Json.createReader(new StringReader(setting)); JsonObject obj= jr.readObject(); JsonArray lst = obj.getJsonArray("rateLimits"); - rateLimits.addAll(gson.fromJson(String.valueOf(lst), + rateLimits.addAll(JsonbBuilder.create().fromJson(String.valueOf(lst), new ArrayList() {}.getClass().getGenericSuperclass())); - } catch (Exception e) { + } catch (JsonException | JsonbException e) { logger.warning("Unable to parse Rate Limit Json: " + e.getLocalizedMessage() + " Json:(" + setting + ")"); rateLimits.add(new RateLimitSetting()); // add a default entry to prevent re-initialization } diff --git a/src/main/resources/db/migration/V6.1.0.3__9356-add-rate-limiting.sql b/src/main/resources/db/migration/V6.1.0.4__9356-add-rate-limiting.sql similarity index 100% rename from src/main/resources/db/migration/V6.1.0.3__9356-add-rate-limiting.sql rename to src/main/resources/db/migration/V6.1.0.4__9356-add-rate-limiting.sql diff --git a/src/test/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBeanTest.java index e4162f20ce3..7438d94ea41 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBeanTest.java @@ -12,8 +12,10 @@ import edu.harvard.iq.dataverse.engine.command.impl.ListExplicitGroupsCommand; import edu.harvard.iq.dataverse.engine.command.impl.ListFacetsCommand; import edu.harvard.iq.dataverse.util.SystemConfig; +import edu.harvard.iq.dataverse.util.testing.Tags; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; @@ -33,12 +35,13 @@ import java.util.Map; import java.util.Set; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) +@Tag(Tags.NOT_ESSENTIAL_UNITTESTS) public class CacheFactoryBeanTest { private SystemConfig mockedSystemConfig; static CacheFactoryBean cache = null; @@ -163,7 +166,8 @@ public void testAuthenticatedUserGettingRateLimited() throws InterruptedExceptio break; } } - assertTrue(rateLimited && cnt == 120, "rateLimited:"+rateLimited + " cnt:"+cnt); + assertTrue(rateLimited); + assertEquals(120, cnt); for (cnt = 0; cnt <60; cnt++) { Thread.sleep(1000);// Wait for bucket to be replenished (check each second for 1 minute max) @@ -172,7 +176,7 @@ public void testAuthenticatedUserGettingRateLimited() throws InterruptedExceptio break; } } - assertTrue(!rateLimited, "rateLimited:"+rateLimited + " cnt:"+cnt); + assertFalse(rateLimited, "rateLimited:"+rateLimited + " cnt:"+cnt); // Now change the user's tier, so it is no longer limited authUser.setRateLimitTier(3); // tier 3 = no limit From ecea90c53c6c6b6782b2ca97ba038cd7dd0e03a5 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Wed, 21 Feb 2024 10:34:44 -0500 Subject: [PATCH 541/689] fixing tests --- .../dataverse/util/cache/RateLimitUtil.java | 16 +++++---- .../util/cache/RateLimitUtilTest.java | 35 +++++++++++-------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java index 35cc1a5e451..54e87e1fcb2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java @@ -1,13 +1,16 @@ package edu.harvard.iq.dataverse.util.cache; +import com.google.gson.Gson; +import com.google.gson.JsonParseException; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.util.SystemConfig; -import jakarta.json.*; -import jakarta.json.bind.JsonbBuilder; -import jakarta.json.bind.JsonbException; - +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonException; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; import javax.cache.Cache; import java.io.StringReader; import java.util.ArrayList; @@ -111,9 +114,10 @@ static void getRateLimitsFromJson(SystemConfig systemConfig) { JsonReader jr = Json.createReader(new StringReader(setting)); JsonObject obj= jr.readObject(); JsonArray lst = obj.getJsonArray("rateLimits"); - rateLimits.addAll(JsonbBuilder.create().fromJson(String.valueOf(lst), + Gson gson = new Gson(); + rateLimits.addAll(gson.fromJson(String.valueOf(lst), new ArrayList() {}.getClass().getGenericSuperclass())); - } catch (JsonException | JsonbException e) { + } catch (JsonException | JsonParseException e) { logger.warning("Unable to parse Rate Limit Json: " + e.getLocalizedMessage() + " Json:(" + setting + ")"); rateLimits.add(new RateLimitSetting()); // add a default entry to prevent re-initialization } diff --git a/src/test/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtilTest.java index 564b69c1402..fb1ba4c3c14 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtilTest.java @@ -73,13 +73,13 @@ static String getJsonSetting() { @BeforeAll public static void setUp() { + doReturn(getJsonSetting()).when(mockedSystemConfig).getRateLimitsJson(); + doReturn("100,200").when(mockedSystemConfig).getRateLimitingDefaultCapacityTiers(); doReturn(settingJsonBad).when(mockedSystemConfigBad).getRateLimitsJson(); doReturn("100,200").when(mockedSystemConfigBad).getRateLimitingDefaultCapacityTiers(); } @BeforeEach - public void resetSettings() { - doReturn(getJsonSetting()).when(mockedSystemConfig).getRateLimitsJson(); - doReturn("100,200").when(mockedSystemConfig).getRateLimitingDefaultCapacityTiers(); + public void resetRateLimitUtilSettings() { RateLimitUtil.rateLimitMap.clear(); RateLimitUtil.rateLimits.clear(); } @@ -123,25 +123,32 @@ public void testGenerateCacheKey() { } @Test public void testGetCapacity() { + SystemConfig config = mock(SystemConfig.class); + resetRateLimitUtil(config, true); + GuestUser guestUser = GuestUser.get(); - assertEquals(10, RateLimitUtil.getCapacity(mockedSystemConfig, guestUser, "GetPrivateUrlCommand")); + assertEquals(10, RateLimitUtil.getCapacity(config, guestUser, "GetPrivateUrlCommand")); AuthenticatedUser authUser = new AuthenticatedUser(); authUser.setRateLimitTier(1); - assertEquals(30, RateLimitUtil.getCapacity(mockedSystemConfig, authUser, "GetPrivateUrlCommand")); + assertEquals(30, RateLimitUtil.getCapacity(config, authUser, "GetPrivateUrlCommand")); authUser.setSuperuser(true); - assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacity(mockedSystemConfig, authUser, "GetPrivateUrlCommand")); + assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacity(config, authUser, "GetPrivateUrlCommand")); // no setting means rate limiting is not on - doReturn("").when(mockedSystemConfig).getRateLimitsJson(); - doReturn("").when(mockedSystemConfig).getRateLimitingDefaultCapacityTiers(); + resetRateLimitUtil(config, false); + + assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacity(config, guestUser, "GetPrivateUrlCommand")); + assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacity(config, guestUser, "xyz")); + assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacity(config, authUser, "GetPrivateUrlCommand")); + assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacity(config, authUser, "abc")); + authUser.setRateLimitTier(99); + assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacity(config, authUser, "def")); + } + private void resetRateLimitUtil(SystemConfig config, boolean enable) { + doReturn(enable ? getJsonSetting() : "").when(config).getRateLimitsJson(); + doReturn(enable ? "100,200" : "").when(config).getRateLimitingDefaultCapacityTiers(); RateLimitUtil.rateLimitMap.clear(); RateLimitUtil.rateLimits.clear(); - assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacity(mockedSystemConfig, guestUser, "GetPrivateUrlCommand")); - assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacity(mockedSystemConfig, guestUser, "xyz")); - assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacity(mockedSystemConfig, authUser, "GetPrivateUrlCommand")); - assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacity(mockedSystemConfig, authUser, "abc")); - authUser.setRateLimitTier(99); - assertEquals(RateLimitUtil.NO_LIMIT, RateLimitUtil.getCapacity(mockedSystemConfig, authUser, "def")); } } From 9d575ed40aa55a15cecd23f023cb7665a5404c0f Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Wed, 21 Feb 2024 11:28:55 -0500 Subject: [PATCH 542/689] fixing tests --- .../iq/dataverse/util/cache/CacheFactoryBeanTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBeanTest.java index 7438d94ea41..f7cf06b7d30 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBeanTest.java @@ -151,7 +151,8 @@ public void testAdminUserExemptFromGettingRateLimited() { break; } } - assertTrue(!rateLimited && cnt >= 99, "rateLimited:"+rateLimited + " cnt:"+cnt); + assertFalse(rateLimited); + assertTrue(cnt >= 99, "cnt:"+cnt); } @Test @@ -186,7 +187,8 @@ public void testAuthenticatedUserGettingRateLimited() throws InterruptedExceptio break; } } - assertTrue(!rateLimited && cnt == 200, "rateLimited:"+rateLimited + " cnt:"+cnt); + assertFalse(rateLimited); + assertEquals(200, cnt); } private Config getConfig() { From 692c65098a91b3fb822954eaba2d4ed9182151e3 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Thu, 22 Feb 2024 11:41:08 -0500 Subject: [PATCH 543/689] more review comments --- doc/release-notes/9356-rate-limiting.md | 2 +- .../examples/rate-limit-actions-setting.json | 6 +- .../source/installation/config.rst | 2 +- .../users/AuthenticatedUser.java | 9 +-- .../dataverse/util/cache/RateLimitUtil.java | 3 +- .../util/cache/CacheFactoryBeanTest.java | 80 +++++++++---------- .../util/cache/RateLimitUtilTest.java | 78 +++++++++--------- 7 files changed, 86 insertions(+), 94 deletions(-) diff --git a/doc/release-notes/9356-rate-limiting.md b/doc/release-notes/9356-rate-limiting.md index b05fa5e2131..5433bc65ad8 100644 --- a/doc/release-notes/9356-rate-limiting.md +++ b/doc/release-notes/9356-rate-limiting.md @@ -15,6 +15,6 @@ Tiers not specified in this setting will default to `-1` (No Limit). `RateLimitingCapacityByTierAndAction` is a JSON object specifying the rate by tier and a list of actions (commands). This allows for more control over the rate limit of individual API command calls. In the following example, calls made by a guest user (tier 0) for API `GetLatestPublishedDatasetVersionCommand` is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. -`curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '{"rateLimits":[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]}'` +`curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]'` Hazelcast is configured in Payara and should not need any changes for this feature \ No newline at end of file diff --git a/doc/sphinx-guides/source/_static/installation/files/examples/rate-limit-actions-setting.json b/doc/sphinx-guides/source/_static/installation/files/examples/rate-limit-actions-setting.json index 1086d0bd51f..3dfc7648dc3 100644 --- a/doc/sphinx-guides/source/_static/installation/files/examples/rate-limit-actions-setting.json +++ b/doc/sphinx-guides/source/_static/installation/files/examples/rate-limit-actions-setting.json @@ -1,5 +1,4 @@ -{ - "rateLimits": [ +[ { "tier": 0, "limitPerHour": 10, @@ -38,5 +37,4 @@ "PublishDatasetCommand" ] } - ] -} \ No newline at end of file +] \ No newline at end of file diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 7d51e006a36..17dc6453a18 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1399,7 +1399,7 @@ Note: If either of these settings exist in the database rate limiting will be en .. code-block:: bash - curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '{"rateLimits":[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]}' + curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]' .. _Branding Your Installation: diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java b/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java index 50a1be7635f..893d7a65485 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java @@ -16,7 +16,6 @@ import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.json.JsonPrinter; import static edu.harvard.iq.dataverse.util.StringUtil.nonEmpty; -import static java.lang.Math.max; import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; import java.io.Serializable; @@ -44,6 +43,7 @@ import jakarta.persistence.PostLoad; import jakarta.persistence.PrePersist; import jakarta.persistence.Transient; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -148,6 +148,8 @@ public class AuthenticatedUser implements User, Serializable { @Transient private Set mutedNotificationsSet = new HashSet<>(); + @Column(nullable=false) + @Min(value = 1, message = "Rate Limit Tier must be greater than 0.") private int rateLimitTier = 1; @PrePersist @@ -404,9 +406,7 @@ public void setDeactivatedTime(Timestamp deactivatedTime) { public int getRateLimitTier() { return rateLimitTier; } - public void setRateLimitTier(int rateLimitTier) { - this.rateLimitTier = max(1,rateLimitTier); - } + public void setRateLimitTier(int rateLimitTier) { this.rateLimitTier = rateLimitTier; } @OneToOne(mappedBy = "authenticatedUser") private AuthenticatedUserLookup authenticatedUserLookup; @@ -446,7 +446,6 @@ public void setShibIdentityProvider(String shibIdentityProvider) { public JsonObjectBuilder toJson() { //JsonObjectBuilder authenicatedUserJson = Json.createObjectBuilder(); - NullSafeJsonBuilder authenicatedUserJson = NullSafeJsonBuilder.jsonObjectBuilder(); authenicatedUserJson.add("id", this.id); diff --git a/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java index 54e87e1fcb2..68a3415e071 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java @@ -112,8 +112,7 @@ static void getRateLimitsFromJson(SystemConfig systemConfig) { if (!setting.isEmpty()) { try { JsonReader jr = Json.createReader(new StringReader(setting)); - JsonObject obj= jr.readObject(); - JsonArray lst = obj.getJsonArray("rateLimits"); + JsonArray lst = jr.readArray(); Gson gson = new Gson(); rateLimits.addAll(gson.fromJson(String.valueOf(lst), new ArrayList() {}.getClass().getGenericSuperclass())); diff --git a/src/test/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBeanTest.java index f7cf06b7d30..92fd6731e93 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBeanTest.java @@ -41,7 +41,6 @@ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -@Tag(Tags.NOT_ESSENTIAL_UNITTESTS) public class CacheFactoryBeanTest { private SystemConfig mockedSystemConfig; static CacheFactoryBean cache = null; @@ -51,48 +50,46 @@ public class CacheFactoryBeanTest { static final String settingDefaultCapacity = "30,60,120"; public String getJsonSetting() { return """ + [ { - "rateLimits": [ - { - "tier": 0, - "limitPerHour": 10, - "actions": [ - "GetLatestPublishedDatasetVersionCommand", - "GetPrivateUrlCommand", - "GetDatasetCommand", - "GetLatestAccessibleDatasetVersionCommand" - ] - }, - { - "tier": 0, - "limitPerHour": 1, - "actions": [ - "CreateGuestbookResponseCommand", - "UpdateDatasetVersionCommand", - "DestroyDatasetCommand", - "DeleteDataFileCommand", - "FinalizeDatasetPublicationCommand", - "PublishDatasetCommand" - ] - }, - { - "tier": 1, - "limitPerHour": 30, - "actions": [ - "CreateGuestbookResponseCommand", - "GetLatestPublishedDatasetVersionCommand", - "GetPrivateUrlCommand", - "GetDatasetCommand", - "GetLatestAccessibleDatasetVersionCommand", - "UpdateDatasetVersionCommand", - "DestroyDatasetCommand", - "DeleteDataFileCommand", - "FinalizeDatasetPublicationCommand", - "PublishDatasetCommand" - ] - } + "tier": 0, + "limitPerHour": 10, + "actions": [ + "GetLatestPublishedDatasetVersionCommand", + "GetPrivateUrlCommand", + "GetDatasetCommand", + "GetLatestAccessibleDatasetVersionCommand" ] - }"""; + }, + { + "tier": 0, + "limitPerHour": 1, + "actions": [ + "CreateGuestbookResponseCommand", + "UpdateDatasetVersionCommand", + "DestroyDatasetCommand", + "DeleteDataFileCommand", + "FinalizeDatasetPublicationCommand", + "PublishDatasetCommand" + ] + }, + { + "tier": 1, + "limitPerHour": 30, + "actions": [ + "CreateGuestbookResponseCommand", + "GetLatestPublishedDatasetVersionCommand", + "GetPrivateUrlCommand", + "GetDatasetCommand", + "GetLatestAccessibleDatasetVersionCommand", + "UpdateDatasetVersionCommand", + "DestroyDatasetCommand", + "DeleteDataFileCommand", + "FinalizeDatasetPublicationCommand", + "PublishDatasetCommand" + ] + } + ]"""; } @BeforeEach public void init() throws IOException { @@ -156,6 +153,7 @@ public void testAdminUserExemptFromGettingRateLimited() { } @Test + @Tag(Tags.NOT_ESSENTIAL_UNITTESTS) public void testAuthenticatedUserGettingRateLimited() throws InterruptedException { Command action = new ListFacetsCommand(null,null); authUser.setRateLimitTier(2); // 120 cals per hour - 1 added token every 30 seconds diff --git a/src/test/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtilTest.java index fb1ba4c3c14..5ddcc190993 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtilTest.java @@ -26,48 +26,46 @@ public class RateLimitUtilTest { static String getJsonSetting() { return """ + [ { - "rateLimits": [ - { - "tier": 0, - "limitPerHour": 10, - "actions": [ - "GetLatestPublishedDatasetVersionCommand", - "GetPrivateUrlCommand", - "GetDatasetCommand", - "GetLatestAccessibleDatasetVersionCommand" - ] - }, - { - "tier": 0, - "limitPerHour": 1, - "actions": [ - "CreateGuestbookResponseCommand", - "UpdateDatasetVersionCommand", - "DestroyDatasetCommand", - "DeleteDataFileCommand", - "FinalizeDatasetPublicationCommand", - "PublishDatasetCommand" - ] - }, - { - "tier": 1, - "limitPerHour": 30, - "actions": [ - "CreateGuestbookResponseCommand", - "GetLatestPublishedDatasetVersionCommand", - "GetPrivateUrlCommand", - "GetDatasetCommand", - "GetLatestAccessibleDatasetVersionCommand", - "UpdateDatasetVersionCommand", - "DestroyDatasetCommand", - "DeleteDataFileCommand", - "FinalizeDatasetPublicationCommand", - "PublishDatasetCommand" - ] - } + "tier": 0, + "limitPerHour": 10, + "actions": [ + "GetLatestPublishedDatasetVersionCommand", + "GetPrivateUrlCommand", + "GetDatasetCommand", + "GetLatestAccessibleDatasetVersionCommand" ] - }"""; + }, + { + "tier": 0, + "limitPerHour": 1, + "actions": [ + "CreateGuestbookResponseCommand", + "UpdateDatasetVersionCommand", + "DestroyDatasetCommand", + "DeleteDataFileCommand", + "FinalizeDatasetPublicationCommand", + "PublishDatasetCommand" + ] + }, + { + "tier": 1, + "limitPerHour": 30, + "actions": [ + "CreateGuestbookResponseCommand", + "GetLatestPublishedDatasetVersionCommand", + "GetPrivateUrlCommand", + "GetDatasetCommand", + "GetLatestAccessibleDatasetVersionCommand", + "UpdateDatasetVersionCommand", + "DestroyDatasetCommand", + "DeleteDataFileCommand", + "FinalizeDatasetPublicationCommand", + "PublishDatasetCommand" + ] + } + ]"""; } static final String settingJsonBad = "{\n"; From 13674df5f24041cfbaa7a0f24082be18c853e917 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Thu, 22 Feb 2024 13:30:11 -0500 Subject: [PATCH 544/689] more review comments --- .../iq/dataverse/authorization/users/AuthenticatedUser.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java b/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java index 893d7a65485..d6d3e0317ed 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java @@ -406,7 +406,9 @@ public void setDeactivatedTime(Timestamp deactivatedTime) { public int getRateLimitTier() { return rateLimitTier; } - public void setRateLimitTier(int rateLimitTier) { this.rateLimitTier = rateLimitTier; } + public void setRateLimitTier(int rateLimitTier) { + this.rateLimitTier = rateLimitTier; + } @OneToOne(mappedBy = "authenticatedUser") private AuthenticatedUserLookup authenticatedUserLookup; From 1e4d3519ec5b26fa707c2bdb58f4e2777f39eb89 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 27 Feb 2024 10:11:39 -0500 Subject: [PATCH 545/689] review comments --- doc/release-notes/9356-rate-limiting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/9356-rate-limiting.md b/doc/release-notes/9356-rate-limiting.md index 5433bc65ad8..9b3d38f950f 100644 --- a/doc/release-notes/9356-rate-limiting.md +++ b/doc/release-notes/9356-rate-limiting.md @@ -9,7 +9,7 @@ If neither setting exists rate limiting is disabled. `RateLimitingDefaultCapacityTiers` is a comma separated list of default values for each tier. In the following example, the default for tier `0` (guest users) is set to 10,000 calls per command per hour and tier `1` (authenticated users) is set to 20,000 calls per command per hour. -Tiers not specified in this setting will default to `-1` (No Limit). +Tiers not specified in this setting will default to `-1` (No Limit). I.e., -d "10000" is equivalent to -d "10000,-1,-1,..." `curl http://localhost:8080/api/admin/settings/:RateLimitingDefaultCapacityTiers -X PUT -d '10000,20000'` `RateLimitingCapacityByTierAndAction` is a JSON object specifying the rate by tier and a list of actions (commands). From 8edbc0473895e7db0fd7cff92a9707d6ab142829 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 27 Feb 2024 10:18:04 -0500 Subject: [PATCH 546/689] review comments --- doc/sphinx-guides/source/installation/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 17dc6453a18..f7a16066839 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1386,7 +1386,7 @@ Two database settings configure the rate limiting. Note: If either of these settings exist in the database rate limiting will be enabled. If neither setting exists rate limiting is disabled. - RateLimitingDefaultCapacityTiers is the number of calls allowed per hour if the specific command is not configured. The values represent the number of calls per hour per user for tiers 0,1,... - A value of -1 can be used to signify no rate limit. Also, by default, a tier not defined would receive a default of no limit. + A value of -1 can be used to signify no rate limit. Tiers not specified in this setting will default to `-1` (No Limit). I.e., -d "10000" is equivalent to -d "10000,-1,-1,..." .. code-block:: bash From 4a0e0af55cc3f406b781a98f669703195d5066b0 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Thu, 29 Feb 2024 14:26:16 -0500 Subject: [PATCH 547/689] rename sql to unique --- ...add-rate-limiting.sql => V6.1.0.5__9356-add-rate-limiting.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V6.1.0.4__9356-add-rate-limiting.sql => V6.1.0.5__9356-add-rate-limiting.sql} (100%) diff --git a/src/main/resources/db/migration/V6.1.0.4__9356-add-rate-limiting.sql b/src/main/resources/db/migration/V6.1.0.5__9356-add-rate-limiting.sql similarity index 100% rename from src/main/resources/db/migration/V6.1.0.4__9356-add-rate-limiting.sql rename to src/main/resources/db/migration/V6.1.0.5__9356-add-rate-limiting.sql From 5233bf2fdd085ab7b0c0ef00a468cc11a623e593 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Thu, 29 Feb 2024 16:05:54 -0500 Subject: [PATCH 548/689] review comments --- doc/release-notes/9356-rate-limiting.md | 4 ++-- doc/sphinx-guides/source/installation/config.rst | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/release-notes/9356-rate-limiting.md b/doc/release-notes/9356-rate-limiting.md index 9b3d38f950f..1d68669af26 100644 --- a/doc/release-notes/9356-rate-limiting.md +++ b/doc/release-notes/9356-rate-limiting.md @@ -7,12 +7,12 @@ Two database settings configure the rate limiting. Note: If either of these settings exist in the database rate limiting will be enabled. If neither setting exists rate limiting is disabled. -`RateLimitingDefaultCapacityTiers` is a comma separated list of default values for each tier. +`:RateLimitingDefaultCapacityTiers` is a comma separated list of default values for each tier. In the following example, the default for tier `0` (guest users) is set to 10,000 calls per command per hour and tier `1` (authenticated users) is set to 20,000 calls per command per hour. Tiers not specified in this setting will default to `-1` (No Limit). I.e., -d "10000" is equivalent to -d "10000,-1,-1,..." `curl http://localhost:8080/api/admin/settings/:RateLimitingDefaultCapacityTiers -X PUT -d '10000,20000'` -`RateLimitingCapacityByTierAndAction` is a JSON object specifying the rate by tier and a list of actions (commands). +`:RateLimitingCapacityByTierAndAction` is a JSON object specifying the rate by tier and a list of actions (commands). This allows for more control over the rate limit of individual API command calls. In the following example, calls made by a guest user (tier 0) for API `GetLatestPublishedDatasetVersionCommand` is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. `curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]'` diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index f7a16066839..460307241e9 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1385,14 +1385,14 @@ Rate limits can be imposed on command APIs by configuring the tier, the command, Two database settings configure the rate limiting. Note: If either of these settings exist in the database rate limiting will be enabled. If neither setting exists rate limiting is disabled. -- RateLimitingDefaultCapacityTiers is the number of calls allowed per hour if the specific command is not configured. The values represent the number of calls per hour per user for tiers 0,1,... +- :RateLimitingDefaultCapacityTiers is the number of calls allowed per hour if the specific command is not configured. The values represent the number of calls per hour per user for tiers 0,1,... A value of -1 can be used to signify no rate limit. Tiers not specified in this setting will default to `-1` (No Limit). I.e., -d "10000" is equivalent to -d "10000,-1,-1,..." .. code-block:: bash curl http://localhost:8080/api/admin/settings/:RateLimitingDefaultCapacityTiers -X PUT -d '10000,20000' -- RateLimitingCapacityByTierAndAction is a Json object specifying the rate by tier and a list of actions (commands). This allows for more control over the rate limit of individual API command calls. +- :RateLimitingCapacityByTierAndAction is a JSON object specifying the rate by tier and a list of actions (commands). This allows for more control over the rate limit of individual API command calls. In the following example, calls made by a guest user (tier 0) for API GetLatestPublishedDatasetVersionCommand is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. :download:`rate-limit-actions.json ` Example json for RateLimitingCapacityByTierAndAction @@ -4531,7 +4531,7 @@ A value of -1 can be used to signify no rate limit. Also, by default, a tier not :RateLimitingCapacityByTierAndAction ++++++++++++++++++++++++++++++++++++ -Json object specifying the rate by tier and a list of actions (commands). This allows for more control over the rate limit of individual API command calls. +JSON object specifying the rate by tier and a list of actions (commands). This allows for more control over the rate limit of individual API command calls. In the following example, calls made by a guest user (tier 0) for API GetLatestPublishedDatasetVersionCommand is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. {"rateLimits":[ {"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, From b66e0002bf3ebdab625e9285c813f980eca34a59 Mon Sep 17 00:00:00 2001 From: Steven Winship <39765413+stevenwinship@users.noreply.github.com> Date: Thu, 29 Feb 2024 16:10:38 -0500 Subject: [PATCH 549/689] Update doc/sphinx-guides/source/installation/config.rst Co-authored-by: Philip Durbin --- doc/sphinx-guides/source/installation/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 460307241e9..70c1e40d76b 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1375,7 +1375,7 @@ Before being moved there, .. _cache-rate-limiting: -Configure Your Dataverse Installation to use JCache (with Hazelcast as provided by Payara) for Rate Limiting +Configure Your Dataverse Installation to Use JCache (with Hazelcast as Provided by Payara) for Rate Limiting ------------------------------------------------------------------------------------------------------------ Rate limiting has been added to prevent users from over taxing the system either deliberately or by runaway automated processes. From 0b3c5e385c6aaeeb67392c89e100c6286ad90f71 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 1 Mar 2024 18:01:55 +0100 Subject: [PATCH 550/689] Cosmetics for 9356 - Rate Limiting PR (#10349) * style(cache): switch from Gson to JSON-B via JSR-367 Avoiding usage of GSON will eventually allow us to reduce dependencies. Standards for the win! * style(cache): address SonarLint suggestions for code improvements - Remove unnecessary StringBuffers - Switch to better readable else-if construction to determine capacity - Add missing generics - Remove stale import --- pom.xml | 12 ++++ .../util/cache/RateLimitSetting.java | 2 - .../dataverse/util/cache/RateLimitUtil.java | 64 +++++++++---------- 3 files changed, 41 insertions(+), 37 deletions(-) diff --git a/pom.xml b/pom.xml index 8c4c2b3c4b8..f736f04cf32 100644 --- a/pom.xml +++ b/pom.xml @@ -210,6 +210,18 @@ provided + + + jakarta.json.bind + jakarta.json.bind-api + + + + org.eclipse + yasson + test + + org.glassfish diff --git a/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitSetting.java b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitSetting.java index 1f781f99a64..54da5a46670 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitSetting.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitSetting.java @@ -1,7 +1,5 @@ package edu.harvard.iq.dataverse.util.cache; -import jakarta.json.bind.annotation.JsonbProperty; - import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java index 68a3415e071..b566cd42fe1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/cache/RateLimitUtil.java @@ -1,18 +1,14 @@ package edu.harvard.iq.dataverse.util.cache; -import com.google.gson.Gson; -import com.google.gson.JsonParseException; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.util.SystemConfig; -import jakarta.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonException; -import jakarta.json.JsonObject; -import jakarta.json.JsonReader; +import jakarta.json.bind.Jsonb; +import jakarta.json.bind.JsonbBuilder; +import jakarta.json.bind.JsonbException; + import javax.cache.Cache; -import java.io.StringReader; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -31,23 +27,19 @@ public class RateLimitUtil { public static final int NO_LIMIT = -1; static String generateCacheKey(final User user, final String action) { - StringBuffer id = new StringBuffer(); - id.append(user != null ? user.getIdentifier() : GuestUser.get().getIdentifier()); - if (action != null) { - id.append(":").append(action); - } - return id.toString(); + return (user != null ? user.getIdentifier() : GuestUser.get().getIdentifier()) + + (action != null ? ":" + action : ""); } static int getCapacity(SystemConfig systemConfig, User user, String action) { if (user != null && user.isSuperuser()) { return NO_LIMIT; - }; + } // get the capacity, i.e. calls per hour, from config - return (user instanceof AuthenticatedUser) ? - getCapacityByTierAndAction(systemConfig, ((AuthenticatedUser) user).getRateLimitTier(), action) : + return (user instanceof AuthenticatedUser authUser) ? + getCapacityByTierAndAction(systemConfig, authUser.getRateLimitTier(), action) : getCapacityByTierAndAction(systemConfig, 0, action); } - static boolean rateLimited(final Cache rateLimitCache, final String key, int capacityPerHour) { + static boolean rateLimited(final Cache rateLimitCache, final String key, int capacityPerHour) { if (capacityPerHour == NO_LIMIT) { return false; } @@ -73,10 +65,14 @@ static int getCapacityByTierAndAction(SystemConfig systemConfig, Integer tier, S if (rateLimits.isEmpty()) { init(systemConfig); } - - return rateLimitMap.containsKey(getMapKey(tier,action)) ? rateLimitMap.get(getMapKey(tier,action)) : - rateLimitMap.containsKey(getMapKey(tier)) ? rateLimitMap.get(getMapKey(tier)) : - getCapacityByTier(systemConfig, tier); + + if (rateLimitMap.containsKey(getMapKey(tier, action))) { + return rateLimitMap.get(getMapKey(tier,action)); + } else if (rateLimitMap.containsKey(getMapKey(tier))) { + return rateLimitMap.get(getMapKey(tier)); + } else { + return getCapacityByTier(systemConfig, tier); + } } static int getCapacityByTier(SystemConfig systemConfig, int tier) { int value = NO_LIMIT; @@ -106,19 +102,22 @@ static void init(SystemConfig systemConfig) { r.getActions().forEach(a -> rateLimitMap.put(getMapKey(r.getTier(), a), r.getLimitPerHour())); }); } + + @SuppressWarnings("java:S2133") // <- To enable casting to generic in JSON-B we need a class instance, false positive static void getRateLimitsFromJson(SystemConfig systemConfig) { String setting = systemConfig.getRateLimitsJson(); rateLimits.clear(); if (!setting.isEmpty()) { - try { - JsonReader jr = Json.createReader(new StringReader(setting)); - JsonArray lst = jr.readArray(); - Gson gson = new Gson(); - rateLimits.addAll(gson.fromJson(String.valueOf(lst), + try (Jsonb jsonb = JsonbBuilder.create()) { + rateLimits.addAll(jsonb.fromJson(setting, new ArrayList() {}.getClass().getGenericSuperclass())); - } catch (JsonException | JsonParseException e) { + } catch (JsonbException e) { logger.warning("Unable to parse Rate Limit Json: " + e.getLocalizedMessage() + " Json:(" + setting + ")"); rateLimits.add(new RateLimitSetting()); // add a default entry to prevent re-initialization + // Note: Usually using Exception in a catch block is an antipattern and should be avoided. + // As the JSON-B interface does not specify a non-generic type, we have to use this. + } catch (Exception e) { + logger.warning("Could not close JSON-B reader"); } } } @@ -126,14 +125,9 @@ static String getMapKey(int tier) { return getMapKey(tier, null); } static String getMapKey(int tier, String action) { - StringBuffer key = new StringBuffer(); - key.append(tier).append(":"); - if (action != null) { - key.append(action); - } - return key.toString(); + return tier + ":" + (action != null ? action : ""); } - static long longFromKey(Cache cache, String key) { + static long longFromKey(Cache cache, String key) { Object l = cache.get(key); return l != null ? Long.parseLong(String.valueOf(l)) : 0L; } From 226622519fc7b44b6ff49537985dec71b730b8fc Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Wed, 6 Mar 2024 13:03:41 -0500 Subject: [PATCH 551/689] rename sql file --- ...add-rate-limiting.sql => V6.1.0.6__9356-add-rate-limiting.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V6.1.0.5__9356-add-rate-limiting.sql => V6.1.0.6__9356-add-rate-limiting.sql} (100%) diff --git a/src/main/resources/db/migration/V6.1.0.5__9356-add-rate-limiting.sql b/src/main/resources/db/migration/V6.1.0.6__9356-add-rate-limiting.sql similarity index 100% rename from src/main/resources/db/migration/V6.1.0.5__9356-add-rate-limiting.sql rename to src/main/resources/db/migration/V6.1.0.6__9356-add-rate-limiting.sql From a1ab6f9e3b65919f79b85b4dd0bdccb70e8cb3a6 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Mon, 18 Mar 2024 13:49:41 -0400 Subject: [PATCH 552/689] change sql script name --- .../{V6.1.0.6__9356-add-rate-limiting.sql => V6.1.0.7.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V6.1.0.6__9356-add-rate-limiting.sql => V6.1.0.7.sql} (100%) diff --git a/src/main/resources/db/migration/V6.1.0.6__9356-add-rate-limiting.sql b/src/main/resources/db/migration/V6.1.0.7.sql similarity index 100% rename from src/main/resources/db/migration/V6.1.0.6__9356-add-rate-limiting.sql rename to src/main/resources/db/migration/V6.1.0.7.sql From 4a45caebfda51b8e445fd3b1936aeea05d54bd81 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Mon, 18 Mar 2024 15:53:24 -0400 Subject: [PATCH 553/689] Change to return null --- src/main/java/edu/harvard/iq/dataverse/DatasetPage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index 400075cd899..fdb563e857b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -5819,7 +5819,7 @@ public String getCroissant() { return croissant; } } - return ""; + return null; } public String getJsonLd() { From d7fa076bc7b0bc3a39c40b6b08797d93372a1847 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Mon, 18 Mar 2024 16:45:48 -0400 Subject: [PATCH 554/689] Empty validation --- src/main/java/edu/harvard/iq/dataverse/DatasetPage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index fdb563e857b..beb4c1f9db2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -5814,7 +5814,7 @@ public String getCroissant() { final String CROISSANT_SCHEMA_NAME = "croissant"; ExportService instance = ExportService.getInstance(); String croissant = instance.getExportAsString(dataset, CROISSANT_SCHEMA_NAME); - if (croissant != null) { + if (croissant != null && !croissant.isEmpty()) { logger.fine("Returning cached CROISSANT."); return croissant; } From ff16a49bd9ca7ca9c5d57a9a1e1b8e06af855e0a Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Tue, 19 Mar 2024 10:40:52 +0100 Subject: [PATCH 555/689] semaphore for async indexing and sync index in transaction after publish --- .../source/installation/config.rst | 9 ++++++ .../FinalizeDatasetPublicationCommand.java | 7 ++++- .../iq/dataverse/search/IndexServiceBean.java | 29 +++++++++++++++++-- .../iq/dataverse/settings/JvmSettings.java | 4 +++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 2baa2827250..06936dab015 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -2352,6 +2352,15 @@ when using it to configure your core name! Can also be set via *MicroProfile Config API* sources, e.g. the environment variable ``DATAVERSE_SOLR_PATH``. +dataverse.concurrency.max-async-indexes ++++++++++++++++++++ + +Maximum number of simultaneously running asynchronous dataset index operations. + +Defaults to ``4``. + +Can also be set via *MicroProfile Config API* sources, e.g. the environment variable ``DATAVERSE_SOLR_CONCURRENCY_MAX_ASYNC_INDEXES``. + dataverse.rserve.host +++++++++++++++++++++ diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java index 37aeee231e1..1277a98aa31 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java @@ -247,6 +247,12 @@ public Dataset execute(CommandContext ctxt) throws CommandException { logger.info("Successfully published the dataset "+readyDataset.getGlobalId().asString()); readyDataset = ctxt.em().merge(readyDataset); + + try { + ctxt.index().indexDataset(readyDataset, true); + } catch (SolrServerException | IOException e) { + throw new CommandException("Indexing failed: " + e.getMessage(), this); + } return readyDataset; } @@ -267,7 +273,6 @@ public boolean onSuccess(CommandContext ctxt, Object r) { } catch (Exception e) { logger.warning("Failure to send dataset published messages for : " + dataset.getId() + " : " + e.getMessage()); } - ctxt.index().asyncIndexDataset(dataset, true); //re-indexing dataverses that have additional subjects if (!dataversesToIndex.isEmpty()){ diff --git a/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java index 5716b39e85c..cf1e58e4028 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java @@ -35,6 +35,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; +import java.util.concurrent.Semaphore; import java.util.function.Function; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -341,6 +342,8 @@ public void indexDatasetInNewTransaction(Long datasetId) { //Dataset dataset) { private static final Map NEXT_TO_INDEX = new ConcurrentHashMap<>(); // indexingNow is a set of dataset ids of datasets being indexed asynchronously right now private static final Map INDEXING_NOW = new ConcurrentHashMap<>(); + // semaphore for async indexing + private static final Semaphore ASYNC_INDEX_SEMAPHORE = new Semaphore(JvmSettings.MAX_ASYNC_INDEXES.lookupOptional(Integer.class).orElse(4), true); // When you pass null as Dataset parameter to this method, it indicates that the indexing of the dataset with "id" has finished // Pass non-null Dataset to schedule it for indexing @@ -385,6 +388,19 @@ synchronized private static Dataset getNextToIndex(Long id, Dataset d) { */ @Asynchronous public void asyncIndexDataset(Dataset dataset, boolean doNormalSolrDocCleanUp) { + try { + ASYNC_INDEX_SEMAPHORE.acquire(); + doAyncIndexDataset(dataset, doNormalSolrDocCleanUp); + } catch (InterruptedException e) { + String failureLogText = "Indexing failed: interrupted. You can kickoff a re-index of this dataset with: \r\n curl http://localhost:8080/api/admin/index/datasets/" + dataset.getId().toString(); + failureLogText += "\r\n" + e.getLocalizedMessage(); + LoggingUtil.writeOnSuccessFailureLog(null, failureLogText, dataset); + } finally { + ASYNC_INDEX_SEMAPHORE.release(); + } + } + + private void doAyncIndexDataset(Dataset dataset, boolean doNormalSolrDocCleanUp) { Long id = dataset.getId(); Dataset next = getNextToIndex(id, dataset); // if there is an ongoing index job for this dataset, next is null (ongoing index job will reindex the newest version after current indexing finishes) while (next != null) { @@ -402,7 +418,16 @@ public void asyncIndexDataset(Dataset dataset, boolean doNormalSolrDocCleanUp) { @Asynchronous public void asyncIndexDatasetList(List datasets, boolean doNormalSolrDocCleanUp) { for(Dataset dataset : datasets) { - asyncIndexDataset(dataset, true); + try { + ASYNC_INDEX_SEMAPHORE.acquire(); + doAyncIndexDataset(dataset, true); + } catch (InterruptedException e) { + String failureLogText = "Indexing failed: interrupted. You can kickoff a re-index of this dataset with: \r\n curl http://localhost:8080/api/admin/index/datasets/" + dataset.getId().toString(); + failureLogText += "\r\n" + e.getLocalizedMessage(); + LoggingUtil.writeOnSuccessFailureLog(null, failureLogText, dataset); + } finally { + ASYNC_INDEX_SEMAPHORE.release(); + } } } @@ -414,7 +439,7 @@ public void indexDvObject(DvObject objectIn) throws SolrServerException, IOExce } } - private void indexDataset(Dataset dataset, boolean doNormalSolrDocCleanUp) throws SolrServerException, IOException { + public void indexDataset(Dataset dataset, boolean doNormalSolrDocCleanUp) throws SolrServerException, IOException { doIndexDataset(dataset, doNormalSolrDocCleanUp); updateLastIndexedTime(dataset.getId()); } 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 b92618dab89..8293c960c3b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java @@ -60,6 +60,10 @@ public enum JvmSettings { SOLR_CORE(SCOPE_SOLR, "core"), SOLR_PATH(SCOPE_SOLR, "path"), + // INDEX CONCURENCY + SCOPE_SOLR_CONCURENCY(SCOPE_SOLR, "concurrency"), + MAX_ASYNC_INDEXES(SCOPE_SOLR_CONCURENCY, "max-async-indexes"), + // RSERVE CONNECTION SCOPE_RSERVE(PREFIX, "rserve"), RSERVE_HOST(SCOPE_RSERVE, "host"), From 0002008ba3d81b1fc64604126b62d9a6b5ef6004 Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Tue, 19 Mar 2024 11:04:30 +0100 Subject: [PATCH 556/689] fixed too short underline in config doc --- doc/sphinx-guides/source/installation/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 06936dab015..318816d1050 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -2353,7 +2353,7 @@ when using it to configure your core name! Can also be set via *MicroProfile Config API* sources, e.g. the environment variable ``DATAVERSE_SOLR_PATH``. dataverse.concurrency.max-async-indexes -+++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++ Maximum number of simultaneously running asynchronous dataset index operations. From 3220048999cfc431963f141717e54d75a4117485 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 19 Mar 2024 16:48:28 -0400 Subject: [PATCH 557/689] Release notes snippet --- doc/release-notes/10382-optional-croissant-exporter.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/release-notes/10382-optional-croissant-exporter.md diff --git a/doc/release-notes/10382-optional-croissant-exporter.md b/doc/release-notes/10382-optional-croissant-exporter.md new file mode 100644 index 00000000000..e5c47409a1b --- /dev/null +++ b/doc/release-notes/10382-optional-croissant-exporter.md @@ -0,0 +1 @@ +When a Dataverse installation is provided with a dataverse-exporter for the croissant format, the content for JSON-LD in the header will be replaced with the croissant format. However, both JSON-LD and Croissant will still be available for download on the Dataset page. \ No newline at end of file From 2925f3bb908c3b5405717288c3a3ab731435697c Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 19 Mar 2024 18:12:09 -0400 Subject: [PATCH 558/689] fix UI issue with default/inherited provider in sub-collections --- .../edu/harvard/iq/dataverse/DataversePage.java | 15 ++++++++++++--- .../harvard/iq/dataverse/DvObjectContainer.java | 4 ++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataversePage.java b/src/main/java/edu/harvard/iq/dataverse/DataversePage.java index 10dfa4a0e4f..f35682b7bd0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataversePage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataversePage.java @@ -1303,9 +1303,18 @@ public Set> getPidProviderOptions() { Set providerIds = PidUtil.getManagedProviderIds(); Set> options = new HashSet>(); if (providerIds.size() > 1) { - String label = defaultPidProvider.getLabel() + BundleUtil.getStringFromBundle("dataverse.default") + ": " - + defaultPidProvider.getProtocol() + ":" + defaultPidProvider.getAuthority() - + defaultPidProvider.getSeparator() + defaultPidProvider.getShoulder(); + + String label = null; + if (this.dataverse.getOwner() != null && this.dataverse.getOwner().getEffectivePidGenerator()!= null) { + PidProvider inheritedPidProvider = this.dataverse.getOwner().getEffectivePidGenerator(); + label = inheritedPidProvider.getLabel() + BundleUtil.getStringFromBundle("dataverse.inherited") + ": " + + inheritedPidProvider.getProtocol() + ":" + inheritedPidProvider.getAuthority() + + inheritedPidProvider.getSeparator() + inheritedPidProvider.getShoulder(); + } else { + label = defaultPidProvider.getLabel() + BundleUtil.getStringFromBundle("dataverse.default") + ": " + + defaultPidProvider.getProtocol() + ":" + defaultPidProvider.getAuthority() + + defaultPidProvider.getSeparator() + defaultPidProvider.getShoulder(); + } Entry option = new AbstractMap.SimpleEntry("default", label); options.add(option); } diff --git a/src/main/java/edu/harvard/iq/dataverse/DvObjectContainer.java b/src/main/java/edu/harvard/iq/dataverse/DvObjectContainer.java index c991c4c02d2..bfb4b3ef749 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DvObjectContainer.java +++ b/src/main/java/edu/harvard/iq/dataverse/DvObjectContainer.java @@ -201,9 +201,9 @@ public void setPidGeneratorSpecs(String pidGeneratorSpecs) { } // Used in JSF when selecting the PidGenerator + // It only returns an id if this dvObjectContainer has PidGenerator specs set on it, otherwise it returns "default" public String getPidGeneratorId() { - PidProvider pidGenerator = getEffectivePidGenerator(); - if (pidGenerator == null) { + if (StringUtils.isBlank(getPidGeneratorSpecs())) { return "default"; } else { return getEffectivePidGenerator().getId(); From 3d2bb41ed38f54b5ddd90a8181f538ac654447f3 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 19 Mar 2024 18:12:54 -0400 Subject: [PATCH 559/689] adjust API to always return an id rather than "default" in some cases per the current documentation --- .../edu/harvard/iq/dataverse/api/Datasets.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 303b6b9adb8..2ea8e50a896 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -39,6 +39,7 @@ import edu.harvard.iq.dataverse.makedatacount.*; import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean.MakeDataCountEntry; import edu.harvard.iq.dataverse.metrics.MetricsUtil; +import edu.harvard.iq.dataverse.pidproviders.PidProvider; import edu.harvard.iq.dataverse.pidproviders.PidUtil; import edu.harvard.iq.dataverse.privateurl.PrivateUrl; import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean; @@ -4575,6 +4576,12 @@ public Response getCanDownloadAtLeastOneFile(@Context ContainerRequestContext cr }, getRequestUser(crc)); } + /** + * Get the PidProvider that will be used for generating new DOIs in this dataset + * + * @return - the id of the effective PID generator for the given dataset + * @throws WrappedResponse + */ @GET @AuthRequired @Path("{identifier}/pidGenerator") @@ -4588,7 +4595,12 @@ public Response getPidGenerator(@Context ContainerRequestContext crc, @PathParam } catch (WrappedResponse ex) { return error(Response.Status.NOT_FOUND, "No such dataset"); } - String pidGeneratorId = dataset.getPidGeneratorId(); + PidProvider pidProvider = dataset.getEffectivePidGenerator(); + if(pidProvider == null) { + //This is basically a config error, e.g. if a valid pid provider was removed after this dataset used it + return error(Response.Status.NOT_FOUND, "No PID Generator found for the give id"); + } + String pidGeneratorId = pidProvider.getId(); return ok(pidGeneratorId); } From e1f2e66661d619d5b86f60f928d138d896520a4f Mon Sep 17 00:00:00 2001 From: landreev Date: Tue, 19 Mar 2024 18:26:24 -0400 Subject: [PATCH 560/689] One extra phrase added to the guide clarifying that "... restart is required ..." --- doc/sphinx-guides/source/installation/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 70c1e40d76b..1a3ef88a5aa 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1383,7 +1383,7 @@ Rate limiting can be configured on a tier level with tier 0 being reserved for g Superuser accounts are exempt from rate limiting. Rate limits can be imposed on command APIs by configuring the tier, the command, and the hourly limit in the database. Two database settings configure the rate limiting. -Note: If either of these settings exist in the database rate limiting will be enabled. If neither setting exists rate limiting is disabled. +Note: If either of these settings exist in the database rate limiting will be enabled (note that a Payara restart is required for the setting to take effect). If neither setting exists rate limiting is disabled. - :RateLimitingDefaultCapacityTiers is the number of calls allowed per hour if the specific command is not configured. The values represent the number of calls per hour per user for tiers 0,1,... A value of -1 can be used to signify no rate limit. Tiers not specified in this setting will default to `-1` (No Limit). I.e., -d "10000" is equivalent to -d "10000,-1,-1,..." From 714f9f6dd4d0aced5a4947f24fb01753335efd6f Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Wed, 20 Mar 2024 12:03:45 +0100 Subject: [PATCH 561/689] fixed new property name scope --- doc/sphinx-guides/source/installation/config.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 318816d1050..dbf0bed234b 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -2352,8 +2352,8 @@ when using it to configure your core name! Can also be set via *MicroProfile Config API* sources, e.g. the environment variable ``DATAVERSE_SOLR_PATH``. -dataverse.concurrency.max-async-indexes -+++++++++++++++++++++++++++++++++++++++ +dataverse.solr.concurrency.max-async-indexes +++++++++++++++++++++++++++++++++++++++++++++ Maximum number of simultaneously running asynchronous dataset index operations. From 1a3783a8a80378818f2e597cb38dd25adff844ce Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Wed, 20 Mar 2024 12:35:06 +0100 Subject: [PATCH 562/689] added short release note --- doc/release-notes/10381-index-after-publish.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 doc/release-notes/10381-index-after-publish.md diff --git a/doc/release-notes/10381-index-after-publish.md b/doc/release-notes/10381-index-after-publish.md new file mode 100644 index 00000000000..84c84d75a28 --- /dev/null +++ b/doc/release-notes/10381-index-after-publish.md @@ -0,0 +1,3 @@ +New release adds a new microprofile setting for maximum number of simultaneously running asynchronous dataset index operations that defaults to ``4``: + +dataverse.solr.concurrency.max-async-indexes \ No newline at end of file From 7023fbfb4d6abd1356882b998941be3f72958544 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Wed, 20 Mar 2024 09:09:17 -0400 Subject: [PATCH 563/689] Update doc/release-notes/10382-optional-croissant-exporter.md Co-authored-by: Philip Durbin --- doc/release-notes/10382-optional-croissant-exporter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/10382-optional-croissant-exporter.md b/doc/release-notes/10382-optional-croissant-exporter.md index e5c47409a1b..e4c96115825 100644 --- a/doc/release-notes/10382-optional-croissant-exporter.md +++ b/doc/release-notes/10382-optional-croissant-exporter.md @@ -1 +1 @@ -When a Dataverse installation is provided with a dataverse-exporter for the croissant format, the content for JSON-LD in the header will be replaced with the croissant format. However, both JSON-LD and Croissant will still be available for download on the Dataset page. \ No newline at end of file +When a Dataverse installation is configured to use a metadata exporter for the [Croissant](https://github.com/mlcommons/croissant) format, the content of the JSON-LD in the `` of dataset landing pages will be replaced with that format. However, both JSON-LD and Croissant will still be available for download from the dataset page and API. \ No newline at end of file From 91bb468a21d9e430ab0c6940c3db09ab011da86c Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Wed, 20 Mar 2024 11:09:12 -0400 Subject: [PATCH 564/689] adding two specific commands CheckRateLimitForDatasetPage and CheckRateLimitForCollectionPage --- .../edu/harvard/iq/dataverse/DatasetPage.java | 9 ++++++++- .../edu/harvard/iq/dataverse/DataversePage.java | 10 +++++++++- .../impl/CheckRateLimitForCollectionPage.java | 16 ++++++++++++++++ .../impl/CheckRateLimitForDatasetPage.java | 17 +++++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForCollectionPage.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForDatasetPage.java diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index 05325a26f3a..4daa1fbadaf 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -24,6 +24,7 @@ import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import edu.harvard.iq.dataverse.engine.command.impl.CheckRateLimitForDatasetPage; import edu.harvard.iq.dataverse.engine.command.impl.CreatePrivateUrlCommand; import edu.harvard.iq.dataverse.engine.command.impl.CuratePublishedDatasetVersionCommand; import edu.harvard.iq.dataverse.engine.command.impl.DeaccessionDatasetVersionCommand; @@ -36,6 +37,7 @@ import edu.harvard.iq.dataverse.engine.command.impl.PublishDataverseCommand; import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand; import edu.harvard.iq.dataverse.export.ExportService; +import edu.harvard.iq.dataverse.util.cache.CacheFactoryBean; import io.gdcc.spi.export.ExportException; import io.gdcc.spi.export.Exporter; import edu.harvard.iq.dataverse.ingest.IngestRequest; @@ -242,6 +244,8 @@ public enum DisplayMode { SolrClientService solrClientService; @EJB DvObjectServiceBean dvObjectService; + @EJB + CacheFactoryBean cacheFactory; @Inject DataverseRequestServiceBean dvRequestService; @Inject @@ -1930,7 +1934,10 @@ private void setIdByPersistentId() { } private String init(boolean initFull) { - + // Check for rate limit exceeded. Must be done before anything else to prevent unnecessary processing. + if (!cacheFactory.checkRate(session.getUser(), new CheckRateLimitForDatasetPage(null,null))) { + return BundleUtil.getStringFromBundle("command.exception.user.ratelimited", Arrays.asList(CheckRateLimitForDatasetPage.class.getSimpleName())); + } //System.out.println("_YE_OLDE_QUERY_COUNTER_"); // for debug purposes setDataverseSiteUrl(systemConfig.getDataverseSiteUrl()); diff --git a/src/main/java/edu/harvard/iq/dataverse/DataversePage.java b/src/main/java/edu/harvard/iq/dataverse/DataversePage.java index 10dfa4a0e4f..4f0a3f14b99 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataversePage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataversePage.java @@ -9,6 +9,7 @@ import edu.harvard.iq.dataverse.dataverse.DataverseUtil; import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import edu.harvard.iq.dataverse.engine.command.impl.CheckRateLimitForCollectionPage; import edu.harvard.iq.dataverse.engine.command.impl.CreateDataverseCommand; import edu.harvard.iq.dataverse.engine.command.impl.CreateSavedSearchCommand; import edu.harvard.iq.dataverse.engine.command.impl.DeleteDataverseCommand; @@ -31,6 +32,8 @@ import static edu.harvard.iq.dataverse.util.JsfHelper.JH; import edu.harvard.iq.dataverse.util.SystemConfig; import java.util.List; + +import edu.harvard.iq.dataverse.util.cache.CacheFactoryBean; import jakarta.ejb.EJB; import jakarta.faces.application.FacesMessage; import jakarta.faces.context.FacesContext; @@ -118,6 +121,8 @@ public enum LinkMode { @Inject DataverseHeaderFragment dataverseHeaderFragment; @EJB PidProviderFactoryBean pidProviderFactoryBean; + @EJB + CacheFactoryBean cacheFactory; private Dataverse dataverse = new Dataverse(); @@ -318,7 +323,10 @@ public void updateOwnerDataverse() { public String init() { //System.out.println("_YE_OLDE_QUERY_COUNTER_"); // for debug purposes - + // Check for rate limit exceeded. Must be done before anything else to prevent unnecessary processing. + if (!cacheFactory.checkRate(session.getUser(), new CheckRateLimitForCollectionPage(null,null))) { + return BundleUtil.getStringFromBundle("command.exception.user.ratelimited", Arrays.asList(CheckRateLimitForCollectionPage.class.getSimpleName())); + } if (this.getAlias() != null || this.getId() != null || this.getOwnerId() == null) {// view mode for a dataverse if (this.getAlias() != null) { dataverse = dataverseService.findByAlias(this.getAlias()); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForCollectionPage.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForCollectionPage.java new file mode 100644 index 00000000000..9dcf0428fff --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForCollectionPage.java @@ -0,0 +1,16 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.DvObject; +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.exception.CommandException; + +public class CheckRateLimitForCollectionPage extends AbstractVoidCommand { + public CheckRateLimitForCollectionPage(DataverseRequest aRequest, DvObject dvObject) { + super(aRequest, dvObject); + } + + @Override + protected void executeImpl(CommandContext ctxt) throws CommandException { } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForDatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForDatasetPage.java new file mode 100644 index 00000000000..04a27d082f4 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForDatasetPage.java @@ -0,0 +1,17 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.DvObject; +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.exception.CommandException; + +public class CheckRateLimitForDatasetPage extends AbstractVoidCommand { + + public CheckRateLimitForDatasetPage(DataverseRequest aRequest, DvObject dvObject) { + super(aRequest, dvObject); + } + + @Override + protected void executeImpl(CommandContext ctxt) throws CommandException { } +} From a9b2514620a6e4f1fb1377936423c2527800400c Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Wed, 20 Mar 2024 15:54:03 -0400 Subject: [PATCH 565/689] add check for existing cache before creating a new one --- .../iq/dataverse/util/cache/CacheFactoryBean.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBean.java b/src/main/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBean.java index c2781f3f4b8..36b2b35b48f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/cache/CacheFactoryBean.java @@ -32,10 +32,13 @@ public class CacheFactoryBean implements java.io.Serializable { @PostConstruct public void init() { - CompleteConfiguration config = - new MutableConfiguration() - .setTypes( String.class, String.class ); - rateLimitCache = manager.createCache(RATE_LIMIT_CACHE, config); + rateLimitCache = manager.getCache(RATE_LIMIT_CACHE); + if (rateLimitCache == null) { + CompleteConfiguration config = + new MutableConfiguration() + .setTypes( String.class, String.class ); + rateLimitCache = manager.createCache(RATE_LIMIT_CACHE, config); + } } /** From caf2d91c234cd2a16930d1211c5e1b3b2b8e5a46 Mon Sep 17 00:00:00 2001 From: Gustavo Durand Date: Wed, 20 Mar 2024 16:19:11 -0400 Subject: [PATCH 566/689] removed required attr from selectone component --- src/main/webapp/metadataFragment.xhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/metadataFragment.xhtml b/src/main/webapp/metadataFragment.xhtml index 200d2917b9a..0fab34e1e58 100755 --- a/src/main/webapp/metadataFragment.xhtml +++ b/src/main/webapp/metadataFragment.xhtml @@ -285,7 +285,7 @@
    + id="unique1" rendered="#{!dsf.datasetFieldType.allowMultiples}" filter="#{(dsf.datasetFieldType.controlledVocabularyValues.size() lt 10) ? 'false':'true'}" filterMatchMode="contains"> From b678cbbcd0c13b1e89761e48b6d89eada46b6367 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Thu, 21 Mar 2024 11:03:07 -0400 Subject: [PATCH 567/689] Documentation update to java requirements Documentation update to java requirements --- doc/sphinx-guides/source/developers/classic-dev-env.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/developers/classic-dev-env.rst b/doc/sphinx-guides/source/developers/classic-dev-env.rst index 6978f389e01..82e10b727ef 100755 --- a/doc/sphinx-guides/source/developers/classic-dev-env.rst +++ b/doc/sphinx-guides/source/developers/classic-dev-env.rst @@ -37,7 +37,7 @@ Windows is gaining support through Docker as described in the :doc:`windows` sec Install Java ~~~~~~~~~~~~ -The Dataverse Software requires Java 11. +The Dataverse Software requires Java 17. We suggest downloading OpenJDK from https://adoptopenjdk.net From 1b826fb4232707d044f5353ce0ac225eaa12f667 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 21 Mar 2024 15:23:19 -0400 Subject: [PATCH 568/689] Fix the issue with the order of the facets and also expose this facet to all users --- .../dataverse/search/SearchServiceBean.java | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java index 0b93c617c1a..079e3ec35c6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java @@ -187,19 +187,11 @@ public SolrQueryResponse search( SolrQuery solrQuery = new SolrQuery(); query = SearchUtil.sanitizeQuery(query); solrQuery.setQuery(query); -// SortClause foo = new SortClause("name", SolrQuery.ORDER.desc); -// if (query.equals("*") || query.equals("*:*")) { -// solrQuery.setSort(new SortClause(SearchFields.NAME_SORT, SolrQuery.ORDER.asc)); if (sortField != null) { // is it ok not to specify any sort? - there are cases where we // don't care, and it must cost some extra cycles -- L.A. solrQuery.setSort(new SortClause(sortField, sortOrder)); } -// } else { -// solrQuery.setSort(sortClause); -// } -// solrQuery.setSort(sortClause); - solrQuery.setParam("fl", "*,score"); solrQuery.setParam("qt", "/select"); @@ -222,6 +214,14 @@ public SolrQueryResponse search( } List metadataBlockFacets = new LinkedList<>(); + + /* + * We talked about this in slack on 2021-09-14, Users can see objects on draft/unpublished + * if the owner gives permissions to all users so it makes sense to expose this facet + * to all users. The request of this change started because the order of the facets were + * changed with the PR #9635 and this was unintended. + */ + solrQuery.addFacetField(SearchFields.PUBLICATION_STATUS); if (addFacets) { // ----------------------------------- @@ -251,6 +251,7 @@ public SolrQueryResponse search( DatasetFieldType datasetField = dataverseFacet.getDatasetFieldType(); solrQuery.addFacetField(datasetField.getSolrField().getNameFacetable()); } + // Get all metadata block facets configured to be displayed metadataBlockFacets.addAll(dataverse.getMetadataBlockFacets()); } @@ -1029,11 +1030,11 @@ private String getPermissionFilterQuery(DataverseRequest dataverseRequest, SolrQ AuthenticatedUser au = (AuthenticatedUser) user; - if (addFacets) { - // Logged in user, has publication status facet - // - solrQuery.addFacetField(SearchFields.PUBLICATION_STATUS); - } + // if (addFacets) { + // // Logged in user, has publication status facet + // // + // solrQuery.addFacetField(SearchFields.PUBLICATION_STATUS); + // } // ---------------------------------------------------- // (3) Is this a Super User? From d3ef749036e782d9de137522e255e94e565491bc Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 21 Mar 2024 15:42:14 -0400 Subject: [PATCH 569/689] Add release notes --- doc/release-notes/10338-expose-and-sort-publish-status-facet.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/release-notes/10338-expose-and-sort-publish-status-facet.md diff --git a/doc/release-notes/10338-expose-and-sort-publish-status-facet.md b/doc/release-notes/10338-expose-and-sort-publish-status-facet.md new file mode 100644 index 00000000000..a14ea62da67 --- /dev/null +++ b/doc/release-notes/10338-expose-and-sort-publish-status-facet.md @@ -0,0 +1 @@ +In version 6.1, the publication status facet location was unintentionally moved to the bottom, and it also prevented it from being visible to guest users. In version 6.2, we have restored its visibility to all users and moved it back to the top of the list. \ No newline at end of file From 77e4eac90fc9f3bd7125209df2a99577606789bf Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 21 Mar 2024 16:01:11 -0400 Subject: [PATCH 570/689] Change should still be inside the logic for the facets --- .../iq/dataverse/search/SearchServiceBean.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java index 079e3ec35c6..4e345a1e036 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java @@ -214,16 +214,17 @@ public SolrQueryResponse search( } List metadataBlockFacets = new LinkedList<>(); - - /* - * We talked about this in slack on 2021-09-14, Users can see objects on draft/unpublished - * if the owner gives permissions to all users so it makes sense to expose this facet - * to all users. The request of this change started because the order of the facets were - * changed with the PR #9635 and this was unintended. - */ - solrQuery.addFacetField(SearchFields.PUBLICATION_STATUS); if (addFacets) { + + /* + * We talked about this in slack on 2021-09-14, Users can see objects on draft/unpublished + * if the owner gives permissions to all users so it makes sense to expose this facet + * to all users. The request of this change started because the order of the facets were + * changed with the PR #9635 and this was unintended. + */ + solrQuery.addFacetField(SearchFields.PUBLICATION_STATUS); + // ----------------------------------- // Facets to Retrieve // ----------------------------------- From 59d16b9ad7b5ee5f72a9f0b433c6a5d78a03d5d4 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 21 Mar 2024 16:04:32 -0400 Subject: [PATCH 571/689] clarify how to increase timeout in docker demo #10409 --- doc/sphinx-guides/source/container/running/demo.rst | 8 +++++++- docker/compose/demo/compose.yml | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/container/running/demo.rst b/doc/sphinx-guides/source/container/running/demo.rst index 24027e677a1..2991c677618 100644 --- a/doc/sphinx-guides/source/container/running/demo.rst +++ b/doc/sphinx-guides/source/container/running/demo.rst @@ -59,6 +59,8 @@ Edit the ``compose.yml`` file and look for the following section. container_name: "bootstrap" image: gdcc/configbaker:alpha restart: "no" + environment: + - TIMEOUT=3m command: - bootstrap.sh - dev @@ -189,13 +191,17 @@ Windows support is experimental but we are very interested in supporting Windows Bootstrapping Did Not Complete ++++++++++++++++++++++++++++++ -In the compose file, try increasing the timeout in the bootstrap container by adding something like this: +In the compose file, try increasing the timeout for the bootstrap container: .. code-block:: bash environment: - TIMEOUT=10m +As described above, you'll want to stop containers, delete data, and start over with ``docker compose up``. To make sure the increased timeout is in effect, you can run ``docker logs bootstrap`` and look for the new value in the output: + +``Waiting for http://dataverse:8080 to become ready in max 10m.`` + Wrapping Up ----------- diff --git a/docker/compose/demo/compose.yml b/docker/compose/demo/compose.yml index e4bcc9778d7..8f1af3e396b 100644 --- a/docker/compose/demo/compose.yml +++ b/docker/compose/demo/compose.yml @@ -49,6 +49,8 @@ services: container_name: "bootstrap" image: gdcc/configbaker:alpha restart: "no" + environment: + - TIMEOUT=3m command: - bootstrap.sh - dev From 4093e18e19a6d872bcd4b24612748193f3080a1e Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 21 Mar 2024 17:04:33 -0400 Subject: [PATCH 572/689] Fix order and patch notes --- .../10338-expose-and-sort-publish-status-facet.md | 2 +- .../iq/dataverse/search/SearchServiceBean.java | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/doc/release-notes/10338-expose-and-sort-publish-status-facet.md b/doc/release-notes/10338-expose-and-sort-publish-status-facet.md index a14ea62da67..b2362ddb2c5 100644 --- a/doc/release-notes/10338-expose-and-sort-publish-status-facet.md +++ b/doc/release-notes/10338-expose-and-sort-publish-status-facet.md @@ -1 +1 @@ -In version 6.1, the publication status facet location was unintentionally moved to the bottom, and it also prevented it from being visible to guest users. In version 6.2, we have restored its visibility to all users and moved it back to the top of the list. \ No newline at end of file +In version 6.1, the publication status facet location was unintentionally moved to the bottom. In this version, we have restored the original order. diff --git a/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java index 4e345a1e036..c6f08151050 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/SearchServiceBean.java @@ -217,13 +217,7 @@ public SolrQueryResponse search( if (addFacets) { - /* - * We talked about this in slack on 2021-09-14, Users can see objects on draft/unpublished - * if the owner gives permissions to all users so it makes sense to expose this facet - * to all users. The request of this change started because the order of the facets were - * changed with the PR #9635 and this was unintended. - */ - solrQuery.addFacetField(SearchFields.PUBLICATION_STATUS); + // ----------------------------------- // Facets to Retrieve @@ -232,6 +226,13 @@ public SolrQueryResponse search( solrQuery.addFacetField(SearchFields.DATAVERSE_CATEGORY); solrQuery.addFacetField(SearchFields.METADATA_SOURCE); solrQuery.addFacetField(SearchFields.PUBLICATION_YEAR); + /* + * We talked about this in slack on 2021-09-14, Users can see objects on draft/unpublished + * if the owner gives permissions to all users so it makes sense to expose this facet + * to all users. The request of this change started because the order of the facets were + * changed with the PR #9635 and this was unintended. + */ + solrQuery.addFacetField(SearchFields.PUBLICATION_STATUS); solrQuery.addFacetField(SearchFields.DATASET_LICENSE); /** * @todo when a new method on datasetFieldService is available From a8aaa11f2709959022171b2e61b552d9c1cd1d35 Mon Sep 17 00:00:00 2001 From: landreev Date: Thu, 21 Mar 2024 17:07:56 -0400 Subject: [PATCH 573/689] The commits that didn't make it. #9356 (#10407) --- src/main/java/edu/harvard/iq/dataverse/DatasetPage.java | 8 +++++--- src/main/java/edu/harvard/iq/dataverse/DataversePage.java | 8 +++++--- .../java/edu/harvard/iq/dataverse/NavigationWrapper.java | 5 +++++ ...e.java => CheckRateLimitForCollectionPageCommand.java} | 4 ++-- ...Page.java => CheckRateLimitForDatasetPageCommand.java} | 4 ++-- 5 files changed, 19 insertions(+), 10 deletions(-) rename src/main/java/edu/harvard/iq/dataverse/engine/command/impl/{CheckRateLimitForCollectionPage.java => CheckRateLimitForCollectionPageCommand.java} (73%) rename src/main/java/edu/harvard/iq/dataverse/engine/command/impl/{CheckRateLimitForDatasetPage.java => CheckRateLimitForDatasetPageCommand.java} (74%) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index d7722f55512..2e4cb56db48 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -24,7 +24,7 @@ import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; -import edu.harvard.iq.dataverse.engine.command.impl.CheckRateLimitForDatasetPage; +import edu.harvard.iq.dataverse.engine.command.impl.CheckRateLimitForDatasetPageCommand; import edu.harvard.iq.dataverse.engine.command.impl.CreatePrivateUrlCommand; import edu.harvard.iq.dataverse.engine.command.impl.CuratePublishedDatasetVersionCommand; import edu.harvard.iq.dataverse.engine.command.impl.DeaccessionDatasetVersionCommand; @@ -252,6 +252,8 @@ public enum DisplayMode { DatasetVersionUI datasetVersionUI; @Inject PermissionsWrapper permissionsWrapper; + @Inject + NavigationWrapper navigationWrapper; @Inject FileDownloadHelper fileDownloadHelper; @Inject @@ -1935,8 +1937,8 @@ private void setIdByPersistentId() { private String init(boolean initFull) { // Check for rate limit exceeded. Must be done before anything else to prevent unnecessary processing. - if (!cacheFactory.checkRate(session.getUser(), new CheckRateLimitForDatasetPage(null,null))) { - return BundleUtil.getStringFromBundle("command.exception.user.ratelimited", Arrays.asList(CheckRateLimitForDatasetPage.class.getSimpleName())); + if (!cacheFactory.checkRate(session.getUser(), new CheckRateLimitForDatasetPageCommand(null,null))) { + return navigationWrapper.tooManyRequests(); } //System.out.println("_YE_OLDE_QUERY_COUNTER_"); // for debug purposes setDataverseSiteUrl(systemConfig.getDataverseSiteUrl()); diff --git a/src/main/java/edu/harvard/iq/dataverse/DataversePage.java b/src/main/java/edu/harvard/iq/dataverse/DataversePage.java index 4f0a3f14b99..afdff38c588 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataversePage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataversePage.java @@ -9,7 +9,7 @@ import edu.harvard.iq.dataverse.dataverse.DataverseUtil; import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; -import edu.harvard.iq.dataverse.engine.command.impl.CheckRateLimitForCollectionPage; +import edu.harvard.iq.dataverse.engine.command.impl.CheckRateLimitForCollectionPageCommand; import edu.harvard.iq.dataverse.engine.command.impl.CreateDataverseCommand; import edu.harvard.iq.dataverse.engine.command.impl.CreateSavedSearchCommand; import edu.harvard.iq.dataverse.engine.command.impl.DeleteDataverseCommand; @@ -118,6 +118,8 @@ public enum LinkMode { @EJB DataverseLinkingServiceBean linkingService; @Inject PermissionsWrapper permissionsWrapper; + @Inject + NavigationWrapper navigationWrapper; @Inject DataverseHeaderFragment dataverseHeaderFragment; @EJB PidProviderFactoryBean pidProviderFactoryBean; @@ -324,8 +326,8 @@ public void updateOwnerDataverse() { public String init() { //System.out.println("_YE_OLDE_QUERY_COUNTER_"); // for debug purposes // Check for rate limit exceeded. Must be done before anything else to prevent unnecessary processing. - if (!cacheFactory.checkRate(session.getUser(), new CheckRateLimitForCollectionPage(null,null))) { - return BundleUtil.getStringFromBundle("command.exception.user.ratelimited", Arrays.asList(CheckRateLimitForCollectionPage.class.getSimpleName())); + if (!cacheFactory.checkRate(session.getUser(), new CheckRateLimitForCollectionPageCommand(null,null))) { + return navigationWrapper.tooManyRequests(); } if (this.getAlias() != null || this.getId() != null || this.getOwnerId() == null) {// view mode for a dataverse if (this.getAlias() != null) { diff --git a/src/main/java/edu/harvard/iq/dataverse/NavigationWrapper.java b/src/main/java/edu/harvard/iq/dataverse/NavigationWrapper.java index 832d7ec19ef..54fb8f211a6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/NavigationWrapper.java +++ b/src/main/java/edu/harvard/iq/dataverse/NavigationWrapper.java @@ -16,6 +16,7 @@ import java.util.logging.Logger; import jakarta.faces.context.FacesContext; import jakarta.faces.view.ViewScoped; +import jakarta.ws.rs.core.Response.Status; import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.servlet.http.HttpServletRequest; @@ -87,6 +88,10 @@ public String notAuthorized(){ } } + public String tooManyRequests() { + return sendError(Status.TOO_MANY_REQUESTS.getStatusCode()); + } + public String notFound() { return sendError(HttpServletResponse.SC_NOT_FOUND); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForCollectionPage.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForCollectionPageCommand.java similarity index 73% rename from src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForCollectionPage.java rename to src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForCollectionPageCommand.java index 9dcf0428fff..b23e6034c9a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForCollectionPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForCollectionPageCommand.java @@ -6,8 +6,8 @@ import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; -public class CheckRateLimitForCollectionPage extends AbstractVoidCommand { - public CheckRateLimitForCollectionPage(DataverseRequest aRequest, DvObject dvObject) { +public class CheckRateLimitForCollectionPageCommand extends AbstractVoidCommand { + public CheckRateLimitForCollectionPageCommand(DataverseRequest aRequest, DvObject dvObject) { super(aRequest, dvObject); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForDatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForDatasetPageCommand.java similarity index 74% rename from src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForDatasetPage.java rename to src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForDatasetPageCommand.java index 04a27d082f4..da8c1e4d8e3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForDatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CheckRateLimitForDatasetPageCommand.java @@ -6,9 +6,9 @@ import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; -public class CheckRateLimitForDatasetPage extends AbstractVoidCommand { +public class CheckRateLimitForDatasetPageCommand extends AbstractVoidCommand { - public CheckRateLimitForDatasetPage(DataverseRequest aRequest, DvObject dvObject) { + public CheckRateLimitForDatasetPageCommand(DataverseRequest aRequest, DvObject dvObject) { super(aRequest, dvObject); } From d0bcddcead068fc99a6fc63c7ac3b3cf9c0c6c1a Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Fri, 22 Mar 2024 10:15:41 -0400 Subject: [PATCH 574/689] add to QA checklist: make sure deploy to beta works --- doc/sphinx-guides/source/qa/qa-workflow.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/sphinx-guides/source/qa/qa-workflow.md b/doc/sphinx-guides/source/qa/qa-workflow.md index 9915fe97d98..af462653dca 100644 --- a/doc/sphinx-guides/source/qa/qa-workflow.md +++ b/doc/sphinx-guides/source/qa/qa-workflow.md @@ -98,3 +98,7 @@ 1. Delete merged branch Just a housekeeping move if the PR is from IQSS. Click the delete branch button where the merge button had been. There is no deletion for outside contributions. + +1. Ensure that deployment to beta.dataverse.org succeeded. + + Go to to keep any eye on the deployment to to make sure it succeeded. The latest commit will appear at the bottom right and . From a022e2e1ec31981130fb2485653168f9d12ab9a0 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 22 Mar 2024 16:19:49 +0100 Subject: [PATCH 575/689] fix(ct): downgrade configbaker to Alpine 3.18 #10413 --- modules/container-configbaker/Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/container-configbaker/Dockerfile b/modules/container-configbaker/Dockerfile index 9b98334d72b..91bf5a2c875 100644 --- a/modules/container-configbaker/Dockerfile +++ b/modules/container-configbaker/Dockerfile @@ -10,7 +10,10 @@ ARG SOLR_VERSION FROM solr:${SOLR_VERSION} AS solr # Let's build us a baker -FROM alpine:3 +# WARNING: +# Do not upgrade the tag to :3 or :3.19 until https://pkgs.alpinelinux.org/package/v3.19/main/x86_64/c-ares is at v1.26.0+! +# See https://github.com/IQSS/dataverse/issues/10413 for more information. +FROM alpine:3.18 ENV SCRIPT_DIR="/scripts" \ SECRETS_DIR="/secrets" \ From 2129555af12b244c92d2c3c82659fd7cb79fc6bd Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Fri, 22 Mar 2024 15:15:05 -0400 Subject: [PATCH 576/689] fixing bug with tabs in log line for MDC --- .../MakeDataCountLoggingServiceBean.java | 18 ++++++++++++------ .../MakeDataCountLoggingServiceBeanTest.java | 13 +++++++++---- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountLoggingServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountLoggingServiceBean.java index 5edf2fde0c3..c3bf85e699a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountLoggingServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountLoggingServiceBean.java @@ -46,6 +46,12 @@ public void logEntry(MakeDataCountEntry entry) { public String getLogFileName() { return "counter_"+new SimpleDateFormat("yyyy-MM-dd").format(new Timestamp(new Date().getTime()))+".log"; } + + // Sanitize the values to a safe string for the log file + static String sanitize(String in) { + // Log lines are tab delimited so tabs must be replaced. Replacing escape sequences with a space. + return in != null ? in.replaceAll("\\s+", " ") : null; + } public static class MakeDataCountEntry { @@ -367,7 +373,7 @@ public String getTitle() { * @param title the title to set */ public final void setTitle(String title) { - this.title = title; + this.title = sanitize(title); } /** @@ -384,7 +390,7 @@ public String getPublisher() { * @param publisher the publisher to set */ public final void setPublisher(String publisher) { - this.publisher = publisher; + this.publisher = sanitize(publisher); } /** @@ -401,7 +407,7 @@ public String getPublisherId() { * @param publisherId the publisherId to set */ public final void setPublisherId(String publisherId) { - this.publisherId = publisherId; + this.publisherId = sanitize(publisherId); } /** @@ -418,7 +424,7 @@ public String getAuthors() { * @param authors the authors to set */ public final void setAuthors(String authors) { - this.authors = authors; + this.authors = sanitize(authors); } /** @@ -452,7 +458,7 @@ public String getVersion() { * @param version the version to set */ public final void setVersion(String version) { - this.version = version; + this.version = sanitize(version); } /** @@ -469,7 +475,7 @@ public String getOtherId() { * @param otherId the otherId to set */ public void setOtherId(String otherId) { - this.otherId = otherId; + this.otherId = sanitize(otherId); } /** diff --git a/src/test/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountLoggingServiceBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountLoggingServiceBeanTest.java index c1051a57db8..2a673ee4e79 100644 --- a/src/test/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountLoggingServiceBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountLoggingServiceBeanTest.java @@ -21,7 +21,6 @@ import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; -import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Test; /** @@ -45,8 +44,8 @@ public void testMainAndFileConstructor() { GlobalId id = dataset.getGlobalId(); dataset.setGlobalId(id); dvVersion.setDataset(dataset); - dvVersion.setAuthorsStr("OneAuthor;TwoAuthor"); - dvVersion.setTitle("Title"); + dvVersion.setAuthorsStr("OneAuthor;\tTwoAuthor"); + dvVersion.setTitle("Title\tWith Tab"); dvVersion.setVersionNumber(1L); dvVersion.setReleaseTime(new Date()); @@ -64,7 +63,13 @@ public void testMainAndFileConstructor() { //lastly setting attributes we don't actually use currently in our logging/constructors, just in case entry.setUserCookieId("UserCookId"); - entry.setOtherId("OtherId"); + entry.setOtherId(null); // null pointer check for sanitize method + assertThat(entry.getOtherId(), is("-")); + entry.setOtherId("OtherId\t\r\nX"); + // escape sequences get replaced with a space in sanitize method + assertThat(entry.getOtherId(), is("OtherId X")); + // check other replacements for author list ";" becomes "|" + assertThat(entry.getAuthors(), is("OneAuthor| TwoAuthor")); //And test. "-" is the default assertThat(entry.getEventTime(), is(not("-"))); From 8299bac272e755dc204afe1160714f86b57c29b0 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Sat, 23 Mar 2024 16:40:52 -0400 Subject: [PATCH 577/689] Menu clarification for Mac/Windows/Linux Menu clarification for Mac/Windows/Linux --- doc/sphinx-guides/source/container/dev-usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/container/dev-usage.rst b/doc/sphinx-guides/source/container/dev-usage.rst index be4eda5da44..6d6291e9924 100644 --- a/doc/sphinx-guides/source/container/dev-usage.rst +++ b/doc/sphinx-guides/source/container/dev-usage.rst @@ -259,7 +259,7 @@ Hotswapping methods requires using JDWP (Debug Mode), but does not allow switchi **IMPORTANT**: This requires installation of the `Docker plugin `_. - **NOTE**: You might need to change the Docker Compose executable in your IDE settings to ``docker`` if you have no ``docker-compose`` bin (*File > Settings > Build > Docker > Tools*). + **NOTE**: You might need to change the Docker Compose executable in your IDE settings to ``docker`` if you have no ``docker-compose`` bin, Start from the ``File`` menu if you are on Linux/Windows or ``IntelliJ IDEA`` on Mac and then go to Settings > Build > Docker > Tools. (*File > Settings > Build > Docker > Tools*). .. image:: img/intellij-compose-add-new-config.png From db9cd865d9b1796d6379c0133aadc329183d83c3 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Mon, 25 Mar 2024 08:38:59 +0100 Subject: [PATCH 578/689] docs(mail): apply suggestions from code review Thanks @pdurbin! Co-authored-by: Philip Durbin --- doc/release-notes/7424-mailsession.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/release-notes/7424-mailsession.md b/doc/release-notes/7424-mailsession.md index 25b1d39a471..43846b0b72d 100644 --- a/doc/release-notes/7424-mailsession.md +++ b/doc/release-notes/7424-mailsession.md @@ -7,6 +7,6 @@ At this point, no action is required if you want to keep your current configurat Warnings will show in your server logs to inform and remind you about the deprecation. A future major release of Dataverse may remove this way of configuration. -For more details on how to configure your the connection to your mail provider, please find updated details within the Installation Guide's main installation and configuration section. +For more details on how to configure the connection to your mail provider, please find updated details within the Installation Guide's main installation and configuration section. -Please note: as there have been problems with mails delivered to SPAM folders when "From" within mail envelope and mail session configuration mismatched, as of this version the sole source for the "From" address is the setting `dataverse.mail.system-email` once you migrate to the new way of configuration. \ No newline at end of file +Please note: as there have been problems with email delivered to SPAM folders when the "From" within mail envelope and the mail session configuration didn't match (#4210), as of this version the sole source for the "From" address is the setting `dataverse.mail.system-email` once you migrate to the new way of configuration. \ No newline at end of file From 83d29b122f89e3d902a8dc9b83938c4a702de1c1 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Mon, 25 Mar 2024 09:08:20 +0100 Subject: [PATCH 579/689] feat(ct): add MTA config to demo compose #7424 --- docker/compose/demo/compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/compose/demo/compose.yml b/docker/compose/demo/compose.yml index 8f1af3e396b..6c2bdcf79a4 100644 --- a/docker/compose/demo/compose.yml +++ b/docker/compose/demo/compose.yml @@ -14,6 +14,8 @@ services: DATAVERSE_DB_PASSWORD: secret DATAVERSE_DB_USER: dataverse DATAVERSE_FEATURE_API_BEARER_AUTH: "1" + DATAVERSE_MAIL_SYSTEM_EMAIL: "Demo Dataverse " + DATAVERSE_MAIL_MTA_HOST: "smtp" JVM_ARGS: -Ddataverse.files.storage-driver-id=file1 -Ddataverse.files.file1.type=file -Ddataverse.files.file1.label=Filesystem From b42983caec39e773f30402077c823ef480bda6a7 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Mon, 25 Mar 2024 11:14:08 +0100 Subject: [PATCH 580/689] feat(index): add timing metrics to measure indexing load - Measure wait times for a permit, a measurement how many indexing requests are currently stuck because of high demand (so you can tune the amount of available parallel indexing threads) - Measure how long the actual indexing task runs, which might be an indication of lots of very large datasets or not enough resources for your index --- pom.xml | 5 +++ .../iq/dataverse/search/IndexServiceBean.java | 31 +++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 8b2850e1df9..df4cadc80ce 100644 --- a/pom.xml +++ b/pom.xml @@ -179,6 +179,11 @@ microprofile-config-api provided + + org.eclipse.microprofile.metrics + microprofile-metrics-api + provided + jakarta.platform jakarta.jakartaee-api diff --git a/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java index cf1e58e4028..5bac9a3e804 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java @@ -48,6 +48,8 @@ import jakarta.ejb.Stateless; import jakarta.ejb.TransactionAttribute; import static jakarta.ejb.TransactionAttributeType.REQUIRES_NEW; + +import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.json.JsonObject; import jakarta.persistence.EntityManager; @@ -72,6 +74,9 @@ import org.apache.tika.sax.BodyContentHandler; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.metrics.MetricUnits; +import org.eclipse.microprofile.metrics.Timer; +import org.eclipse.microprofile.metrics.annotation.Metric; import org.xml.sax.ContentHandler; @Stateless @@ -344,6 +349,27 @@ public void indexDatasetInNewTransaction(Long datasetId) { //Dataset dataset) { private static final Map INDEXING_NOW = new ConcurrentHashMap<>(); // semaphore for async indexing private static final Semaphore ASYNC_INDEX_SEMAPHORE = new Semaphore(JvmSettings.MAX_ASYNC_INDEXES.lookupOptional(Integer.class).orElse(4), true); + + @Inject + @Metric(name = "index_permit_wait_time", absolute = true, unit = MetricUnits.NANOSECONDS, + description = "Displays how long does it take to receive a permit to index a dataset") + Timer indexPermitWaitTimer; + + @Inject + @Metric(name = "index_time", absolute = true, unit = MetricUnits.NANOSECONDS, + description = "Displays how long does it take to index a dataset") + Timer indexTimer; + + /** + * Try to acquire a permit from the semaphore avoiding too many parallel indexes, potentially overwhelming Solr. + * This method will time the duration waiting for the permit, allowing indexing performance to be measured. + * @throws InterruptedException + */ + private void acquirePermitFromSemaphore() throws InterruptedException { + try (var timeContext = indexPermitWaitTimer.time()) { + ASYNC_INDEX_SEMAPHORE.acquire(); + } + } // When you pass null as Dataset parameter to this method, it indicates that the indexing of the dataset with "id" has finished // Pass non-null Dataset to schedule it for indexing @@ -389,7 +415,7 @@ synchronized private static Dataset getNextToIndex(Long id, Dataset d) { @Asynchronous public void asyncIndexDataset(Dataset dataset, boolean doNormalSolrDocCleanUp) { try { - ASYNC_INDEX_SEMAPHORE.acquire(); + acquirePermitFromSemaphore(); doAyncIndexDataset(dataset, doNormalSolrDocCleanUp); } catch (InterruptedException e) { String failureLogText = "Indexing failed: interrupted. You can kickoff a re-index of this dataset with: \r\n curl http://localhost:8080/api/admin/index/datasets/" + dataset.getId().toString(); @@ -404,7 +430,8 @@ private void doAyncIndexDataset(Dataset dataset, boolean doNormalSolrDocCleanUp) Long id = dataset.getId(); Dataset next = getNextToIndex(id, dataset); // if there is an ongoing index job for this dataset, next is null (ongoing index job will reindex the newest version after current indexing finishes) while (next != null) { - try { + // Time context will automatically start on creation and stop when leaving the try block + try (var timeContext = indexTimer.time()) { indexDataset(next, doNormalSolrDocCleanUp); } catch (Exception e) { // catch all possible exceptions; otherwise when something unexpected happes the dataset wold remain locked and impossible to reindex String failureLogText = "Indexing failed. You can kickoff a re-index of this dataset with: \r\n curl http://localhost:8080/api/admin/index/datasets/" + dataset.getId().toString(); From 77789db2ace41462e405864405b6daba0c17b1cd Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Mon, 25 Mar 2024 12:39:16 +0100 Subject: [PATCH 581/689] reverted back to async index after publish, but now with em.flush() after index time udate --- .../command/impl/FinalizeDatasetPublicationCommand.java | 7 +------ .../edu/harvard/iq/dataverse/search/IndexServiceBean.java | 1 + 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java index 1277a98aa31..3b124b539c2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java @@ -247,12 +247,6 @@ public Dataset execute(CommandContext ctxt) throws CommandException { logger.info("Successfully published the dataset "+readyDataset.getGlobalId().asString()); readyDataset = ctxt.em().merge(readyDataset); - - try { - ctxt.index().indexDataset(readyDataset, true); - } catch (SolrServerException | IOException e) { - throw new CommandException("Indexing failed: " + e.getMessage(), this); - } return readyDataset; } @@ -273,6 +267,7 @@ public boolean onSuccess(CommandContext ctxt, Object r) { } catch (Exception e) { logger.warning("Failure to send dataset published messages for : " + dataset.getId() + " : " + e.getMessage()); } + ctxt.index().asyncIndexDataset(dataset, true); //re-indexing dataverses that have additional subjects if (!dataversesToIndex.isEmpty()){ diff --git a/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java index cf1e58e4028..a5ea46e45b9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java @@ -1503,6 +1503,7 @@ private void updateLastIndexedTimeInNewTransaction(Long id) { DvObject dvObjectToModify = em.find(DvObject.class, id); dvObjectToModify.setIndexTime(new Timestamp(new Date().getTime())); dvObjectToModify = em.merge(dvObjectToModify); + em.flush(); } /** From 8945ec86acce638222a2c0b7eaa77ede0a9be3f5 Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Mon, 25 Mar 2024 13:29:02 +0100 Subject: [PATCH 582/689] moved indexing after last dataset merge to assure indextime is not overwritten --- .../command/impl/FinalizeDatasetPublicationCommand.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java index 3b124b539c2..287e877f6e0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java @@ -267,7 +267,6 @@ public boolean onSuccess(CommandContext ctxt, Object r) { } catch (Exception e) { logger.warning("Failure to send dataset published messages for : " + dataset.getId() + " : " + e.getMessage()); } - ctxt.index().asyncIndexDataset(dataset, true); //re-indexing dataverses that have additional subjects if (!dataversesToIndex.isEmpty()){ @@ -297,7 +296,8 @@ public boolean onSuccess(CommandContext ctxt, Object r) { logger.log(Level.WARNING, "Finalization: exception caught while exporting: "+ex.getMessage(), ex); // ... but it is important to only update the export time stamp if the // export was indeed successful. - } + } + ctxt.index().asyncIndexDataset(dataset, true); return retVal; } From 053ebdb1e0805de73d0c41cdb8d9fd4b9c25abe3 Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Mon, 25 Mar 2024 13:52:40 +0100 Subject: [PATCH 583/689] metric fix --- .../java/edu/harvard/iq/dataverse/search/IndexServiceBean.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java index eae8e470ddc..cf0b177df95 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java @@ -446,7 +446,7 @@ private void doAyncIndexDataset(Dataset dataset, boolean doNormalSolrDocCleanUp) public void asyncIndexDatasetList(List datasets, boolean doNormalSolrDocCleanUp) { for(Dataset dataset : datasets) { try { - ASYNC_INDEX_SEMAPHORE.acquire(); + acquirePermitFromSemaphore(); doAyncIndexDataset(dataset, true); } catch (InterruptedException e) { String failureLogText = "Indexing failed: interrupted. You can kickoff a re-index of this dataset with: \r\n curl http://localhost:8080/api/admin/index/datasets/" + dataset.getId().toString(); From 744113d91461e79c37a997fdbea22d4ab0a7eb40 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Mon, 25 Mar 2024 09:37:34 -0400 Subject: [PATCH 584/689] Update doc/sphinx-guides/source/container/dev-usage.rst Co-authored-by: Philip Durbin --- doc/sphinx-guides/source/container/dev-usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/container/dev-usage.rst b/doc/sphinx-guides/source/container/dev-usage.rst index 6d6291e9924..a28757165c5 100644 --- a/doc/sphinx-guides/source/container/dev-usage.rst +++ b/doc/sphinx-guides/source/container/dev-usage.rst @@ -259,7 +259,7 @@ Hotswapping methods requires using JDWP (Debug Mode), but does not allow switchi **IMPORTANT**: This requires installation of the `Docker plugin `_. - **NOTE**: You might need to change the Docker Compose executable in your IDE settings to ``docker`` if you have no ``docker-compose`` bin, Start from the ``File`` menu if you are on Linux/Windows or ``IntelliJ IDEA`` on Mac and then go to Settings > Build > Docker > Tools. (*File > Settings > Build > Docker > Tools*). + **NOTE**: You might need to change the Docker Compose executable in your IDE settings to ``docker`` if you have no ``docker-compose`` binary. Start from the ``File`` menu if you are on Linux/Windows or ``IntelliJ IDEA`` on Mac and then go to Settings > Build > Docker > Tools. .. image:: img/intellij-compose-add-new-config.png From a2c50b62e430cc829a4aeccaa5bdfc9baf5213e2 Mon Sep 17 00:00:00 2001 From: Benedikt Meier Date: Mon, 25 Mar 2024 15:29:48 +0100 Subject: [PATCH 585/689] log file with a different instance root directory add two files #10373 --- .../edu/harvard/iq/dataverse/authorization/AuthFilter.java | 4 +--- .../iq/dataverse/harvest/client/HarvesterServiceBean.java | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthFilter.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthFilter.java index a2cf3082ae7..c93a1496c17 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthFilter.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthFilter.java @@ -29,9 +29,7 @@ public void init(FilterConfig filterConfig) throws ServletException { logger.info(AuthFilter.class.getName() + "initialized. filterConfig.getServletContext().getServerInfo(): " + filterConfig.getServletContext().getServerInfo()); try { - String glassfishLogsDirectory = "logs"; - - FileHandler logFile = new FileHandler(".." + File.separator + glassfishLogsDirectory + File.separator + "authfilter.log"); + FileHandler logFile = new FileHandler( System.getProperty("com.sun.aas.instanceRoot") + File.separator + "logs" + File.separator + "authfilter.log"); SimpleFormatter formatterTxt = new SimpleFormatter(); logFile.setFormatter(formatterTxt); logger.addHandler(logFile); diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvesterServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvesterServiceBean.java index 20884e3360c..e0b5c2dfbfb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvesterServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvesterServiceBean.java @@ -88,7 +88,6 @@ public class HarvesterServiceBean { public static final String HARVEST_RESULT_FAILED="failed"; public static final String DATAVERSE_PROPRIETARY_METADATA_FORMAT="dataverse_json"; public static final String DATAVERSE_PROPRIETARY_METADATA_API="/api/datasets/export?exporter="+DATAVERSE_PROPRIETARY_METADATA_FORMAT+"&persistentId="; - public static final String DATAVERSE_HARVEST_STOP_FILE="../logs/stopharvest_"; public HarvesterServiceBean() { @@ -399,7 +398,7 @@ private void deleteHarvestedDatasetIfExists(String persistentIdentifier, Dataver private boolean checkIfStoppingJob(HarvestingClient harvestingClient) { Long pid = ProcessHandle.current().pid(); - String stopFileName = DATAVERSE_HARVEST_STOP_FILE + harvestingClient.getName() + "." + pid; + String stopFileName = System.getProperty("com.sun.aas.instanceRoot") + File.separator + "logs" + File.separator + "stopharvest_" + harvestingClient.getName() + "." + pid; Path stopFilePath = Paths.get(stopFileName); if (Files.exists(stopFilePath)) { From ac74b23a4e316e6f142e82582165d35f720604ed Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Mon, 25 Mar 2024 14:03:10 -0400 Subject: [PATCH 586/689] removed outdated "problems sending email" section #9939 --- .../source/installation/installation-main.rst | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/doc/sphinx-guides/source/installation/installation-main.rst b/doc/sphinx-guides/source/installation/installation-main.rst index 5f44ef1e348..9f935db6510 100755 --- a/doc/sphinx-guides/source/installation/installation-main.rst +++ b/doc/sphinx-guides/source/installation/installation-main.rst @@ -141,19 +141,6 @@ Got ERR_ADDRESS_UNREACHABLE While Navigating on Interface or API Calls If you are receiving an ``ERR_ADDRESS_UNREACHABLE`` while navigating the GUI or making an API call, make sure the ``siteUrl`` JVM option is defined. For details on how to set ``siteUrl``, please refer to :ref:`dataverse.siteUrl` from the :doc:`config` section. For context on why setting this option is necessary, refer to :ref:`dataverse.fqdn` from the :doc:`config` section. -Problems Sending Email -^^^^^^^^^^^^^^^^^^^^^^ - -If your Dataverse installation is not sending system emails, you may need to provide authentication for your mail host. First, double check the SMTP server being used with this Payara asadmin command: - -``./asadmin get server.resources.mail-resource.mail/notifyMailSession.host`` - -This should return the DNS of the mail host you configured during or after installation. mail/notifyMailSession is the JavaMail Session that's used to send emails to users. - -If the command returns a host you don't want to use, you can modify your notifyMailSession with the Payara ``asadmin set`` command with necessary options (`click here for the manual page `_), or via the admin console at http://localhost:4848 with your domain running. - -If your mail host requires a username/password for access, continue to the next section. - Mail Host Configuration & Authentication ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 909244b63e167daf5e065b25d6ecced241ec9d42 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Mon, 25 Mar 2024 14:55:57 -0400 Subject: [PATCH 587/689] Missing env var --- .../source/container/dev-usage.rst | 2 +- .../container/img/intellij-compose-setup.png | Bin 45986 -> 52130 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/container/dev-usage.rst b/doc/sphinx-guides/source/container/dev-usage.rst index be4eda5da44..9f2b2648165 100644 --- a/doc/sphinx-guides/source/container/dev-usage.rst +++ b/doc/sphinx-guides/source/container/dev-usage.rst @@ -263,7 +263,7 @@ Hotswapping methods requires using JDWP (Debug Mode), but does not allow switchi .. image:: img/intellij-compose-add-new-config.png - Give your configuration a meaningful name, select the compose file to use (in this case the default one), add the environment variable ``SKIP_DEPLOY=1``, and optionally select the services to start. + Give your configuration a meaningful name, select the compose file to use (in this case the default one), add the environment variables ``SKIP_DEPLOY=1`` and ``POSTGRES_VERSION=16``, optionally select the services to start. You might also want to change other options like attaching to containers to view the logs within the "Services" tab. .. image:: img/intellij-compose-setup.png diff --git a/doc/sphinx-guides/source/container/img/intellij-compose-setup.png b/doc/sphinx-guides/source/container/img/intellij-compose-setup.png index 42c2accf2b459b0502d2fb42be5058e07b0e9376..0ab73e125b2897176750ac81baa8c8a6223d3aa2 100644 GIT binary patch literal 52130 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%0DDsE$N)Hyv_0!XVmW&99 zSg@o&^BkXkxC{{v3};hgbdbv5-nT;7Zm`+Ot#miZ)dT;c44X-4JVV9J?V#n#i5Ad@ zycNNN*0r_8ux+HIY;>kED#!7e(XlnaIbX(T2r9;jRAiMm6{T@?#AfQ8VMAQdO=XQFnXdn z#(FII%}8K*Vm_ot%xNI zPW(vz@Zi)TtnbA*mzcS!7(2*|s(scevlgp0O;1T4|em6*@y< zkj=Bb8=uy6NBaOB>FS<7x5jE&5L1En0xqR=7z${v0&N;JU{>&jaUnrLX|aAca0V|Hgp zbuwDDWl3BEYn4c~JEDHYnIiQEzV<-e;Np4yFFGI-RkraNiN}Lo8-aaVvoy!3lDf|8 zH{zOHO}mh}ES7gnx6e#?;@wdD9#6P*4HvA*^%7;aKb#+aZTtBmcc6U|_cI{L(x*+L@yN#N&b837O{E1`Uueckc#pS_Li|gpC*Vygm>}MLDnwoQ364L(Edn))p zQQ&6mM71|zt0@e;#*5I#!^z;h5tdrzv|r->7bXjpJ1u1%Ja$*8x)>-5eOXiBL~q*)mNE{2!*0N~mY9w!AbOr5 zxcUy*rdAT|=E9H_cC+R+&XxL4T+1{)wIUnf|o#v+`f8F>pG} z%P*8RCFt~tO15j)-f-@SV@bbtQSwD0Fvp>Vqlg((%G{C~b%F|$iGV~&$Y6w}0 z(Lq2wmYGp7c3>wi)+vYaK>*J`V&9-adcA@5ZYq$G&U4_)cJz~)MvARYHv9V< z)2Bz5G?_>Q5my;`Zz@`KHC%Dpfk6`24u&9{@@UPL*5|mNtKQi3YvdDrc0Scgt<^oE z40NbLRnMz$8;s%qv#*X3_J0VS_FIe`K-Xr#PalZCNs+TM42zfSY`dA?kz6u~RpKy| z{+p5znyDfd|C3?q3Hmph;r}av*zOD@D)`sQ5|J;Z2#9;9s+No4{x<_djla^>|a}MC080l%0`ha5V_5}@1VA@eYybHy)OaTo7537pw zJ)d5FPxCl<_qiy7trg43HUc@IC}G|m{|8iUH<|bTO67jfdh&l(sp^X=oSW)=BSqiC zg&=|X{Uo{Q_b1gw$Kd=vO_J)C!VZG;>C@Jew!#Dpysuze`aAQIiWiR0tGM-ai6Ngl z_hF?&D3ull<`U%ZQIiAG_bka)wLliEnkV4D_?!RKu}L#@WB6tt68M?J=dIH>LbO-6 zR#l0{0TS&fsBboaXDiwr{A#H^;hmHCwuN_$O z<65n#{>Zs#!1FhZ0S7Di$*vLO_AfFM=@~biGsitQr z+R#@mT@{R9jUOf;=#^c{KObj*Td&A(t9EVrBH88vS5H>{ew^TKT_7BOWwUq7X>0ae zO=6B-XGEu&$MU({n6sRk_O`?%pm;eXIB38RoQ4!@Te&eIn3e52gE$*!UC{@ucZE8N z!m3skOset(SkOik278iV2ZyLSmGqZTt^*BGXhaPS<7_;>&1k6>aYp;yGo&_R zZF5s4D6{S$OO%$aT^wKw5q(s`-6?}A*k$M%#Sadlj@J)ciZ@j^*9`xiykU)*RYs<0 zUeKT3T53+T=#=4m;T36As!SMD>+b{`;c|NH=Y+d|01f(yz|KE-2!SzyhaiWc1mbCYTR5bm=Soe~vso+Mw9 znvF{w(d|XGU7)^aCO<3cx?LTXM{_V?DHKvI9_-1@8oB!yFJbVpoT*oYf+;ISGS8ml z>sKF>OMkYxFD@EjB1SZal@b0#OCg@D_w4McH;&dIoKp@uapb2DlU%<`AU~JZSn&GK z3=nww3svo+d)Xq-JlOWyRDyUuT((}gTzVXT;FZU$8-61FR`Rjp17+x<(f zUc^#Uwp(L9kdR5L_VQw-LFb*|yqgbfV|~yvHWf=i5Vc5-+oGnIhN^N855+70WS`@ zBWzFVYu@tbrp{FRQONywl=xXL{PW<&>@&+oLV7BQ6d0i8@{d zS2~EMl!li%Ie@@B(mf+ZQZl8Eq)54BX4w(ZdF=w6KS}&@9^R4x*Ll}!PS!Iu$bM=t z`HcYI%jH9L_zV&ixI}ZGN9M+Fy056TEJ$j`PcA31zUrdlEE#iAM?l_^aBYNhzu1Rs zI`L+j2}a61z<=)Lz+9&b*q-;GQOqhqGoi~jBvFiU4OM7 z>X{3gCFB5XzTX22OS~Hb_gdY94Tt~By;c9^-srjp{YfGaZt(GjBWJ8eHn==S23H*>CUb~0C{|G^4 zvgEc~3bXaYi}nksxfH(}8m)n#*{2N3C9d64gmSN?rdR-0YoudFOP4wSG0B7v&+ko0 zOKlGyrPreKxMr&$bD_NC!~t;_BF~HE4vGnN;RIM zlt!C1SSPIA&|BRH5LZl7`T!t?LKrca+Me>LyHhbEOHeiIx==Cu$Jq!KE|0CASe3*g z*=Bza!QaOurh*8bQ?rqxrjqV0`K#+|5WWQl5)pZmH|~^WlCuYR@GjnYInYFLat~6e z@z4HKw5<28W2SP(SlBr#PX@AwPJq9kgL;KibHi(Bd_)H6Z@J2H~q>y2tp~~7K7k&_He{!N8zLqxcNA%H6~X# zq8onD`R~&FBAv62!CeSyhBe*Zx;(D$yq2&ja+p*Z8 zslktlb=i*|lI;@OKPNJ!?G@Ed&k_4C(6<#Z*L*~XacMm#QzK2GR#+sw^BH8@iusK}hJ*VP&G zc)dDTc2agElvQcjo$sH@xW&7x%Jk>q0Nc)JT5UJ`AbCM$7)NF-nIou7`0as9N8Sej zD#~rbc%WOIX!tSzYx^Y6QxE1;f2fJVugh~)`_tM6FV;NR24@<*pjQqsu6&ef&a`*I zd!f^Pw)>JtJY__jm3vN~}5zco^s;-ZWmqYRDprkeM zY7zT81WfxYt%12?Udt{j=0a#4%aMfHbfgA5LTsF#XY}^hTYVd)&?+GIfHId2;XXv` z{pK@AZc$Pv#rCuTC@A{%N^$?loFD9tjKeLy4Hv^6|LNp)U3>O_lzpdhkWrr zzd6!RgG1_gLx+vF@i;fUx-}Wn0ebam^}ZH{`Cmi?Ax=wHNj62Jivc#aVO|WYA$^A->6HXz*av&i!9mv)1|9ih} z%sZuiG=ECESy04Yy&wY6f0IrX6=m$VTm2MqaZW0?``QM*^i}6;QJBCtU~D-Rwag|a zfEjX`AsD$8=bfSrDRUTD>&bhz>nt;;&S>TH-5l20n;z%e+>%6$s(2g+e9vlqkl!VT-}wAomgad}}f{U9FU>2ZE`=Gh(p z0ayCJ(OIhnpwdPN0LxVm%y6}8B2#-zoyK(KtQJlb4qh)Vs=POdE>W}RIlVbu$$3#% zg!0>Y$5%YFc9?12^BVA9zjNIW-fm$ecdb7e>$PO^#gH-4Do!*<>Xq^@w|040I;NiO z@ZC0@+qGTu=CG z9$!^gmnRZT_#1zOTH7Dks)+xW-1dz5t>UZ^loek(EO#$$oauH`1otO4@v@)_rE3-J7iV{igJGr@ z_Wi(6_`%?tbcMFA`PDr|jMq{lhv32Yu>Vh4ug=|S$q4dGea6D}yW^s7>7y?cCsVuyh;cWY@At5i9 z6K0_BHo|?hf9m+baH)6=sFbQX8uLqDb@ZEBO1b@K%t8X4Dd4M|TSc`k*_Cm%XRqwxkyhfB5yBUi9z32ELnN&8F|4#A$ z{BtAnFQH)St8iTpCoN~A{`*TFdJ0vVhE$c6L(iN=1*ciyARpY&|G)_CE}J!`o~A)h-(ipeP**Is5N8kHaU+ng@>f59kH83Uik1YUK@0Oh##>fg0A-|q{>lK;h0 zV(HwA66l_mYQBbiHj$bM`;kRu^mYlAwDYReI(0Y-_5^PND(7SQla|W`5&==PgZYA2v^9) z-{Tt|MX0$MnsdaJ{J+?H%cv^bXl+zQR6>xF25F?bL#08wVUd#3-7J*ulI~VIq`SMN zyFt2h!MS%-wm!H4%j3VKVk1kJWY`I6P2XKWg^t$e5YiL2c`3p68X$d#lVq$@q%693;zTyINJ}>d(91$Uici@VF%wZx_ql$!)5xIFwP6`SN z>Y(qVCLJbc3zLR1spW`j;^MDB2QJTDJ?xY}Q)1-jXXevrm%OZB_%^lMX~3eK^io+F zcKehoMFzsa`NI3JVAH4xo%H4O9gg(4P5lD+x*78e;;DVdHG3WymfBSR2e*t!ICrHqE#Sn*-G`J}^TyDW8O-4tD9Np`) zVA;K6ug`|ZjDxI37dtjl$M#_J@p!AOkPlOYf{%~DYjss0df$HlBr*|s_l*^6F3|G( zU8QT+Z{Lf(=k`BMosU^a3yX@To8pOxh}`cgD|07)reu?59E@9KtSG;}487)IqDLP5s5HpgV7aeL?HyEuBKs*uk_xi~K_I#_NaS zef<0;YElMO+WN5ixoodSB#1%Uv5hE@ElZ~QueZ{MV^p=JD~zPeX3PiD1jB3XwxZI~ zhR>&#K8)u|U!U~ju5WB)?DD8WY$!Z#&WyT37?~32AdWMJpHpr~5_4_lEUHJ-Ka%kg zacY;u@w;6dZKXjYA|q`eyH$(<-#lS2tgknOZ0fas=1a=RpzKZ-ec9;4;vzs{lV0<~ z7agm$qw0G>jf*S_w8aEI7Z%a1J}svOX>)Tr?GnHK4gI;Q{ehI*mttYr<5^-(#SJGk zO?M|!CDd+WviT)ZGuz{SPo{_Rnd2czzC%Qv>KePiCt+YnB@trC6bmPM?7PRUqvyQ~rKzc@bEEd`Z_{@A>iNEs z=KHpM<$_lT2(9~$U;*O$p*H&o-d}l76sV%TRe=%zD)-6(V~I@2^TT4U>U5A(=eES| zaM1&YV-FD>14FIZlW+<8z|YOijdfrH_Cf1-{Utn|_HVDf!U4SMU|I2O=)=dmBiIy` z$3Wd>OaZff!x>L;^SvLU;FR`kWPJ3Vi)v=Q-RasTOZqqal_}+-ewomWwH%=xbe%y* zKgu&g0k2q+^R(`0JB3WOZ-jHGlsZmnG~Mm5-2Q~`AgT88{N&tzR=|XzjbYL4*!$IF_?qC zzJASmloU8R^TE?oV03hp@9s#A$laPwH0v9f4?h$_Q3!;t&eL36R=uR_HRpT_ZOP|! z!~WR4r8LoK%1~Uvdo0nAnipDAT+T;N;vlxnS)!qBz$V+)nvA4B{n8yq!Oa~gkdELjV^4P(`9dDj(AJS@!_nhAaI#6hM>%#FFQ{-IkvXF6X)5~uBoi-3TDiH(hh zjomMV%0J+7x!~Wa-@10pV@}H|MqXiyVK?rzfDG? zz&isY$*kH`K$5%VnicdGpRtiGRWuZD^|)qdG>aE@bKVZh|JZWxEYxE^+)PQG?M|WA zFUTx4VUJXrVMDEgr*_XT$tlRmk1m@ZQu-O{efH<7BV%F);ZXT{$in%+=~o9r=Ps3>&8;;#2FTb=XC#j!JOIH6m;2Yv*K=7 zXB7PM0_UZF>N*YGbOz6u2{sbxHC54MUT9i+zV(RKo?k-M|LwY747<2U!?$B!dm9Zp+c5~oumf4-xpFd)pHceDfo?g8=fYL@l;90&D>0*;3=8*^68UHgs|&d2&h zZhP|J_|OvoJ2;>Jayi{xUtec(_^Z@(^H=JVkPt5a4begKgC~#uwpaVgJ3+zbPZULT z?{;S^@vE0Sd7B>uf{)j_6_u34?d>ri?zW`nUA7j?XMZP9NX0Ulj*%ZUIw4mW4;xM7 zD~Axckgcq&fFWN4eZW9N`zSAuw(Ry?d`6rAV3S^h)hH@%`x%=J&i_t4 z2_fW-ZS`ijHekr1-=M|>NM$_#96$eKcRhs*EV4(p{fP0Bzu-d^{b5OT?eF%L3 zpvVY}eZx`PDJJInc&Xl(MVBqD;+j>-w+xT~@XN)9>xQdUB(Jdi8^2~>KR*`hr<<4ZNLT+j#!rEP4`?6!ssS33fQg@s3&!NHl& z@h!gC+Mp?$tHF#bNmfl88*vZ5pk>P~ANj7=x$xLbdcffH4i8^~;LzXvg6+jdUdE=Y zXTU28PdU`t@3iqAHuR@BEp~_m@Q2 z{n0EhS{5qd!euoS;`;yPWkjyk9eizJLJ^no1@SfZCpr z^s#B}k685eH9X*vFHi`4rgljHkekv@`kYLStHJ=~ z?#N*@&c{O=KMi{=SQ-O^4)*&ST-=?xPg4a6WV+;ZbRngs)({3N#XQd*Fu~$WO&$r{ zr&Mgx?dBX9goL4;`T)YmaMUkolSqrmQ`6BI^v4c5^H~DCSHPEQrR^68_LzRfOgx7L z4F!b=IR%By*|vP{=Y&rs+Kt;|IY@I=7Hhyx7y{1~Lgax8s>Njp`JYUvvQF37kTEke z3yX=31AGAG-mbH9RyHzv(b?JgIfW+_coLi4qUx9n$17K7(J+GEjW|?8aGDtt3rjhE z8=|JxGFxc|;CaD>q@*MjgaKPI*F9k2;2i@)SaLFf!1cOfox|?OGy#w4Qaw1a@Z9li z35*x0L~WBrn!P<-ebZ%M0>i>6d3f;R`Z>ef&8u{oa9*LJk^%an<=+*GKMvfd=hX@? zum<4pJ~f9X0zjc{mepSX?Euv*H;ro`mMFvm17P<6Gts6iH|p>@xO0`*a}!DUeC$cp z6aV&)&7LrT58fptBz#U2NCD+H^*NHtG{O%R>}w%(+6@7>hfpujoHEM=9v-KIzDJf( zZYb5$)6-&J1!&>Q3K|%kxEIAuEBc0);_4+o7PQ-1y~%-d zN%m$c{9og9SUVt{wRFsr{4!e`gU;hl+Z(9oh_pw3z;5;OVEDQGH&cPU{_8!=T$dsqV&x~UqLRjFG_SFw*iZ`>TB$Qgs z;l@NMt)8!2KIe@d9!FeIUP3Y3vlJPOVIITG{gWVA43X%4hOxjHu--Ea4ht424bYb? zM}mWg^x5&~QkXYGRxZ0mB{Ld7UvgcPT_SC6+yBM&XXo(yK67{y$`PXJD)uEL$ouu- z>3l~m!9>fyaqSp}N&US^>tuX^ts*B+iV31^HJT#-2SBdK(evipr7Sp@*LlLG0XUofwjWC%fze(uEC5)H z(R_`Kfw3_LF0OiKhq;c3;QRN1r$fA`6*B`pk(~M*n&MJYNHv)e`T01=O@V)AW@h&0 zs>1>Ldt9Y6=Jq>dzHvI9xZbS+qb9nCi);c{y6Avfi^!XdWixN3I1!bVmCbwJ?oaQd zTpTP&fqfp%TgBK?Hj@4k4uyzMSViS+WTZSeFH%) zQAQmurbvqSJPQg!G?}kKOCSFkdB_J4WX*1I{d*c3QhxpvfR96d|Mmcm9|H@^H$NYM zGkIV!z-Bca&7{cBXEE&a55d1A2M$UWG`=5@)+^xYL4^cCG&D3EWIx1@$tClz!3mQ@ zcC(=3-MyveW;U+Uu|_vkfO2|zxFn5?D2*!MtLKCO1;^I1fA8yi{m5j1f$}Eyyh@FV zLMK3>0+?~56Z;{scoz_B$Nkw(K>E9Y*}4WCvIgv*n&t0EGnHnX!Pt!Y2lIk}4f*)^ z5LDyE_8RE#&sI+FJ2KQPe*zv{B2OkIAmG)Pov|FyCbmH#V3IBu-53FXi~mdslp@7E znT+)GCt6dciYP68Q)MwXbPv0|-p&fgVcisX^yI)~1M3SX2s^`2 zS63&eqPlgfTwPlOh*rNdXya!-(0#;rDqk6cIeh{Sr`-Xxz9Xu|#9%3e4$8-kd!iHHZC{2SwwP(U!*LIM&Y!-Hq(kCC@^#VUvk+-ti(>hVcGW10Pqt?T81$W*Zw zpv6qlRL#QxetwdWKmhidjGjKUw3G=bCg{1+Nh+Nuzn71%y7qvZkOQ9RlXyVP&!5yr z16RpKn$;@x-xZ|8CZs$H*seX##t;h*2g?I@$ixgwu|GoSfVQ;O4) zM>QLOuFA@W5BG36jc-(*%#}==bMQF){hmoZZ$B>mD5HRNs9)*F zWHGB0M!*de(c*}ylg3X?i!XIbIx0u8{w0WGUy7Gb!_ zomi8*z#P63P~UXDc2S_1KAM1F#wM?E(>(bszj?sCg+=`y0fQ&(oD&FXv=8~ZhEo#b zVOosdt>QK|Bv#EZK@Q9L7Ias&QbS zh@f{ifNp3KpctF#S{RIGihzE@m8c3$eN@H*nlw11oAc2;{sAwJP*E8R?0WS^*ViQt zCy3nK>VSU)1CYkZ>$qnCUfI3C2(a|`0Qj zrj<9F^V9|}8LtkTHVF#;PbD1(t5RJGx|r&8kc{@EI+Mr3i9~|0jPtQ+E~nitO1Hxo znh-g4O%~FcHYpllW_T!1WO;&{65IbHM2ixP6zQ;3OL>-xE4+uG`@?#C>ZnvFzDwtI7M0L8BG!&e$4D;mM(V@ll?yy(xi zSvVn!Tep;e<&lEP06@753Xj{7}>gB$gSO?pGgOM#)G8wtj# zdL03~@tjsfL75TJ(d}2qjN`Q+fC+dm3L#GiDAV&vSlCC9UhAdz$B)Xd%@^uIv1r{3 z1PHmTy}&wBz9tAi=~KBoapnW6G8!J9BrwF>?pJRZ;(|v;-f&ng4geRgXh|KWpn*^g zid%!)>tsMZg?Gm|a5`M{%_Zb5p31nHuOl(*x{Klg)I@M~v7b1Q-w&mau1tz*`q;R;Q}!5bC(@dwdJ)HOMp_ zHv$Ty(QxYEYG-g@z$-xNyO@CZ6icVx2Z%-ivN#Bk0FSG;H|<}u8boU{S%@nYOGm=V z2_hZ}v(ZMzC+oe1I?apFG{Ch0UXfS!-RO@4Qeqs$Wg`ZeSRffFdyxqV5@1$<5(&0f zG?+9rEG!8iGR8CMJ!^D5*9TI-{Yn7vu3!d7f#_tfm6x6Uykh1vFu*CiPM^l@Sy(Fq z!oyD)vE8>vNPzI5mFlv|t)$#(RB;Vg2{d9-G9bfHUA(rktLMMZFjCfHR8|I|kCkx2 z%~FTm$GFa;XpRjTdBki09~I*rasx+dT!N7-q%e^GYxu}f*-re4JrzoKnQrHrjdwHc zVsGqqKQsSyxQY$|WgV2W{4IaCCu8?Za%UU2Fqj^za5s8FMRcKU=z)d`3FG{yB%hsN&)oO{0V&(SnX{Utbi?|_|JOQQN29r3i{W)XcaT9a(nDFkl4jkP~Ml<2FL}9N* zjw>w-@+E>f2b70^8?+H^c5Y=-429q4(S4hUIMMhAivLOT#a zVm2D^c=TjI_=-+W9spvB?~J+rN*g?|CZ|A`DF;5EQu;=ag_RZ7UiRse2i94+d)%

    6ClIT#FkocsR_bd&&*_)f$|cfq<9+!X9^kK>%DCB8maRHic?s-{Fxq$D3d- z5SvacpaT+vN165_^iDfLYPw8Uem~y_eFEIJb~AB>?{<$B1jYTpejE$W{Zi@oL!i1{ zOHZV#oW$~l_ct&K(LfI7zr6A@q}Qs;9)rVe1kU}@H_3);TMi)sV4Vh*v3GK^IRG95 z7S#(NVqzyDF1`%d@D`A8K`vbL=FtjSM`zAcM3sUG9To7kK*Lih(�n(wWg((VVM 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 literal 45986 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 E0UQjieE Date: Mon, 25 Mar 2024 16:01:21 -0400 Subject: [PATCH 588/689] adding api for new mdc processing state db table --- .../iq/dataverse/api/MakeDataCountApi.java | 54 ++++++++++++++- .../MakeDataCountProcessState.java | 67 +++++++++++++++++++ .../MakeDataCountProcessStateServiceBean.java | 61 +++++++++++++++++ src/main/resources/db/migration/V6.1.0.8.sql | 10 +++ .../iq/dataverse/api/MakeDataCountApiIT.java | 40 +++++++++++ .../edu/harvard/iq/dataverse/api/UtilIT.java | 13 ++++ 6 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountProcessState.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountProcessStateServiceBean.java create mode 100644 src/main/resources/db/migration/V6.1.0.8.sql diff --git a/src/main/java/edu/harvard/iq/dataverse/api/MakeDataCountApi.java b/src/main/java/edu/harvard/iq/dataverse/api/MakeDataCountApi.java index 08e776a3eb8..38023327274 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/MakeDataCountApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/MakeDataCountApi.java @@ -7,6 +7,8 @@ import edu.harvard.iq.dataverse.makedatacount.DatasetExternalCitationsServiceBean; import edu.harvard.iq.dataverse.makedatacount.DatasetMetrics; import edu.harvard.iq.dataverse.makedatacount.DatasetMetricsServiceBean; +import edu.harvard.iq.dataverse.makedatacount.MakeDataCountProcessState; +import edu.harvard.iq.dataverse.makedatacount.MakeDataCountProcessStateServiceBean; import edu.harvard.iq.dataverse.pidproviders.PidProvider; import edu.harvard.iq.dataverse.pidproviders.PidUtil; import edu.harvard.iq.dataverse.pidproviders.doi.datacite.DataCiteDOIProvider; @@ -29,6 +31,8 @@ import jakarta.json.JsonObject; import jakarta.json.JsonObjectBuilder; import jakarta.json.JsonValue; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; @@ -47,6 +51,8 @@ public class MakeDataCountApi extends AbstractApiBean { @EJB DatasetMetricsServiceBean datasetMetricsService; @EJB + MakeDataCountProcessStateServiceBean makeDataCountProcessStateService; + @EJB DatasetExternalCitationsServiceBean datasetExternalCitationsService; @EJB DatasetServiceBean datasetService; @@ -110,7 +116,7 @@ public Response addUsageMetricsFromSushiReport(@PathParam("id") String id, @Quer @POST @Path("/addUsageMetricsFromSushiReport") - public Response addUsageMetricsFromSushiReportAll(@PathParam("id") String id, @QueryParam("reportOnDisk") String reportOnDisk) { + public Response addUsageMetricsFromSushiReportAll(@QueryParam("reportOnDisk") String reportOnDisk) { try { JsonObject report = JsonUtil.getJsonObjectFromFile(reportOnDisk); @@ -200,5 +206,51 @@ public Response updateCitationsForDataset(@PathParam("id") String id) throws IOE return wr.getResponse(); } } + @GET + @Path("{yearMonth}/processingState") + public Response getProcessingState(@PathParam("yearMonth") String yearMonth) { + MakeDataCountProcessState mdcps; + try { + mdcps = makeDataCountProcessStateService.getMakeDataCountProcessState(yearMonth); + } catch (IllegalArgumentException e) { + return error(Status.BAD_REQUEST,e.getMessage()); + } + if (mdcps != null) { + JsonObjectBuilder output = Json.createObjectBuilder(); + output.add("yearMonth", mdcps.getYearMonth()); + output.add("state", mdcps.getState().name()); + output.add("state-change-timestamp", mdcps.getStateChangeTime().toString()); + return ok(output); + } else { + return error(Status.NOT_FOUND, "Could not find an existing process state for " + yearMonth); + } + } + @POST + @Path("{yearMonth}/processingState") + public Response updateProcessingState(@PathParam("yearMonth") String yearMonth, @QueryParam("state") String state) { + MakeDataCountProcessState mdcps; + try { + mdcps = makeDataCountProcessStateService.setMakeDataCountProcessState(yearMonth, state); + } catch (IllegalArgumentException e) { + return error(Status.BAD_REQUEST,e.getMessage()); + } + + JsonObjectBuilder output = Json.createObjectBuilder(); + output.add("yearMonth", mdcps.getYearMonth()); + output.add("state", mdcps.getState().name()); + output.add("state-change-timestamp", mdcps.getStateChangeTime().toString()); + return ok(output); + } + + @DELETE + @Path("{yearMonth}/processingState") + public Response deleteProcessingState(@PathParam("yearMonth") String yearMonth) { + boolean deleted = makeDataCountProcessStateService.deleteMakeDataCountProcessState(yearMonth); + if (deleted) { + return ok("Processing State deleted for " + yearMonth); + } else { + return notFound("Processing State not found for " + yearMonth); + } + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountProcessState.java b/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountProcessState.java new file mode 100644 index 00000000000..f49640214e9 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountProcessState.java @@ -0,0 +1,67 @@ +package edu.harvard.iq.dataverse.makedatacount; + +import jakarta.persistence.*; + +import java.io.Serializable; +import java.sql.Timestamp; +import java.time.Instant; + +@Entity +public class MakeDataCountProcessState implements Serializable { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + private Long id; + + public enum MDCProcessState { + NEW("new"), DONE("done"), SKIP("skip"), PROCESSING("processing"), FAILED("failed"); + private final String text; + private MDCProcessState(final String text) { + this.text = text; + } + @Override + public String toString() { + return text; + } + } + @Column(nullable = false) + private String yearMonth; + @Column(nullable = false) + private MDCProcessState state; + @Column(nullable = true) + private Timestamp state_change_time; + + public MakeDataCountProcessState() { } + public MakeDataCountProcessState (String yearMonth, String state) { + this.setYearMonth(yearMonth); + this.setState(state); + } + + public void setYearMonth(String yearMonth) throws IllegalArgumentException { + // Todo: add constraint + if (yearMonth == null || (!yearMonth.matches("\\d{4}-\\d{2}") && !yearMonth.matches("\\d{4}-\\d{2}-\\d{2}"))) { + throw new IllegalArgumentException("YEAR-MONTH date format must be either yyyy-mm or yyyy-mm-dd"); + } + this.yearMonth = yearMonth; + } + public String getYearMonth() { + return this.yearMonth; + } + public void setState(MDCProcessState state) { + this.state = state; + this.state_change_time = Timestamp.from(Instant.now()); + } + public void setState(String state) throws IllegalArgumentException { + if (state != null) { + setState(MDCProcessState.valueOf(state.toUpperCase())); + } else { + throw new IllegalArgumentException("State is required and can not be null"); + } + } + public MDCProcessState getState() { + return this.state; + } + public Timestamp getStateChangeTime() { + return state_change_time; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountProcessStateServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountProcessStateServiceBean.java new file mode 100644 index 00000000000..5d7ec8ff047 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountProcessStateServiceBean.java @@ -0,0 +1,61 @@ +package edu.harvard.iq.dataverse.makedatacount; + +import jakarta.ejb.EJBException; +import jakarta.ejb.Stateless; +import jakarta.inject.Named; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; + +import java.util.List; + +@Named +@Stateless +public class MakeDataCountProcessStateServiceBean { + + @PersistenceContext(unitName = "VDCNet-ejbPU") + protected EntityManager em; + + public MakeDataCountProcessState getMakeDataCountProcessState(String yearMonth) { + validateYearMonth(yearMonth); + MakeDataCountProcessState mdcps = null; + String queryStr = "SELECT d FROM MakeDataCountProcessState d WHERE d.yearMonth = '" + yearMonth + "' "; + Query query = em.createQuery(queryStr); + List resultList = query.getResultList(); + if (resultList.size() > 1) { + throw new EJBException("More than one MakeDataCount Process State record found for YearMonth " + yearMonth + "."); + } + if (resultList.size() == 1) { + mdcps = (MakeDataCountProcessState) resultList.get(0); + } + return mdcps; + } + + public MakeDataCountProcessState setMakeDataCountProcessState(String yearMonth, String state) { + MakeDataCountProcessState mdcps = getMakeDataCountProcessState(yearMonth); + if (mdcps == null) { + mdcps = new MakeDataCountProcessState(yearMonth, state); + } else { + mdcps.setState(state); + } + return em.merge(mdcps); + } + + public boolean deleteMakeDataCountProcessState(String yearMonth) { + MakeDataCountProcessState mdcps = getMakeDataCountProcessState(yearMonth); + if (mdcps == null) { + return false; + } else { + em.remove(mdcps); + em.flush(); + return true; + } + } + + private void validateYearMonth(String yearMonth) { + // Check yearMonth format. either yyyy-mm or yyyy-mm-dd + if (yearMonth == null || (!yearMonth.matches("\\d{4}-\\d{2}") && !yearMonth.matches("\\d{4}-\\d{2}-\\d{2}"))) { + throw new IllegalArgumentException("YEAR-MONTH date format must be either yyyy-mm or yyyy-mm-dd"); + } + } +} diff --git a/src/main/resources/db/migration/V6.1.0.8.sql b/src/main/resources/db/migration/V6.1.0.8.sql new file mode 100644 index 00000000000..b8f466c0b73 --- /dev/null +++ b/src/main/resources/db/migration/V6.1.0.8.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS makedatacountprocessstate ( + id SERIAL NOT NULL, + yearMonth VARCHAR(16) NOT NULL UNIQUE, + state ENUM('new', 'done', 'skip', 'processing', 'failed') NOT NULL, + state_change_time TIMESTAMP WITHOUT TIME ZONE DEFAULT now(), + PRIMARY KEY (ID) + ); + +CREATE INDEX IF NOT EXISTS INDEX_makedatacountprocessstate_yearMonth ON makedatacountprocessstate (yearMonth); + diff --git a/src/test/java/edu/harvard/iq/dataverse/api/MakeDataCountApiIT.java b/src/test/java/edu/harvard/iq/dataverse/api/MakeDataCountApiIT.java index 7a113fd4caa..dbfd853edd1 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/MakeDataCountApiIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/MakeDataCountApiIT.java @@ -1,5 +1,7 @@ package edu.harvard.iq.dataverse.api; +import edu.harvard.iq.dataverse.makedatacount.MakeDataCountProcessState; +import io.restassured.path.json.JsonPath; import io.restassured.RestAssured; import io.restassured.response.Response; import java.io.File; @@ -7,8 +9,13 @@ import static jakarta.ws.rs.core.Response.Status.CREATED; import static jakarta.ws.rs.core.Response.Status.OK; import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; +import static jakarta.ws.rs.core.Response.Status.NOT_FOUND; import org.apache.commons.io.FileUtils; +import static org.hamcrest.CoreMatchers.anyOf; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -171,6 +178,39 @@ public void testMakeDataCountGetMetric() throws IOException { } + @Test + public void testGetUpdateDeleteProcessingState() { + String yearMonth = "2000-01"; + // make sure it isn't in the DB + Response deleteState = UtilIT.makeDataCountDeleteProcessingState(yearMonth); + deleteState.then().assertThat().statusCode(anyOf(equalTo(200), equalTo(404))); + + Response getState = UtilIT.makeDataCountGetProcessingState(yearMonth); + getState.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); + Response updateState = UtilIT.makeDataCountUpdateProcessingState(yearMonth, MakeDataCountProcessState.MDCProcessState.PROCESSING.toString()); + updateState.then().assertThat().statusCode(OK.getStatusCode()); + getState = UtilIT.makeDataCountGetProcessingState(yearMonth); + getState.then().assertThat().statusCode(OK.getStatusCode()); + JsonPath stateJson = JsonPath.from(getState.body().asString()); + stateJson.prettyPrint(); + String state1 = stateJson.getString("data.state"); + assertThat(state1, Matchers.equalTo(MakeDataCountProcessState.MDCProcessState.PROCESSING.name())); + String updateTimestamp1 = stateJson.getString("data.state-change-timestamp"); + + updateState = UtilIT.makeDataCountUpdateProcessingState(yearMonth, MakeDataCountProcessState.MDCProcessState.DONE.toString()); + updateState.then().assertThat().statusCode(OK.getStatusCode()); + stateJson = JsonPath.from(updateState.body().asString()); + stateJson.prettyPrint(); + String state2 = stateJson.getString("data.state"); + String updateTimestamp2 = stateJson.getString("data.state-change-timestamp"); + assertThat(state2, Matchers.equalTo(MakeDataCountProcessState.MDCProcessState.DONE.name())); + + assertThat(updateTimestamp2, Matchers.is(Matchers.greaterThan(updateTimestamp1))); + + deleteState = UtilIT.makeDataCountDeleteProcessingState(yearMonth); + deleteState.then().assertThat().statusCode(OK.getStatusCode()); + } + /** * Ignore is set on this test because it requires database edits to pass. * There are currently two citions for doi:10.7910/DVN/HQZOOB but you have 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 080ca0c43e9..ba36911ffae 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -3135,6 +3135,19 @@ static Response makeDataCountUpdateCitationsForDataset(String idOrPersistentIdOf return requestSpecification.post("/api/admin/makeDataCount/" + idInPath + "/updateCitationsForDataset"+ optionalQueryParam); } + static Response makeDataCountGetProcessingState(String yearMonth) { + RequestSpecification requestSpecification = given(); + return requestSpecification.get("/api/admin/makeDataCount/" + yearMonth + "/processingState"); + } + static Response makeDataCountUpdateProcessingState(String yearMonth, String state) { + RequestSpecification requestSpecification = given(); + return requestSpecification.post("/api/admin/makeDataCount/" + yearMonth + "/processingState?state=" + state); + } + static Response makeDataCountDeleteProcessingState(String yearMonth) { + RequestSpecification requestSpecification = given(); + return requestSpecification.delete("/api/admin/makeDataCount/" + yearMonth + "/processingState"); + } + static Response editDDI(String body, String fileId, String apiToken) { if (apiToken == null) { apiToken = ""; From f263a4e698e63fbcc5dc5cf017cbaa4cdf44deb9 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Mon, 25 Mar 2024 18:00:11 -0400 Subject: [PATCH 589/689] update docs and release note #7424 --- doc/release-notes/7424-mailsession.md | 11 ++++++----- doc/sphinx-guides/source/installation/config.rst | 1 + .../source/installation/installation-main.rst | 4 +++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/doc/release-notes/7424-mailsession.md b/doc/release-notes/7424-mailsession.md index 43846b0b72d..faaf618bc17 100644 --- a/doc/release-notes/7424-mailsession.md +++ b/doc/release-notes/7424-mailsession.md @@ -1,12 +1,13 @@ -## New way to configure mail transfer agent +## Simplified SMTP configuration -With this release, we deprecate the usage of `asadmin create-javamail-resource` to configure your MTA. -Instead, we provide the ability to configure your SMTP mail host using JVM options only, with the flexibility of MicroProfile Config. +With this release, we deprecate the usage of `asadmin create-javamail-resource` to configure Dataverse to send mail using your SMTP server and provide a simplified, standard alternative using JVM options or MicroProfile Config. At this point, no action is required if you want to keep your current configuration. Warnings will show in your server logs to inform and remind you about the deprecation. A future major release of Dataverse may remove this way of configuration. -For more details on how to configure the connection to your mail provider, please find updated details within the Installation Guide's main installation and configuration section. +Please do take the opportunity to update your SMTP configuration. Details can be found in the [dataverse.mail.mta.*](https://guides.dataverse.org/en/6.2/installation/config.html#dataverse-mail-mta) section of the Installation Guide. -Please note: as there have been problems with email delivered to SPAM folders when the "From" within mail envelope and the mail session configuration didn't match (#4210), as of this version the sole source for the "From" address is the setting `dataverse.mail.system-email` once you migrate to the new way of configuration. \ No newline at end of file +Once reconfiguration is complete, you should remove legacy, unused config. First, run `asadmin delete-javamail-resource mail/notifyMailSession` as described in the [6.1 guides](https://guides.dataverse.org/en/6.1/installation/installation-main.html#mail-host-configuration-authentication). Then run `curl -X DELETE http://localhost:8080/api/admin/settings/:SystemEmail` as this database setting has been replace with `dataverse.mail.system-email` as described below. + +Please note: as there have been problems with email delivered to SPAM folders when the "From" within mail envelope and the mail session configuration didn't match (#4210), as of this version the sole source for the "From" address is the setting `dataverse.mail.system-email` once you migrate to the new way of configuration. diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 889cee537d0..30d0567c557 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -3025,6 +3025,7 @@ Detailed description for every setting can be found in the table included within ``dataverse.mail.mta.noop.strict``, ``dataverse.mail.mta.mailextension`` +See also :ref:`mail-host-config-auth`. dataverse.ui.allow-review-for-incomplete ++++++++++++++++++++++++++++++++++++++++ diff --git a/doc/sphinx-guides/source/installation/installation-main.rst b/doc/sphinx-guides/source/installation/installation-main.rst index 9f935db6510..c20b848e1f5 100755 --- a/doc/sphinx-guides/source/installation/installation-main.rst +++ b/doc/sphinx-guides/source/installation/installation-main.rst @@ -141,6 +141,8 @@ Got ERR_ADDRESS_UNREACHABLE While Navigating on Interface or API Calls If you are receiving an ``ERR_ADDRESS_UNREACHABLE`` while navigating the GUI or making an API call, make sure the ``siteUrl`` JVM option is defined. For details on how to set ``siteUrl``, please refer to :ref:`dataverse.siteUrl` from the :doc:`config` section. For context on why setting this option is necessary, refer to :ref:`dataverse.fqdn` from the :doc:`config` section. +.. _mail-host-config-auth: + Mail Host Configuration & Authentication ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -149,7 +151,7 @@ If you need to alter your mail host address, user, or provide a password to conn To enable authentication with your mail server, simply configure the following options: - ``dataverse.mail.mta.auth = true`` -- ``dataverse.mail.mta.username = `` +- ``dataverse.mail.mta.user = `` - ``dataverse.mail.mta.password`` **WARNING**: From caf56823905a0e68ddfda2b855abc9e39a6af59e Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Mon, 25 Mar 2024 18:14:37 -0400 Subject: [PATCH 590/689] link higher up in the guides #7424 --- doc/release-notes/7424-mailsession.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/7424-mailsession.md b/doc/release-notes/7424-mailsession.md index faaf618bc17..37fede1bb1f 100644 --- a/doc/release-notes/7424-mailsession.md +++ b/doc/release-notes/7424-mailsession.md @@ -6,7 +6,7 @@ At this point, no action is required if you want to keep your current configurat Warnings will show in your server logs to inform and remind you about the deprecation. A future major release of Dataverse may remove this way of configuration. -Please do take the opportunity to update your SMTP configuration. Details can be found in the [dataverse.mail.mta.*](https://guides.dataverse.org/en/6.2/installation/config.html#dataverse-mail-mta) section of the Installation Guide. +Please do take the opportunity to update your SMTP configuration. Details can be found in section of the Installation Guide starting with the [dataverse.mail.system.email](https://guides.dataverse.org/en/6.2/installation/config.html#dataverse-mail-system-email) section of the Installation Guide. Once reconfiguration is complete, you should remove legacy, unused config. First, run `asadmin delete-javamail-resource mail/notifyMailSession` as described in the [6.1 guides](https://guides.dataverse.org/en/6.1/installation/installation-main.html#mail-host-configuration-authentication). Then run `curl -X DELETE http://localhost:8080/api/admin/settings/:SystemEmail` as this database setting has been replace with `dataverse.mail.system-email` as described below. From 362b87e1e7dd079a28c159246daa211525fc0bb3 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 26 Mar 2024 13:57:15 +0100 Subject: [PATCH 591/689] fix(mail): remove duplicate JvmSettings.MAIL_MTA_HOST The setting is already covered by the "host" property string in MailSessionProducer. --- .../edu/harvard/iq/dataverse/settings/JvmSettings.java | 1 - .../java/edu/harvard/iq/dataverse/MailServiceBeanIT.java | 2 +- .../harvard/iq/dataverse/util/MailSessionProducerIT.java | 8 ++++---- 3 files changed, 5 insertions(+), 6 deletions(-) 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 e71cabceffe..524df1e1ce9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java @@ -195,7 +195,6 @@ public enum JvmSettings { MAIL_DEBUG(SCOPE_MAIL, "debug"), // Mail Transfer Agent settings SCOPE_MAIL_MTA(SCOPE_MAIL, "mta"), - MAIL_MTA_HOST(SCOPE_MAIL_MTA, "host"), MAIL_MTA_AUTH(SCOPE_MAIL_MTA, "auth"), MAIL_MTA_USER(SCOPE_MAIL_MTA, "user"), MAIL_MTA_PASSWORD(SCOPE_MAIL_MTA, "password"), diff --git a/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanIT.java b/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanIT.java index 08eed9fe295..17dede5e9f3 100644 --- a/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/MailServiceBeanIT.java @@ -42,7 +42,7 @@ @Testcontainers(disabledWithoutDocker = true) @ExtendWith(MockitoExtension.class) @LocalJvmSettings -@JvmSetting(key = JvmSettings.MAIL_MTA_HOST, method = "tcSmtpHost") +@JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, method = "tcSmtpHost", varArgs = "host") @JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, method = "tcSmtpPort", varArgs = "port") class MailServiceBeanIT { diff --git a/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java b/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java index c4893652153..29b6598b1a9 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/MailSessionProducerIT.java @@ -71,7 +71,7 @@ static void tearDown() { @Nested @LocalJvmSettings - @JvmSetting(key = JvmSettings.MAIL_MTA_HOST, method = "tcSmtpHost") + @JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, method = "tcSmtpHost", varArgs = "host") @JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, method = "tcSmtpPort", varArgs = "port") class WithoutAuthentication { @Container @@ -121,7 +121,7 @@ void createSession() { @Nested @LocalJvmSettings - @JvmSetting(key = JvmSettings.MAIL_MTA_HOST, method = "tcSmtpHost") + @JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, method = "tcSmtpHost", varArgs = "host") @JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, method = "tcSmtpPort", varArgs = "port") @JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, varArgs = "ssl.enable", value = "true") @JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, varArgs = "ssl.trust", value = "*") @@ -183,7 +183,7 @@ void createSession() { @Nested @LocalJvmSettings - @JvmSetting(key = JvmSettings.MAIL_MTA_HOST, method = "tcSmtpHost") + @JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, method = "tcSmtpHost", varArgs = "host") @JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, method = "tcSmtpPort", varArgs = "port") @JvmSetting(key = JvmSettings.MAIL_MTA_AUTH, value = "yes") @JvmSetting(key = JvmSettings.MAIL_MTA_USER, value = username) @@ -252,7 +252,7 @@ void invalidConfigItemsAreIgnoredOnSessionBuild() { } @Test - @JvmSetting(key = JvmSettings.MAIL_MTA_HOST, value = "foobar") + @JvmSetting(key = JvmSettings.MAIL_MTA_SETTING, value = "foobar", varArgs = "host") void invalidHostnameIsFailingWhenSending() { assertDoesNotThrow(() -> new MailSessionProducer().getSession()); From b8ca4a70788943a05188945709a084156eb8f7bb Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 26 Mar 2024 14:05:51 +0100 Subject: [PATCH 592/689] fix(mail): do not add a default for SMPT host in ct profile As Payara 6.2023.7 still suffers from the MPCONFIG bug where a profiled setting is not easy to override, lets just remove the default for the container profile and make people add it even for containers. --- doc/sphinx-guides/source/installation/config.rst | 3 +-- src/main/resources/META-INF/microprofile-config.properties | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 30d0567c557..6d061ece384 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -2946,8 +2946,7 @@ The following table describes the most important settings commonly used. - Default Value * - ``dataverse.mail.mta.host`` - The SMTP server to connect to. - - | *No default* - | (``smtp`` in our :ref:`Dataverse container `) + - *No default* * - ``dataverse.mail.mta.port`` - The SMTP server port to connect to. - ``25`` diff --git a/src/main/resources/META-INF/microprofile-config.properties b/src/main/resources/META-INF/microprofile-config.properties index 9924d2518ca..517a4e9513b 100644 --- a/src/main/resources/META-INF/microprofile-config.properties +++ b/src/main/resources/META-INF/microprofile-config.properties @@ -45,8 +45,6 @@ dataverse.rserve.tempdir=/tmp/Rserv # MAIL dataverse.mail.mta.auth=false dataverse.mail.mta.allow-utf8-addresses=true -# In containers, default to hostname smtp, a container on the same network -%ct.dataverse.mail.mta.host=smtp # OAI SERVER dataverse.oai.server.maxidentifiers=100 From d8198b53c3c92af2e91fe6e1df65af791b356b77 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 26 Mar 2024 14:40:11 +0100 Subject: [PATCH 593/689] style(mail): enable more debug output from session producer In case people want to debug Jakarta Mail, they activate dataverse.mail.debug. Let's hook into that and add more verbose output from the session producer, too. That way people can make sure everything is set up as they wish. --- .../dataverse/util/MailSessionProducer.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java b/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java index 13fedb94014..149f92761d2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java @@ -16,6 +16,7 @@ import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; @ApplicationScoped public class MailSessionProducer { @@ -42,6 +43,12 @@ public class MailSessionProducer { private static final String PREFIX = "mail.smtp."; private static final Logger logger = Logger.getLogger(MailSessionProducer.class.getCanonicalName()); + static { + if (Boolean.TRUE.equals(JvmSettings.MAIL_DEBUG.lookup(Boolean.class))) { + logger.setLevel(Level.FINE); + } + } + Session systemMailSession; /** @@ -60,7 +67,7 @@ public MailSessionProducer() { } catch (NamingException e) { // This exception simply means the appserver did not provide the legacy mail session. // Debug level output is just fine. - logger.log(Level.FINE, "Error during mail resource lookup", e); + logger.log(Level.FINER, "Error during legacy appserver-level mail resource lookup", e); } } @@ -75,14 +82,21 @@ public Session getSession() { } if (systemMailSession == null) { + logger.fine("Setting up new mail session"); + // Initialize with null (= no authenticator) is a valid argument for the session factory method. Authenticator authenticator = null; // In case we want auth, create an authenticator (default = false from microprofile-config.properties) - if (JvmSettings.MAIL_MTA_AUTH.lookup(Boolean.class)) { + if (Boolean.TRUE.equals(JvmSettings.MAIL_MTA_AUTH.lookup(Boolean.class))) { + logger.fine("Mail Authentication is enabled, building authenticator"); authenticator = new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { + logger.fine(() -> + String.format("Returning PasswordAuthenticator with username='%s', password='%s'", + JvmSettings.MAIL_MTA_USER.lookup(), + "*".repeat(JvmSettings.MAIL_MTA_PASSWORD.lookup().length()))); return new PasswordAuthentication(JvmSettings.MAIL_MTA_USER.lookup(), JvmSettings.MAIL_MTA_PASSWORD.lookup()); } }; @@ -116,6 +130,10 @@ Properties getMailProperties() { prop -> JvmSettings.MAIL_MTA_SETTING.lookupOptional(Integer.class, prop).ifPresent( number -> configuration.put(PREFIX + prop, number.toString()))); + logger.fine(() -> "Compiled properties:" + configuration.entrySet().stream() + .map(entry -> "\"" + entry.getKey() + "\": \"" + entry.getValue() + "\"") + .collect(Collectors.joining(",\n"))); + return configuration; } From 2a73426d87c755f97ca9cc9af6c80f2a3f2347be Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 26 Mar 2024 14:58:51 +0100 Subject: [PATCH 594/689] fix(mail): do not fail to deploy when debugging is not configured --- .../java/edu/harvard/iq/dataverse/util/MailSessionProducer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java b/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java index 149f92761d2..202772201de 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java @@ -44,7 +44,7 @@ public class MailSessionProducer { private static final Logger logger = Logger.getLogger(MailSessionProducer.class.getCanonicalName()); static { - if (Boolean.TRUE.equals(JvmSettings.MAIL_DEBUG.lookup(Boolean.class))) { + if (Boolean.TRUE.equals(JvmSettings.MAIL_DEBUG.lookupOptional(Boolean.class).orElse(false))) { logger.setLevel(Level.FINE); } } From 21aa73d31ef8cd96b8b32f3e8de140352c120ef3 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 26 Mar 2024 15:00:34 +0100 Subject: [PATCH 595/689] style(mail): applying better fix for default value of mail debugging --- .../java/edu/harvard/iq/dataverse/util/MailSessionProducer.java | 2 +- src/main/resources/META-INF/microprofile-config.properties | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java b/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java index 202772201de..149f92761d2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/MailSessionProducer.java @@ -44,7 +44,7 @@ public class MailSessionProducer { private static final Logger logger = Logger.getLogger(MailSessionProducer.class.getCanonicalName()); static { - if (Boolean.TRUE.equals(JvmSettings.MAIL_DEBUG.lookupOptional(Boolean.class).orElse(false))) { + if (Boolean.TRUE.equals(JvmSettings.MAIL_DEBUG.lookup(Boolean.class))) { logger.setLevel(Level.FINE); } } diff --git a/src/main/resources/META-INF/microprofile-config.properties b/src/main/resources/META-INF/microprofile-config.properties index 517a4e9513b..b0bc92cf975 100644 --- a/src/main/resources/META-INF/microprofile-config.properties +++ b/src/main/resources/META-INF/microprofile-config.properties @@ -43,6 +43,7 @@ dataverse.rserve.password=rserve dataverse.rserve.tempdir=/tmp/Rserv # MAIL +dataverse.mail.debug=false dataverse.mail.mta.auth=false dataverse.mail.mta.allow-utf8-addresses=true From a33168df198c3bb977ec8d8e051ee845a11c413c Mon Sep 17 00:00:00 2001 From: Jose Lucas Cordeiro Date: Tue, 26 Mar 2024 11:12:34 -0300 Subject: [PATCH 596/689] #10411: Update Explicit Groups Documentation Adding more details in the request for the explicit groups documentation. Issue listed here: https://github.com/IQSS/dataverse/issues/10411 --- doc/sphinx-guides/source/api/native-api.rst | 82 +++++++++++++++++++-- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 144e3ac8e5e..def894aec6d 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -4168,33 +4168,99 @@ Data being POSTed is json-formatted description of the group:: "aliasInOwner":"ccs" } +A curl example: + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export SERVER_URL=https://demo.dataverse.org + export ID=24 + + curl -H "X-Dataverse-key:$API_TOKEN" -X POST "$SERVER_URL/api/dataverses/$ID/groups" --data '{"description":"Describe the group here","displayName":"Close Collaborators", "aliasInOwner":"ccs"}' + +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/24/groups" --data '{"description":"Describe the group here","displayName":"Close Collaborators", "aliasInOwner":"ccs"}' + List Explicit Groups in a Dataverse Collection ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -List explicit groups under Dataverse collection ``$id``:: +List explicit groups under Dataverse collection ``ID``. A curl example using an ``ID``: - GET http://$server/api/dataverses/$id/groups +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export SERVER_URL=https://demo.dataverse.org + export ID=24 + + curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/dataverses/$ID/groups" + +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/24/groups" Show Single Group in a Dataverse Collection ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Show group ``$groupAlias`` under dataverse ``$dv``:: +Show group ``$GROUP_ALIAS`` under dataverse ``$DATAVERSE_ID`` and a ``$GROUP_ALIAS``: - GET http://$server/api/dataverses/$dv/groups/$groupAlias +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export SERVER_URL=https://demo.dataverse.org + export GROUP_ALIAS=ccs + export DATAVERSE_ID=24 + + curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/dataverses/$DATAVERSE_ID/groups/$GROUP_ALIAS" + +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/24/groups/ccs" Update Group in a Dataverse Collection ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Show group ``$GROUP_ALIAS`` under dataverse ``$DATAVERSE_ID`` and a ``$GROUP_ALIAS``. The request body is the same as the create group one, except that the group alias cannot be changed. Thus, the field ``aliasInOwner`` is ignored.: + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export SERVER_URL=https://demo.dataverse.org + export GROUP_ALIAS=ccs + export DATAVERSE_ID=24 + + curl -H "X-Dataverse-key:$API_TOKEN" -X PUT "$SERVER_URL/api/dataverses/$DATAVERSE_ID/groups/$GROUP_ALIAS" --data '{"description":"Describe the group here","displayName":"Close Collaborators"}' -Update group ``$groupAlias`` under Dataverse collection ``$dv``. The request body is the same as the create group one, except that the group alias cannot be changed. Thus, the field ``aliasInOwner`` is ignored. :: +The fully expanded example above (without environment variables) looks like this: + +.. code-block:: bash - PUT http://$server/api/dataverses/$dv/groups/$groupAlias + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X PUT "https://demo.dataverse.org/api/dataverses/24/groups/ccs" --data '{"description":"Describe the group here","displayName":"Close Collaborators"}' Delete Group from a Dataverse Collection ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Delete group ``$groupAlias`` under Dataverse collection ``$dv``:: +Delete group ``$GROUP_ALIAS`` under Dataverse collection ``$DATAVERSE_ID``: + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export SERVER_URL=https://demo.dataverse.org + export GROUP_ALIAS=ccs + export DATAVERSE_ID=24 + + curl -H "X-Dataverse-key:$API_TOKEN" -X DELETE "$SERVER_URL/api/dataverses/$DATAVERSE_ID/groups/$GROUP_ALIAS" + +The fully expanded example above (without environment variables) looks like this: + +.. code-block:: bash - DELETE http://$server/api/dataverses/$dv/groups/$groupAlias + curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE "https://demo.dataverse.org/api/dataverses/24/groups/ccs" Add Multiple Role Assignees to an Explicit Group ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 8898d5367b34215c0991f300b072d2fe6fd4de91 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 26 Mar 2024 10:25:04 -0400 Subject: [PATCH 597/689] adding release note --- doc/release-notes/10424-new-api-for-mdc.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 doc/release-notes/10424-new-api-for-mdc.md diff --git a/doc/release-notes/10424-new-api-for-mdc.md b/doc/release-notes/10424-new-api-for-mdc.md new file mode 100644 index 00000000000..8fb1f6d9e3d --- /dev/null +++ b/doc/release-notes/10424-new-api-for-mdc.md @@ -0,0 +1,11 @@ +The API endpoint `api/admin/makeDataCount/{yearMonth}/processingState` has been added to Get, Create/Update(POST), and Delete a State for processing Make Data Count logged metrics +For Create/Update the 'state' is passed in through a query parameter. +Example +- `curl POST http://localhost:8080/api/admin/makeDataCount/2024-03/processingState?state=Skip` + +Valid values for state are [New, Done, Skip, Processing, and Failed] +'New' can be used to re-trigger the processing of the data for the year-month specified. +'Skip' will prevent the file from being processed. +'Processing' shows the state where the file is currently being processed. +'Failed' shows the state where the file has failed and will be re-processed in the next run. If you don't want the file to be re-processed set the state to 'Skip'. +'Done' is the state where the file has been successfully processed. From 36193714c14de8b13e33cfe0dfeabf8c730e1fe2 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 26 Mar 2024 10:37:25 -0400 Subject: [PATCH 598/689] fix dot to dash --- doc/release-notes/7424-mailsession.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/7424-mailsession.md b/doc/release-notes/7424-mailsession.md index 37fede1bb1f..470b78cf2de 100644 --- a/doc/release-notes/7424-mailsession.md +++ b/doc/release-notes/7424-mailsession.md @@ -6,7 +6,7 @@ At this point, no action is required if you want to keep your current configurat Warnings will show in your server logs to inform and remind you about the deprecation. A future major release of Dataverse may remove this way of configuration. -Please do take the opportunity to update your SMTP configuration. Details can be found in section of the Installation Guide starting with the [dataverse.mail.system.email](https://guides.dataverse.org/en/6.2/installation/config.html#dataverse-mail-system-email) section of the Installation Guide. +Please do take the opportunity to update your SMTP configuration. Details can be found in section of the Installation Guide starting with the [dataverse.mail.system-email](https://guides.dataverse.org/en/6.2/installation/config.html#dataverse-mail-system-email) section of the Installation Guide. Once reconfiguration is complete, you should remove legacy, unused config. First, run `asadmin delete-javamail-resource mail/notifyMailSession` as described in the [6.1 guides](https://guides.dataverse.org/en/6.1/installation/installation-main.html#mail-host-configuration-authentication). Then run `curl -X DELETE http://localhost:8080/api/admin/settings/:SystemEmail` as this database setting has been replace with `dataverse.mail.system-email` as described below. From c498cebb31783f242f025d829bfdb11a2c46e79a Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 26 Mar 2024 16:07:10 +0100 Subject: [PATCH 599/689] doc(mail): add ssl.enable setting to shortlist Also add notes about common ports in use. --- doc/sphinx-guides/source/installation/config.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 6d061ece384..25afbcc8fff 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -2948,8 +2948,11 @@ The following table describes the most important settings commonly used. - The SMTP server to connect to. - *No default* * - ``dataverse.mail.mta.port`` - - The SMTP server port to connect to. + - The SMTP server port to connect to. (Common are ``25`` for plain, ``587`` for SSL, ``465`` for legacy SSL) - ``25`` + * - ``dataverse.mail.mta.ssl.enable`` + - Enable if your mail provider uses SSL. + - ``false`` * - ``dataverse.mail.mta.auth`` - If ``true``, attempt to authenticate the user using the AUTH command. - ``false`` @@ -2981,7 +2984,6 @@ Detailed description for every setting can be found in the table included within * SSL/TLS: ``dataverse.mail.mta.starttls.enable``, ``dataverse.mail.mta.starttls.required``, - ``dataverse.mail.mta.ssl.enable``, ``dataverse.mail.mta.ssl.checkserveridentity``, ``dataverse.mail.mta.ssl.trust``, ``dataverse.mail.mta.ssl.protocols``, From 3e9d992a9abdeae80a796a96ff93246e2817119f Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 26 Mar 2024 16:11:24 +0100 Subject: [PATCH 600/689] doc(mail): add newly added settings to release note --- doc/release-notes/7424-mailsession.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/doc/release-notes/7424-mailsession.md b/doc/release-notes/7424-mailsession.md index 470b78cf2de..f67dbd6efc5 100644 --- a/doc/release-notes/7424-mailsession.md +++ b/doc/release-notes/7424-mailsession.md @@ -11,3 +11,14 @@ Please do take the opportunity to update your SMTP configuration. Details can be Once reconfiguration is complete, you should remove legacy, unused config. First, run `asadmin delete-javamail-resource mail/notifyMailSession` as described in the [6.1 guides](https://guides.dataverse.org/en/6.1/installation/installation-main.html#mail-host-configuration-authentication). Then run `curl -X DELETE http://localhost:8080/api/admin/settings/:SystemEmail` as this database setting has been replace with `dataverse.mail.system-email` as described below. Please note: as there have been problems with email delivered to SPAM folders when the "From" within mail envelope and the mail session configuration didn't match (#4210), as of this version the sole source for the "From" address is the setting `dataverse.mail.system-email` once you migrate to the new way of configuration. + +List of options added: +- dataverse.mail.system-email +- dataverse.mail.mta.host +- dataverse.mail.mta.port +- dataverse.mail.mta.ssl.enable +- dataverse.mail.mta.auth +- dataverse.mail.mta.user +- dataverse.mail.mta.password +- dataverse.mail.mta.allow-utf8-addresses +- Plus many more for advanced usage and special provider requirements. See [configuration guide for a full list](https://guides.dataverse.org/en/6.2/installation/config.html#dataverse-mail-mta). \ No newline at end of file From 785dfc5251d8544ba2681eb4a1d82856baebe1e3 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 26 Mar 2024 16:21:09 +0100 Subject: [PATCH 601/689] chore(build): update Maven and test framework dependencies --- modules/dataverse-parent/pom.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/dataverse-parent/pom.xml b/modules/dataverse-parent/pom.xml index a15575e6e50..db8b3186efc 100644 --- a/modules/dataverse-parent/pom.xml +++ b/modules/dataverse-parent/pom.xml @@ -168,11 +168,11 @@ 5.2.0 - 1.19.1 - 3.4.1 - 5.10.0 - 5.6.0 - 0.8.10 + 1.19.7 + 3.7.1 + 5.10.2 + 5.11.0 + 0.8.11 9.3 @@ -182,8 +182,8 @@ 3.3.2 3.5.0 3.1.1 - 3.1.0 - 3.1.0 + 3.2.5 + 3.2.5 3.6.0 3.3.1 3.0.0-M7 @@ -199,7 +199,7 @@ 1.7.0 - 0.43.4 + 0.44.0 From 243bafed1363c2edd67d8a3dcf75b6ae76b29bfd Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Tue, 26 Mar 2024 11:35:49 -0400 Subject: [PATCH 602/689] adding test for invalid state --- .../iq/dataverse/api/MakeDataCountApi.java | 4 ++-- .../MakeDataCountProcessState.java | 17 ++++++++++----- .../iq/dataverse/api/MakeDataCountApiIT.java | 21 +++++++++++++++++++ 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/MakeDataCountApi.java b/src/main/java/edu/harvard/iq/dataverse/api/MakeDataCountApi.java index 38023327274..d94ab42c516 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/MakeDataCountApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/MakeDataCountApi.java @@ -232,8 +232,8 @@ public Response updateProcessingState(@PathParam("yearMonth") String yearMonth, MakeDataCountProcessState mdcps; try { mdcps = makeDataCountProcessStateService.setMakeDataCountProcessState(yearMonth, state); - } catch (IllegalArgumentException e) { - return error(Status.BAD_REQUEST,e.getMessage()); + } catch (Exception e) { + return badRequest(e.getMessage()); } JsonObjectBuilder output = Json.createObjectBuilder(); diff --git a/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountProcessState.java b/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountProcessState.java index f49640214e9..bde705abf44 100644 --- a/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountProcessState.java +++ b/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountProcessState.java @@ -5,6 +5,7 @@ import java.io.Serializable; import java.sql.Timestamp; import java.time.Instant; +import java.util.Arrays; @Entity public class MakeDataCountProcessState implements Serializable { @@ -19,6 +20,16 @@ public enum MDCProcessState { private MDCProcessState(final String text) { this.text = text; } + public static MDCProcessState fromString(String text) { + if (text != null) { + for (MDCProcessState state : MDCProcessState.values()) { + if (text.equals(state.text)) { + return state; + } + } + } + throw new IllegalArgumentException("State must be one of these values: " + Arrays.asList(MDCProcessState.values()) + "."); + } @Override public String toString() { return text; @@ -52,11 +63,7 @@ public void setState(MDCProcessState state) { this.state_change_time = Timestamp.from(Instant.now()); } public void setState(String state) throws IllegalArgumentException { - if (state != null) { - setState(MDCProcessState.valueOf(state.toUpperCase())); - } else { - throw new IllegalArgumentException("State is required and can not be null"); - } + setState(MDCProcessState.fromString(state)); } public MDCProcessState getState() { return this.state; diff --git a/src/test/java/edu/harvard/iq/dataverse/api/MakeDataCountApiIT.java b/src/test/java/edu/harvard/iq/dataverse/api/MakeDataCountApiIT.java index dbfd853edd1..64856461703 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/MakeDataCountApiIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/MakeDataCountApiIT.java @@ -211,6 +211,27 @@ public void testGetUpdateDeleteProcessingState() { deleteState.then().assertThat().statusCode(OK.getStatusCode()); } + @Test + public void testUpdateProcessingStateWithInvalidState() { + String yearMonth = "2000-02"; + // make sure it isn't in the DB + Response deleteState = UtilIT.makeDataCountDeleteProcessingState(yearMonth); + deleteState.then().assertThat().statusCode(anyOf(equalTo(200), equalTo(404))); + + Response stateResponse = UtilIT.makeDataCountUpdateProcessingState(yearMonth, "InvalidState"); + stateResponse.then().assertThat().statusCode(BAD_REQUEST.getStatusCode()); + + stateResponse = UtilIT.makeDataCountUpdateProcessingState(yearMonth, "new"); + stateResponse.then().assertThat().statusCode(OK.getStatusCode()); + stateResponse = UtilIT.makeDataCountUpdateProcessingState(yearMonth, "InvalidState"); + stateResponse.then().assertThat().statusCode(BAD_REQUEST.getStatusCode()); + stateResponse = UtilIT.makeDataCountGetProcessingState(yearMonth); + stateResponse.then().assertThat().statusCode(OK.getStatusCode()); + JsonPath stateJson = JsonPath.from(stateResponse.body().asString()); + String state = stateJson.getString("data.state"); + assertThat(state, Matchers.equalTo(MakeDataCountProcessState.MDCProcessState.NEW.name())); + } + /** * Ignore is set on this test because it requires database edits to pass. * There are currently two citions for doi:10.7910/DVN/HQZOOB but you have From 6b8b90743e2349e90ee9f3ff9f4597dc572fab0f Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 26 Mar 2024 16:44:37 +0100 Subject: [PATCH 603/689] chore(build): downgrade DMP to 0.43.4 We need to downgrade to 0.43.4 again because of this regression: fabric8io/docker-maven-plugin#1756 Once they release a new version, try again. --- modules/dataverse-parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/dataverse-parent/pom.xml b/modules/dataverse-parent/pom.xml index db8b3186efc..1a538905a8d 100644 --- a/modules/dataverse-parent/pom.xml +++ b/modules/dataverse-parent/pom.xml @@ -199,7 +199,7 @@ 1.7.0 - 0.44.0 + 0.43.4 From df4838241914dcc5320a4ad81f2798fe094878bb Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 26 Mar 2024 12:05:58 -0400 Subject: [PATCH 604/689] simply smtp config docs #7424 --- .../source/installation/config.rst | 25 +++++++++++++++- .../source/installation/installation-main.rst | 30 ------------------- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 25afbcc8fff..28b549ec765 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -718,6 +718,19 @@ To enable bearer tokens, you must install and configure Keycloak (for now, see : You can test that bearer tokens are working by following the example under :ref:`bearer-tokens` in the API Guide. +.. _smtp-config: + +SMTP/Email Configuration +------------------------ + +The installer prompts you for some basic options to configure Dataverse to send email using your SMTP server, but in many cases, extra configuration may be necessary. + +Make sure the :ref:`dataverse.mail.support-email` has been set. Email will not be sent without it. + +Then check the list of commonly used settings at the top of :ref:`dataverse.mail.mta`. + +If you have trouble, consider turning on debugging with :ref:`dataverse.mail.debug`. + .. _database-persistence: Database Persistence @@ -2889,6 +2902,8 @@ Please note that if you're having any trouble sending email, you can refer to "T Can also be set via any `supported MicroProfile Config API source`_, e.g. the environment variable ``DATAVERSE_MAIL_SYSTEM_EMAIL``. +See also :ref:`smtp-config`. + .. _dataverse.mail.support-email: dataverse.mail.support-email @@ -2904,6 +2919,8 @@ If you don't include the text, the installation name (see :ref:`Branding Your In Can also be set via any `supported MicroProfile Config API source`_, e.g. the environment variable ``DATAVERSE_MAIL_SUPPORT_EMAIL``. +See also :ref:`smtp-config`. + .. _dataverse.mail.cc-support-on-contact-email: dataverse.mail.cc-support-on-contact-email @@ -2915,6 +2932,10 @@ The default is false. Can also be set via *MicroProfile Config API* sources, e.g. the environment variable ``DATAVERSE_MAIL_CC_SUPPORT_ON_CONTACT_EMAIL``. +See also :ref:`smtp-config`. + +.. _dataverse.mail.debug: + dataverse.mail.debug ++++++++++++++++++++ @@ -2923,6 +2944,8 @@ Defaults to ``false``. Can also be set via *MicroProfile Config API* sources, e.g. the environment variable ``DATAVERSE_MAIL_DEBUG``. +See also :ref:`smtp-config`. + .. _dataverse.mail.mta: dataverse.mail.mta.* @@ -3026,7 +3049,7 @@ Detailed description for every setting can be found in the table included within ``dataverse.mail.mta.noop.strict``, ``dataverse.mail.mta.mailextension`` -See also :ref:`mail-host-config-auth`. +See also :ref:`smtp-config`. dataverse.ui.allow-review-for-incomplete ++++++++++++++++++++++++++++++++++++++++ diff --git a/doc/sphinx-guides/source/installation/installation-main.rst b/doc/sphinx-guides/source/installation/installation-main.rst index c20b848e1f5..3c3376e3c85 100755 --- a/doc/sphinx-guides/source/installation/installation-main.rst +++ b/doc/sphinx-guides/source/installation/installation-main.rst @@ -141,36 +141,6 @@ Got ERR_ADDRESS_UNREACHABLE While Navigating on Interface or API Calls If you are receiving an ``ERR_ADDRESS_UNREACHABLE`` while navigating the GUI or making an API call, make sure the ``siteUrl`` JVM option is defined. For details on how to set ``siteUrl``, please refer to :ref:`dataverse.siteUrl` from the :doc:`config` section. For context on why setting this option is necessary, refer to :ref:`dataverse.fqdn` from the :doc:`config` section. -.. _mail-host-config-auth: - -Mail Host Configuration & Authentication -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If you need to alter your mail host address, user, or provide a password to connect with, these settings are easily changed using JVM options group :ref:`dataverse.mail.mta`. - -To enable authentication with your mail server, simply configure the following options: - -- ``dataverse.mail.mta.auth = true`` -- ``dataverse.mail.mta.user = `` -- ``dataverse.mail.mta.password`` - -**WARNING**: -We strongly recommend not using plaintext storage or environment variables, but relying on :ref:`secure-password-storage`. - -**WARNING**: -It’s recommended to use an *app password* (for smtp.gmail.com users) or utilize a dedicated/non-personal user account with SMTP server auths so that you do not risk compromising your password. - -If your installation’s mail host uses SSL (like smtp.gmail.com) you’ll need to configure these options: - -- ``dataverse.mail.mta.ssl.enable = true`` -- ``dataverse.mail.mta.port = 587`` - -**NOTE**: Some mail providers might still support using port 465, which formerly was assigned to be SMTP over SSL (SMTPS). -However, this is no longer standardized and the port has been reassigned by the IANA to a different service. -If your provider supports using port 587, be advised to migrate your configuration. - -As the mail server connection (session) is cached once created, you need to restart Payara when applying configuration changes. - UnknownHostException While Deploying ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From cb144236b5469dcb6ef23f78d411ab5150c64656 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 26 Mar 2024 17:13:51 +0100 Subject: [PATCH 605/689] doc(mail): fix some typos, add hint about support in new SMTP config section --- doc/sphinx-guides/source/installation/config.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 28b549ec765..207b6acb305 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -725,7 +725,8 @@ SMTP/Email Configuration The installer prompts you for some basic options to configure Dataverse to send email using your SMTP server, but in many cases, extra configuration may be necessary. -Make sure the :ref:`dataverse.mail.support-email` has been set. Email will not be sent without it. +Make sure the :ref:`dataverse.mail.system-email` has been set. Email will not be sent without it. A hint will be logged about this fact. +If you want to separate system email from your support team's email, take a look at :ref:`dataverse.mail.support-email`. Then check the list of commonly used settings at the top of :ref:`dataverse.mail.mta`. From e784eb33848085c7e10f736db60e2ae2e8d42541 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 26 Mar 2024 12:18:12 -0400 Subject: [PATCH 606/689] point release note at new SMTP section #7424 --- doc/release-notes/7424-mailsession.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/7424-mailsession.md b/doc/release-notes/7424-mailsession.md index f67dbd6efc5..67c876f7ad5 100644 --- a/doc/release-notes/7424-mailsession.md +++ b/doc/release-notes/7424-mailsession.md @@ -6,7 +6,7 @@ At this point, no action is required if you want to keep your current configurat Warnings will show in your server logs to inform and remind you about the deprecation. A future major release of Dataverse may remove this way of configuration. -Please do take the opportunity to update your SMTP configuration. Details can be found in section of the Installation Guide starting with the [dataverse.mail.system-email](https://guides.dataverse.org/en/6.2/installation/config.html#dataverse-mail-system-email) section of the Installation Guide. +Please do take the opportunity to update your SMTP configuration. Details can be found in section of the Installation Guide starting with the [SMTP/Email Configuration](https://guides.dataverse.org/en/6.2/installation/config.html#smtp-email-configuration) section of the Installation Guide. Once reconfiguration is complete, you should remove legacy, unused config. First, run `asadmin delete-javamail-resource mail/notifyMailSession` as described in the [6.1 guides](https://guides.dataverse.org/en/6.1/installation/installation-main.html#mail-host-configuration-authentication). Then run `curl -X DELETE http://localhost:8080/api/admin/settings/:SystemEmail` as this database setting has been replace with `dataverse.mail.system-email` as described below. From b8822b9934e4f5591b3d094fd1a7719dc2217d2b Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 26 Mar 2024 12:24:40 -0400 Subject: [PATCH 607/689] Initial version --- doc/release-notes/6.2-release-notes.md | 242 +++++++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 doc/release-notes/6.2-release-notes.md diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md new file mode 100644 index 00000000000..48903fb8b34 --- /dev/null +++ b/doc/release-notes/6.2-release-notes.md @@ -0,0 +1,242 @@ +# Dataverse 6.2 + +Please note: To read these instructions in full, please go to https://github.com/IQSS/dataverse/releases/tag/v6.2 rather than the list of releases, which will cut them off. + +This release brings new features, enhancements, and bug fixes to the Dataverse software. +Thank you to all of the community members who contributed code, suggestions, bug reports, and other assistance across the project. + +## Release highlights + +### New API Endpoint for Clearing an Individual Dataset From Solr + +A new Index API endpoint has been added allowing an admin to clear an individual dataset from Solr. + +### Return to Author Now Requires a Reason + +The Popup for returning to author now requires a reason that will be sent by email to the author. + +Please note that you can still type a creative and meaningful comment such as "The author would like to modify his dataset", "Files are missing", "Nothing to report" or "A curation report with comments and suggestions/instructions will follow in another email" that suits your situation. + +### Support for Using Multiple PID Providers + +This release adds support for using multiple PID (DOI, Handle, PermalLink) providers, multiple PID provider accounts +(managing a given protocol, authority,separator, shoulder combination), assigning PID provider accounts to specific collections, +and supporting transferred PIDs (where a PID is managed by an account when it's authority, separator, and/or shoulder don't match +the combination where the account can mint new PIDs). It also adds the ability for additional provider services beyond the existing +DataCite, EZId, Handle, and PermaLink providers to be dynamically added as separate jar files. + +These changes require per-provider settings rather than the global PID settings previously supported. While backward compatibility +for installations using a single PID Provider account is provided, updating to use the new microprofile settings is highly recommended +and will be required in a future version. + +New microprofile settings (where * indicates a provider id indicating which provider the setting is for): + +dataverse.pid.providers +dataverse.pid.default-provider +dataverse.pid.*.type +dataverse.pid.*.label +dataverse.pid.*.authority +dataverse.pid.*.shoulder +dataverse.pid.*.identifier-generation-style +dataverse.pid.*.datafile-pid-format +dataverse.pid.*.managed-list +dataverse.pid.*.excluded-list +dataverse.pid.*.datacite.mds-api-url +dataverse.pid.*.datacite.rest-api-url +dataverse.pid.*.datacite.username +dataverse.pid.*.datacite.password +dataverse.pid.*.ezid.api-url +dataverse.pid.*.ezid.username +dataverse.pid.*.ezid.password +dataverse.pid.*.permalink.base-url +dataverse.pid.*.permalink.separator +dataverse.pid.*.handlenet.index +dataverse.pid.*.handlenet.independent-service +dataverse.pid.*.handlenet.auth-handle +dataverse.pid.*.handlenet.key.path +dataverse.pid.*.handlenet.key.passphrase +dataverse.spi.pidproviders.directory + +### Geospatial Metadata Block Fields for North and South Renamed + +The Geospatial metadata block fields for north and south were labeled incorrectly as ‘Longitudes,’ as reported on #5645. After updating to this version of Dataverse, users will need to update all the endpoints that used ‘northLongitude’ and ‘southLongitude’ to ‘northLatitude’ and ‘southLatitude,’ respectively. + + +TODO: Whoever puts the release notes together should make sure there is the standard note about updating the schema after upgrading. + +### Add .QPJ and .QMD Extensions to Shapefile Handling + +- Support for `.qpj` and `.qmd` files in shapefile uploads has been introduced, ensuring that these files are properly recognized and handled as part of geospatial datasets in Dataverse. + +### Ingested Tabular Data Files Can Be Stored Without the Variable Name Header + +Tabular Data Ingest can now save the generated archival files with the list of variable names added as the first tab-delimited line. As the most significant effect of this feature. + +Access API will be able to take advantage of Direct Download for tab. files saved with these headers on S3 - since they no longer have to be generated and added to the streamed content on the fly. + +This behavior is controlled by the new setting `:StoreIngestedTabularFilesWithVarHeaders`. It is false by default, preserving the legacy behavior. When enabled, Dataverse will be able to handle both the newly ingested files, and any already-existing legacy files stored without these headers transparently to the user. E.g. the access API will continue delivering tab-delimited files **with** this header line, whether it needs to add it dynamically for the legacy files, or reading complete files directly from storage for the ones stored with it. + +An API for converting existing legacy tabular files will be added separately. [this line will need to be changed if we have time to add said API before 6.2 is released]. [TODO] + +### Search by License + +A new search facet called "License" has been added and will be displayed as long as there is more than one license in datasets and datafiles in browse/search results. This facet allow you to filter by license such as CC0, etc. + +Also, the Search API now handles license filtering using the `fq` parameter, for example : `/api/search?q=*&fq=license%3A%22CC0+1.0%22` for CC0 1.0. See PR #10204 + +### OAI-PMH Error Handling Has Been Improved + +OAI-PMH error handling has been improved to display a machine-readable error in XML rather than a 500 error with no further information. + +- /oai?foo=bar will show "No argument 'verb' found" +- /oai?verb=foo&verb=bar will show "Verb must be singular, given: '[foo, bar]'" + +### Rate Limiting Using JCache (With Hazelcast As Provided by Payara) + +The option to rate limit has been added to prevent users from over taxing the system either deliberately or by runaway automated processes. +Rate limiting can be configured on a tier level with tier 0 being reserved for guest users and tiers 1-any for authenticated users. +Superuser accounts are exempt from rate limiting. +Rate limits can be imposed on command APIs by configuring the tier, the command, and the hourly limit in the database. +Two database settings configure the rate limiting. +Note: If either of these settings exist in the database rate limiting will be enabled. +If neither setting exists rate limiting is disabled. + +`:RateLimitingDefaultCapacityTiers` is a comma separated list of default values for each tier. +In the following example, the default for tier `0` (guest users) is set to 10,000 calls per command per hour and tier `1` (authenticated users) is set to 20,000 calls per command per hour. +Tiers not specified in this setting will default to `-1` (No Limit). I.e., -d "10000" is equivalent to -d "10000,-1,-1,..." +`curl http://localhost:8080/api/admin/settings/:RateLimitingDefaultCapacityTiers -X PUT -d '10000,20000'` + +`:RateLimitingCapacityByTierAndAction` is a JSON object specifying the rate by tier and a list of actions (commands). +This allows for more control over the rate limit of individual API command calls. +In the following example, calls made by a guest user (tier 0) for API `GetLatestPublishedDatasetVersionCommand` is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. +`curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]'` + +Hazelcast is configured in Payara and should not need any changes for this feature + +### Container Guide, Documentation for Faster Redeploy + +In the Container Guide, documentation for developers on how to quickly redeploy code has been added for Netbeans and improved for IntelliJ. + +Also in the context of containers, a new option to skip deployment has been added and the war file is now consistently named "dataverse.war" rather than having a version in the filename, such as "dataverse-6.1.war". This predictability makes tooling easier. + +Finally, an option to create tabs in the guides using [Sphinx Tabs](https://sphinx-tabs.readthedocs.io) has been added. (You can see the tabs in action in the "dev usage" page of the Container Guide.) To continue building the guides, you will need to install this new dependency by re-running `pip install -r requirements.txt`. + +### Universe Field in Variablemetadata Table Changed + +Universe field in variablemetadata table was changed from varchar(255) to text. The change was made to support longer strings in "universe" metadata field, similar to the rest of text fields in variablemetadata table. + +### Postgres Versions + +This release adds install script support for the new permissions model in Postgres versions 15+, and bumps FlyWay to support Postgres 16. + +Postgres 13 remains the version used with automated testing. + +### Listing Collection/Dataverse API + +Listing collection/dataverse role assignments via API still requires ManageDataversePermissions, but listing dataset role assignments via API now requires only ManageDatasetPermissions. + +### Missing Database Constraints + +This release adds two missing database constraints that will assure that the externalvocabularyvalue table only has one entry for each uri and that the oaiset table only has one set for each spec. (In the very unlikely case that your existing database has duplicate entries now, install would fail. This can be checked by running + +SELECT uri, count(*) FROM externalvocabularyvaluet group by uri; + +and + +SELECT spec, count(*) FROM oaiset group by spec; + +and then removing any duplicate rows (where count>1). + +TODO: Whoever puts the release notes together should make sure there is the standard note about reloading metadata blocks for the citation, astrophysics, and biomedical blocks (plus any others from other PRs) after upgrading. + +### Harvesting Client API + +The API endpoint `api/harvest/clients/{harvestingClientNickname}` has been extended to include the following fields: + +- `allowHarvestingMissingCVV`: enable/disable allowing datasets to be harvested with Controlled Vocabulary Values that existed in the originating Dataverse Project but are not in the harvesting Dataverse Project. Default is false. +Note: This setting is only available to the API and not currently accessible/settable via the UI + +### New QA Guide + +A new QA Guide is intended mostly for the core development team but may be of interest to contributors. + +### New Accounts Metrics API + +Users can retrieve new types of metrics related to user accounts. The new capabilities are [described](https://guides.dataverse.org/en/6.2/api/metrics.html) in the guides. + +### New canDownloadAtLeastOneFile API + +The GET canDownloadAtLeastOneFile (/api/datasets/{id}/versions/{versionId}/canDownloadAtLeastOneFile) endpoint has been created. + +This API endpoint indicates if the calling user can download at least one file from a dataset version. Note that Shibboleth group permissions are not considered. + +### Extended getVersionFiles API + +The response for getVersionFiles (/api/datasets/{id}/versions/{versionId}/files) endpoint has been modified to include a total count of records available (totalCount:x). +This will aid in pagination by allowing the caller to know how many pages can be iterated through. The existing API (getVersionFileCounts) to return the count will still be available. + +### Extended Metadata Blocks API + +The API endpoint `/api/metadatablocks/{block_id}` has been extended to include the following fields: + +- `isRequired`: Whether or not this field is required +- `displayOrder`: The display order of the field in create/edit forms +- `typeClass`: The type class of this field ("controlledVocabulary", "compound", or "primitive") + +### Evaluation Version Tutorial on the Containers Guide + +The Container Guide now containers a tutorial for running Dataverse in containers for demo or evaluation purposes: https://guides.dataverse.org/en/6.2/container + +### Get File Citation As JSON + +It is now possible to retrieve via API the file citation as it appears on the file landing page. It is formatted in HTML and encoded in JSON. + +This API is not for downloading various citation formats such as EndNote XML, RIS, or BibTeX. This functionality has been requested in https://github.com/IQSS/dataverse/issues/3140 and https://github.com/IQSS/dataverse/issues/9994 + +### Extended Files API + +The API endpoint `api/files/{id}` has been extended to support the following optional query parameters: + +- `includeDeaccessioned`: Indicates whether or not to consider deaccessioned dataset versions in the latest file search. (Default: `false`). +- `returnDatasetVersion`: Indicates whether or not to include the dataset version of the file in the response. (Default: `false`). + +A new endpoint `api/files/{id}/versions/{datasetVersionId}` has been created. This endpoint returns the file metadata present in the requested dataset version. To specify the dataset version, you can use ``:latest-published``, or ``:latest``, or ``:draft`` or ``1.0`` or any other available version identifier. + +The endpoint supports the `includeDeaccessioned` and `returnDatasetVersion` optional query parameters, as does the `api/files/{id}` endpoint. + +`api/files/{id}/draft` endpoint is no longer available in favor of the new endpoint `api/files/{id}/versions/{datasetVersionId}`, which can use the version identifier ``:draft`` (`api/files/{id}/versions/:draft`) to obtain the same result. + +### Endpoint Extended: Datasets, Dataverse Collections, and Datafiles + +The API endpoints for getting datasets, Dataverse collections, and datafiles have been extended to support the following optional 'returnOwners' query parameter. + +Including the parameter and setting it to true will add a hierarchy showing which dataset and dataverse collection(s) the object is part of to the json object returned. + +### Endpoint Fixed: Datasets Metadata + +The API endpoint `api/datasets/{id}/metadata` has been changed to default to the latest version of the dataset that the user has access. + +### Uningest/Reingest Options Available in the File Page Edit Menu + +New Uningest/Reingest options are available in the File Page Edit menu, allowing ingest errors to be cleared (by users who can published the associated dataset) +and (by superusers) for a successful ingest to be undone or retried (e.g. after a Dataverse version update or if ingest size limits are changed). +The /api/files//uningest api also now allows users who can publish the dataset to undo an ingest failure. + +### Publication Status Facet Restored + +In version 6.1, the publication status facet location was unintentionally moved to the bottom. In this version, we have restored the original order. + +### Permissions Required To Assign a Role Have Been Fixed + +The permissions required to assign a role have been fixed. It is no longer possible to assign a role that includes permissions that the assigning user doesn't have. + +### Binder Redirect + +If your installation is configured to use Binder, you should remove the old "girder_ythub" tool and replace it with the tool described at https://github.com/IQSS/dataverse-binder-redirect + +For more information, see #10360. + + +### Optional Croissant Exporter Support + +When a Dataverse installation is configured to use a metadata exporter for the [Croissant](https://github.com/mlcommons/croissant) format, the content of the JSON-LD in the `` of dataset landing pages will be replaced with that format. However, both JSON-LD and Croissant will still be available for download from the dataset page and API. From edb141fb35294423c7866e45c61fcdbf60859de1 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 26 Mar 2024 12:27:37 -0400 Subject: [PATCH 608/689] Update doc/sphinx-guides/source/api/native-api.rst --- 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 def894aec6d..5c34543d6aa 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -4195,7 +4195,7 @@ List explicit groups under Dataverse collection ``ID``. A curl example using an export SERVER_URL=https://demo.dataverse.org export ID=24 - curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/dataverses/$ID/groups" + curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/dataverses/$ID/groups" The fully expanded example above (without environment variables) looks like this: From 4ef47742ed9462d80ea13945186434a02567bdc2 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 26 Mar 2024 12:37:30 -0400 Subject: [PATCH 609/689] Delete files --- doc/release-notes/10101-qa-guide.md | 1 - ...17-api-metrics-add-user-accounts-number.md | 3 -- ...datasets-can-download-at-least-one-file.md | 3 -- ...onFiles-api-to-include-total-file-count.md | 2 - doc/release-notes/10216-metadatablocks.md | 5 --- doc/release-notes/10238-container-demo.md | 1 - doc/release-notes/10240-file-citation.md | 5 --- .../10280-get-file-api-extension.md | 10 ----- .../10286-return-owner-added-to-get-apis.md | 5 --- doc/release-notes/10297-metadata-api-fix.md | 1 - .../10318-uningest-and-reingest.md | 3 -- ...38-expose-and-sort-publish-status-facet.md | 1 - ...sign-roles-without-privilege-escalation.md | 1 - doc/release-notes/10360-binder-redirect.md | 3 -- .../10382-optional-croissant-exporter.md | 1 - doc/release-notes/3437-new-index-api-added.md | 4 -- doc/release-notes/3623-multipid.md | 37 ------------------- doc/release-notes/3702-return-to-author.md | 4 -- .../5645-geospatial-props-nslong-fix.md | 4 -- .../8134-add-qpj-qmd-extensions.md | 3 -- ...4-storing-tabular-files-with-varheaders.md | 6 --- ...482-make-licenses-searchable-faceatable.md | 6 --- .../9275-harvest-invalid-query-params.md | 4 -- doc/release-notes/9356-rate-limiting.md | 20 ---------- doc/release-notes/9590-faster-redeploy.md | 5 --- .../9728-universe-variablemetadata.md | 1 - doc/release-notes/9920-postgres16.md | 3 -- .../9926-list-role-assignments-permissions.md | 1 - doc/release-notes/9983-unique-constraints.md | 14 ------- ...harvest-metadata-values-not-in-cvv-list.md | 4 -- 30 files changed, 161 deletions(-) delete mode 100644 doc/release-notes/10101-qa-guide.md delete mode 100644 doc/release-notes/10117-api-metrics-add-user-accounts-number.md delete mode 100644 doc/release-notes/10155-datasets-can-download-at-least-one-file.md delete mode 100644 doc/release-notes/10202-extend-getVersionFiles-api-to-include-total-file-count.md delete mode 100644 doc/release-notes/10216-metadatablocks.md delete mode 100644 doc/release-notes/10238-container-demo.md delete mode 100644 doc/release-notes/10240-file-citation.md delete mode 100644 doc/release-notes/10280-get-file-api-extension.md delete mode 100644 doc/release-notes/10286-return-owner-added-to-get-apis.md delete mode 100644 doc/release-notes/10297-metadata-api-fix.md delete mode 100644 doc/release-notes/10318-uningest-and-reingest.md delete mode 100644 doc/release-notes/10338-expose-and-sort-publish-status-facet.md delete mode 100644 doc/release-notes/10342-assign-roles-without-privilege-escalation.md delete mode 100644 doc/release-notes/10360-binder-redirect.md delete mode 100644 doc/release-notes/10382-optional-croissant-exporter.md delete mode 100644 doc/release-notes/3437-new-index-api-added.md delete mode 100644 doc/release-notes/3623-multipid.md delete mode 100644 doc/release-notes/3702-return-to-author.md delete mode 100644 doc/release-notes/5645-geospatial-props-nslong-fix.md delete mode 100644 doc/release-notes/8134-add-qpj-qmd-extensions.md delete mode 100644 doc/release-notes/8524-storing-tabular-files-with-varheaders.md delete mode 100644 doc/release-notes/9060-7482-make-licenses-searchable-faceatable.md delete mode 100644 doc/release-notes/9275-harvest-invalid-query-params.md delete mode 100644 doc/release-notes/9356-rate-limiting.md delete mode 100644 doc/release-notes/9590-faster-redeploy.md delete mode 100644 doc/release-notes/9728-universe-variablemetadata.md delete mode 100644 doc/release-notes/9920-postgres16.md delete mode 100644 doc/release-notes/9926-list-role-assignments-permissions.md delete mode 100644 doc/release-notes/9983-unique-constraints.md delete mode 100644 doc/release-notes/9992-harvest-metadata-values-not-in-cvv-list.md diff --git a/doc/release-notes/10101-qa-guide.md b/doc/release-notes/10101-qa-guide.md deleted file mode 100644 index 11fbd7df2c4..00000000000 --- a/doc/release-notes/10101-qa-guide.md +++ /dev/null @@ -1 +0,0 @@ -A new QA Guide is intended mostly for the core development team but may be of interest to contributors. diff --git a/doc/release-notes/10117-api-metrics-add-user-accounts-number.md b/doc/release-notes/10117-api-metrics-add-user-accounts-number.md deleted file mode 100644 index 566815d6e5e..00000000000 --- a/doc/release-notes/10117-api-metrics-add-user-accounts-number.md +++ /dev/null @@ -1,3 +0,0 @@ -### New Accounts Metrics API - -Users can retrieve new types of metrics related to user accounts. The new capabilities are [described](https://guides.dataverse.org/en/6.2/api/metrics.html) in the guides. \ No newline at end of file diff --git a/doc/release-notes/10155-datasets-can-download-at-least-one-file.md b/doc/release-notes/10155-datasets-can-download-at-least-one-file.md deleted file mode 100644 index a0b0d02310a..00000000000 --- a/doc/release-notes/10155-datasets-can-download-at-least-one-file.md +++ /dev/null @@ -1,3 +0,0 @@ -The getCanDownloadAtLeastOneFile (/api/datasets/{id}/versions/{versionId}/canDownloadAtLeastOneFile) endpoint has been created. - -This API endpoint indicates if the calling user can download at least one file from a dataset version. Note that Shibboleth group permissions are not considered. diff --git a/doc/release-notes/10202-extend-getVersionFiles-api-to-include-total-file-count.md b/doc/release-notes/10202-extend-getVersionFiles-api-to-include-total-file-count.md deleted file mode 100644 index 80a71e9bb7e..00000000000 --- a/doc/release-notes/10202-extend-getVersionFiles-api-to-include-total-file-count.md +++ /dev/null @@ -1,2 +0,0 @@ -The response for getVersionFiles (/api/datasets/{id}/versions/{versionId}/files) endpoint has been modified to include a total count of records available (totalCount:x). -This will aid in pagination by allowing the caller to know how many pages can be iterated through. The existing API (getVersionFileCounts) to return the count will still be available. \ No newline at end of file diff --git a/doc/release-notes/10216-metadatablocks.md b/doc/release-notes/10216-metadatablocks.md deleted file mode 100644 index 59d9c1640a5..00000000000 --- a/doc/release-notes/10216-metadatablocks.md +++ /dev/null @@ -1,5 +0,0 @@ -The API endpoint `/api/metadatablocks/{block_id}` has been extended to include the following fields: - -- `isRequired`: Whether or not this field is required -- `displayOrder`: The display order of the field in create/edit forms -- `typeClass`: The type class of this field ("controlledVocabulary", "compound", or "primitive") diff --git a/doc/release-notes/10238-container-demo.md b/doc/release-notes/10238-container-demo.md deleted file mode 100644 index edc4db4b650..00000000000 --- a/doc/release-notes/10238-container-demo.md +++ /dev/null @@ -1 +0,0 @@ -The Container Guide now containers a tutorial for running Dataverse in containers for demo or evaluation purposes: https://guides.dataverse.org/en/6.2/container diff --git a/doc/release-notes/10240-file-citation.md b/doc/release-notes/10240-file-citation.md deleted file mode 100644 index fb747527669..00000000000 --- a/doc/release-notes/10240-file-citation.md +++ /dev/null @@ -1,5 +0,0 @@ -## Get file citation as JSON - -It is now possible to retrieve via API the file citation as it appears on the file landing page. It is formatted in HTML and encoded in JSON. - -This API is not for downloading various citation formats such as EndNote XML, RIS, or BibTeX. This functionality has been requested in https://github.com/IQSS/dataverse/issues/3140 and https://github.com/IQSS/dataverse/issues/9994 diff --git a/doc/release-notes/10280-get-file-api-extension.md b/doc/release-notes/10280-get-file-api-extension.md deleted file mode 100644 index 7ed70e93dc9..00000000000 --- a/doc/release-notes/10280-get-file-api-extension.md +++ /dev/null @@ -1,10 +0,0 @@ -The API endpoint `api/files/{id}` has been extended to support the following optional query parameters: - -- `includeDeaccessioned`: Indicates whether or not to consider deaccessioned dataset versions in the latest file search. (Default: `false`). -- `returnDatasetVersion`: Indicates whether or not to include the dataset version of the file in the response. (Default: `false`). - -A new endpoint `api/files/{id}/versions/{datasetVersionId}` has been created. This endpoint returns the file metadata present in the requested dataset version. To specify the dataset version, you can use ``:latest-published``, or ``:latest``, or ``:draft`` or ``1.0`` or any other available version identifier. - -The endpoint supports the `includeDeaccessioned` and `returnDatasetVersion` optional query parameters, as does the `api/files/{id}` endpoint. - -`api/files/{id}/draft` endpoint is no longer available in favor of the new endpoint `api/files/{id}/versions/{datasetVersionId}`, which can use the version identifier ``:draft`` (`api/files/{id}/versions/:draft`) to obtain the same result. diff --git a/doc/release-notes/10286-return-owner-added-to-get-apis.md b/doc/release-notes/10286-return-owner-added-to-get-apis.md deleted file mode 100644 index b0aba92f537..00000000000 --- a/doc/release-notes/10286-return-owner-added-to-get-apis.md +++ /dev/null @@ -1,5 +0,0 @@ -The API endpoints for getting datasets, Dataverse collections, and datafiles have been extended to support the following optional 'returnOwners' query parameter. - -Including the parameter and setting it to true will add a hierarchy showing which dataset and dataverse collection(s) the object is part of to the json object returned. - - diff --git a/doc/release-notes/10297-metadata-api-fix.md b/doc/release-notes/10297-metadata-api-fix.md deleted file mode 100644 index 11ee086af04..00000000000 --- a/doc/release-notes/10297-metadata-api-fix.md +++ /dev/null @@ -1 +0,0 @@ -The API endpoint `api/datasets/{id}/metadata` has been changed to default to the latest version of the dataset that the user has access. diff --git a/doc/release-notes/10318-uningest-and-reingest.md b/doc/release-notes/10318-uningest-and-reingest.md deleted file mode 100644 index 80ca6be57ea..00000000000 --- a/doc/release-notes/10318-uningest-and-reingest.md +++ /dev/null @@ -1,3 +0,0 @@ -New Uningest/Reingest options are available in the File Page Edit menu, allowing ingest errors to be cleared (by users who can published the associated dataset) -and (by superusers) for a successful ingest to be undone or retried (e.g. after a Dataverse version update or if ingest size limits are changed). -The /api/files//uningest api also now allows users who can publish the dataset to undo an ingest failure. diff --git a/doc/release-notes/10338-expose-and-sort-publish-status-facet.md b/doc/release-notes/10338-expose-and-sort-publish-status-facet.md deleted file mode 100644 index b2362ddb2c5..00000000000 --- a/doc/release-notes/10338-expose-and-sort-publish-status-facet.md +++ /dev/null @@ -1 +0,0 @@ -In version 6.1, the publication status facet location was unintentionally moved to the bottom. In this version, we have restored the original order. diff --git a/doc/release-notes/10342-assign-roles-without-privilege-escalation.md b/doc/release-notes/10342-assign-roles-without-privilege-escalation.md deleted file mode 100644 index a4ef743f50d..00000000000 --- a/doc/release-notes/10342-assign-roles-without-privilege-escalation.md +++ /dev/null @@ -1 +0,0 @@ -The permissions required to assign a role have been fixed. It is no longer possible to assign a role that includes permissions that the assigning user doesn't have. \ No newline at end of file diff --git a/doc/release-notes/10360-binder-redirect.md b/doc/release-notes/10360-binder-redirect.md deleted file mode 100644 index fcf5feea69e..00000000000 --- a/doc/release-notes/10360-binder-redirect.md +++ /dev/null @@ -1,3 +0,0 @@ -If your installation is configured to use Binder, you should remove the old "girder_ythub" tool and replace it with the tool described at https://github.com/IQSS/dataverse-binder-redirect - -For more information, see #10360. diff --git a/doc/release-notes/10382-optional-croissant-exporter.md b/doc/release-notes/10382-optional-croissant-exporter.md deleted file mode 100644 index e4c96115825..00000000000 --- a/doc/release-notes/10382-optional-croissant-exporter.md +++ /dev/null @@ -1 +0,0 @@ -When a Dataverse installation is configured to use a metadata exporter for the [Croissant](https://github.com/mlcommons/croissant) format, the content of the JSON-LD in the `` of dataset landing pages will be replaced with that format. However, both JSON-LD and Croissant will still be available for download from the dataset page and API. \ No newline at end of file diff --git a/doc/release-notes/3437-new-index-api-added.md b/doc/release-notes/3437-new-index-api-added.md deleted file mode 100644 index 2f40c65073f..00000000000 --- a/doc/release-notes/3437-new-index-api-added.md +++ /dev/null @@ -1,4 +0,0 @@ -(this API was added as a side feature of the pr #10222. the main point of the pr was an improvement in the OAI set housekeeping logic, I believe it's too obscure part of the system to warrant a relase note by itself. but the new API below needs to be announced). - -A new Index API endpoint has been added allowing an admin to clear an individual dataset from Solr. - diff --git a/doc/release-notes/3623-multipid.md b/doc/release-notes/3623-multipid.md deleted file mode 100644 index 8c13eb1aec6..00000000000 --- a/doc/release-notes/3623-multipid.md +++ /dev/null @@ -1,37 +0,0 @@ -This release adds support for using multiple PID (DOI, Handle, PermalLink) providers, multiple PID provider accounts -(managing a given protocol, authority,separator, shoulder combination), assigning PID provider accounts to specific collections, -and supporting transferred PIDs (where a PID is managed by an account when it's authority, separator, and/or shoulder don't match -the combination where the account can mint new PIDs). It also adds the ability for additional provider services beyond the existing -DataCite, EZId, Handle, and PermaLink providers to be dynamically added as separate jar files. - -These changes require per-provider settings rather than the global PID settings previously supported. While backward compatibility -for installations using a single PID Provider account is provided, updating to use the new microprofile settings is highly recommended -and will be required in a future version. - -New microprofile settings (where * indicates a provider id indicating which provider the setting is for): - -dataverse.pid.providers -dataverse.pid.default-provider -dataverse.pid.*.type -dataverse.pid.*.label -dataverse.pid.*.authority -dataverse.pid.*.shoulder -dataverse.pid.*.identifier-generation-style -dataverse.pid.*.datafile-pid-format -dataverse.pid.*.managed-list -dataverse.pid.*.excluded-list -dataverse.pid.*.datacite.mds-api-url -dataverse.pid.*.datacite.rest-api-url -dataverse.pid.*.datacite.username -dataverse.pid.*.datacite.password -dataverse.pid.*.ezid.api-url -dataverse.pid.*.ezid.username -dataverse.pid.*.ezid.password -dataverse.pid.*.permalink.base-url -dataverse.pid.*.permalink.separator -dataverse.pid.*.handlenet.index -dataverse.pid.*.handlenet.independent-service -dataverse.pid.*.handlenet.auth-handle -dataverse.pid.*.handlenet.key.path -dataverse.pid.*.handlenet.key.passphrase -dataverse.spi.pidproviders.directory diff --git a/doc/release-notes/3702-return-to-author.md b/doc/release-notes/3702-return-to-author.md deleted file mode 100644 index aa7dd9feaef..00000000000 --- a/doc/release-notes/3702-return-to-author.md +++ /dev/null @@ -1,4 +0,0 @@ -### Return to author - -Popup for returning to author now requires a reason that will be sent by email to the author. -Please note that you can still type a creative and meaningful comment such as "The author would like to modify his dataset", "Files are missing", "Nothing to report" or "A curation report with comments and suggestions/instructions will follow in another email" that suits your situation. \ No newline at end of file diff --git a/doc/release-notes/5645-geospatial-props-nslong-fix.md b/doc/release-notes/5645-geospatial-props-nslong-fix.md deleted file mode 100644 index 4004bf38c78..00000000000 --- a/doc/release-notes/5645-geospatial-props-nslong-fix.md +++ /dev/null @@ -1,4 +0,0 @@ -Across the application, the Geospatial metadata block fields for north and south were labeled incorrectly as ‘Longitudes,’ as reported on #5645. After updating to this version of Dataverse, users will need to update all the endpoints that used ‘northLongitude’ and ‘southLongitude’ to ‘northLatitude’ and ‘southLatitude,’ respectively. - - -TODO: Whoever puts the release notes together should make sure there is the standard note about updating the schema after upgrading. \ No newline at end of file diff --git a/doc/release-notes/8134-add-qpj-qmd-extensions.md b/doc/release-notes/8134-add-qpj-qmd-extensions.md deleted file mode 100644 index 65f4485354b..00000000000 --- a/doc/release-notes/8134-add-qpj-qmd-extensions.md +++ /dev/null @@ -1,3 +0,0 @@ -Add .qpj and .qmd Extensions to Shapefile Handling - -- Support for `.qpj` and `.qmd` files in shapefile uploads has been introduced, ensuring that these files are properly recognized and handled as part of geospatial datasets in Dataverse. diff --git a/doc/release-notes/8524-storing-tabular-files-with-varheaders.md b/doc/release-notes/8524-storing-tabular-files-with-varheaders.md deleted file mode 100644 index f7034c846f6..00000000000 --- a/doc/release-notes/8524-storing-tabular-files-with-varheaders.md +++ /dev/null @@ -1,6 +0,0 @@ -Tabular Data Ingest can now save the generated archival files with the list of variable names added as the first tab-delimited line. As the most significant effect of this feature, -Access API will be able to take advantage of Direct Download for tab. files saved with these headers on S3 - since they no longer have to be generated and added to the streamed content on the fly. - -This behavior is controlled by the new setting `:StoreIngestedTabularFilesWithVarHeaders`. It is false by default, preserving the legacy behavior. When enabled, Dataverse will be able to handle both the newly ingested files, and any already-existing legacy files stored without these headers transparently to the user. E.g. the access API will continue delivering tab-delimited files **with** this header line, whether it needs to add it dynamically for the legacy files, or reading complete files directly from storage for the ones stored with it. - -An API for converting existing legacy tabular files will be added separately. [this line will need to be changed if we have time to add said API before 6.2 is released]. \ No newline at end of file diff --git a/doc/release-notes/9060-7482-make-licenses-searchable-faceatable.md b/doc/release-notes/9060-7482-make-licenses-searchable-faceatable.md deleted file mode 100644 index 1758fd4de22..00000000000 --- a/doc/release-notes/9060-7482-make-licenses-searchable-faceatable.md +++ /dev/null @@ -1,6 +0,0 @@ -### Search by License - -A browse/search facet called "License" has been added and will be displayed as long as there is more than one license in datasets and datafiles in browse/search results. This facet allow you to filter by license such as CC0, etc. -Also, the Search API now handles license filtering using the `fq` parameter, for example : `/api/search?q=*&fq=license%3A%22CC0+1.0%22` for CC0 1.0. See PR #10204 - - diff --git a/doc/release-notes/9275-harvest-invalid-query-params.md b/doc/release-notes/9275-harvest-invalid-query-params.md deleted file mode 100644 index 33d7c7bac13..00000000000 --- a/doc/release-notes/9275-harvest-invalid-query-params.md +++ /dev/null @@ -1,4 +0,0 @@ -OAI-PMH error handling has been improved to display a machine-readable error in XML rather than a 500 error with no further information. - -- /oai?foo=bar will show "No argument 'verb' found" -- /oai?verb=foo&verb=bar will show "Verb must be singular, given: '[foo, bar]'" diff --git a/doc/release-notes/9356-rate-limiting.md b/doc/release-notes/9356-rate-limiting.md deleted file mode 100644 index 1d68669af26..00000000000 --- a/doc/release-notes/9356-rate-limiting.md +++ /dev/null @@ -1,20 +0,0 @@ -## Rate Limiting using JCache (with Hazelcast as provided by Payara) -The option to rate limit has been added to prevent users from over taxing the system either deliberately or by runaway automated processes. -Rate limiting can be configured on a tier level with tier 0 being reserved for guest users and tiers 1-any for authenticated users. -Superuser accounts are exempt from rate limiting. -Rate limits can be imposed on command APIs by configuring the tier, the command, and the hourly limit in the database. -Two database settings configure the rate limiting. -Note: If either of these settings exist in the database rate limiting will be enabled. -If neither setting exists rate limiting is disabled. - -`:RateLimitingDefaultCapacityTiers` is a comma separated list of default values for each tier. -In the following example, the default for tier `0` (guest users) is set to 10,000 calls per command per hour and tier `1` (authenticated users) is set to 20,000 calls per command per hour. -Tiers not specified in this setting will default to `-1` (No Limit). I.e., -d "10000" is equivalent to -d "10000,-1,-1,..." -`curl http://localhost:8080/api/admin/settings/:RateLimitingDefaultCapacityTiers -X PUT -d '10000,20000'` - -`:RateLimitingCapacityByTierAndAction` is a JSON object specifying the rate by tier and a list of actions (commands). -This allows for more control over the rate limit of individual API command calls. -In the following example, calls made by a guest user (tier 0) for API `GetLatestPublishedDatasetVersionCommand` is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. -`curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]'` - -Hazelcast is configured in Payara and should not need any changes for this feature \ No newline at end of file diff --git a/doc/release-notes/9590-faster-redeploy.md b/doc/release-notes/9590-faster-redeploy.md deleted file mode 100644 index ed903849444..00000000000 --- a/doc/release-notes/9590-faster-redeploy.md +++ /dev/null @@ -1,5 +0,0 @@ -In the Container Guide, documentation for developers on how to quickly redeploy code has been added for Netbeans and improved for IntelliJ. - -Also in the context of containers, a new option to skip deployment has been added and the war file is now consistently named "dataverse.war" rather than having a version in the filename, such as "dataverse-6.1.war". This predictability makes tooling easier. - -Finally, an option to create tabs in the guides using [Sphinx Tabs](https://sphinx-tabs.readthedocs.io) has been added. (You can see the tabs in action in the "dev usage" page of the Container Guide.) To continue building the guides, you will need to install this new dependency by re-running `pip install -r requirements.txt`. \ No newline at end of file diff --git a/doc/release-notes/9728-universe-variablemetadata.md b/doc/release-notes/9728-universe-variablemetadata.md deleted file mode 100644 index 66a2daf151b..00000000000 --- a/doc/release-notes/9728-universe-variablemetadata.md +++ /dev/null @@ -1 +0,0 @@ -universe field in variablemetadata table was changed from varchar(255) to text. The change was made to support longer strings in "universe" metadata field, similar to the rest of text fields in variablemetadata table. diff --git a/doc/release-notes/9920-postgres16.md b/doc/release-notes/9920-postgres16.md deleted file mode 100644 index 8aab76e98b9..00000000000 --- a/doc/release-notes/9920-postgres16.md +++ /dev/null @@ -1,3 +0,0 @@ -This release adds install script support for the new permissions model in Postgres versions 15+, and bumps FlyWay to support Postgres 16. - -Postgres 13 remains the version used with automated testing. diff --git a/doc/release-notes/9926-list-role-assignments-permissions.md b/doc/release-notes/9926-list-role-assignments-permissions.md deleted file mode 100644 index 43cd83dc5c9..00000000000 --- a/doc/release-notes/9926-list-role-assignments-permissions.md +++ /dev/null @@ -1 +0,0 @@ -Listing collction/dataverse role assignments via API still requires ManageDataversePermissions, but listing dataset role assignments via API now requires only ManageDatasetPermissions. diff --git a/doc/release-notes/9983-unique-constraints.md b/doc/release-notes/9983-unique-constraints.md deleted file mode 100644 index d889beb0718..00000000000 --- a/doc/release-notes/9983-unique-constraints.md +++ /dev/null @@ -1,14 +0,0 @@ -This release adds two missing database constraints that will assure that the externalvocabularyvalue table only has one entry for each uri and that the oaiset table only has one set for each spec. (In the very unlikely case that your existing database has duplicate entries now, install would fail. This can be checked by running - -SELECT uri, count(*) FROM externalvocabularyvaluet group by uri; - -and - -SELECT spec, count(*) FROM oaiset group by spec; - -and then removing any duplicate rows (where count>1). - - - - -TODO: Whoever puts the release notes together should make sure there is the standard note about reloading metadata blocks for the citation, astrophysics, and biomedical blocks (plus any others from other PRs) after upgrading. \ No newline at end of file diff --git a/doc/release-notes/9992-harvest-metadata-values-not-in-cvv-list.md b/doc/release-notes/9992-harvest-metadata-values-not-in-cvv-list.md deleted file mode 100644 index 88ca6cf0e79..00000000000 --- a/doc/release-notes/9992-harvest-metadata-values-not-in-cvv-list.md +++ /dev/null @@ -1,4 +0,0 @@ -The API endpoint `api/harvest/clients/{harvestingClientNickname}` has been extended to include the following fields: - -- `allowHarvestingMissingCVV`: enable/disable allowing datasets to be harvested with Controlled Vocabulary Values that existed in the originating Dataverse Project but are not in the harvesting Dataverse Project. Default is false. -Note: This setting is only available to the API and not currently accessible/settable via the UI \ No newline at end of file From 0f5bff94bde9938ba2e032bd598119ea25dbf4d8 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 26 Mar 2024 13:44:05 -0400 Subject: [PATCH 610/689] Changes per review request, fix error handling --- .../edu/harvard/iq/dataverse/DvObjectContainer.java | 6 +++++- .../java/edu/harvard/iq/dataverse/api/Datasets.java | 2 +- src/main/java/propertyFiles/Bundle.properties | 1 + .../iq/dataverse/pidproviders/PidUtilTest.java | 12 ++++++++++-- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DvObjectContainer.java b/src/main/java/edu/harvard/iq/dataverse/DvObjectContainer.java index bfb4b3ef749..56d26a7260d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DvObjectContainer.java +++ b/src/main/java/edu/harvard/iq/dataverse/DvObjectContainer.java @@ -252,7 +252,11 @@ public PidProvider getEffectivePidGenerator() { providerSpecs.getString("authority"), providerSpecs.getString("shoulder")); } } - setPidGenerator(pidGenerator); + if(pidGenerator!=null && pidGenerator.canManagePID()) { + setPidGenerator(pidGenerator); + } else { + setPidGenerator(null); + } } return pidGenerator; } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 2ea8e50a896..6d8fbe1808c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -4598,7 +4598,7 @@ public Response getPidGenerator(@Context ContainerRequestContext crc, @PathParam PidProvider pidProvider = dataset.getEffectivePidGenerator(); if(pidProvider == null) { //This is basically a config error, e.g. if a valid pid provider was removed after this dataset used it - return error(Response.Status.NOT_FOUND, "No PID Generator found for the give id"); + return error(Response.Status.NOT_FOUND, BundleUtil.getStringFromBundle("datasets.api.pidgenerator.notfound")); } String pidGeneratorId = pidProvider.getId(); return ok(pidGeneratorId); diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 17dd0933f55..4bb0659a7d6 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -2684,6 +2684,7 @@ datasets.api.deaccessionDataset.invalid.forward.url=Invalid deaccession forward datasets.api.globusdownloaddisabled=File transfer from Dataverse via Globus is not available for this dataset. datasets.api.globusdownloadnotfound=List of files to transfer not found. datasets.api.globusuploaddisabled=File transfer to Dataverse via Globus is not available for this dataset. +datasets.api.pidgenerator.notfound=No PID Generator configured for the give id. #Dataverses.java dataverses.api.update.default.contributor.role.failure.role.not.found=Role {0} not found. diff --git a/src/test/java/edu/harvard/iq/dataverse/pidproviders/PidUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/pidproviders/PidUtilTest.java index dc226d2e85b..cffac741c78 100644 --- a/src/test/java/edu/harvard/iq/dataverse/pidproviders/PidUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/pidproviders/PidUtilTest.java @@ -388,8 +388,16 @@ public void testFindingPidGenerators() throws IOException { assertEquals("fake1", dataset1.getGlobalId().getProviderId()); assertEquals("ez1", dataset1.getEffectivePidGenerator().getId()); - - + //Now test failure case + dataverse1.setPidGenerator(null); + dataset1.setPidGenerator(null); + pidGeneratorSpecs = Json.createObjectBuilder().add("protocol", AbstractDOIProvider.DOI_PROTOCOL).add("authority","10.9999").add("shoulder", "FK2").build().toString(); + //Set a PID generator on the parent + dataverse1.setPidGeneratorSpecs(pidGeneratorSpecs); + assertEquals(pidGeneratorSpecs, dataverse1.getPidGeneratorSpecs()); + //Verify that the parent's PID generator is the effective one + assertNull(dataverse1.getEffectivePidGenerator()); + assertNull(dataset1.getEffectivePidGenerator()); } @Test From 24b4bbe89a20a73c8f13ffe80bc70c5b4bcdad71 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 26 Mar 2024 13:46:07 -0400 Subject: [PATCH 611/689] Revert "Missing env var" This reverts commit 909244b63e167daf5e065b25d6ecced241ec9d42. --- .../source/container/dev-usage.rst | 2 +- .../container/img/intellij-compose-setup.png | Bin 52130 -> 45986 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/container/dev-usage.rst b/doc/sphinx-guides/source/container/dev-usage.rst index 9f2b2648165..be4eda5da44 100644 --- a/doc/sphinx-guides/source/container/dev-usage.rst +++ b/doc/sphinx-guides/source/container/dev-usage.rst @@ -263,7 +263,7 @@ Hotswapping methods requires using JDWP (Debug Mode), but does not allow switchi .. image:: img/intellij-compose-add-new-config.png - Give your configuration a meaningful name, select the compose file to use (in this case the default one), add the environment variables ``SKIP_DEPLOY=1`` and ``POSTGRES_VERSION=16``, optionally select the services to start. + Give your configuration a meaningful name, select the compose file to use (in this case the default one), add the environment variable ``SKIP_DEPLOY=1``, and optionally select the services to start. You might also want to change other options like attaching to containers to view the logs within the "Services" tab. .. image:: img/intellij-compose-setup.png diff --git a/doc/sphinx-guides/source/container/img/intellij-compose-setup.png b/doc/sphinx-guides/source/container/img/intellij-compose-setup.png index 0ab73e125b2897176750ac81baa8c8a6223d3aa2..42c2accf2b459b0502d2fb42be5058e07b0e9376 100644 GIT binary patch literal 45986 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 E0UQjieE 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%0DDsE$N)Hyv_0!XVmW&99 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�n(wWg((VVM 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 From 5ade4103553aa20aa1483fc68cc74d51f03afa74 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 26 Mar 2024 13:47:42 -0400 Subject: [PATCH 612/689] Bump for PG 16 so. --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index ae266af80da..d5cffcec0aa 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ APP_IMAGE=gdcc/dataverse:unstable -POSTGRES_VERSION=13 +POSTGRES_VERSION=16 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.3.0 SKIP_DEPLOY=0 \ No newline at end of file From 0400037b3c83b64a466b68e64b92caefa593ea74 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 26 Mar 2024 14:08:47 -0400 Subject: [PATCH 613/689] Formating --- doc/release-notes/6.2-release-notes.md | 187 ++++++++++++++++--------- 1 file changed, 117 insertions(+), 70 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 48903fb8b34..57f188bdd20 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -7,10 +7,6 @@ Thank you to all of the community members who contributed code, suggestions, bug ## Release highlights -### New API Endpoint for Clearing an Individual Dataset From Solr - -A new Index API endpoint has been added allowing an admin to clear an individual dataset from Solr. - ### Return to Author Now Requires a Reason The Popup for returning to author now requires a reason that will be sent by email to the author. @@ -31,38 +27,119 @@ and will be required in a future version. New microprofile settings (where * indicates a provider id indicating which provider the setting is for): -dataverse.pid.providers -dataverse.pid.default-provider -dataverse.pid.*.type -dataverse.pid.*.label -dataverse.pid.*.authority -dataverse.pid.*.shoulder -dataverse.pid.*.identifier-generation-style -dataverse.pid.*.datafile-pid-format -dataverse.pid.*.managed-list -dataverse.pid.*.excluded-list -dataverse.pid.*.datacite.mds-api-url -dataverse.pid.*.datacite.rest-api-url -dataverse.pid.*.datacite.username -dataverse.pid.*.datacite.password -dataverse.pid.*.ezid.api-url -dataverse.pid.*.ezid.username -dataverse.pid.*.ezid.password -dataverse.pid.*.permalink.base-url -dataverse.pid.*.permalink.separator -dataverse.pid.*.handlenet.index -dataverse.pid.*.handlenet.independent-service -dataverse.pid.*.handlenet.auth-handle -dataverse.pid.*.handlenet.key.path -dataverse.pid.*.handlenet.key.passphrase -dataverse.spi.pidproviders.directory +- `dataverse.pid.providers` +- `dataverse.pid.default-provider` +- `dataverse.pid.*.type` +- `dataverse.pid.*.label` +- `dataverse.pid.*.authority` +- `dataverse.pid.*.shoulder` +- `dataverse.pid.*.identifier-generation-style` +- `dataverse.pid.*.datafile-pid-format` +- `dataverse.pid.*.managed-list` +- `dataverse.pid.*.excluded-list` +- `dataverse.pid.*.datacite.mds-api-url` +- `dataverse.pid.*.datacite.rest-api-url` +- `dataverse.pid.*.datacite.username` +- `dataverse.pid.*.datacite.password` +- `dataverse.pid.*.ezid.api-url` +- `dataverse.pid.*.ezid.username` +- `dataverse.pid.*.ezid.password` +- `dataverse.pid.*.permalink.base-url` +- `dataverse.pid.*.permalink.separator` +- `dataverse.pid.*.handlenet.index` +- `dataverse.pid.*.handlenet.independent-service` +- `dataverse.pid.*.handlenet.auth-handle` +- `dataverse.pid.*.handlenet.key.path` +- `dataverse.pid.*.handlenet.key.passphrase` +- `dataverse.spi.pidproviders.directory` -### Geospatial Metadata Block Fields for North and South Renamed +### Rate Limiting Using JCache (With Hazelcast As Provided by Payara) -The Geospatial metadata block fields for north and south were labeled incorrectly as ‘Longitudes,’ as reported on #5645. After updating to this version of Dataverse, users will need to update all the endpoints that used ‘northLongitude’ and ‘southLongitude’ to ‘northLatitude’ and ‘southLatitude,’ respectively. +The option to rate limit has been added to prevent users from over taxing the system either deliberately or by runaway automated processes. +Rate limiting can be configured on a tier level with tier 0 being reserved for guest users and tiers 1-any for authenticated users. +Superuser accounts are exempt from rate limiting. +Rate limits can be imposed on command APIs by configuring the tier, the command, and the hourly limit in the database. +Two database settings configure the rate limiting. +Note: If either of these settings exist in the database rate limiting will be enabled. +If neither setting exists rate limiting is disabled. +`:RateLimitingDefaultCapacityTiers` is a comma separated list of default values for each tier. +In the following example, the default for tier `0` (guest users) is set to 10,000 calls per command per hour and tier `1` (authenticated users) is set to 20,000 calls per command per hour. +Tiers not specified in this setting will default to `-1` (No Limit). I.e., -d "10000" is equivalent to -d "10000,-1,-1,..." -TODO: Whoever puts the release notes together should make sure there is the standard note about updating the schema after upgrading. +`'curl http://localhost:8080/api/admin/settings/:RateLimitingDefaultCapacityTiers -X PUT -d '10000,20000'` + +`:RateLimitingCapacityByTierAndAction` is a JSON object specifying the rate by tier and a list of actions (commands). +This allows for more control over the rate limit of individual API command calls. +In the following example, calls made by a guest user (tier 0) for API `GetLatestPublishedDatasetVersionCommand` is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. + +`curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]'` + + +``` +curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '[{ + "tier": 0, + "limitPerHour": 10, + "actions": [ + "GetLatestPublishedDatasetVersionCommand", + "GetPrivateUrlCommand", + "GetDatasetCommand", + "GetLatestAccessibleDatasetVersionCommand" + ] +}, +{ + "tier": 0, + "limitPerHour": 1, + "actions": [ + "CreateGuestbookResponseCommand", + "UpdateDatasetVersionCommand", + "DestroyDatasetCommand", + "DeleteDataFileCommand", + "FinalizeDatasetPublicationCommand", + "PublishDatasetCommand" + ] +}, +{ + "tier": 1, + "limitPerHour": 30, + "actions": [ + "CreateGuestbookResponseCommand", + "GetLatestPublishedDatasetVersionCommand", + "GetPrivateUrlCommand", + "GetDatasetCommand", + "GetLatestAccessibleDatasetVersionCommand", + "UpdateDatasetVersionCommand", + "DestroyDatasetCommand", + "DeleteDataFileCommand", + "FinalizeDatasetPublicationCommand", + "PublishDatasetCommand" + ] +}]' +``` + +### Binder Redirect + +If your installation is configured to use Binder, you should remove the old "girder_ythub" tool and replace it with the tool described at https://github.com/IQSS/dataverse-binder-redirect + +For more information, see [#10360](https://github.com/IQSS/dataverse/issues/10360). + +### Optional Croissant Exporter Support + +When a Dataverse installation is configured to use a metadata exporter for the [Croissant](https://github.com/mlcommons/croissant) format, the content of the JSON-LD in the `` of dataset landing pages will be replaced with that format. However, both JSON-LD and Croissant will still be available for download from the dataset page and API. + + + +Hazelcast is configured in Payara and should not need any changes for this feature + +### Search by License + +A new search facet called "License" has been added and will be displayed as long as there is more than one license in datasets and datafiles in browse/search results. This facet allow you to filter by license such as CC0, etc. + +Also, the Search API now handles license filtering using the `fq` parameter, for example : `/api/search?q=*&fq=license%3A%22CC0+1.0%22` for CC0 1.0. See PR #10204 + +### New API Endpoint for Clearing an Individual Dataset From Solr + +A new Index API endpoint has been added allowing an admin to clear an individual dataset from Solr. ### Add .QPJ and .QMD Extensions to Shapefile Handling @@ -78,11 +155,12 @@ This behavior is controlled by the new setting `:StoreIngestedTabularFilesWithVa An API for converting existing legacy tabular files will be added separately. [this line will need to be changed if we have time to add said API before 6.2 is released]. [TODO] -### Search by License +### Geospatial Metadata Block Fields for North and South Renamed -A new search facet called "License" has been added and will be displayed as long as there is more than one license in datasets and datafiles in browse/search results. This facet allow you to filter by license such as CC0, etc. +The Geospatial metadata block fields for north and south were labeled incorrectly as ‘Longitudes,’ as reported on #5645. After updating to this version of Dataverse, users will need to update all the endpoints that used ‘northLongitude’ and ‘southLongitude’ to ‘northLatitude’ and ‘southLatitude,’ respectively. -Also, the Search API now handles license filtering using the `fq` parameter, for example : `/api/search?q=*&fq=license%3A%22CC0+1.0%22` for CC0 1.0. See PR #10204 + +TODO: Whoever puts the release notes together should make sure there is the standard note about updating the schema after upgrading. ### OAI-PMH Error Handling Has Been Improved @@ -91,28 +169,6 @@ OAI-PMH error handling has been improved to display a machine-readable error in - /oai?foo=bar will show "No argument 'verb' found" - /oai?verb=foo&verb=bar will show "Verb must be singular, given: '[foo, bar]'" -### Rate Limiting Using JCache (With Hazelcast As Provided by Payara) - -The option to rate limit has been added to prevent users from over taxing the system either deliberately or by runaway automated processes. -Rate limiting can be configured on a tier level with tier 0 being reserved for guest users and tiers 1-any for authenticated users. -Superuser accounts are exempt from rate limiting. -Rate limits can be imposed on command APIs by configuring the tier, the command, and the hourly limit in the database. -Two database settings configure the rate limiting. -Note: If either of these settings exist in the database rate limiting will be enabled. -If neither setting exists rate limiting is disabled. - -`:RateLimitingDefaultCapacityTiers` is a comma separated list of default values for each tier. -In the following example, the default for tier `0` (guest users) is set to 10,000 calls per command per hour and tier `1` (authenticated users) is set to 20,000 calls per command per hour. -Tiers not specified in this setting will default to `-1` (No Limit). I.e., -d "10000" is equivalent to -d "10000,-1,-1,..." -`curl http://localhost:8080/api/admin/settings/:RateLimitingDefaultCapacityTiers -X PUT -d '10000,20000'` - -`:RateLimitingCapacityByTierAndAction` is a JSON object specifying the rate by tier and a list of actions (commands). -This allows for more control over the rate limit of individual API command calls. -In the following example, calls made by a guest user (tier 0) for API `GetLatestPublishedDatasetVersionCommand` is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. -`curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]'` - -Hazelcast is configured in Payara and should not need any changes for this feature - ### Container Guide, Documentation for Faster Redeploy In the Container Guide, documentation for developers on how to quickly redeploy code has been added for Netbeans and improved for IntelliJ. @@ -139,11 +195,11 @@ Listing collection/dataverse role assignments via API still requires ManageDatav This release adds two missing database constraints that will assure that the externalvocabularyvalue table only has one entry for each uri and that the oaiset table only has one set for each spec. (In the very unlikely case that your existing database has duplicate entries now, install would fail. This can be checked by running -SELECT uri, count(*) FROM externalvocabularyvaluet group by uri; +`SELECT uri, count(*) FROM externalvocabularyvaluet group by uri;` and -SELECT spec, count(*) FROM oaiset group by spec; +`SELECT spec, count(*) FROM oaiset group by spec;` and then removing any duplicate rows (where count>1). @@ -156,9 +212,7 @@ The API endpoint `api/harvest/clients/{harvestingClientNickname}` has been exten - `allowHarvestingMissingCVV`: enable/disable allowing datasets to be harvested with Controlled Vocabulary Values that existed in the originating Dataverse Project but are not in the harvesting Dataverse Project. Default is false. Note: This setting is only available to the API and not currently accessible/settable via the UI -### New QA Guide -A new QA Guide is intended mostly for the core development team but may be of interest to contributors. ### New Accounts Metrics API @@ -230,13 +284,6 @@ In version 6.1, the publication status facet location was unintentionally moved The permissions required to assign a role have been fixed. It is no longer possible to assign a role that includes permissions that the assigning user doesn't have. -### Binder Redirect - -If your installation is configured to use Binder, you should remove the old "girder_ythub" tool and replace it with the tool described at https://github.com/IQSS/dataverse-binder-redirect - -For more information, see #10360. - - -### Optional Croissant Exporter Support +### New QA Guide -When a Dataverse installation is configured to use a metadata exporter for the [Croissant](https://github.com/mlcommons/croissant) format, the content of the JSON-LD in the `` of dataset landing pages will be replaced with that format. However, both JSON-LD and Croissant will still be available for download from the dataset page and API. +A new QA Guide is intended mostly for the core development team but may be of interest to contributors. From d76a59072ca5d4390fdd920aeea2f9f6cee313a6 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 26 Mar 2024 14:56:21 -0400 Subject: [PATCH 614/689] Update --- doc/release-notes/6.2-release-notes.md | 202 ++++++++++++------------- 1 file changed, 101 insertions(+), 101 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 57f188bdd20..f3f892ce4e2 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -5,7 +5,7 @@ Please note: To read these instructions in full, please go to https://github.com This release brings new features, enhancements, and bug fixes to the Dataverse software. Thank you to all of the community members who contributed code, suggestions, bug reports, and other assistance across the project. -## Release highlights +## 💡Release highlights ### Return to Author Now Requires a Reason @@ -27,31 +27,31 @@ and will be required in a future version. New microprofile settings (where * indicates a provider id indicating which provider the setting is for): -- `dataverse.pid.providers` -- `dataverse.pid.default-provider` -- `dataverse.pid.*.type` -- `dataverse.pid.*.label` -- `dataverse.pid.*.authority` -- `dataverse.pid.*.shoulder` -- `dataverse.pid.*.identifier-generation-style` -- `dataverse.pid.*.datafile-pid-format` -- `dataverse.pid.*.managed-list` -- `dataverse.pid.*.excluded-list` -- `dataverse.pid.*.datacite.mds-api-url` -- `dataverse.pid.*.datacite.rest-api-url` -- `dataverse.pid.*.datacite.username` -- `dataverse.pid.*.datacite.password` -- `dataverse.pid.*.ezid.api-url` -- `dataverse.pid.*.ezid.username` -- `dataverse.pid.*.ezid.password` -- `dataverse.pid.*.permalink.base-url` -- `dataverse.pid.*.permalink.separator` -- `dataverse.pid.*.handlenet.index` -- `dataverse.pid.*.handlenet.independent-service` -- `dataverse.pid.*.handlenet.auth-handle` -- `dataverse.pid.*.handlenet.key.path` -- `dataverse.pid.*.handlenet.key.passphrase` -- `dataverse.spi.pidproviders.directory` +> - dataverse.pid.providers +> - dataverse.pid.default-provider +> - dataverse.pid.*.type +> - dataverse.pid.*.label +> - dataverse.pid.*.authority +> - dataverse.pid.*.shoulder +> - dataverse.pid.*.identifier-generation-style +> - dataverse.pid.*.datafile-pid-format +> - dataverse.pid.*.managed-list +> - dataverse.pid.*.excluded-list +> - dataverse.pid.*.datacite.mds-api-url +> - dataverse.pid.*.datacite.rest-api-url +> - dataverse.pid.*.datacite.username +> - dataverse.pid.*.datacite.password +> - dataverse.pid.*.ezid.api-url +> - dataverse.pid.*.ezid.username +> - dataverse.pid.*.ezid.password +> - dataverse.pid.*.permalink.base-url +> - dataverse.pid.*.permalink.separator +> - dataverse.pid.*.handlenet.index +> - dataverse.pid.*.handlenet.independent-service +> - dataverse.pid.*.handlenet.auth-handle +> - dataverse.pid.*.handlenet.key.path +> - dataverse.pid.*.handlenet.key.passphrase +> - dataverse.spi.pidproviders.directory ### Rate Limiting Using JCache (With Hazelcast As Provided by Payara) @@ -67,13 +67,6 @@ If neither setting exists rate limiting is disabled. In the following example, the default for tier `0` (guest users) is set to 10,000 calls per command per hour and tier `1` (authenticated users) is set to 20,000 calls per command per hour. Tiers not specified in this setting will default to `-1` (No Limit). I.e., -d "10000" is equivalent to -d "10000,-1,-1,..." -`'curl http://localhost:8080/api/admin/settings/:RateLimitingDefaultCapacityTiers -X PUT -d '10000,20000'` - -`:RateLimitingCapacityByTierAndAction` is a JSON object specifying the rate by tier and a list of actions (commands). -This allows for more control over the rate limit of individual API command calls. -In the following example, calls made by a guest user (tier 0) for API `GetLatestPublishedDatasetVersionCommand` is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. - -`curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '[{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]'` ``` @@ -117,33 +110,29 @@ curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndActi }]' ``` +Hazelcast is configured in Payara and should not need any changes for this feature + ### Binder Redirect If your installation is configured to use Binder, you should remove the old "girder_ythub" tool and replace it with the tool described at https://github.com/IQSS/dataverse-binder-redirect For more information, see [#10360](https://github.com/IQSS/dataverse/issues/10360). -### Optional Croissant Exporter Support - -When a Dataverse installation is configured to use a metadata exporter for the [Croissant](https://github.com/mlcommons/croissant) format, the content of the JSON-LD in the `` of dataset landing pages will be replaced with that format. However, both JSON-LD and Croissant will still be available for download from the dataset page and API. +### Optional Croissant 🥐 Exporter Support - - -Hazelcast is configured in Payara and should not need any changes for this feature +When a Dataverse installation is configured to use a metadata exporter for the [Croissant](https://github.com/mlcommons/croissant) format, the content of the JSON-LD in the **<head>** of dataset landing pages will be replaced with that format. However, both JSON-LD and Croissant will still be available for download from the dataset page and API. ### Search by License A new search facet called "License" has been added and will be displayed as long as there is more than one license in datasets and datafiles in browse/search results. This facet allow you to filter by license such as CC0, etc. -Also, the Search API now handles license filtering using the `fq` parameter, for example : `/api/search?q=*&fq=license%3A%22CC0+1.0%22` for CC0 1.0. See PR #10204 - -### New API Endpoint for Clearing an Individual Dataset From Solr +Also, the Search API now handles license filtering using the `fq` parameter, for example : `/api/search?q=*&fq=license%3A%22CC0+1.0%22` for CC0 1.0. -A new Index API endpoint has been added allowing an admin to clear an individual dataset from Solr. +For more information, see [#10204](https://github.com/IQSS/dataverse/issues/10204). ### Add .QPJ and .QMD Extensions to Shapefile Handling -- Support for `.qpj` and `.qmd` files in shapefile uploads has been introduced, ensuring that these files are properly recognized and handled as part of geospatial datasets in Dataverse. +- Support for **.qpj** and **.qmd** files in shapefile uploads has been introduced, ensuring that these files are properly recognized and handled as part of geospatial datasets in Dataverse. ### Ingested Tabular Data Files Can Be Stored Without the Variable Name Header @@ -151,35 +140,59 @@ Tabular Data Ingest can now save the generated archival files with the list of v Access API will be able to take advantage of Direct Download for tab. files saved with these headers on S3 - since they no longer have to be generated and added to the streamed content on the fly. -This behavior is controlled by the new setting `:StoreIngestedTabularFilesWithVarHeaders`. It is false by default, preserving the legacy behavior. When enabled, Dataverse will be able to handle both the newly ingested files, and any already-existing legacy files stored without these headers transparently to the user. E.g. the access API will continue delivering tab-delimited files **with** this header line, whether it needs to add it dynamically for the legacy files, or reading complete files directly from storage for the ones stored with it. +This behavior is controlled by the new setting **:StoreIngestedTabularFilesWithVarHeaders**. It is false by default, preserving the legacy behavior. When enabled, Dataverse will be able to handle both the newly ingested files, and any already-existing legacy files stored without these headers transparently to the user. E.g. the access API will continue delivering tab-delimited files **with** this header line, whether it needs to add it dynamically for the legacy files, or reading complete files directly from storage for the ones stored with it. An API for converting existing legacy tabular files will be added separately. [this line will need to be changed if we have time to add said API before 6.2 is released]. [TODO] +### Uningest/Reingest Options Available in the File Page Edit Menu + +New Uningest/Reingest options are available in the File Page Edit menu. Ingest errors can be cleared by users who can published the associated dataset and by superusers, allowing for a successful ingest to be undone or retried (e.g. after a Dataverse version update or if ingest size limits are changed). + +The /api/files//uningest api also now allows users who can publish the dataset to undo an ingest failure. + +## 🪲 Bugs + +### Publication Status Facet Restored + +In version 6.1, the publication status facet location was unintentionally moved to the bottom. In this version, we have restored the original order. + +### Permissions Required To Assign a Role Have Been Fixed + +The permissions required to assign a role have been fixed. It is no longer possible to assign a role that includes permissions that the assigning user doesn't have. + ### Geospatial Metadata Block Fields for North and South Renamed The Geospatial metadata block fields for north and south were labeled incorrectly as ‘Longitudes,’ as reported on #5645. After updating to this version of Dataverse, users will need to update all the endpoints that used ‘northLongitude’ and ‘southLongitude’ to ‘northLatitude’ and ‘southLatitude,’ respectively. - TODO: Whoever puts the release notes together should make sure there is the standard note about updating the schema after upgrading. ### OAI-PMH Error Handling Has Been Improved OAI-PMH error handling has been improved to display a machine-readable error in XML rather than a 500 error with no further information. -- /oai?foo=bar will show "No argument 'verb' found" -- /oai?verb=foo&verb=bar will show "Verb must be singular, given: '[foo, bar]'" +> - /oai?foo=bar will show "No argument 'verb' found" +> - /oai?verb=foo&verb=bar will show "Verb must be singular, given: '[foo, bar]'" -### Container Guide, Documentation for Faster Redeploy +## 💾 Persistence -In the Container Guide, documentation for developers on how to quickly redeploy code has been added for Netbeans and improved for IntelliJ. +### Missing Database Constraints -Also in the context of containers, a new option to skip deployment has been added and the war file is now consistently named "dataverse.war" rather than having a version in the filename, such as "dataverse-6.1.war". This predictability makes tooling easier. +This release adds two missing database constraints that will assure that the externalvocabularyvalue table only has one entry for each uri and that the oaiset table only has one set for each spec. (In the very unlikely case that your existing database has duplicate entries now, install would fail. This can be checked by running -Finally, an option to create tabs in the guides using [Sphinx Tabs](https://sphinx-tabs.readthedocs.io) has been added. (You can see the tabs in action in the "dev usage" page of the Container Guide.) To continue building the guides, you will need to install this new dependency by re-running `pip install -r requirements.txt`. +``` +SELECT uri, count(*) FROM externalvocabularyvaluet group by uri; +``` +and +``` +SELECT spec, count(*) FROM oaiset group by spec; +``` +and then removing any duplicate rows (where count>1). + +TODO: Whoever puts the release notes together should make sure there is the standard note about reloading metadata blocks for the citation, astrophysics, and biomedical blocks (plus any others from other PRs) after upgrading. ### Universe Field in Variablemetadata Table Changed -Universe field in variablemetadata table was changed from varchar(255) to text. The change was made to support longer strings in "universe" metadata field, similar to the rest of text fields in variablemetadata table. +Universe field in variablemetadata table was changed from **varchar(255)** to **text**. The change was made to support longer strings in "universe" metadata field, similar to the rest of text fields in variablemetadata table. ### Postgres Versions @@ -187,80 +200,67 @@ This release adds install script support for the new permissions model in Postgr Postgres 13 remains the version used with automated testing. +## 🌐 API + ### Listing Collection/Dataverse API Listing collection/dataverse role assignments via API still requires ManageDataversePermissions, but listing dataset role assignments via API now requires only ManageDatasetPermissions. -### Missing Database Constraints +### New API Endpoint for Clearing an Individual Dataset From Solr -This release adds two missing database constraints that will assure that the externalvocabularyvalue table only has one entry for each uri and that the oaiset table only has one set for each spec. (In the very unlikely case that your existing database has duplicate entries now, install would fail. This can be checked by running +A new Index API endpoint has been added allowing an admin to clear an individual dataset from Solr. -`SELECT uri, count(*) FROM externalvocabularyvaluet group by uri;` +### New Accounts Metrics API -and +Users can retrieve new types of metrics related to user accounts. The new capabilities are [described](https://guides.dataverse.org/en/6.2/api/metrics.html) in the guides. -`SELECT spec, count(*) FROM oaiset group by spec;` +### New canDownloadAtLeastOneFile Endpoint -and then removing any duplicate rows (where count>1). +The `/api/datasets/{id}/versions/{versionId}/canDownloadAtLeastOneFile` endpoint has been created. -TODO: Whoever puts the release notes together should make sure there is the standard note about reloading metadata blocks for the citation, astrophysics, and biomedical blocks (plus any others from other PRs) after upgrading. +This API endpoint indicates if the calling user can download at least one file from a dataset version. Note that Shibboleth group permissions are not considered. -### Harvesting Client API +### Harvesting Client Endpoint Extended The API endpoint `api/harvest/clients/{harvestingClientNickname}` has been extended to include the following fields: -- `allowHarvestingMissingCVV`: enable/disable allowing datasets to be harvested with Controlled Vocabulary Values that existed in the originating Dataverse Project but are not in the harvesting Dataverse Project. Default is false. -Note: This setting is only available to the API and not currently accessible/settable via the UI - - - -### New Accounts Metrics API - -Users can retrieve new types of metrics related to user accounts. The new capabilities are [described](https://guides.dataverse.org/en/6.2/api/metrics.html) in the guides. - -### New canDownloadAtLeastOneFile API +- **allowHarvestingMissingCVV**: enable/disable allowing datasets to be harvested with Controlled Vocabulary Values that existed in the originating Dataverse Project but are not in the harvesting Dataverse Project. Default is false. -The GET canDownloadAtLeastOneFile (/api/datasets/{id}/versions/{versionId}/canDownloadAtLeastOneFile) endpoint has been created. - -This API endpoint indicates if the calling user can download at least one file from a dataset version. Note that Shibboleth group permissions are not considered. +*Note: This setting is only available to the API and not currently accessible/settable via the UI* -### Extended getVersionFiles API +### Version Files Endpoint Extended -The response for getVersionFiles (/api/datasets/{id}/versions/{versionId}/files) endpoint has been modified to include a total count of records available (totalCount:x). +The response for getVersionFiles `/api/datasets/{id}/versions/{versionId}/files` endpoint has been modified to include a total count of records available **totalCount:x**. This will aid in pagination by allowing the caller to know how many pages can be iterated through. The existing API (getVersionFileCounts) to return the count will still be available. -### Extended Metadata Blocks API +### Metadata Blocks Endpoint Extended The API endpoint `/api/metadatablocks/{block_id}` has been extended to include the following fields: -- `isRequired`: Whether or not this field is required -- `displayOrder`: The display order of the field in create/edit forms -- `typeClass`: The type class of this field ("controlledVocabulary", "compound", or "primitive") - -### Evaluation Version Tutorial on the Containers Guide - -The Container Guide now containers a tutorial for running Dataverse in containers for demo or evaluation purposes: https://guides.dataverse.org/en/6.2/container +- **isRequired**: Whether or not this field is required +- **displayOrder**: The display order of the field in create/edit forms +- **typeClass**: The type class of this field ("controlledVocabulary", "compound", or "primitive") ### Get File Citation As JSON It is now possible to retrieve via API the file citation as it appears on the file landing page. It is formatted in HTML and encoded in JSON. -This API is not for downloading various citation formats such as EndNote XML, RIS, or BibTeX. This functionality has been requested in https://github.com/IQSS/dataverse/issues/3140 and https://github.com/IQSS/dataverse/issues/9994 +This API is not for downloading various citation formats such as EndNote XML, RIS, or BibTeX. This functionality has been requested in [#3140](https://github.com/IQSS/dataverse/issues/3140) and [#9994](https://github.com/IQSS/dataverse/issues/9994) -### Extended Files API +### Files Endpoint Extended The API endpoint `api/files/{id}` has been extended to support the following optional query parameters: -- `includeDeaccessioned`: Indicates whether or not to consider deaccessioned dataset versions in the latest file search. (Default: `false`). -- `returnDatasetVersion`: Indicates whether or not to include the dataset version of the file in the response. (Default: `false`). +- **includeDeaccessioned**: Indicates whether or not to consider deaccessioned dataset versions in the latest file search. (Default: `false`). +- **returnDatasetVersion**: Indicates whether or not to include the dataset version of the file in the response. (Default: `false`). -A new endpoint `api/files/{id}/versions/{datasetVersionId}` has been created. This endpoint returns the file metadata present in the requested dataset version. To specify the dataset version, you can use ``:latest-published``, or ``:latest``, or ``:draft`` or ``1.0`` or any other available version identifier. +A new endpoint `api/files/{id}/versions/{datasetVersionId}` has been created. This endpoint returns the file metadata present in the requested dataset version. To specify the dataset version, you can use `:latest-published`, `:latest`, `:draft` or `1.0` or any other available version identifier. -The endpoint supports the `includeDeaccessioned` and `returnDatasetVersion` optional query parameters, as does the `api/files/{id}` endpoint. +The endpoint supports the *includeDeaccessioned* and *returnDatasetVersion* optional query parameters, as does the `api/files/{id}` endpoint. `api/files/{id}/draft` endpoint is no longer available in favor of the new endpoint `api/files/{id}/versions/{datasetVersionId}`, which can use the version identifier ``:draft`` (`api/files/{id}/versions/:draft`) to obtain the same result. -### Endpoint Extended: Datasets, Dataverse Collections, and Datafiles +### Datasets, Dataverse Collections, and Datafiles Endpoints Extended The API endpoints for getting datasets, Dataverse collections, and datafiles have been extended to support the following optional 'returnOwners' query parameter. @@ -270,20 +270,20 @@ Including the parameter and setting it to true will add a hierarchy showing whic The API endpoint `api/datasets/{id}/metadata` has been changed to default to the latest version of the dataset that the user has access. -### Uningest/Reingest Options Available in the File Page Edit Menu +## 📖 Guides -New Uningest/Reingest options are available in the File Page Edit menu, allowing ingest errors to be cleared (by users who can published the associated dataset) -and (by superusers) for a successful ingest to be undone or retried (e.g. after a Dataverse version update or if ingest size limits are changed). -The /api/files//uningest api also now allows users who can publish the dataset to undo an ingest failure. +### New QA Guide -### Publication Status Facet Restored +A new QA Guide is intended mostly for the core development team but may be of interest to contributors. -In version 6.1, the publication status facet location was unintentionally moved to the bottom. In this version, we have restored the original order. +### Container Guide, Documentation for Faster Redeploy -### Permissions Required To Assign a Role Have Been Fixed +In the Container Guide, documentation for developers on how to quickly redeploy code has been added for Netbeans and improved for IntelliJ. -The permissions required to assign a role have been fixed. It is no longer possible to assign a role that includes permissions that the assigning user doesn't have. +Also in the context of containers, a new option to skip deployment has been added and the war file is now consistently named "dataverse.war" rather than having a version in the filename, such as "dataverse-6.1.war". This predictability makes tooling easier. -### New QA Guide +Finally, an option to create tabs in the guides using [Sphinx Tabs](https://sphinx-tabs.readthedocs.io) has been added. (You can see the tabs in action in the "dev usage" page of the Container Guide.) To continue building the guides, you will need to install this new dependency by re-running `pip install -r requirements.txt`. -A new QA Guide is intended mostly for the core development team but may be of interest to contributors. +### Evaluation Version Tutorial on the Containers Guide + +The Container Guide now containers a tutorial for running Dataverse in containers for demo or evaluation purposes: https://guides.dataverse.org/en/6.2/container \ No newline at end of file From ccc839d7ba492cf511c217765c0c5f60e1ea2f1e Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 26 Mar 2024 14:58:48 -0400 Subject: [PATCH 615/689] Spacing --- doc/release-notes/6.2-release-notes.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index f3f892ce4e2..7eb162c20bc 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -5,6 +5,7 @@ Please note: To read these instructions in full, please go to https://github.com This release brings new features, enhancements, and bug fixes to the Dataverse software. Thank you to all of the community members who contributed code, suggestions, bug reports, and other assistance across the project. + ## 💡Release highlights ### Return to Author Now Requires a Reason @@ -67,8 +68,6 @@ If neither setting exists rate limiting is disabled. In the following example, the default for tier `0` (guest users) is set to 10,000 calls per command per hour and tier `1` (authenticated users) is set to 20,000 calls per command per hour. Tiers not specified in this setting will default to `-1` (No Limit). I.e., -d "10000" is equivalent to -d "10000,-1,-1,..." - - ``` curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '[{ "tier": 0, @@ -150,6 +149,7 @@ New Uningest/Reingest options are available in the File Page Edit menu. Ingest e The /api/files//uningest api also now allows users who can publish the dataset to undo an ingest failure. + ## 🪲 Bugs ### Publication Status Facet Restored @@ -173,6 +173,7 @@ OAI-PMH error handling has been improved to display a machine-readable error in > - /oai?foo=bar will show "No argument 'verb' found" > - /oai?verb=foo&verb=bar will show "Verb must be singular, given: '[foo, bar]'" + ## 💾 Persistence ### Missing Database Constraints @@ -200,6 +201,7 @@ This release adds install script support for the new permissions model in Postgr Postgres 13 remains the version used with automated testing. + ## 🌐 API ### Listing Collection/Dataverse API @@ -270,6 +272,7 @@ Including the parameter and setting it to true will add a hierarchy showing whic The API endpoint `api/datasets/{id}/metadata` has been changed to default to the latest version of the dataset that the user has access. + ## 📖 Guides ### New QA Guide From bcddcf6a9f4ae4b4bd9048aa702019b5110264d5 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 26 Mar 2024 14:59:48 -0400 Subject: [PATCH 616/689] Bugs --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 7eb162c20bc..70b64c15550 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -150,7 +150,7 @@ New Uningest/Reingest options are available in the File Page Edit menu. Ingest e The /api/files//uningest api also now allows users who can publish the dataset to undo an ingest failure. -## 🪲 Bugs +## 🪲 Bug fixes ### Publication Status Facet Restored From 09d746d9cf2d9fa9558016a8f9194c8fdfa7d4bd Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Tue, 26 Mar 2024 16:20:43 -0400 Subject: [PATCH 617/689] 7424 Updated --- doc/release-notes/6.2-release-notes.md | 25 +++++++++++++++++++++++++ doc/release-notes/7424-mailsession.md | 24 ------------------------ 2 files changed, 25 insertions(+), 24 deletions(-) delete mode 100644 doc/release-notes/7424-mailsession.md diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 70b64c15550..519f342b2d0 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -111,6 +111,31 @@ curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndActi Hazelcast is configured in Payara and should not need any changes for this feature +## Simplified SMTP configuration + +With this release, we deprecate the usage of `asadmin create-javamail-resource` to configure Dataverse to send mail using your SMTP server and provide a simplified, standard alternative using JVM options or MicroProfile Config. + +At this point, no action is required if you want to keep your current configuration. +Warnings will show in your server logs to inform and remind you about the deprecation. +A future major release of Dataverse may remove this way of configuration. + +Please do take the opportunity to update your SMTP configuration. Details can be found in section of the Installation Guide starting with the [SMTP/Email Configuration](https://guides.dataverse.org/en/6.2/installation/config.html#smtp-email-configuration) section of the Installation Guide. + +Once reconfiguration is complete, you should remove legacy, unused config. First, run `asadmin delete-javamail-resource mail/notifyMailSession` as described in the [6.1 guides](https://guides.dataverse.org/en/6.1/installation/installation-main.html#mail-host-configuration-authentication). Then run `curl -X DELETE http://localhost:8080/api/admin/settings/:SystemEmail` as this database setting has been replace with `dataverse.mail.system-email` as described below. + +Please note: as there have been problems with email delivered to SPAM folders when the "From" within mail envelope and the mail session configuration didn't match (#4210), as of this version the sole source for the "From" address is the setting `dataverse.mail.system-email` once you migrate to the new way of configuration. + +List of options added: +> - dataverse.mail.system-email +> - dataverse.mail.mta.host +> - dataverse.mail.mta.port +> - dataverse.mail.mta.ssl.enable +> - dataverse.mail.mta.auth +> - dataverse.mail.mta.user +> - dataverse.mail.mta.password +> - dataverse.mail.mta.allow-utf8-addresses +> - Plus many more for advanced usage and special provider requirements. See [configuration guide for a full list](https://guides.dataverse.org/en/6.2/installation/config.html#dataverse-mail-mta). + ### Binder Redirect If your installation is configured to use Binder, you should remove the old "girder_ythub" tool and replace it with the tool described at https://github.com/IQSS/dataverse-binder-redirect diff --git a/doc/release-notes/7424-mailsession.md b/doc/release-notes/7424-mailsession.md deleted file mode 100644 index 67c876f7ad5..00000000000 --- a/doc/release-notes/7424-mailsession.md +++ /dev/null @@ -1,24 +0,0 @@ -## Simplified SMTP configuration - -With this release, we deprecate the usage of `asadmin create-javamail-resource` to configure Dataverse to send mail using your SMTP server and provide a simplified, standard alternative using JVM options or MicroProfile Config. - -At this point, no action is required if you want to keep your current configuration. -Warnings will show in your server logs to inform and remind you about the deprecation. -A future major release of Dataverse may remove this way of configuration. - -Please do take the opportunity to update your SMTP configuration. Details can be found in section of the Installation Guide starting with the [SMTP/Email Configuration](https://guides.dataverse.org/en/6.2/installation/config.html#smtp-email-configuration) section of the Installation Guide. - -Once reconfiguration is complete, you should remove legacy, unused config. First, run `asadmin delete-javamail-resource mail/notifyMailSession` as described in the [6.1 guides](https://guides.dataverse.org/en/6.1/installation/installation-main.html#mail-host-configuration-authentication). Then run `curl -X DELETE http://localhost:8080/api/admin/settings/:SystemEmail` as this database setting has been replace with `dataverse.mail.system-email` as described below. - -Please note: as there have been problems with email delivered to SPAM folders when the "From" within mail envelope and the mail session configuration didn't match (#4210), as of this version the sole source for the "From" address is the setting `dataverse.mail.system-email` once you migrate to the new way of configuration. - -List of options added: -- dataverse.mail.system-email -- dataverse.mail.mta.host -- dataverse.mail.mta.port -- dataverse.mail.mta.ssl.enable -- dataverse.mail.mta.auth -- dataverse.mail.mta.user -- dataverse.mail.mta.password -- dataverse.mail.mta.allow-utf8-addresses -- Plus many more for advanced usage and special provider requirements. See [configuration guide for a full list](https://guides.dataverse.org/en/6.2/installation/config.html#dataverse-mail-mta). \ No newline at end of file From 797bc38d4e43807dd16d62af27bca97fa8b137b5 Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Wed, 27 Mar 2024 09:54:43 +0100 Subject: [PATCH 618/689] moved indexed time by 3 hours to prevent false negatives in isIndexedVersion test --- .../java/edu/harvard/iq/dataverse/DatasetPage.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index 2e4cb56db48..98069b31c54 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -790,11 +790,15 @@ public boolean isIndexedVersion() { return isIndexedVersion = false; } // If this is the latest published version, we want to confirm that this - // version was successfully indexed after the last publication - + // version was successfully indexed after the last publication + // We add 3 hours to the indexed time to prevent false negatives + // when indexed time gets overwritten in finalizing the publication step + // by a value before the release time + final long duration = 3 * 60 * 60 * 1000; + final Timestamp movedIndexTime = new Timestamp(workingVersion.getDataset().getIndexTime().getTime() + duration); if (isThisLatestReleasedVersion()) { return isIndexedVersion = (workingVersion.getDataset().getIndexTime() != null) - && workingVersion.getDataset().getIndexTime().after(workingVersion.getReleaseTime()); + && movedIndexTime.after(workingVersion.getReleaseTime()); } // Drafts don't have the indextime stamps set/incremented when indexed, From 06c82a8cefc390bd0742e453bf2b82853bbeccbe Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Wed, 27 Mar 2024 09:09:10 -0400 Subject: [PATCH 619/689] Update doc/release-notes/6.2-release-notes.md Co-authored-by: Philip Durbin --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 519f342b2d0..3fdb25d8bfb 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -111,7 +111,7 @@ curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndActi Hazelcast is configured in Payara and should not need any changes for this feature -## Simplified SMTP configuration +### Simplified SMTP configuration With this release, we deprecate the usage of `asadmin create-javamail-resource` to configure Dataverse to send mail using your SMTP server and provide a simplified, standard alternative using JVM options or MicroProfile Config. From 3b7e729d5092ed43dfb12f4a6829ed87d2871852 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Wed, 27 Mar 2024 09:45:18 -0400 Subject: [PATCH 620/689] First round changes --- doc/release-notes/6.2-release-notes.md | 85 +++++++++++++++----------- 1 file changed, 49 insertions(+), 36 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 3fdb25d8bfb..5e211f96284 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -5,8 +5,15 @@ Please note: To read these instructions in full, please go to https://github.com This release brings new features, enhancements, and bug fixes to the Dataverse software. Thank you to all of the community members who contributed code, suggestions, bug reports, and other assistance across the project. +### Search and Facet by License -## 💡Release highlights +A new search facet called "License" has been added and will be displayed as long as there is more than one license in datasets and datafiles in browse/search results. This facet allow you to filter by license such as CC0, etc. + +Also, the Search API now handles license filtering using the `fq` parameter, for example : `/api/search?q=*&fq=license%3A%22CC0+1.0%22` for CC0 1.0. + +For more information, see [#10204](https://github.com/IQSS/dataverse/issues/10204). + +## 💡Release Highlights ### Return to Author Now Requires a Reason @@ -14,9 +21,10 @@ The Popup for returning to author now requires a reason that will be sent by ema Please note that you can still type a creative and meaningful comment such as "The author would like to modify his dataset", "Files are missing", "Nothing to report" or "A curation report with comments and suggestions/instructions will follow in another email" that suits your situation. + ### Support for Using Multiple PID Providers -This release adds support for using multiple PID (DOI, Handle, PermalLink) providers, multiple PID provider accounts +This release adds support for using multiple PID (DOI, Handle, PermaLink) providers, multiple PID provider accounts (managing a given protocol, authority,separator, shoulder combination), assigning PID provider accounts to specific collections, and supporting transferred PIDs (where a PID is managed by an account when it's authority, separator, and/or shoulder don't match the combination where the account can mint new PIDs). It also adds the ability for additional provider services beyond the existing @@ -26,33 +34,8 @@ These changes require per-provider settings rather than the global PID settings for installations using a single PID Provider account is provided, updating to use the new microprofile settings is highly recommended and will be required in a future version. -New microprofile settings (where * indicates a provider id indicating which provider the setting is for): +[New microprofile settings](#new-microprofile-settings) -> - dataverse.pid.providers -> - dataverse.pid.default-provider -> - dataverse.pid.*.type -> - dataverse.pid.*.label -> - dataverse.pid.*.authority -> - dataverse.pid.*.shoulder -> - dataverse.pid.*.identifier-generation-style -> - dataverse.pid.*.datafile-pid-format -> - dataverse.pid.*.managed-list -> - dataverse.pid.*.excluded-list -> - dataverse.pid.*.datacite.mds-api-url -> - dataverse.pid.*.datacite.rest-api-url -> - dataverse.pid.*.datacite.username -> - dataverse.pid.*.datacite.password -> - dataverse.pid.*.ezid.api-url -> - dataverse.pid.*.ezid.username -> - dataverse.pid.*.ezid.password -> - dataverse.pid.*.permalink.base-url -> - dataverse.pid.*.permalink.separator -> - dataverse.pid.*.handlenet.index -> - dataverse.pid.*.handlenet.independent-service -> - dataverse.pid.*.handlenet.auth-handle -> - dataverse.pid.*.handlenet.key.path -> - dataverse.pid.*.handlenet.key.passphrase -> - dataverse.spi.pidproviders.directory ### Rate Limiting Using JCache (With Hazelcast As Provided by Payara) @@ -125,7 +108,8 @@ Once reconfiguration is complete, you should remove legacy, unused config. First Please note: as there have been problems with email delivered to SPAM folders when the "From" within mail envelope and the mail session configuration didn't match (#4210), as of this version the sole source for the "From" address is the setting `dataverse.mail.system-email` once you migrate to the new way of configuration. -List of options added: +For a list of new settings see the section below: + > - dataverse.mail.system-email > - dataverse.mail.mta.host > - dataverse.mail.mta.port @@ -146,13 +130,7 @@ For more information, see [#10360](https://github.com/IQSS/dataverse/issues/1036 When a Dataverse installation is configured to use a metadata exporter for the [Croissant](https://github.com/mlcommons/croissant) format, the content of the JSON-LD in the **<head>** of dataset landing pages will be replaced with that format. However, both JSON-LD and Croissant will still be available for download from the dataset page and API. -### Search by License -A new search facet called "License" has been added and will be displayed as long as there is more than one license in datasets and datafiles in browse/search results. This facet allow you to filter by license such as CC0, etc. - -Also, the Search API now handles license filtering using the `fq` parameter, for example : `/api/search?q=*&fq=license%3A%22CC0+1.0%22` for CC0 1.0. - -For more information, see [#10204](https://github.com/IQSS/dataverse/issues/10204). ### Add .QPJ and .QMD Extensions to Shapefile Handling @@ -314,4 +292,39 @@ Finally, an option to create tabs in the guides using [Sphinx Tabs](https://sphi ### Evaluation Version Tutorial on the Containers Guide -The Container Guide now containers a tutorial for running Dataverse in containers for demo or evaluation purposes: https://guides.dataverse.org/en/6.2/container \ No newline at end of file +The Container Guide now containers a tutorial for running Dataverse in containers for demo or evaluation purposes: https://guides.dataverse.org/en/6.2/container + +*** + +# New Settings + + +### New microprofile settings : [Go back](#multiple-pid-sup) + +*The * indicates a provider id indicating which provider the setting is for* + +> - dataverse.pid.providers +> - dataverse.pid.default-provider +> - dataverse.pid.*.type +> - dataverse.pid.*.label +> - dataverse.pid.*.authority +> - dataverse.pid.*.shoulder +> - dataverse.pid.*.identifier-generation-style +> - dataverse.pid.*.datafile-pid-format +> - dataverse.pid.*.managed-list +> - dataverse.pid.*.excluded-list +> - dataverse.pid.*.datacite.mds-api-url +> - dataverse.pid.*.datacite.rest-api-url +> - dataverse.pid.*.datacite.username +> - dataverse.pid.*.datacite.password +> - dataverse.pid.*.ezid.api-url +> - dataverse.pid.*.ezid.username +> - dataverse.pid.*.ezid.password +> - dataverse.pid.*.permalink.base-url +> - dataverse.pid.*.permalink.separator +> - dataverse.pid.*.handlenet.index +> - dataverse.pid.*.handlenet.independent-service +> - dataverse.pid.*.handlenet.auth-handle +> - dataverse.pid.*.handlenet.key.path +> - dataverse.pid.*.handlenet.key.passphrase +> - dataverse.spi.pidproviders.directory \ No newline at end of file From b196f54f9d1be40b6c4c92b5b6c2263f779bc3a0 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 27 Mar 2024 11:40:22 -0400 Subject: [PATCH 621/689] add spaces before default and inherited in parens #10390 --- src/main/java/edu/harvard/iq/dataverse/DataversePage.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DataversePage.java b/src/main/java/edu/harvard/iq/dataverse/DataversePage.java index f35682b7bd0..a299bcd4227 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataversePage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataversePage.java @@ -1307,11 +1307,11 @@ public Set> getPidProviderOptions() { String label = null; if (this.dataverse.getOwner() != null && this.dataverse.getOwner().getEffectivePidGenerator()!= null) { PidProvider inheritedPidProvider = this.dataverse.getOwner().getEffectivePidGenerator(); - label = inheritedPidProvider.getLabel() + BundleUtil.getStringFromBundle("dataverse.inherited") + ": " + label = inheritedPidProvider.getLabel() + " " + BundleUtil.getStringFromBundle("dataverse.inherited") + ": " + inheritedPidProvider.getProtocol() + ":" + inheritedPidProvider.getAuthority() + inheritedPidProvider.getSeparator() + inheritedPidProvider.getShoulder(); } else { - label = defaultPidProvider.getLabel() + BundleUtil.getStringFromBundle("dataverse.default") + ": " + label = defaultPidProvider.getLabel() + " " + BundleUtil.getStringFromBundle("dataverse.default") + ": " + defaultPidProvider.getProtocol() + ":" + defaultPidProvider.getAuthority() + defaultPidProvider.getSeparator() + defaultPidProvider.getShoulder(); } From 0769ee9de49bffa8eca042e39b1668565330ae8f Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Wed, 27 Mar 2024 17:00:35 +0100 Subject: [PATCH 622/689] nullpointer fix --- .../edu/harvard/iq/dataverse/DatasetPage.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index 98069b31c54..6af1872b63b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -791,14 +791,16 @@ public boolean isIndexedVersion() { } // If this is the latest published version, we want to confirm that this // version was successfully indexed after the last publication - // We add 3 hours to the indexed time to prevent false negatives - // when indexed time gets overwritten in finalizing the publication step - // by a value before the release time - final long duration = 3 * 60 * 60 * 1000; - final Timestamp movedIndexTime = new Timestamp(workingVersion.getDataset().getIndexTime().getTime() + duration); if (isThisLatestReleasedVersion()) { - return isIndexedVersion = (workingVersion.getDataset().getIndexTime() != null) - && movedIndexTime.after(workingVersion.getReleaseTime()); + if (workingVersion.getDataset().getIndexTime() == null) { + return isIndexedVersion = false; + } + // We add 3 hours to the indexed time to prevent false negatives + // when indexed time gets overwritten in finalizing the publication step + // by a value before the release time + final long duration = 3 * 60 * 60 * 1000; + final Timestamp movedIndexTime = new Timestamp(workingVersion.getDataset().getIndexTime().getTime() + duration); + return isIndexedVersion = movedIndexTime.after(workingVersion.getReleaseTime()); } // Drafts don't have the indextime stamps set/incremented when indexed, From 1479403d9ac92145edbb806cb798f1ef52240219 Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Wed, 27 Mar 2024 17:18:40 +0100 Subject: [PATCH 623/689] quick info on the new metrics added for indexing --- doc/sphinx-guides/source/admin/monitoring.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/sphinx-guides/source/admin/monitoring.rst b/doc/sphinx-guides/source/admin/monitoring.rst index 04fba23a3e8..ef306c88c6f 100644 --- a/doc/sphinx-guides/source/admin/monitoring.rst +++ b/doc/sphinx-guides/source/admin/monitoring.rst @@ -150,3 +150,11 @@ Tips: - It's possible to view and act on **RDS Events** such as snapshots, parameter changes, etc. See `Working with Amazon RDS events `_ for details. - RDS monitoring is available via API and the ``aws`` command line tool. For example, see `Retrieving metrics with the Performance Insights API `_. - To play with monitoring RDS using a server configured by `dataverse-ansible `_ set ``use_rds`` to true to skip some steps that aren't necessary when using RDS. See also the :doc:`/developers/deployment` section of the Developer Guide. + +MicroProfile Metrics endpoint +----------------------------- + +Payara provides the metrics endpoint: _ +The metrics you can retrieve that way: +- `index_permit_wait_time_seconds_mean` displays how long does it take to receive a permit to index a dataset. +- `index_time_seconds` displays how long does it take to index a dataset. From ca5506a780389f70e9f18d37d4a081659929d156 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Wed, 27 Mar 2024 12:20:34 -0400 Subject: [PATCH 624/689] Corrections and instructions for update --- doc/release-notes/6.1-release-notes.md | 3 +- doc/release-notes/6.2-release-notes.md | 162 +++++++++++++++++++++---- 2 files changed, 138 insertions(+), 27 deletions(-) diff --git a/doc/release-notes/6.1-release-notes.md b/doc/release-notes/6.1-release-notes.md index 1279d09a023..dbeda726aad 100644 --- a/doc/release-notes/6.1-release-notes.md +++ b/doc/release-notes/6.1-release-notes.md @@ -247,7 +247,7 @@ Upgrading requires a maintenance window and downtime. Please plan ahead, create These instructions assume that you've already upgraded through all the 5.x releases and are now running Dataverse 6.0. -0\. These instructions assume that you are upgrading from 6.0. If you are running an earlier version, the only safe way to upgrade is to progress through the upgrades to all the releases in between before attempting the upgrade to 5.14. +0\. These instructions assume that you are upgrading from 6.0. If you are running an earlier version, the only safe way to upgrade is to progress through the upgrades to all the releases in between before attempting the upgrade to 6.1. If you are running Payara as a non-root user (and you should be!), **remember not to execute the commands below as root**. Use `sudo` to change to that user first. For example, `sudo -i -u dataverse` if `dataverse` is your dedicated application user. @@ -288,6 +288,7 @@ As noted above, deployment of the war file might take several minutes due a data 6a\. Update Citation Metadata Block (to make Alternative Title repeatable) +- `wget https://github.com/IQSS/dataverse/releases/download/v6.1/citation.tsv` - `curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/tab-separated-values" -X POST --upload-file scripts/api/data/metadatablocks/citation.tsv` 7\. Upate Solr schema.xml to allow multiple Alternative Titles to be used. See specific instructions below for those installations without custom metadata blocks (7a) and those with custom metadata blocks (7b). diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 5e211f96284..529b8e59075 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -17,16 +17,16 @@ For more information, see [#10204](https://github.com/IQSS/dataverse/issues/1020 ### Return to Author Now Requires a Reason -The Popup for returning to author now requires a reason that will be sent by email to the author. +The Popup for returning to author now allows to type in a message to explain the reasons of return and potential edits needed, that will be sent by email to the author. -Please note that you can still type a creative and meaningful comment such as "The author would like to modify his dataset", "Files are missing", "Nothing to report" or "A curation report with comments and suggestions/instructions will follow in another email" that suits your situation. +Please note that this note is mandatory, but that you can still type a creative and meaningful comment such as "The author would like to modify his dataset", "Files are missing", "Nothing to report" or "A curation report with comments and suggestions/instructions will follow in another email" that suits your situation. ### Support for Using Multiple PID Providers This release adds support for using multiple PID (DOI, Handle, PermaLink) providers, multiple PID provider accounts (managing a given protocol, authority,separator, shoulder combination), assigning PID provider accounts to specific collections, -and supporting transferred PIDs (where a PID is managed by an account when it's authority, separator, and/or shoulder don't match +and supporting transferred PIDs (where a PID is managed by an account when its authority, separator, and/or shoulder don't match the combination where the account can mint new PIDs). It also adds the ability for additional provider services beyond the existing DataCite, EZId, Handle, and PermaLink providers to be dynamically added as separate jar files. @@ -108,17 +108,9 @@ Once reconfiguration is complete, you should remove legacy, unused config. First Please note: as there have been problems with email delivered to SPAM folders when the "From" within mail envelope and the mail session configuration didn't match (#4210), as of this version the sole source for the "From" address is the setting `dataverse.mail.system-email` once you migrate to the new way of configuration. -For a list of new settings see the section below: +[New SMTP settings](#smtp-settings): + -> - dataverse.mail.system-email -> - dataverse.mail.mta.host -> - dataverse.mail.mta.port -> - dataverse.mail.mta.ssl.enable -> - dataverse.mail.mta.auth -> - dataverse.mail.mta.user -> - dataverse.mail.mta.password -> - dataverse.mail.mta.allow-utf8-addresses -> - Plus many more for advanced usage and special provider requirements. See [configuration guide for a full list](https://guides.dataverse.org/en/6.2/installation/config.html#dataverse-mail-mta). ### Binder Redirect @@ -144,7 +136,7 @@ Access API will be able to take advantage of Direct Download for tab. files save This behavior is controlled by the new setting **:StoreIngestedTabularFilesWithVarHeaders**. It is false by default, preserving the legacy behavior. When enabled, Dataverse will be able to handle both the newly ingested files, and any already-existing legacy files stored without these headers transparently to the user. E.g. the access API will continue delivering tab-delimited files **with** this header line, whether it needs to add it dynamically for the legacy files, or reading complete files directly from storage for the ones stored with it. -An API for converting existing legacy tabular files will be added separately. [this line will need to be changed if we have time to add said API before 6.2 is released]. [TODO] +We are planning to add an API for converting existing legacy tabular files in a future release. ### Uningest/Reingest Options Available in the File Page Edit Menu @@ -167,8 +159,6 @@ The permissions required to assign a role have been fixed. It is no longer possi The Geospatial metadata block fields for north and south were labeled incorrectly as ‘Longitudes,’ as reported on #5645. After updating to this version of Dataverse, users will need to update all the endpoints that used ‘northLongitude’ and ‘southLongitude’ to ‘northLatitude’ and ‘southLatitude,’ respectively. -TODO: Whoever puts the release notes together should make sure there is the standard note about updating the schema after upgrading. - ### OAI-PMH Error Handling Has Been Improved OAI-PMH error handling has been improved to display a machine-readable error in XML rather than a 500 error with no further information. @@ -176,6 +166,9 @@ OAI-PMH error handling has been improved to display a machine-readable error in > - /oai?foo=bar will show "No argument 'verb' found" > - /oai?verb=foo&verb=bar will show "Verb must be singular, given: '[foo, bar]'" +### Granting File Access Without Access Request + +A bug introduced with the guestboook-at-request, requests are not deleted when granted, they are now given the state granted. ## 💾 Persistence @@ -186,13 +179,12 @@ This release adds two missing database constraints that will assure that the ext ``` SELECT uri, count(*) FROM externalvocabularyvaluet group by uri; ``` -and +And: ``` SELECT spec, count(*) FROM oaiset group by spec; ``` -and then removing any duplicate rows (where count>1). +Then removing any duplicate rows (where count>1). -TODO: Whoever puts the release notes together should make sure there is the standard note about reloading metadata blocks for the citation, astrophysics, and biomedical blocks (plus any others from other PRs) after upgrading. ### Universe Field in Variablemetadata Table Changed @@ -278,10 +270,6 @@ The API endpoint `api/datasets/{id}/metadata` has been changed to default to the ## 📖 Guides -### New QA Guide - -A new QA Guide is intended mostly for the core development team but may be of interest to contributors. - ### Container Guide, Documentation for Faster Redeploy In the Container Guide, documentation for developers on how to quickly redeploy code has been added for Netbeans and improved for IntelliJ. @@ -294,12 +282,16 @@ Finally, an option to create tabs in the guides using [Sphinx Tabs](https://sphi The Container Guide now containers a tutorial for running Dataverse in containers for demo or evaluation purposes: https://guides.dataverse.org/en/6.2/container +### New QA Guide + +A new QA Guide is intended mostly for the core development team but may be of interest to contributors. + *** -# New Settings +# ⚙️ New Settings -### New microprofile settings : [Go back](#multiple-pid-sup) +### Microprofile settings : *The * indicates a provider id indicating which provider the setting is for* @@ -327,4 +319,122 @@ The Container Guide now containers a tutorial for running Dataverse in container > - dataverse.pid.*.handlenet.auth-handle > - dataverse.pid.*.handlenet.key.path > - dataverse.pid.*.handlenet.key.passphrase -> - dataverse.spi.pidproviders.directory \ No newline at end of file +> - dataverse.spi.pidproviders.directory + +[⬅️ Go back](#multiple-pid-sup) + +## SMTP Settings: + +> - dataverse.mail.system-email +> - dataverse.mail.mta.host +> - dataverse.mail.mta.port +> - dataverse.mail.mta.ssl.enable +> - dataverse.mail.mta.auth +> - dataverse.mail.mta.user +> - dataverse.mail.mta.password +> - dataverse.mail.mta.allow-utf8-addresses +> - Plus many more for advanced usage and special provider requirements. See [configuration guide for a full list](https://guides.dataverse.org/en/6.2/installation/config.html#dataverse-mail-mta). + +[⬅️ Go back](#simplified-smtp-configuration) + +## Upgrade instructions +Upgrading requires a maintenance window and downtime. Please plan ahead, create backups of your database, etc. + +These instructions assume that you've already upgraded through all the 5.x releases and are now running Dataverse 6.1. + +0\. These instructions assume that you are upgrading from the immediate previous version. If you are running an earlier version, the only safe way to upgrade is to progress through the upgrades to all the releases in between before attempting the upgrade to this version. + +If you are running Payara as a non-root user (and you should be!), **remember not to execute the commands below as root**. Use `sudo` to change to that user first. For example, `sudo -i -u dataverse` if `dataverse` is your dedicated application user. + +In the following commands we assume that Payara 6 is installed in `/usr/local/payara6`. If not, adjust as needed. + +`export PAYARA=/usr/local/payara6` + +(or `setenv PAYARA /usr/local/payara6` if you are using a `csh`-like shell) + +1\. Undeploy the previous version. + +- `$PAYARA/bin/asadmin undeploy dataverse-6.0` + +2\. Stop Payara and remove the generated directory + +- `service payara stop` +- `rm -rf $PAYARA/glassfish/domains/domain1/generated` + +3\. Start Payara + +- `service payara start` + +4\. Deploy this version. + +- `$PAYARA/bin/asadmin deploy dataverse-6.1.war` + +As noted above, deployment of the war file might take several minutes due a database migration script required for the new storage quotas feature. + +5\. Restart Payara + +- `service payara stop` +- `service payara start` + +6\. Update Geospatial Metadata Block. + + ``` + wget https://github.com/IQSS/dataverse/releases/download/v6.1/geospatial.tsv + + curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/tab-separated-values" -X POST --upload-file @geospatial.tsv + ``` + +6a\. Update Citation Metadata Block. + +``` +wget https://github.com/IQSS/dataverse/releases/download/v6.2/citation.tsv + +curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/tab-separated-values" -X POST --upload-file scripts/api/data/metadatablocks/citation.tsv +``` + +6b\. Update Astrophysics Metadata Block. + +``` +wget https://github.com/IQSS/dataverse/releases/download/v6.2/astrophysics.tsv + +curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/tab-separated-values" -X POST --upload-file scripts/api/data/metadatablocks/astrophysics.tsv +``` + +6c\. Update Biomedical Metadata Block (to make Alternative Title repeatable) + +``` +wget https://github.com/IQSS/dataverse/releases/download/v6.2/biomedical.tsv + +curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/tab-separated-values" -X POST --upload-file scripts/api/data/metadatablocks/biomedical.tsv +``` + +7\. Upate Solr schema.xml. See specific instructions below for those installations without custom metadata blocks (7a) and those with custom metadata blocks (7b). + +7a\. For installations without custom or experimental metadata blocks: + +- Stop Solr instance (usually `service solr stop`, depending on Solr installation/OS, see the [Installation Guide](https://guides.dataverse.org/en/5.14/installation/prerequisites.html#solr-init-script)) + +- Replace schema.xml + + - `cp /tmp/dvinstall/schema.xml /usr/local/solr/solr-9.3.0/server/solr/collection1/conf` + +- Start Solr instance (usually `service solr start`, depending on Solr/OS) + +7b\. For installations with custom or experimental metadata blocks: + +- Stop Solr instance (usually `service solr stop`, depending on Solr installation/OS, see the [Installation Guide](https://guides.dataverse.org/en/5.14/installation/prerequisites.html#solr-init-script)) + +- There are 2 ways to regenerate the schema: Either by collecting the output of the Dataverse schema API and feeding it to the `update-fields.sh` script that we supply, as in the example below (modify the command lines as needed): +``` + wget https://raw.githubusercontent.com/IQSS/dataverse/master/conf/solr/9.3.0/update-fields.sh + chmod +x update-fields.sh + curl "http://localhost:8080/api/admin/index/solr/schema" | ./update-fields.sh /usr/local/solr/solr-9.3.0/server/solr/collection1/conf/schema.xml +``` +OR, alternatively, you can edit the following line in your schema.xml by hand as follows (to indicate that alternative title is now `multiValued="true"`): +``` + +``` + +- Restart Solr instance (usually `service solr restart` depending on solr/OS) + +8\. Run ReExportAll to update dataset metadata exports. Follow the directions in the [Admin Guide](http://guides.dataverse.org/en/5.14/admin/metadataexport.html#batch-exports-through-the-api). \ No newline at end of file From db46350a0278a66f990272058c0c0bebb96cf1c2 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Wed, 27 Mar 2024 12:46:21 -0400 Subject: [PATCH 625/689] remove sql table create in favor of automatic table creation by JPA --- .../makedatacount/MakeDataCountProcessState.java | 1 + src/main/resources/db/migration/V6.1.0.8.sql | 10 ---------- 2 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 src/main/resources/db/migration/V6.1.0.8.sql diff --git a/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountProcessState.java b/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountProcessState.java index bde705abf44..9b6ce457de9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountProcessState.java +++ b/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountProcessState.java @@ -8,6 +8,7 @@ import java.util.Arrays; @Entity +@Table(indexes = {@Index(columnList="yearMonth")}) public class MakeDataCountProcessState implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/resources/db/migration/V6.1.0.8.sql b/src/main/resources/db/migration/V6.1.0.8.sql deleted file mode 100644 index b8f466c0b73..00000000000 --- a/src/main/resources/db/migration/V6.1.0.8.sql +++ /dev/null @@ -1,10 +0,0 @@ -CREATE TABLE IF NOT EXISTS makedatacountprocessstate ( - id SERIAL NOT NULL, - yearMonth VARCHAR(16) NOT NULL UNIQUE, - state ENUM('new', 'done', 'skip', 'processing', 'failed') NOT NULL, - state_change_time TIMESTAMP WITHOUT TIME ZONE DEFAULT now(), - PRIMARY KEY (ID) - ); - -CREATE INDEX IF NOT EXISTS INDEX_makedatacountprocessstate_yearMonth ON makedatacountprocessstate (yearMonth); - From bda01859486c9a6887f2bf054375aefc4f54e2f9 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Wed, 27 Mar 2024 13:16:21 -0400 Subject: [PATCH 626/689] Update with initial feedback --- doc/release-notes/6.2-release-notes.md | 126 +++++++++++++------------ 1 file changed, 66 insertions(+), 60 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 529b8e59075..8e5bf32b0e1 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -34,7 +34,7 @@ These changes require per-provider settings rather than the global PID settings for installations using a single PID Provider account is provided, updating to use the new microprofile settings is highly recommended and will be required in a future version. -[New microprofile settings](#new-microprofile-settings) +[New microprofile settings](#microprofile-settings) ### Rate Limiting Using JCache (With Hazelcast As Provided by Payara) @@ -110,8 +110,6 @@ Please note: as there have been problems with email delivered to SPAM folders wh [New SMTP settings](#smtp-settings): - - ### Binder Redirect If your installation is configured to use Binder, you should remove the old "girder_ythub" tool and replace it with the tool described at https://github.com/IQSS/dataverse-binder-redirect @@ -122,11 +120,13 @@ For more information, see [#10360](https://github.com/IQSS/dataverse/issues/1036 When a Dataverse installation is configured to use a metadata exporter for the [Croissant](https://github.com/mlcommons/croissant) format, the content of the JSON-LD in the **<head>** of dataset landing pages will be replaced with that format. However, both JSON-LD and Croissant will still be available for download from the dataset page and API. +### Harvesting Handle Missing Controlled Values +Allows datasets to be harvested with Controlled Vocabulary Values that existed in the originating Dataverse Project but are not in the harvesting Dataverse Project. For mor information view the changes to the endpoint [here](#harvesting-client-endpoint-extended). ### Add .QPJ and .QMD Extensions to Shapefile Handling -- Support for **.qpj** and **.qmd** files in shapefile uploads has been introduced, ensuring that these files are properly recognized and handled as part of geospatial datasets in Dataverse. +Support for **.qpj** and **.qmd** files in shapefile uploads has been introduced, ensuring that these files are properly recognized and handled as part of geospatial datasets in Dataverse. ### Ingested Tabular Data Files Can Be Stored Without the Variable Name Header @@ -144,14 +144,19 @@ New Uningest/Reingest options are available in the File Page Edit menu. Ingest e The /api/files//uningest api also now allows users who can publish the dataset to undo an ingest failure. +### Sphinx Guides now Support Markdown Format and Tabs +Our guides now support the Markdown format with the extension **.md**. Additionally, an option to create tabs in the guides using [Sphinx Tabs](https://sphinx-tabs.readthedocs.io) has been added. (You can see the tabs in action in the "dev usage" page of the Container Guide.) To continue building the guides, you will need to install this new dependency by re-running: +``` +pip install -r requirements.txt. +``` ## 🪲 Bug fixes ### Publication Status Facet Restored In version 6.1, the publication status facet location was unintentionally moved to the bottom. In this version, we have restored the original order. -### Permissions Required To Assign a Role Have Been Fixed +### Assign a Role With Higher Permissions Than Its Own Role Has Been Fixed The permissions required to assign a role have been fixed. It is no longer possible to assign a role that includes permissions that the assigning user doesn't have. @@ -267,7 +272,6 @@ Including the parameter and setting it to true will add a hierarchy showing whic The API endpoint `api/datasets/{id}/metadata` has been changed to default to the latest version of the dataset that the user has access. - ## 📖 Guides ### Container Guide, Documentation for Faster Redeploy @@ -276,68 +280,20 @@ In the Container Guide, documentation for developers on how to quickly redeploy Also in the context of containers, a new option to skip deployment has been added and the war file is now consistently named "dataverse.war" rather than having a version in the filename, such as "dataverse-6.1.war". This predictability makes tooling easier. -Finally, an option to create tabs in the guides using [Sphinx Tabs](https://sphinx-tabs.readthedocs.io) has been added. (You can see the tabs in action in the "dev usage" page of the Container Guide.) To continue building the guides, you will need to install this new dependency by re-running `pip install -r requirements.txt`. - ### Evaluation Version Tutorial on the Containers Guide -The Container Guide now containers a tutorial for running Dataverse in containers for demo or evaluation purposes: https://guides.dataverse.org/en/6.2/container +The Container Guide now containers a tutorial for running Dataverse in containers for demo or evaluation purposes: https://guides.dataverse.org/en/container ### New QA Guide -A new QA Guide is intended mostly for the core development team but may be of interest to contributors. +A new QA Guide is intended mostly for the core development team but may be of interest to contributors on: https://guides.dataverse.org/en/develop/qa -*** +## ⚠️ Breaking Changes https://guides.dataverse.org/en/en/develop/qa/index.html -# ⚙️ New Settings +To view a list of changes that can be impactful to your implementation please visit our detailed [list of changes to the API](https://guides.dataverse.org/en/develop/api/changelog.html). - -### Microprofile settings : - -*The * indicates a provider id indicating which provider the setting is for* - -> - dataverse.pid.providers -> - dataverse.pid.default-provider -> - dataverse.pid.*.type -> - dataverse.pid.*.label -> - dataverse.pid.*.authority -> - dataverse.pid.*.shoulder -> - dataverse.pid.*.identifier-generation-style -> - dataverse.pid.*.datafile-pid-format -> - dataverse.pid.*.managed-list -> - dataverse.pid.*.excluded-list -> - dataverse.pid.*.datacite.mds-api-url -> - dataverse.pid.*.datacite.rest-api-url -> - dataverse.pid.*.datacite.username -> - dataverse.pid.*.datacite.password -> - dataverse.pid.*.ezid.api-url -> - dataverse.pid.*.ezid.username -> - dataverse.pid.*.ezid.password -> - dataverse.pid.*.permalink.base-url -> - dataverse.pid.*.permalink.separator -> - dataverse.pid.*.handlenet.index -> - dataverse.pid.*.handlenet.independent-service -> - dataverse.pid.*.handlenet.auth-handle -> - dataverse.pid.*.handlenet.key.path -> - dataverse.pid.*.handlenet.key.passphrase -> - dataverse.spi.pidproviders.directory - -[⬅️ Go back](#multiple-pid-sup) - -## SMTP Settings: - -> - dataverse.mail.system-email -> - dataverse.mail.mta.host -> - dataverse.mail.mta.port -> - dataverse.mail.mta.ssl.enable -> - dataverse.mail.mta.auth -> - dataverse.mail.mta.user -> - dataverse.mail.mta.password -> - dataverse.mail.mta.allow-utf8-addresses -> - Plus many more for advanced usage and special provider requirements. See [configuration guide for a full list](https://guides.dataverse.org/en/6.2/installation/config.html#dataverse-mail-mta). -[⬅️ Go back](#simplified-smtp-configuration) - -## Upgrade instructions +## 💻 Upgrade instructions Upgrading requires a maintenance window and downtime. Please plan ahead, create backups of your database, etc. These instructions assume that you've already upgraded through all the 5.x releases and are now running Dataverse 6.1. @@ -437,4 +393,54 @@ OR, alternatively, you can edit the following line in your schema.xml by hand as - Restart Solr instance (usually `service solr restart` depending on solr/OS) -8\. Run ReExportAll to update dataset metadata exports. Follow the directions in the [Admin Guide](http://guides.dataverse.org/en/5.14/admin/metadataexport.html#batch-exports-through-the-api). \ No newline at end of file +8\. Run ReExportAll to update dataset metadata exports. Follow the directions in the [Admin Guide](http://guides.dataverse.org/en/5.14/admin/metadataexport.html#batch-exports-through-the-api). + +*** + +## ⚙️ New Settings + +### Microprofile settings + +*The * indicates a provider id indicating which provider the setting is for* + +> - dataverse.pid.providers +> - dataverse.pid.default-provider +> - dataverse.pid.*.type +> - dataverse.pid.*.label +> - dataverse.pid.*.authority +> - dataverse.pid.*.shoulder +> - dataverse.pid.*.identifier-generation-style +> - dataverse.pid.*.datafile-pid-format +> - dataverse.pid.*.managed-list +> - dataverse.pid.*.excluded-list +> - dataverse.pid.*.datacite.mds-api-url +> - dataverse.pid.*.datacite.rest-api-url +> - dataverse.pid.*.datacite.username +> - dataverse.pid.*.datacite.password +> - dataverse.pid.*.ezid.api-url +> - dataverse.pid.*.ezid.username +> - dataverse.pid.*.ezid.password +> - dataverse.pid.*.permalink.base-url +> - dataverse.pid.*.permalink.separator +> - dataverse.pid.*.handlenet.index +> - dataverse.pid.*.handlenet.independent-service +> - dataverse.pid.*.handlenet.auth-handle +> - dataverse.pid.*.handlenet.key.path +> - dataverse.pid.*.handlenet.key.passphrase +> - dataverse.spi.pidproviders.directory + +[⬅️ Go back](#multiple-pid-sup) + +## SMTP Settings: + +> - dataverse.mail.system-email +> - dataverse.mail.mta.host +> - dataverse.mail.mta.port +> - dataverse.mail.mta.ssl.enable +> - dataverse.mail.mta.auth +> - dataverse.mail.mta.user +> - dataverse.mail.mta.password +> - dataverse.mail.mta.allow-utf8-addresses +> - Plus many more for advanced usage and special provider requirements. See [configuration guide for a full list](https://guides.dataverse.org/en/6.2/installation/config.html#dataverse-mail-mta). + +[⬅️ Go back](#simplified-smtp-configuration) \ No newline at end of file From cf9b1bb3be9ecf080ff20b4dbb52c33790926a20 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Wed, 27 Mar 2024 13:40:55 -0400 Subject: [PATCH 627/689] Added a one sentence release note, plus another comment to the Dataset class where the redirect url is made. #10254 --- doc/release-notes/10254-fix-harvested-redirects.md | 1 + src/main/java/edu/harvard/iq/dataverse/Dataset.java | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 doc/release-notes/10254-fix-harvested-redirects.md diff --git a/doc/release-notes/10254-fix-harvested-redirects.md b/doc/release-notes/10254-fix-harvested-redirects.md new file mode 100644 index 00000000000..02ee5ddaf4d --- /dev/null +++ b/doc/release-notes/10254-fix-harvested-redirects.md @@ -0,0 +1 @@ +Redirects from search cards back to the original source for datasets harvested from "Generic OAI Archives", i.e. non-Dataverse OAI servers, have been fixed. diff --git a/src/main/java/edu/harvard/iq/dataverse/Dataset.java b/src/main/java/edu/harvard/iq/dataverse/Dataset.java index bb406c9f2fa..eaf406d01bf 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Dataset.java +++ b/src/main/java/edu/harvard/iq/dataverse/Dataset.java @@ -859,6 +859,14 @@ public String getRemoteArchiveURL() { String harvestingUrl = this.getHarvestedFrom().getHarvestingUrl(); String archivalUrl = this.getHarvestedFrom().getArchiveUrl(); if (!harvestingUrl.contains(archivalUrl)) { + // When a Harvesting Client is created, the “archive url” is set to + // just the host part of the OAI url automatically. + // For example, if the OAI url was "https://remote.edu/oai", + // the archive url will default to "https://remote.edu/". + // If this is no longer true, we know it means the admin + // went to the trouble of setting it to something else - + // so we should use this url for the redirects back to source, + // instead of the global id resolver. return archivalUrl + this.getAuthority() + "/" + this.getIdentifier(); } // ... if not, we'll redirect to the resolver for the global id: From 3d0b7f8c5cd4a92b258f6c56e933ffc4a6c5eccc Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 27 Mar 2024 14:11:39 -0400 Subject: [PATCH 628/689] bump version to 6.2 #10423 --- doc/sphinx-guides/source/conf.py | 4 ++-- doc/sphinx-guides/source/versions.rst | 3 ++- modules/dataverse-parent/pom.xml | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/sphinx-guides/source/conf.py b/doc/sphinx-guides/source/conf.py index 98d10526517..5a4b124cf2e 100755 --- a/doc/sphinx-guides/source/conf.py +++ b/doc/sphinx-guides/source/conf.py @@ -67,9 +67,9 @@ # built documents. # # The short X.Y version. -version = '6.1' +version = '6.2' # The full version, including alpha/beta/rc tags. -release = '6.1' +release = '6.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/sphinx-guides/source/versions.rst b/doc/sphinx-guides/source/versions.rst index 2cf7f46dc5e..d76f9a889cb 100755 --- a/doc/sphinx-guides/source/versions.rst +++ b/doc/sphinx-guides/source/versions.rst @@ -7,7 +7,8 @@ Dataverse Software Documentation Versions This list provides a way to refer to the documentation for previous and future versions of the Dataverse Software. In order to learn more about the updates delivered from one version to another, visit the `Releases `__ page in our GitHub repo. - pre-release `HTML (not final!) `__ and `PDF (experimental!) `__ built from the :doc:`develop ` branch :doc:`(how to contribute!) ` -- 6.1 +- 6.2 +- `6.1 `__ - `6.0 `__ - `5.14 `__ - `5.13 `__ diff --git a/modules/dataverse-parent/pom.xml b/modules/dataverse-parent/pom.xml index 1a538905a8d..612902b47a4 100644 --- a/modules/dataverse-parent/pom.xml +++ b/modules/dataverse-parent/pom.xml @@ -131,7 +131,7 @@ - 6.1 + 6.2 17 UTF-8 From b974f14e45fc2dfc3e2db0dbc2fd8724775ef0ab Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Wed, 27 Mar 2024 14:32:26 -0400 Subject: [PATCH 629/689] review comments --- .../java/edu/harvard/iq/dataverse/api/MakeDataCountApi.java | 4 ++-- .../dataverse/makedatacount/MakeDataCountProcessState.java | 6 +++--- .../edu/harvard/iq/dataverse/api/MakeDataCountApiIT.java | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/MakeDataCountApi.java b/src/main/java/edu/harvard/iq/dataverse/api/MakeDataCountApi.java index d94ab42c516..1f2f1039327 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/MakeDataCountApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/MakeDataCountApi.java @@ -219,7 +219,7 @@ public Response getProcessingState(@PathParam("yearMonth") String yearMonth) { JsonObjectBuilder output = Json.createObjectBuilder(); output.add("yearMonth", mdcps.getYearMonth()); output.add("state", mdcps.getState().name()); - output.add("state-change-timestamp", mdcps.getStateChangeTime().toString()); + output.add("stateChangeTimestamp", mdcps.getStateChangeTime().toString()); return ok(output); } else { return error(Status.NOT_FOUND, "Could not find an existing process state for " + yearMonth); @@ -239,7 +239,7 @@ public Response updateProcessingState(@PathParam("yearMonth") String yearMonth, JsonObjectBuilder output = Json.createObjectBuilder(); output.add("yearMonth", mdcps.getYearMonth()); output.add("state", mdcps.getState().name()); - output.add("state-change-timestamp", mdcps.getStateChangeTime().toString()); + output.add("stateChangeTimestamp", mdcps.getStateChangeTime().toString()); return ok(output); } diff --git a/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountProcessState.java b/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountProcessState.java index 9b6ce457de9..2241a2c4ca8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountProcessState.java +++ b/src/main/java/edu/harvard/iq/dataverse/makedatacount/MakeDataCountProcessState.java @@ -41,7 +41,7 @@ public String toString() { @Column(nullable = false) private MDCProcessState state; @Column(nullable = true) - private Timestamp state_change_time; + private Timestamp stateChangeTimestamp; public MakeDataCountProcessState() { } public MakeDataCountProcessState (String yearMonth, String state) { @@ -61,7 +61,7 @@ public String getYearMonth() { } public void setState(MDCProcessState state) { this.state = state; - this.state_change_time = Timestamp.from(Instant.now()); + this.stateChangeTimestamp = Timestamp.from(Instant.now()); } public void setState(String state) throws IllegalArgumentException { setState(MDCProcessState.fromString(state)); @@ -70,6 +70,6 @@ public MDCProcessState getState() { return this.state; } public Timestamp getStateChangeTime() { - return state_change_time; + return stateChangeTimestamp; } } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/MakeDataCountApiIT.java b/src/test/java/edu/harvard/iq/dataverse/api/MakeDataCountApiIT.java index 64856461703..69bdd8ee515 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/MakeDataCountApiIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/MakeDataCountApiIT.java @@ -195,14 +195,14 @@ public void testGetUpdateDeleteProcessingState() { stateJson.prettyPrint(); String state1 = stateJson.getString("data.state"); assertThat(state1, Matchers.equalTo(MakeDataCountProcessState.MDCProcessState.PROCESSING.name())); - String updateTimestamp1 = stateJson.getString("data.state-change-timestamp"); + String updateTimestamp1 = stateJson.getString("data.stateChangeTimestamp"); updateState = UtilIT.makeDataCountUpdateProcessingState(yearMonth, MakeDataCountProcessState.MDCProcessState.DONE.toString()); updateState.then().assertThat().statusCode(OK.getStatusCode()); stateJson = JsonPath.from(updateState.body().asString()); stateJson.prettyPrint(); String state2 = stateJson.getString("data.state"); - String updateTimestamp2 = stateJson.getString("data.state-change-timestamp"); + String updateTimestamp2 = stateJson.getString("data.stateChangeTimestamp"); assertThat(state2, Matchers.equalTo(MakeDataCountProcessState.MDCProcessState.DONE.name())); assertThat(updateTimestamp2, Matchers.is(Matchers.greaterThan(updateTimestamp1))); From 00c9807840c76b31879f0d6f725d931dbbdd0fee Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Wed, 27 Mar 2024 14:41:54 -0400 Subject: [PATCH 630/689] Link to the guide code removed --- doc/release-notes/6.2-release-notes.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 8e5bf32b0e1..c1f23c1e703 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -51,6 +51,8 @@ If neither setting exists rate limiting is disabled. In the following example, the default for tier `0` (guest users) is set to 10,000 calls per command per hour and tier `1` (authenticated users) is set to 20,000 calls per command per hour. Tiers not specified in this setting will default to `-1` (No Limit). I.e., -d "10000" is equivalent to -d "10000,-1,-1,..." +For more details check the detailed guide on [this link](https://guides.dataverse.org/en/6.2/installation/config.html#configure-your-dataverse-installation-to-use-jcache-with-hazelcast-as-provided-by-payara-for-rate-limiting). + ``` curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '[{ "tier": 0, @@ -104,7 +106,7 @@ A future major release of Dataverse may remove this way of configuration. Please do take the opportunity to update your SMTP configuration. Details can be found in section of the Installation Guide starting with the [SMTP/Email Configuration](https://guides.dataverse.org/en/6.2/installation/config.html#smtp-email-configuration) section of the Installation Guide. -Once reconfiguration is complete, you should remove legacy, unused config. First, run `asadmin delete-javamail-resource mail/notifyMailSession` as described in the [6.1 guides](https://guides.dataverse.org/en/6.1/installation/installation-main.html#mail-host-configuration-authentication). Then run `curl -X DELETE http://localhost:8080/api/admin/settings/:SystemEmail` as this database setting has been replace with `dataverse.mail.system-email` as described below. +Once reconfiguration is complete, you should remove legacy, unused config. First, run `asadmin delete-javamail-resource mail/notifyMailSession` as described in the [6.1 guides](https://guides.dataverse.org/en/6.2/installation/installation-main.html#mail-host-configuration-authentication). Then run `curl -X DELETE http://localhost:8080/api/admin/settings/:SystemEmail` as this database setting has been replace with `dataverse.mail.system-email` as described below. Please note: as there have been problems with email delivered to SPAM folders when the "From" within mail envelope and the mail session configuration didn't match (#4210), as of this version the sole source for the "From" address is the setting `dataverse.mail.system-email` once you migrate to the new way of configuration. @@ -282,15 +284,15 @@ Also in the context of containers, a new option to skip deployment has been adde ### Evaluation Version Tutorial on the Containers Guide -The Container Guide now containers a tutorial for running Dataverse in containers for demo or evaluation purposes: https://guides.dataverse.org/en/container +The Container Guide now containers a tutorial for running Dataverse in containers for demo or evaluation purposes: https://guides.dataverse.org/en/6.2/container ### New QA Guide -A new QA Guide is intended mostly for the core development team but may be of interest to contributors on: https://guides.dataverse.org/en/develop/qa +A new QA Guide is intended mostly for the core development team but may be of interest to contributors on: https://guides.dataverse.org/en/6.2/develop/qa ## ⚠️ Breaking Changes https://guides.dataverse.org/en/en/develop/qa/index.html -To view a list of changes that can be impactful to your implementation please visit our detailed [list of changes to the API](https://guides.dataverse.org/en/develop/api/changelog.html). +To view a list of changes that can be impactful to your implementation please visit our detailed [list of changes to the API](https://guides.dataverse.org/en/6.2/develop/api/changelog.html). ## 💻 Upgrade instructions @@ -368,7 +370,7 @@ curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/ta 7a\. For installations without custom or experimental metadata blocks: -- Stop Solr instance (usually `service solr stop`, depending on Solr installation/OS, see the [Installation Guide](https://guides.dataverse.org/en/5.14/installation/prerequisites.html#solr-init-script)) +- Stop Solr instance (usually `service solr stop`, depending on Solr installation/OS, see the [Installation Guide](https://guides.dataverse.org/en/6.2/installation/prerequisites.html#solr-init-script)) - Replace schema.xml @@ -378,7 +380,7 @@ curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/ta 7b\. For installations with custom or experimental metadata blocks: -- Stop Solr instance (usually `service solr stop`, depending on Solr installation/OS, see the [Installation Guide](https://guides.dataverse.org/en/5.14/installation/prerequisites.html#solr-init-script)) +- Stop Solr instance (usually `service solr stop`, depending on Solr installation/OS, see the [Installation Guide](https://guides.dataverse.org/en/6.2/installation/prerequisites.html#solr-init-script)) - There are 2 ways to regenerate the schema: Either by collecting the output of the Dataverse schema API and feeding it to the `update-fields.sh` script that we supply, as in the example below (modify the command lines as needed): ``` @@ -393,7 +395,7 @@ OR, alternatively, you can edit the following line in your schema.xml by hand as - Restart Solr instance (usually `service solr restart` depending on solr/OS) -8\. Run ReExportAll to update dataset metadata exports. Follow the directions in the [Admin Guide](http://guides.dataverse.org/en/5.14/admin/metadataexport.html#batch-exports-through-the-api). +8\. Run ReExportAll to update dataset metadata exports. Follow the directions in the [Admin Guide](http://guides.dataverse.org/en/6.2/admin/metadataexport.html#batch-exports-through-the-api). *** @@ -428,6 +430,7 @@ OR, alternatively, you can edit the following line in your schema.xml by hand as > - dataverse.pid.*.handlenet.key.path > - dataverse.pid.*.handlenet.key.passphrase > - dataverse.spi.pidproviders.directory +> - dataverse.solr.concurrency.max-async-indexes [⬅️ Go back](#multiple-pid-sup) From 117ee0f21f380f7b5cc12b914095dba5f0b4160a Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 27 Mar 2024 14:45:31 -0400 Subject: [PATCH 631/689] shorten heading, other tweaks #9356 --- .../source/installation/config.rst | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 1f56fbdb848..ff786e900cc 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1427,8 +1427,8 @@ Before being moved there, .. _cache-rate-limiting: -Configure Your Dataverse Installation to Use JCache (with Hazelcast as Provided by Payara) for Rate Limiting ------------------------------------------------------------------------------------------------------------- +Rate Limiting +------------- Rate limiting has been added to prevent users from over taxing the system either deliberately or by runaway automated processes. Rate limiting can be configured on a tier level with tier 0 being reserved for guest users and tiers 1-any for authenticated users. @@ -1447,7 +1447,7 @@ Note: If either of these settings exist in the database rate limiting will be en - :RateLimitingCapacityByTierAndAction is a JSON object specifying the rate by tier and a list of actions (commands). This allows for more control over the rate limit of individual API command calls. In the following example, calls made by a guest user (tier 0) for API GetLatestPublishedDatasetVersionCommand is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. -:download:`rate-limit-actions.json ` Example json for RateLimitingCapacityByTierAndAction +:download:`rate-limit-actions.json ` Example JSON for RateLimitingCapacityByTierAndAction .. code-block:: bash @@ -4714,17 +4714,22 @@ The setting is ``false`` by default, preserving the legacy behavior. Number of calls allowed per hour if the specific command is not configured. The values represent the number of calls per hour per user for tiers 0,1,... A value of -1 can be used to signify no rate limit. Also, by default, a tier not defined would receive a default of no limit. +See also :ref:`cache-rate-limiting`. + :RateLimitingCapacityByTierAndAction ++++++++++++++++++++++++++++++++++++ JSON object specifying the rate by tier and a list of actions (commands). This allows for more control over the rate limit of individual API command calls. In the following example, calls made by a guest user (tier 0) for API GetLatestPublishedDatasetVersionCommand is further limited to only 10 calls per hour, while an authenticated user (tier 1) will be able to make 30 calls per hour to the same API. -{"rateLimits":[ -{"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, -{"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, -{"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}]} +.. code-block:: shell + {"rateLimits":[ + {"tier": 0, "limitPerHour": 10, "actions": ["GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand"]}, + {"tier": 0, "limitPerHour": 1, "actions": ["CreateGuestbookResponseCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]}, + {"tier": 1, "limitPerHour": 30, "actions": ["CreateGuestbookResponseCommand", "GetLatestPublishedDatasetVersionCommand", "GetPrivateUrlCommand", "GetDatasetCommand", "GetLatestAccessibleDatasetVersionCommand", "UpdateDatasetVersionCommand", "DestroyDatasetCommand", "DeleteDataFileCommand", "FinalizeDatasetPublicationCommand", "PublishDatasetCommand"]} + ]} +See also :ref:`cache-rate-limiting`. .. _supported MicroProfile Config API source: https://docs.payara.fish/community/docs/Technical%20Documentation/MicroProfile/Config/Overview.html .. _password alias: https://docs.payara.fish/community/docs/Technical%20Documentation/Payara%20Server%20Documentation/Server%20Configuration%20And%20Management/Configuration%20Options/Password%20Aliases.html From 3556555f718ae64322ac33517130f393090ae357 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Wed, 27 Mar 2024 14:54:06 -0400 Subject: [PATCH 632/689] Header placed in the right place --- doc/release-notes/6.2-release-notes.md | 47 ++------------------------ 1 file changed, 3 insertions(+), 44 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index c1f23c1e703..f5343631b8e 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -5,6 +5,8 @@ Please note: To read these instructions in full, please go to https://github.com This release brings new features, enhancements, and bug fixes to the Dataverse software. Thank you to all of the community members who contributed code, suggestions, bug reports, and other assistance across the project. +## 💡Release Highlights + ### Search and Facet by License A new search facet called "License" has been added and will be displayed as long as there is more than one license in datasets and datafiles in browse/search results. This facet allow you to filter by license such as CC0, etc. @@ -13,8 +15,6 @@ Also, the Search API now handles license filtering using the `fq` parameter, for For more information, see [#10204](https://github.com/IQSS/dataverse/issues/10204). -## 💡Release Highlights - ### Return to Author Now Requires a Reason The Popup for returning to author now allows to type in a message to explain the reasons of return and potential edits needed, that will be sent by email to the author. @@ -53,47 +53,6 @@ Tiers not specified in this setting will default to `-1` (No Limit). I.e., -d "1 For more details check the detailed guide on [this link](https://guides.dataverse.org/en/6.2/installation/config.html#configure-your-dataverse-installation-to-use-jcache-with-hazelcast-as-provided-by-payara-for-rate-limiting). -``` -curl http://localhost:8080/api/admin/settings/:RateLimitingCapacityByTierAndAction -X PUT -d '[{ - "tier": 0, - "limitPerHour": 10, - "actions": [ - "GetLatestPublishedDatasetVersionCommand", - "GetPrivateUrlCommand", - "GetDatasetCommand", - "GetLatestAccessibleDatasetVersionCommand" - ] -}, -{ - "tier": 0, - "limitPerHour": 1, - "actions": [ - "CreateGuestbookResponseCommand", - "UpdateDatasetVersionCommand", - "DestroyDatasetCommand", - "DeleteDataFileCommand", - "FinalizeDatasetPublicationCommand", - "PublishDatasetCommand" - ] -}, -{ - "tier": 1, - "limitPerHour": 30, - "actions": [ - "CreateGuestbookResponseCommand", - "GetLatestPublishedDatasetVersionCommand", - "GetPrivateUrlCommand", - "GetDatasetCommand", - "GetLatestAccessibleDatasetVersionCommand", - "UpdateDatasetVersionCommand", - "DestroyDatasetCommand", - "DeleteDataFileCommand", - "FinalizeDatasetPublicationCommand", - "PublishDatasetCommand" - ] -}]' -``` - Hazelcast is configured in Payara and should not need any changes for this feature ### Simplified SMTP configuration @@ -184,7 +143,7 @@ A bug introduced with the guestboook-at-request, requests are not deleted when g This release adds two missing database constraints that will assure that the externalvocabularyvalue table only has one entry for each uri and that the oaiset table only has one set for each spec. (In the very unlikely case that your existing database has duplicate entries now, install would fail. This can be checked by running ``` -SELECT uri, count(*) FROM externalvocabularyvaluet group by uri; +SELECT uri, count(*) FROM externalvocabularyvalue group by uri; ``` And: ``` From 92c27e7fedfbf4d8918e8f2d8704e2eb20a9ccd0 Mon Sep 17 00:00:00 2001 From: landreev Date: Wed, 27 Mar 2024 15:00:45 -0400 Subject: [PATCH 633/689] Update 6.2-release-notes.md changes to the upgrade instructions order, because of the breaking change in solr schema. --- doc/release-notes/6.2-release-notes.md | 49 +++++++++++++------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 8e5bf32b0e1..bc1314d6728 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -308,31 +308,44 @@ In the following commands we assume that Payara 6 is installed in `/usr/local/pa (or `setenv PAYARA /usr/local/payara6` if you are using a `csh`-like shell) -1\. Undeploy the previous version. +1\. Usually, when a Solr schema update is released, we recommend deploying the new version of Dataverse, then updating the `schema.xml` on the solr side. With 6.2, we recommend to install the base schema first. Without it Dataverse 6.2 is not going to be able to show any results after the initial deployment. If your instance is using any custom metadata blocks, you will need to further modify the schema, see the laset step of this instruction (step 8). + +- Stop Solr instance (usually `service solr stop`, depending on Solr installation/OS, see the [Installation Guide](https://guides.dataverse.org/en/5.14/installation/prerequisites.html#solr-init-script)) + +- Replace schema.xml + + - `cd /usr/local/solr/solr-9.3.0/server/solr/collection1/conf` + - `wget https://raw.githubusercontent.com/IQSS/dataverse/master/conf/solr/9.3.0/schema.xml` + +- Start Solr instance (usually `service solr start`, depending on Solr/OS) + +2\. Undeploy the previous version. - `$PAYARA/bin/asadmin undeploy dataverse-6.0` -2\. Stop Payara and remove the generated directory +3\. Stop Payara and remove the generated directory - `service payara stop` - `rm -rf $PAYARA/glassfish/domains/domain1/generated` -3\. Start Payara +4\. Start Payara - `service payara start` -4\. Deploy this version. +5\. Deploy this version. - `$PAYARA/bin/asadmin deploy dataverse-6.1.war` As noted above, deployment of the war file might take several minutes due a database migration script required for the new storage quotas feature. -5\. Restart Payara +6\. Restart Payara - `service payara stop` - `service payara start` -6\. Update Geospatial Metadata Block. +7\. Update the standard Metadata Blocks: + +7a\. Update Geospatial Metadata Block. ``` wget https://github.com/IQSS/dataverse/releases/download/v6.1/geospatial.tsv @@ -340,7 +353,7 @@ As noted above, deployment of the war file might take several minutes due a data curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/tab-separated-values" -X POST --upload-file @geospatial.tsv ``` -6a\. Update Citation Metadata Block. +7b\. Update Citation Metadata Block. ``` wget https://github.com/IQSS/dataverse/releases/download/v6.2/citation.tsv @@ -348,7 +361,7 @@ wget https://github.com/IQSS/dataverse/releases/download/v6.2/citation.tsv curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/tab-separated-values" -X POST --upload-file scripts/api/data/metadatablocks/citation.tsv ``` -6b\. Update Astrophysics Metadata Block. +7c\. Update Astrophysics Metadata Block. ``` wget https://github.com/IQSS/dataverse/releases/download/v6.2/astrophysics.tsv @@ -356,7 +369,7 @@ wget https://github.com/IQSS/dataverse/releases/download/v6.2/astrophysics.tsv curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/tab-separated-values" -X POST --upload-file scripts/api/data/metadatablocks/astrophysics.tsv ``` -6c\. Update Biomedical Metadata Block (to make Alternative Title repeatable) +7d\. Update Biomedical Metadata Block (to make Alternative Title repeatable) ``` wget https://github.com/IQSS/dataverse/releases/download/v6.2/biomedical.tsv @@ -364,19 +377,7 @@ wget https://github.com/IQSS/dataverse/releases/download/v6.2/biomedical.tsv curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/tab-separated-values" -X POST --upload-file scripts/api/data/metadatablocks/biomedical.tsv ``` -7\. Upate Solr schema.xml. See specific instructions below for those installations without custom metadata blocks (7a) and those with custom metadata blocks (7b). - -7a\. For installations without custom or experimental metadata blocks: - -- Stop Solr instance (usually `service solr stop`, depending on Solr installation/OS, see the [Installation Guide](https://guides.dataverse.org/en/5.14/installation/prerequisites.html#solr-init-script)) - -- Replace schema.xml - - - `cp /tmp/dvinstall/schema.xml /usr/local/solr/solr-9.3.0/server/solr/collection1/conf` - -- Start Solr instance (usually `service solr start`, depending on Solr/OS) - -7b\. For installations with custom or experimental metadata blocks: +8\. For installations with custom or experimental metadata blocks: - Stop Solr instance (usually `service solr stop`, depending on Solr installation/OS, see the [Installation Guide](https://guides.dataverse.org/en/5.14/installation/prerequisites.html#solr-init-script)) @@ -393,7 +394,7 @@ OR, alternatively, you can edit the following line in your schema.xml by hand as - Restart Solr instance (usually `service solr restart` depending on solr/OS) -8\. Run ReExportAll to update dataset metadata exports. Follow the directions in the [Admin Guide](http://guides.dataverse.org/en/5.14/admin/metadataexport.html#batch-exports-through-the-api). +9\. Run ReExportAll to update dataset metadata exports. Follow the directions in the [Admin Guide](http://guides.dataverse.org/en/5.14/admin/metadataexport.html#batch-exports-through-the-api). *** @@ -443,4 +444,4 @@ OR, alternatively, you can edit the following line in your schema.xml by hand as > - dataverse.mail.mta.allow-utf8-addresses > - Plus many more for advanced usage and special provider requirements. See [configuration guide for a full list](https://guides.dataverse.org/en/6.2/installation/config.html#dataverse-mail-mta). -[⬅️ Go back](#simplified-smtp-configuration) \ No newline at end of file +[⬅️ Go back](#simplified-smtp-configuration) From 82e35b842958e7a272748c17a16bf8c3935124f7 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Wed, 27 Mar 2024 15:01:20 -0400 Subject: [PATCH 634/689] adding docs --- .../source/developers/make-data-count.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/doc/sphinx-guides/source/developers/make-data-count.rst b/doc/sphinx-guides/source/developers/make-data-count.rst index 8eaa5c0d7f8..d64fff9ccc7 100644 --- a/doc/sphinx-guides/source/developers/make-data-count.rst +++ b/doc/sphinx-guides/source/developers/make-data-count.rst @@ -88,6 +88,23 @@ To read more about the Make Data Count api, see https://github.com/datacite/sash You can compare the MDC metrics display with the Dataverse installation's original by toggling the ``:DisplayMDCMetrics`` setting (true by default to display MDC metrics). +New Make Data Count Processing for Your Dataverse Installation +-------------------------------------------------------------- + +A new script (release date TBD) will be available for processing archived Dataverse log files. Monthly logs that are zipped, TARed, and copied to an archive can be processed by this script running nightly or weekly. +The script will keep track of the state of each tar file they are processed. Through the following APIs the state of each file can be checked or modified. +Setting the state to 'Skip' will prevent the file from being processed if the developer needs to analyze the contents. +'Failed' files will be re-tried in a later run. +'Done' files are successful and will be ignored going forward. +The file(s) currently being processed will have the state 'Processing'. +The states are [NEW, DONE, SKIP, PROCESSING, FAILED] +The script will process the newest set of log files (merging files from multiple nodes) and calling counter_processor. +The Admin APIs to manage the states include a GET, POST, and DELETE(For Testing). +yearMonth must be in the format yyyymm or yyyymmdd +``curl -X GET http://localhost:8080/api/admin/{yearMonth}/processingState`` +``curl -X POST http://localhost:8080/api/admin/{yearMonth}/processingState?state=done`` +``curl -X DELETE http://localhost:8080/api/admin/{yearMonth}/processingState`` + Resources --------- From aeee15431df48cdb403c84fbc8bea3e2e14ea2c0 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Wed, 27 Mar 2024 15:37:41 -0400 Subject: [PATCH 635/689] Rate limit URL change --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 83f4572f9a3..54e968684b5 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -51,7 +51,7 @@ If neither setting exists rate limiting is disabled. In the following example, the default for tier `0` (guest users) is set to 10,000 calls per command per hour and tier `1` (authenticated users) is set to 20,000 calls per command per hour. Tiers not specified in this setting will default to `-1` (No Limit). I.e., -d "10000" is equivalent to -d "10000,-1,-1,..." -For more details check the detailed guide on [this link](https://guides.dataverse.org/en/6.2/installation/config.html#configure-your-dataverse-installation-to-use-jcache-with-hazelcast-as-provided-by-payara-for-rate-limiting). +For more details check the detailed guide on [this link](https://guides.dataverse.org/en/6.2/installation/config.html#rate-limiting). Hazelcast is configured in Payara and should not need any changes for this feature From dd0587159cc99f14d53b9dae98a850685d9037d3 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Wed, 27 Mar 2024 15:42:50 -0400 Subject: [PATCH 636/689] Collapsed scripts --- doc/release-notes/6.2-release-notes.md | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 54e968684b5..cce5db596c7 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -304,35 +304,21 @@ As noted above, deployment of the war file might take several minutes due a data - `service payara stop` - `service payara start` -7\. Update the standard Metadata Blocks: - -7a\. Update Geospatial Metadata Block. +7\. Update the following Metadata Blocks: ``` wget https://github.com/IQSS/dataverse/releases/download/v6.1/geospatial.tsv curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/tab-separated-values" -X POST --upload-file @geospatial.tsv - ``` - -7b\. Update Citation Metadata Block. -``` wget https://github.com/IQSS/dataverse/releases/download/v6.2/citation.tsv curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/tab-separated-values" -X POST --upload-file scripts/api/data/metadatablocks/citation.tsv -``` -7c\. Update Astrophysics Metadata Block. - -``` wget https://github.com/IQSS/dataverse/releases/download/v6.2/astrophysics.tsv curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/tab-separated-values" -X POST --upload-file scripts/api/data/metadatablocks/astrophysics.tsv -``` -7d\. Update Biomedical Metadata Block (to make Alternative Title repeatable) - -``` wget https://github.com/IQSS/dataverse/releases/download/v6.2/biomedical.tsv curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/tab-separated-values" -X POST --upload-file scripts/api/data/metadatablocks/biomedical.tsv From da397eac9c9bb89323928072603f5b7bdb5a3179 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Wed, 27 Mar 2024 15:44:32 -0400 Subject: [PATCH 637/689] Incorrect URL on script --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index cce5db596c7..93a9a6b4960 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -307,7 +307,7 @@ As noted above, deployment of the war file might take several minutes due a data 7\. Update the following Metadata Blocks: ``` - wget https://github.com/IQSS/dataverse/releases/download/v6.1/geospatial.tsv + wget https://github.com/IQSS/dataverse/releases/download/v6.2/geospatial.tsv curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/tab-separated-values" -X POST --upload-file @geospatial.tsv From 5c9cc55643ea6c50184df3c1edb2185a0d97887f Mon Sep 17 00:00:00 2001 From: landreev Date: Wed, 27 Mar 2024 15:50:08 -0400 Subject: [PATCH 638/689] Update 6.2-release-notes.md --- doc/release-notes/6.2-release-notes.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 93a9a6b4960..5350e7f80b7 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -328,16 +328,12 @@ curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/ta - Stop Solr instance (usually `service solr stop`, depending on Solr installation/OS, see the [Installation Guide](https://guides.dataverse.org/en/6.2/installation/prerequisites.html#solr-init-script)) -- There are 2 ways to regenerate the schema: Either by collecting the output of the Dataverse schema API and feeding it to the `update-fields.sh` script that we supply, as in the example below (modify the command lines as needed): +- Run the `update-fields.sh` script that we supply, as in the example below (modify the command lines as needed to reflect the correct path of your solr installation): ``` wget https://raw.githubusercontent.com/IQSS/dataverse/master/conf/solr/9.3.0/update-fields.sh chmod +x update-fields.sh curl "http://localhost:8080/api/admin/index/solr/schema" | ./update-fields.sh /usr/local/solr/solr-9.3.0/server/solr/collection1/conf/schema.xml ``` -OR, alternatively, you can edit the following line in your schema.xml by hand as follows (to indicate that alternative title is now `multiValued="true"`): -``` - -``` - Restart Solr instance (usually `service solr restart` depending on solr/OS) From 51fc6654190cf9639ec8e9824be5925b104daca9 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Wed, 27 Mar 2024 16:02:11 -0400 Subject: [PATCH 639/689] Removed the last step reExportAll, it is not required --- doc/release-notes/6.2-release-notes.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 5350e7f80b7..34c3171c3e1 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -337,8 +337,6 @@ curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/ta - Restart Solr instance (usually `service solr restart` depending on solr/OS) -9\. Run ReExportAll to update dataset metadata exports. Follow the directions in the [Admin Guide](http://guides.dataverse.org/en/5.14/admin/metadataexport.html#batch-exports-through-the-api). - *** ## ⚙️ New Settings From 6cb9a4c76f970b5c60d30e2cb33b23af85e13911 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 27 Mar 2024 16:09:35 -0400 Subject: [PATCH 640/689] doc tweaks for MDC processingState API #10424 --- doc/release-notes/10424-new-api-for-mdc.md | 14 +++----- .../source/developers/make-data-count.rst | 34 +++++++++++++------ 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/doc/release-notes/10424-new-api-for-mdc.md b/doc/release-notes/10424-new-api-for-mdc.md index 8fb1f6d9e3d..fef8ee2af22 100644 --- a/doc/release-notes/10424-new-api-for-mdc.md +++ b/doc/release-notes/10424-new-api-for-mdc.md @@ -1,11 +1,5 @@ -The API endpoint `api/admin/makeDataCount/{yearMonth}/processingState` has been added to Get, Create/Update(POST), and Delete a State for processing Make Data Count logged metrics -For Create/Update the 'state' is passed in through a query parameter. -Example -- `curl POST http://localhost:8080/api/admin/makeDataCount/2024-03/processingState?state=Skip` +(Please put at the bottom of the list under 🌐 API) -Valid values for state are [New, Done, Skip, Processing, and Failed] -'New' can be used to re-trigger the processing of the data for the year-month specified. -'Skip' will prevent the file from being processed. -'Processing' shows the state where the file is currently being processed. -'Failed' shows the state where the file has failed and will be re-processed in the next run. If you don't want the file to be re-processed set the state to 'Skip'. -'Done' is the state where the file has been successfully processed. +### Experimental Make Data Count processingState API + +An experimental Make Data Count processingState API has been added. For now it has been documented in the developer guide: https://guides.dataverse.org/en/6.2/developers/make-data-count.html#processing-archived-logs diff --git a/doc/sphinx-guides/source/developers/make-data-count.rst b/doc/sphinx-guides/source/developers/make-data-count.rst index d64fff9ccc7..43779c35f7c 100644 --- a/doc/sphinx-guides/source/developers/make-data-count.rst +++ b/doc/sphinx-guides/source/developers/make-data-count.rst @@ -88,21 +88,33 @@ To read more about the Make Data Count api, see https://github.com/datacite/sash You can compare the MDC metrics display with the Dataverse installation's original by toggling the ``:DisplayMDCMetrics`` setting (true by default to display MDC metrics). -New Make Data Count Processing for Your Dataverse Installation --------------------------------------------------------------- +Processing Archived Logs +------------------------ A new script (release date TBD) will be available for processing archived Dataverse log files. Monthly logs that are zipped, TARed, and copied to an archive can be processed by this script running nightly or weekly. -The script will keep track of the state of each tar file they are processed. Through the following APIs the state of each file can be checked or modified. -Setting the state to 'Skip' will prevent the file from being processed if the developer needs to analyze the contents. -'Failed' files will be re-tried in a later run. -'Done' files are successful and will be ignored going forward. -The file(s) currently being processed will have the state 'Processing'. -The states are [NEW, DONE, SKIP, PROCESSING, FAILED] -The script will process the newest set of log files (merging files from multiple nodes) and calling counter_processor. -The Admin APIs to manage the states include a GET, POST, and DELETE(For Testing). -yearMonth must be in the format yyyymm or yyyymmdd + +The script will keep track of the state of each tar file they are processed and will make use of the following "processingState" API endpoints, which allow the state of each file to be checked or modified. + +The possible states are new, done, skip, processing, and failed. + +Setting the state to "skip" will prevent the file from being processed if the developer needs to analyze the contents. + +"failed" files will be re-tried in a later run. + +"done" files are successful and will be ignored going forward. + +The files currently being processed will have the state "processing". + +The script will process the newest set of log files (merging files from multiple nodes) and call Counter Processor. + +APIs to manage the states include GET, POST, and DELETE (for testing), as shown below. + +Note: ``yearMonth`` must be in the format ``yyyymm`` or ``yyyymmdd``. + ``curl -X GET http://localhost:8080/api/admin/{yearMonth}/processingState`` + ``curl -X POST http://localhost:8080/api/admin/{yearMonth}/processingState?state=done`` + ``curl -X DELETE http://localhost:8080/api/admin/{yearMonth}/processingState`` Resources From 18ac44fc5ea31010fc0e0583b3d3a9d8f3af3c32 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Wed, 27 Mar 2024 16:22:12 -0400 Subject: [PATCH 641/689] Reindex solr change --- doc/release-notes/6.2-release-notes.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 34c3171c3e1..4017ae37b99 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -337,6 +337,14 @@ curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/ta - Restart Solr instance (usually `service solr restart` depending on solr/OS) +9\. Reindex Solr: + + For details, see https://guides.dataverse.org/en/6.0/admin/solr-search-index.html but here is the reindex command: + +``` + curl http://localhost:8080/api/admin/index +``` + *** ## ⚙️ New Settings From dff1669c82a92a7d75ce26e623bba607b299e72c Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Wed, 27 Mar 2024 16:32:32 -0400 Subject: [PATCH 642/689] 10424 added --- doc/release-notes/10424-new-api-for-mdc.md | 5 ----- doc/release-notes/6.2-release-notes.md | 4 ++++ 2 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 doc/release-notes/10424-new-api-for-mdc.md diff --git a/doc/release-notes/10424-new-api-for-mdc.md b/doc/release-notes/10424-new-api-for-mdc.md deleted file mode 100644 index fef8ee2af22..00000000000 --- a/doc/release-notes/10424-new-api-for-mdc.md +++ /dev/null @@ -1,5 +0,0 @@ -(Please put at the bottom of the list under 🌐 API) - -### Experimental Make Data Count processingState API - -An experimental Make Data Count processingState API has been added. For now it has been documented in the developer guide: https://guides.dataverse.org/en/6.2/developers/make-data-count.html#processing-archived-logs diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 4017ae37b99..854bb606349 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -233,6 +233,10 @@ Including the parameter and setting it to true will add a hierarchy showing whic The API endpoint `api/datasets/{id}/metadata` has been changed to default to the latest version of the dataset that the user has access. +### Experimental Make Data Count processingState API + +An experimental Make Data Count processingState API has been added. For now it has been documented in the (developer guide)[https://guides.dataverse.org/en/6.2/developers/make-data-count.html#processing-archived-logs]. + ## 📖 Guides ### Container Guide, Documentation for Faster Redeploy From bfee2f5e82967fc93152392b6436a806101fc26f Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Wed, 27 Mar 2024 17:25:41 -0400 Subject: [PATCH 643/689] Fixed pip script --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 854bb606349..fe032d74236 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -109,7 +109,7 @@ The /api/files//uningest api also now allows users who can publish the datas Our guides now support the Markdown format with the extension **.md**. Additionally, an option to create tabs in the guides using [Sphinx Tabs](https://sphinx-tabs.readthedocs.io) has been added. (You can see the tabs in action in the "dev usage" page of the Container Guide.) To continue building the guides, you will need to install this new dependency by re-running: ``` -pip install -r requirements.txt. +pip install -r requirements.txt ``` ## 🪲 Bug fixes From 56b2cd51e2af073ad8b478fc6a9bf256854c37db Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Thu, 28 Mar 2024 00:36:09 -0400 Subject: [PATCH 644/689] Update 6.2-release-notes.md Co-authored-by: Philip Durbin --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index fe032d74236..a4a8494a49b 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -15,7 +15,7 @@ Also, the Search API now handles license filtering using the `fq` parameter, for For more information, see [#10204](https://github.com/IQSS/dataverse/issues/10204). -### Return to Author Now Requires a Reason +### When Returning Datasets to Authors, Reviewers Can Add a Note to the Author The Popup for returning to author now allows to type in a message to explain the reasons of return and potential edits needed, that will be sent by email to the author. From 5f5b0c07817dd936c2e998910082fe33759cdc75 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Thu, 28 Mar 2024 00:36:22 -0400 Subject: [PATCH 645/689] Update 6.2-release-notes.md Co-authored-by: Philip Durbin --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index a4a8494a49b..3f5f80df52f 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -25,7 +25,7 @@ Please note that this note is mandatory, but that you can still type a creative ### Support for Using Multiple PID Providers This release adds support for using multiple PID (DOI, Handle, PermaLink) providers, multiple PID provider accounts -(managing a given protocol, authority,separator, shoulder combination), assigning PID provider accounts to specific collections, +(managing a given protocol, authority, separator, shoulder combination), assigning PID provider accounts to specific collections, and supporting transferred PIDs (where a PID is managed by an account when its authority, separator, and/or shoulder don't match the combination where the account can mint new PIDs). It also adds the ability for additional provider services beyond the existing DataCite, EZId, Handle, and PermaLink providers to be dynamically added as separate jar files. From a67b7f13a996857d1a0b4f30adda269a19e45599 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Thu, 28 Mar 2024 00:37:39 -0400 Subject: [PATCH 646/689] Update 6.2-release-notes.md Co-authored-by: Philip Durbin --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 3f5f80df52f..0437940ba53 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -37,7 +37,7 @@ and will be required in a future version. [New microprofile settings](#microprofile-settings) -### Rate Limiting Using JCache (With Hazelcast As Provided by Payara) +### Rate Limiting The option to rate limit has been added to prevent users from over taxing the system either deliberately or by runaway automated processes. Rate limiting can be configured on a tier level with tier 0 being reserved for guest users and tiers 1-any for authenticated users. From ad407c77da5ec2aacb1747c096ae81ddfdd27818 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Thu, 28 Mar 2024 00:38:02 -0400 Subject: [PATCH 647/689] Update 6.2-release-notes.md Co-authored-by: Philip Durbin --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 0437940ba53..7a6859698b9 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -55,7 +55,7 @@ For more details check the detailed guide on [this link](https://guides.datavers Hazelcast is configured in Payara and should not need any changes for this feature -### Simplified SMTP configuration +### Simplified SMTP Configuration With this release, we deprecate the usage of `asadmin create-javamail-resource` to configure Dataverse to send mail using your SMTP server and provide a simplified, standard alternative using JVM options or MicroProfile Config. From 8f56d98f1bc7b38e69dacaf02ef2022969147c7e Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Thu, 28 Mar 2024 00:38:19 -0400 Subject: [PATCH 648/689] Update 6.2-release-notes.md Co-authored-by: Philip Durbin --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 7a6859698b9..746c2702b3b 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -91,7 +91,7 @@ Support for **.qpj** and **.qmd** files in shapefile uploads has been introduced ### Ingested Tabular Data Files Can Be Stored Without the Variable Name Header -Tabular Data Ingest can now save the generated archival files with the list of variable names added as the first tab-delimited line. As the most significant effect of this feature. +Tabular Data Ingest can now save the generated archival files with the list of variable names added as the first tab-delimited line. Access API will be able to take advantage of Direct Download for tab. files saved with these headers on S3 - since they no longer have to be generated and added to the streamed content on the fly. From ab3d9d3b0eec575fbef1935a2ec505607d970a5c Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Thu, 28 Mar 2024 00:39:21 -0400 Subject: [PATCH 649/689] Update 6.2-release-notes.md Co-authored-by: Philip Durbin --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 746c2702b3b..8e04c93d479 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -93,7 +93,7 @@ Support for **.qpj** and **.qmd** files in shapefile uploads has been introduced Tabular Data Ingest can now save the generated archival files with the list of variable names added as the first tab-delimited line. -Access API will be able to take advantage of Direct Download for tab. files saved with these headers on S3 - since they no longer have to be generated and added to the streamed content on the fly. +Access API will be able to take advantage of Direct Download for .tab files saved with these headers on S3 - since they no longer have to be generated and added to the streamed content on the fly. This behavior is controlled by the new setting **:StoreIngestedTabularFilesWithVarHeaders**. It is false by default, preserving the legacy behavior. When enabled, Dataverse will be able to handle both the newly ingested files, and any already-existing legacy files stored without these headers transparently to the user. E.g. the access API will continue delivering tab-delimited files **with** this header line, whether it needs to add it dynamically for the legacy files, or reading complete files directly from storage for the ones stored with it. From a4a9f8293a0b75e929b9443b5f0384c799067558 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Thu, 28 Mar 2024 00:39:40 -0400 Subject: [PATCH 650/689] Update 6.2-release-notes.md Co-authored-by: Philip Durbin --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 8e04c93d479..c52ac13e9db 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -105,7 +105,7 @@ New Uningest/Reingest options are available in the File Page Edit menu. Ingest e The /api/files//uningest api also now allows users who can publish the dataset to undo an ingest failure. -### Sphinx Guides now Support Markdown Format and Tabs +### Sphinx Guides Now Support Markdown Format and Tabs Our guides now support the Markdown format with the extension **.md**. Additionally, an option to create tabs in the guides using [Sphinx Tabs](https://sphinx-tabs.readthedocs.io) has been added. (You can see the tabs in action in the "dev usage" page of the Container Guide.) To continue building the guides, you will need to install this new dependency by re-running: ``` From 917534b44ddbe29ae68e02ea4136f76bb3894db6 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Thu, 28 Mar 2024 00:39:56 -0400 Subject: [PATCH 651/689] Update 6.2-release-notes.md Co-authored-by: Philip Durbin --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index c52ac13e9db..fe633589115 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -134,7 +134,7 @@ OAI-PMH error handling has been improved to display a machine-readable error in ### Granting File Access Without Access Request -A bug introduced with the guestboook-at-request, requests are not deleted when granted, they are now given the state granted. +A bug introduced with the guestbook-at-request, requests are not deleted when granted, they are now given the state granted. ## 💾 Persistence From 4767a35dd90a9026fa45ee3e4a4e4b7be96e59ce Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Thu, 28 Mar 2024 00:41:11 -0400 Subject: [PATCH 652/689] Update 6.2-release-notes.md Co-authored-by: Philip Durbin --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index fe633589115..20f405829a8 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -187,7 +187,7 @@ This API endpoint indicates if the calling user can download at least one file f The API endpoint `api/harvest/clients/{harvestingClientNickname}` has been extended to include the following fields: -- **allowHarvestingMissingCVV**: enable/disable allowing datasets to be harvested with Controlled Vocabulary Values that existed in the originating Dataverse Project but are not in the harvesting Dataverse Project. Default is false. +- **allowHarvestingMissingCVV**: enable/disable allowing datasets to be harvested with controlled vocabulary values that exist in the originating Dataverse server but are not present in the harvesting Dataverse server. The default is false. *Note: This setting is only available to the API and not currently accessible/settable via the UI* From ef9d3876290b72edc0fee0c69c8e4a8747ebe215 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Thu, 28 Mar 2024 00:41:23 -0400 Subject: [PATCH 653/689] Update 6.2-release-notes.md Co-authored-by: Philip Durbin --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 20f405829a8..09d6f70c9f2 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -189,7 +189,7 @@ The API endpoint `api/harvest/clients/{harvestingClientNickname}` has been exten - **allowHarvestingMissingCVV**: enable/disable allowing datasets to be harvested with controlled vocabulary values that exist in the originating Dataverse server but are not present in the harvesting Dataverse server. The default is false. -*Note: This setting is only available to the API and not currently accessible/settable via the UI* +*Note: This setting is only available to the API and not currently accessible/settable via the UI.* ### Version Files Endpoint Extended From 8a03f69447407a1304348d77d4bcfba1f4677caf Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Thu, 28 Mar 2024 00:41:30 -0400 Subject: [PATCH 654/689] Update 6.2-release-notes.md Co-authored-by: Philip Durbin --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 09d6f70c9f2..3a9f1392735 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -204,7 +204,7 @@ The API endpoint `/api/metadatablocks/{block_id}` has been extended to include t - **displayOrder**: The display order of the field in create/edit forms - **typeClass**: The type class of this field ("controlledVocabulary", "compound", or "primitive") -### Get File Citation As JSON +### Get File Citation as JSON It is now possible to retrieve via API the file citation as it appears on the file landing page. It is formatted in HTML and encoded in JSON. From 5e35796f1eebe6af08620568c0c6f3364f4be602 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Thu, 28 Mar 2024 00:42:12 -0400 Subject: [PATCH 655/689] Update 6.2-release-notes.md Co-authored-by: Philip Durbin --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 3a9f1392735..8efdec3c578 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -275,7 +275,7 @@ In the following commands we assume that Payara 6 is installed in `/usr/local/pa 1\. Usually, when a Solr schema update is released, we recommend deploying the new version of Dataverse, then updating the `schema.xml` on the solr side. With 6.2, we recommend to install the base schema first. Without it Dataverse 6.2 is not going to be able to show any results after the initial deployment. If your instance is using any custom metadata blocks, you will need to further modify the schema, see the laset step of this instruction (step 8). -- Stop Solr instance (usually `service solr stop`, depending on Solr installation/OS, see the [Installation Guide](https://guides.dataverse.org/en/5.14/installation/prerequisites.html#solr-init-script)) +- Stop Solr instance (usually `service solr stop`, depending on Solr installation/OS, see the [Installation Guide](https://guides.dataverse.org/en/6.2/installation/prerequisites.html#solr-init-script)) - Replace schema.xml From 8c591c919a5c43f79250a445e02ea591c5a98fcb Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Thu, 28 Mar 2024 00:42:37 -0400 Subject: [PATCH 656/689] Update 6.2-release-notes.md Co-authored-by: Philip Durbin --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 8efdec3c578..33db417c1dd 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -286,7 +286,7 @@ In the following commands we assume that Payara 6 is installed in `/usr/local/pa 2\. Undeploy the previous version. -- `$PAYARA/bin/asadmin undeploy dataverse-6.0` +- `$PAYARA/bin/asadmin undeploy dataverse-6.1` 3\. Stop Payara and remove the generated directory From 1d04d024472fff294533e184894499f71dcc4d95 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Thu, 28 Mar 2024 00:42:46 -0400 Subject: [PATCH 657/689] Update 6.2-release-notes.md Co-authored-by: Philip Durbin --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 33db417c1dd..914856e4d60 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -299,7 +299,7 @@ In the following commands we assume that Payara 6 is installed in `/usr/local/pa 5\. Deploy this version. -- `$PAYARA/bin/asadmin deploy dataverse-6.1.war` +- `$PAYARA/bin/asadmin deploy dataverse-6.2.war` As noted above, deployment of the war file might take several minutes due a database migration script required for the new storage quotas feature. From 40b2152999695ed2a75064c7f9cf0afb39a0697c Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Thu, 28 Mar 2024 00:43:00 -0400 Subject: [PATCH 658/689] Update 6.2-release-notes.md Co-authored-by: Philip Durbin --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 914856e4d60..a08a14f9403 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -343,7 +343,7 @@ curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/ta 9\. Reindex Solr: - For details, see https://guides.dataverse.org/en/6.0/admin/solr-search-index.html but here is the reindex command: + For details, see https://guides.dataverse.org/en/6.2/admin/solr-search-index.html but here is the reindex command: ``` curl http://localhost:8080/api/admin/index From 25ba92622fdfc8ac8d707c3924c5be0c3180c6af Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva <142103991+jp-tosca@users.noreply.github.com> Date: Thu, 28 Mar 2024 00:43:08 -0400 Subject: [PATCH 659/689] Update 6.2-release-notes.md Co-authored-by: Philip Durbin --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index a08a14f9403..1de95ecf466 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -353,7 +353,7 @@ curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/ta ## ⚙️ New Settings -### Microprofile settings +### MicroProfile Settings *The * indicates a provider id indicating which provider the setting is for* From 08c5b90ec6b1678bd7881b45521dc64fb3138a73 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Thu, 28 Mar 2024 09:43:05 -0400 Subject: [PATCH 660/689] #10442 fix null from template --- .../java/edu/harvard/iq/dataverse/search/IndexServiceBean.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java index cf0b177df95..d6b3fd8c339 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java @@ -1016,7 +1016,7 @@ public SolrInputDocuments toSolrDocs(IndexableDataset indexableDataset, Set Date: Thu, 28 Mar 2024 12:19:06 -0400 Subject: [PATCH 661/689] Update doc/release-notes/6.2-release-notes.md Co-authored-by: Philip Durbin --- doc/release-notes/6.2-release-notes.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 1de95ecf466..f2ef4e84074 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -47,13 +47,7 @@ Two database settings configure the rate limiting. Note: If either of these settings exist in the database rate limiting will be enabled. If neither setting exists rate limiting is disabled. -`:RateLimitingDefaultCapacityTiers` is a comma separated list of default values for each tier. -In the following example, the default for tier `0` (guest users) is set to 10,000 calls per command per hour and tier `1` (authenticated users) is set to 20,000 calls per command per hour. -Tiers not specified in this setting will default to `-1` (No Limit). I.e., -d "10000" is equivalent to -d "10000,-1,-1,..." - -For more details check the detailed guide on [this link](https://guides.dataverse.org/en/6.2/installation/config.html#rate-limiting). - -Hazelcast is configured in Payara and should not need any changes for this feature +For more information check the detailed guide at [this link](https://guides.dataverse.org/en/6.2/installation/config.html#rate-limiting). ### Simplified SMTP Configuration From 4e3a024b13fe9d48949e19f1dee1e64e08265116 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 28 Mar 2024 12:21:17 -0400 Subject: [PATCH 662/689] Rate limit --- doc/release-notes/6.2-release-notes.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index f2ef4e84074..3874cbb7b52 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -42,12 +42,11 @@ and will be required in a future version. The option to rate limit has been added to prevent users from over taxing the system either deliberately or by runaway automated processes. Rate limiting can be configured on a tier level with tier 0 being reserved for guest users and tiers 1-any for authenticated users. Superuser accounts are exempt from rate limiting. + Rate limits can be imposed on command APIs by configuring the tier, the command, and the hourly limit in the database. -Two database settings configure the rate limiting. -Note: If either of these settings exist in the database rate limiting will be enabled. -If neither setting exists rate limiting is disabled. +Two database settings configure the rate limiting **:RateLimitingDefaultCapacityTiers** and **RateLimitingCapacityByTierAndAction**, If either of these settings exist in the database rate limiting will be enabled and If neither setting exists rate limiting is disabled. -For more information check the detailed guide at [this link](https://guides.dataverse.org/en/6.2/installation/config.html#rate-limiting). +For more details check the detailed guide on [this link](https://guides.dataverse.org/en/6.2/installation/config.html#rate-limiting). ### Simplified SMTP Configuration From 4a9c9337cc0ada383a143109becbf45d72a98f52 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 28 Mar 2024 13:12:46 -0400 Subject: [PATCH 663/689] Update with morning chages --- doc/release-notes/6.2-release-notes.md | 42 ++++++++++++++++++++------ 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 3874cbb7b52..9bec047cbe3 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -5,15 +5,31 @@ Please note: To read these instructions in full, please go to https://github.com This release brings new features, enhancements, and bug fixes to the Dataverse software. Thank you to all of the community members who contributed code, suggestions, bug reports, and other assistance across the project. +# Index +- [💡Release Highlights](#release-highlights) +- [🪲 Bug fixes](#-bug-fixes) +- [💾 Persistence](#-persistence) +- [🌐 API](#-api) +- [📖 Guides](#-guides) +- [⚠️ Breaking Changes](#-breaking-changes) +- [💻 Upgrade instructions](#-upgrade-instructions) +- [⚙️ New Settings](#-new-settings) + + + ## 💡Release Highlights -### Search and Facet by License +### Search and Facet by License +License have been added to the search facets in the search side panel to filter datasets by license (e.g. CC0). + +Datasets with Custom Terms are aggregated under the "Custom Terms" value of this facet. See the [Licensing](https://guides.dataverse.org/en/6.2/installation/advanced.html#licensing) section of the guide for more details on configured Licenses and Custom Terms. -A new search facet called "License" has been added and will be displayed as long as there is more than one license in datasets and datafiles in browse/search results. This facet allow you to filter by license such as CC0, etc. +For more information, see [#9060](https://github.com/IQSS/dataverse/issues/9060). -Also, the Search API now handles license filtering using the `fq` parameter, for example : `/api/search?q=*&fq=license%3A%22CC0+1.0%22` for CC0 1.0. -For more information, see [#10204](https://github.com/IQSS/dataverse/issues/10204). +Licenses can also be used to filter the Search API results using the `fq` parameter, for example : `/api/search?q=*&fq=license%3A%22CC0+1.0%22` for CC0 1.0, see the [Search API guide](https://guides.dataverse.org/en/6.1/api/search.html) for more examples. + +For more information, see [#10204](https://github.com/IQSS/dataverse/pull/10204). ### When Returning Datasets to Authors, Reviewers Can Add a Note to the Author @@ -34,6 +50,8 @@ These changes require per-provider settings rather than the global PID settings for installations using a single PID Provider account is provided, updating to use the new microprofile settings is highly recommended and will be required in a future version. +For more information check the PID settings on [this link](https://guides.dataverse.org/en/6.2/installation/config.html#global-settings). + [New microprofile settings](#microprofile-settings) @@ -116,7 +134,7 @@ The permissions required to assign a role have been fixed. It is no longer possi ### Geospatial Metadata Block Fields for North and South Renamed -The Geospatial metadata block fields for north and south were labeled incorrectly as ‘Longitudes,’ as reported on #5645. After updating to this version of Dataverse, users will need to update all the endpoints that used ‘northLongitude’ and ‘southLongitude’ to ‘northLatitude’ and ‘southLatitude,’ respectively. +The Geospatial metadata block fields for north and south were labeled incorrectly as ‘Longitudes,’ as reported in #5645. After updating to this version of Dataverse, users will need to update all the endpoints that used ‘northLongitude’ and ‘southLongitude’ to ‘northLatitude’ and ‘southLatitude,’ respectively. ### OAI-PMH Error Handling Has Been Improved @@ -133,7 +151,7 @@ A bug introduced with the guestbook-at-request, requests are not deleted when gr ### Missing Database Constraints -This release adds two missing database constraints that will assure that the externalvocabularyvalue table only has one entry for each uri and that the oaiset table only has one set for each spec. (In the very unlikely case that your existing database has duplicate entries now, install would fail. This can be checked by running +This release adds two missing database constraints that will assure that the externalvocabularyvalue table only has one entry for each uri and that the oaiset table only has one set for each spec. (In the very unlikely case that your existing database has duplicate entries now, install would fail. This can be checked by running the following commands: ``` SELECT uri, count(*) FROM externalvocabularyvalue group by uri; @@ -149,11 +167,11 @@ Then removing any duplicate rows (where count>1). Universe field in variablemetadata table was changed from **varchar(255)** to **text**. The change was made to support longer strings in "universe" metadata field, similar to the rest of text fields in variablemetadata table. -### Postgres Versions +### PostgreSQL Versions -This release adds install script support for the new permissions model in Postgres versions 15+, and bumps FlyWay to support Postgres 16. +This release adds install script support for the new permissions model in PostgreSQL versions 15+, and bumps Flyway to support PostgreSQL 16. -Postgres 13 remains the version used with automated testing. +PostgreSQL 13 remains the version used with automated testing. ## 🌐 API @@ -166,6 +184,8 @@ Listing collection/dataverse role assignments via API still requires ManageDatav A new Index API endpoint has been added allowing an admin to clear an individual dataset from Solr. +For more information visit the documentation on [this link](https://guides.dataverse.org/en/6.2/admin/solr-search-index.html#clearing-a-dataset-from-solr) + ### New Accounts Metrics API Users can retrieve new types of metrics related to user accounts. The new capabilities are [described](https://guides.dataverse.org/en/6.2/api/metrics.html) in the guides. @@ -201,7 +221,9 @@ The API endpoint `/api/metadatablocks/{block_id}` has been extended to include t It is now possible to retrieve via API the file citation as it appears on the file landing page. It is formatted in HTML and encoded in JSON. -This API is not for downloading various citation formats such as EndNote XML, RIS, or BibTeX. This functionality has been requested in [#3140](https://github.com/IQSS/dataverse/issues/3140) and [#9994](https://github.com/IQSS/dataverse/issues/9994) +This API is not for downloading various citation formats such as EndNote XML, RIS, or BibTeX. + +For mor information check the documentation on [this link](https://guides.dataverse.org/en/6.2/api/native-api.html#get-file-citation-as-json) ### Files Endpoint Extended From 4385372235e0ba085995201748ca98ae228d4b2b Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 28 Mar 2024 13:14:45 -0400 Subject: [PATCH 664/689] Deleted url --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 9bec047cbe3..58cee103f94 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -268,7 +268,7 @@ The Container Guide now containers a tutorial for running Dataverse in container A new QA Guide is intended mostly for the core development team but may be of interest to contributors on: https://guides.dataverse.org/en/6.2/develop/qa -## ⚠️ Breaking Changes https://guides.dataverse.org/en/en/develop/qa/index.html +## ⚠️ Breaking Changes To view a list of changes that can be impactful to your implementation please visit our detailed [list of changes to the API](https://guides.dataverse.org/en/6.2/develop/api/changelog.html). From 452e79c710058e596b89c323e6ff60b35c9e3826 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 28 Mar 2024 13:16:57 -0400 Subject: [PATCH 665/689] Update --- doc/release-notes/6.2-release-notes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 58cee103f94..6e162cd6c09 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -11,9 +11,9 @@ Thank you to all of the community members who contributed code, suggestions, bug - [💾 Persistence](#-persistence) - [🌐 API](#-api) - [📖 Guides](#-guides) -- [⚠️ Breaking Changes](#-breaking-changes) +- [⚠️ Breaking Changes](#%EF%B8%8F-breaking-changes) - [💻 Upgrade instructions](#-upgrade-instructions) -- [⚙️ New Settings](#-new-settings) +- [⚙️ New Settings](#%EF%B8%8F-new-settings) From 5150f5b249163244760d714e5f9f811f42106889 Mon Sep 17 00:00:00 2001 From: Steven Winship Date: Thu, 28 Mar 2024 13:27:56 -0400 Subject: [PATCH 666/689] fix bad curl url in doc --- doc/sphinx-guides/source/developers/make-data-count.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/sphinx-guides/source/developers/make-data-count.rst b/doc/sphinx-guides/source/developers/make-data-count.rst index 43779c35f7c..edad580e451 100644 --- a/doc/sphinx-guides/source/developers/make-data-count.rst +++ b/doc/sphinx-guides/source/developers/make-data-count.rst @@ -111,11 +111,11 @@ APIs to manage the states include GET, POST, and DELETE (for testing), as shown Note: ``yearMonth`` must be in the format ``yyyymm`` or ``yyyymmdd``. -``curl -X GET http://localhost:8080/api/admin/{yearMonth}/processingState`` +``curl -X GET http://localhost:8080/api/admin/makeDataCount/{yearMonth}/processingState`` -``curl -X POST http://localhost:8080/api/admin/{yearMonth}/processingState?state=done`` +``curl -X POST http://localhost:8080/api/admin/makeDataCount/{yearMonth}/processingState?state=done`` -``curl -X DELETE http://localhost:8080/api/admin/{yearMonth}/processingState`` +``curl -X DELETE http://localhost:8080/api/admin/makeDataCount/{yearMonth}/processingState`` Resources --------- From b102bf7b5dbda99f194a8695149dedb0f658a331 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 28 Mar 2024 13:32:25 -0400 Subject: [PATCH 667/689] Latest update post index --- doc/release-notes/6.2-release-notes.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 6e162cd6c09..bd584feb619 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -242,7 +242,9 @@ The endpoint supports the *includeDeaccessioned* and *returnDatasetVersion* opti The API endpoints for getting datasets, Dataverse collections, and datafiles have been extended to support the following optional 'returnOwners' query parameter. -Including the parameter and setting it to true will add a hierarchy showing which dataset and dataverse collection(s) the object is part of to the json object returned. +Including the parameter and setting it to true will add a hierarchy showing which dataset and dataverse collection(s) the object is part of to the json object returned. + +For more information visit the full native API guide on [this link](https://guides.dataverse.org/en/6.2/api/native-api.html) ### Endpoint Fixed: Datasets Metadata From 9457ea1659872754f47549d6f30ff3049250ca97 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 28 Mar 2024 13:36:22 -0400 Subject: [PATCH 668/689] Change order of index api-breaking --- doc/release-notes/6.2-release-notes.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index bd584feb619..335fd606f80 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -10,8 +10,8 @@ Thank you to all of the community members who contributed code, suggestions, bug - [🪲 Bug fixes](#-bug-fixes) - [💾 Persistence](#-persistence) - [🌐 API](#-api) -- [📖 Guides](#-guides) - [⚠️ Breaking Changes](#%EF%B8%8F-breaking-changes) +- [📖 Guides](#-guides) - [💻 Upgrade instructions](#-upgrade-instructions) - [⚙️ New Settings](#%EF%B8%8F-new-settings) @@ -254,6 +254,10 @@ The API endpoint `api/datasets/{id}/metadata` has been changed to default to the An experimental Make Data Count processingState API has been added. For now it has been documented in the (developer guide)[https://guides.dataverse.org/en/6.2/developers/make-data-count.html#processing-archived-logs]. +## ⚠️ Breaking Changes + +To view a list of changes that can be impactful to your implementation please visit our detailed [list of changes to the API](https://guides.dataverse.org/en/6.2/develop/api/changelog.html). + ## 📖 Guides ### Container Guide, Documentation for Faster Redeploy @@ -270,11 +274,6 @@ The Container Guide now containers a tutorial for running Dataverse in container A new QA Guide is intended mostly for the core development team but may be of interest to contributors on: https://guides.dataverse.org/en/6.2/develop/qa -## ⚠️ Breaking Changes - -To view a list of changes that can be impactful to your implementation please visit our detailed [list of changes to the API](https://guides.dataverse.org/en/6.2/develop/api/changelog.html). - - ## 💻 Upgrade instructions Upgrading requires a maintenance window and downtime. Please plan ahead, create backups of your database, etc. From b46c582213e33711e6813108d8b4314e2a04c651 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 28 Mar 2024 14:11:13 -0400 Subject: [PATCH 669/689] describe #10422 in release notes --- doc/release-notes/10381-index-after-publish.md | 3 --- doc/release-notes/6.2-release-notes.md | 5 +++++ 2 files changed, 5 insertions(+), 3 deletions(-) delete mode 100644 doc/release-notes/10381-index-after-publish.md diff --git a/doc/release-notes/10381-index-after-publish.md b/doc/release-notes/10381-index-after-publish.md deleted file mode 100644 index 84c84d75a28..00000000000 --- a/doc/release-notes/10381-index-after-publish.md +++ /dev/null @@ -1,3 +0,0 @@ -New release adds a new microprofile setting for maximum number of simultaneously running asynchronous dataset index operations that defaults to ``4``: - -dataverse.solr.concurrency.max-async-indexes \ No newline at end of file diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 335fd606f80..d7eeeebde2f 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -122,6 +122,11 @@ Our guides now support the Markdown format with the extension **.md**. Additiona ``` pip install -r requirements.txt ``` + +### Number of Concurrent Indexing Operations Now Configurable + +A new MicroProfile setting called `dataverse.solr.concurrency.max-async-indexes` has been added that controls the maximum number of simultaneously running asynchronous dataset index operations (defaults to 4). + ## 🪲 Bug fixes ### Publication Status Facet Restored From 0ab80d7f3c34f2e9005c8ec82eac7c6a29c9e301 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 28 Mar 2024 14:27:42 -0400 Subject: [PATCH 670/689] improve guidance on writing release note snippets Please link to the HTML preview. Otherwise, it's time-consuming to find it later. --- doc/sphinx-guides/source/developers/version-control.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/sphinx-guides/source/developers/version-control.rst b/doc/sphinx-guides/source/developers/version-control.rst index c5669d02e77..a9a60de380c 100644 --- a/doc/sphinx-guides/source/developers/version-control.rst +++ b/doc/sphinx-guides/source/developers/version-control.rst @@ -117,10 +117,9 @@ As described at :ref:`write-release-notes`, at release time we compile together Here's how to add a release note snippet to your pull request: - Create a Markdown file under ``doc/release-notes``. You can reuse the name of your branch and append ".md" to it, e.g. ``3728-doc-apipolicy-fix.md`` -- Edit the snippet to include anything you think should be mentioned in the release notes, such as: +- Edit the snippet to include anything you think should be mentioned in the release notes. Please include the following if they apply: - - Descriptions of new features - - Explanations of bugs fixed + - Descriptions of new features or bug fixed, including a link to the HTML preview of the docs you wrote (e.g. https://dataverse-guide--9939.org.readthedocs.build/en/9939/installation/config.html#smtp-email-configuration ) - New configuration settings - Upgrade instructions - Etc. From aac525600b306522d7fa50c1e8201735c922c33e Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 28 Mar 2024 14:41:07 -0400 Subject: [PATCH 671/689] tweaks #10422 --- doc/release-notes/6.2-release-notes.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index d7eeeebde2f..bd72b78d3d7 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -5,12 +5,12 @@ Please note: To read these instructions in full, please go to https://github.com This release brings new features, enhancements, and bug fixes to the Dataverse software. Thank you to all of the community members who contributed code, suggestions, bug reports, and other assistance across the project. -# Index +# Table of Contents - [💡Release Highlights](#release-highlights) - [🪲 Bug fixes](#-bug-fixes) - [💾 Persistence](#-persistence) - [🌐 API](#-api) -- [⚠️ Breaking Changes](#%EF%B8%8F-breaking-changes) +- [⚠️ Backward Incompatibilities](#%EF%B8%8F-backward-incompatibilities) - [📖 Guides](#-guides) - [💻 Upgrade instructions](#-upgrade-instructions) - [⚙️ New Settings](#%EF%B8%8F-new-settings) @@ -259,7 +259,7 @@ The API endpoint `api/datasets/{id}/metadata` has been changed to default to the An experimental Make Data Count processingState API has been added. For now it has been documented in the (developer guide)[https://guides.dataverse.org/en/6.2/developers/make-data-count.html#processing-archived-logs]. -## ⚠️ Breaking Changes +## ⚠️ Backward Incompatibilities To view a list of changes that can be impactful to your implementation please visit our detailed [list of changes to the API](https://guides.dataverse.org/en/6.2/develop/api/changelog.html). From 8c8fe789ef3f1d000756e6f3017f706ce1d7aedf Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 28 Mar 2024 14:56:34 -0400 Subject: [PATCH 672/689] add settings, reformat --- doc/release-notes/6.2-release-notes.md | 76 ++++++++++++++------------ 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index bd72b78d3d7..5b77d4a78c6 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -378,45 +378,51 @@ curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/ta *The * indicates a provider id indicating which provider the setting is for* -> - dataverse.pid.providers -> - dataverse.pid.default-provider -> - dataverse.pid.*.type -> - dataverse.pid.*.label -> - dataverse.pid.*.authority -> - dataverse.pid.*.shoulder -> - dataverse.pid.*.identifier-generation-style -> - dataverse.pid.*.datafile-pid-format -> - dataverse.pid.*.managed-list -> - dataverse.pid.*.excluded-list -> - dataverse.pid.*.datacite.mds-api-url -> - dataverse.pid.*.datacite.rest-api-url -> - dataverse.pid.*.datacite.username -> - dataverse.pid.*.datacite.password -> - dataverse.pid.*.ezid.api-url -> - dataverse.pid.*.ezid.username -> - dataverse.pid.*.ezid.password -> - dataverse.pid.*.permalink.base-url -> - dataverse.pid.*.permalink.separator -> - dataverse.pid.*.handlenet.index -> - dataverse.pid.*.handlenet.independent-service -> - dataverse.pid.*.handlenet.auth-handle -> - dataverse.pid.*.handlenet.key.path -> - dataverse.pid.*.handlenet.key.passphrase -> - dataverse.spi.pidproviders.directory -> - dataverse.solr.concurrency.max-async-indexes +- dataverse.pid.providers +- dataverse.pid.default-provider +- dataverse.pid.*.type +- dataverse.pid.*.label +- dataverse.pid.*.authority +- dataverse.pid.*.shoulder +- dataverse.pid.*.identifier-generation-style +- dataverse.pid.*.datafile-pid-format +- dataverse.pid.*.managed-list +- dataverse.pid.*.excluded-list +- dataverse.pid.*.datacite.mds-api-url +- dataverse.pid.*.datacite.rest-api-url +- dataverse.pid.*.datacite.username +- dataverse.pid.*.datacite.password +- dataverse.pid.*.ezid.api-url +- dataverse.pid.*.ezid.username +- dataverse.pid.*.ezid.password +- dataverse.pid.*.permalink.base-url +- dataverse.pid.*.permalink.separator +- dataverse.pid.*.handlenet.index +- dataverse.pid.*.handlenet.independent-service +- dataverse.pid.*.handlenet.auth-handle +- dataverse.pid.*.handlenet.key.path +- dataverse.pid.*.handlenet.key.passphrase +- dataverse.spi.pidproviders.directory +- dataverse.solr.concurrency.max-async-indexes [⬅️ Go back](#multiple-pid-sup) ## SMTP Settings: -> - dataverse.mail.system-email -> - dataverse.mail.mta.host -> - dataverse.mail.mta.port -> - dataverse.mail.mta.ssl.enable -> - dataverse.mail.mta.auth -> - dataverse.mail.mta.user -> - dataverse.mail.mta.password -> - dataverse.mail.mta.allow-utf8-addresses -> - Plus many more for advanced usage and special provider requirements. See [configuration guide for a full list](https://guides.dataverse.org/en/6.2/installation/config.html#dataverse-mail-mta). +- dataverse.mail.system-email +- dataverse.mail.mta.host +- dataverse.mail.mta.port +- dataverse.mail.mta.ssl.enable +- dataverse.mail.mta.auth +- dataverse.mail.mta.user +- dataverse.mail.mta.password +- dataverse.mail.mta.allow-utf8-addresses +- Plus many more for advanced usage and special provider requirements. See [configuration guide for a full list](https://guides.dataverse.org/en/6.2/installation/config.html#dataverse-mail-mta). [⬅️ Go back](#simplified-smtp-configuration) + +## Database Settings: + +- :RateLimitingDefaultCapacityTiers +- :RateLimitingCapacityByTierAndAction +- :StoreIngestedTabularFilesWithVarHeaders From 950146c23f862007cfc6c3531235888905268781 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 28 Mar 2024 15:00:52 -0400 Subject: [PATCH 673/689] a few more sections, move settings --- doc/release-notes/6.2-release-notes.md | 119 +++++++++++++------------ 1 file changed, 63 insertions(+), 56 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 5b77d4a78c6..81db2e3a93e 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -279,6 +279,69 @@ The Container Guide now containers a tutorial for running Dataverse in container A new QA Guide is intended mostly for the core development team but may be of interest to contributors on: https://guides.dataverse.org/en/6.2/develop/qa +## ⚙️ New Settings + +### MicroProfile Settings + +*The * indicates a provider id indicating which provider the setting is for* + +- dataverse.pid.providers +- dataverse.pid.default-provider +- dataverse.pid.*.type +- dataverse.pid.*.label +- dataverse.pid.*.authority +- dataverse.pid.*.shoulder +- dataverse.pid.*.identifier-generation-style +- dataverse.pid.*.datafile-pid-format +- dataverse.pid.*.managed-list +- dataverse.pid.*.excluded-list +- dataverse.pid.*.datacite.mds-api-url +- dataverse.pid.*.datacite.rest-api-url +- dataverse.pid.*.datacite.username +- dataverse.pid.*.datacite.password +- dataverse.pid.*.ezid.api-url +- dataverse.pid.*.ezid.username +- dataverse.pid.*.ezid.password +- dataverse.pid.*.permalink.base-url +- dataverse.pid.*.permalink.separator +- dataverse.pid.*.handlenet.index +- dataverse.pid.*.handlenet.independent-service +- dataverse.pid.*.handlenet.auth-handle +- dataverse.pid.*.handlenet.key.path +- dataverse.pid.*.handlenet.key.passphrase +- dataverse.spi.pidproviders.directory +- dataverse.solr.concurrency.max-async-indexes + +[⬅️ Go back](#multiple-pid-sup) + +## SMTP Settings: + +- dataverse.mail.system-email +- dataverse.mail.mta.host +- dataverse.mail.mta.port +- dataverse.mail.mta.ssl.enable +- dataverse.mail.mta.auth +- dataverse.mail.mta.user +- dataverse.mail.mta.password +- dataverse.mail.mta.allow-utf8-addresses +- Plus many more for advanced usage and special provider requirements. See [configuration guide for a full list](https://guides.dataverse.org/en/6.2/installation/config.html#dataverse-mail-mta). + +[⬅️ Go back](#simplified-smtp-configuration) + +## Database Settings: + +- :RateLimitingDefaultCapacityTiers +- :RateLimitingCapacityByTierAndAction +- :StoreIngestedTabularFilesWithVarHeaders + +## Complete List of Changes + +For the complete list of code changes in this release, see the [6.2 Milestone](https://github.com/IQSS/dataverse/issues?q=milestone%3A6.2+is%3Aclosed) in GitHub. + +## Getting Help + +For help with upgrading, installing, or general questions please post to the [Dataverse Community Google Group](https://groups.google.com/forum/#!forum/dataverse-community) or email support@dataverse.org. + ## 💻 Upgrade instructions Upgrading requires a maintenance window and downtime. Please plan ahead, create backups of your database, etc. @@ -370,59 +433,3 @@ curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/ta curl http://localhost:8080/api/admin/index ``` -*** - -## ⚙️ New Settings - -### MicroProfile Settings - -*The * indicates a provider id indicating which provider the setting is for* - -- dataverse.pid.providers -- dataverse.pid.default-provider -- dataverse.pid.*.type -- dataverse.pid.*.label -- dataverse.pid.*.authority -- dataverse.pid.*.shoulder -- dataverse.pid.*.identifier-generation-style -- dataverse.pid.*.datafile-pid-format -- dataverse.pid.*.managed-list -- dataverse.pid.*.excluded-list -- dataverse.pid.*.datacite.mds-api-url -- dataverse.pid.*.datacite.rest-api-url -- dataverse.pid.*.datacite.username -- dataverse.pid.*.datacite.password -- dataverse.pid.*.ezid.api-url -- dataverse.pid.*.ezid.username -- dataverse.pid.*.ezid.password -- dataverse.pid.*.permalink.base-url -- dataverse.pid.*.permalink.separator -- dataverse.pid.*.handlenet.index -- dataverse.pid.*.handlenet.independent-service -- dataverse.pid.*.handlenet.auth-handle -- dataverse.pid.*.handlenet.key.path -- dataverse.pid.*.handlenet.key.passphrase -- dataverse.spi.pidproviders.directory -- dataverse.solr.concurrency.max-async-indexes - -[⬅️ Go back](#multiple-pid-sup) - -## SMTP Settings: - -- dataverse.mail.system-email -- dataverse.mail.mta.host -- dataverse.mail.mta.port -- dataverse.mail.mta.ssl.enable -- dataverse.mail.mta.auth -- dataverse.mail.mta.user -- dataverse.mail.mta.password -- dataverse.mail.mta.allow-utf8-addresses -- Plus many more for advanced usage and special provider requirements. See [configuration guide for a full list](https://guides.dataverse.org/en/6.2/installation/config.html#dataverse-mail-mta). - -[⬅️ Go back](#simplified-smtp-configuration) - -## Database Settings: - -- :RateLimitingDefaultCapacityTiers -- :RateLimitingCapacityByTierAndAction -- :StoreIngestedTabularFilesWithVarHeaders From bfd94074018d7dd85b1c5b6a0632ab5ec5311ab8 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 28 Mar 2024 15:12:09 -0400 Subject: [PATCH 674/689] Table of contets reorg --- doc/release-notes/6.2-release-notes.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 81db2e3a93e..6becea8d05d 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -12,10 +12,10 @@ Thank you to all of the community members who contributed code, suggestions, bug - [🌐 API](#-api) - [⚠️ Backward Incompatibilities](#%EF%B8%8F-backward-incompatibilities) - [📖 Guides](#-guides) -- [💻 Upgrade instructions](#-upgrade-instructions) - [⚙️ New Settings](#%EF%B8%8F-new-settings) - - +- [📋 Complete List of Changes](#-upgrade-instructions) +- [🛟 Getting Help](#-upgrade-instructions) +- [💻 Upgrade instructions](#-upgrade-instructions) ## 💡Release Highlights @@ -312,9 +312,7 @@ A new QA Guide is intended mostly for the core development team but may be of in - dataverse.spi.pidproviders.directory - dataverse.solr.concurrency.max-async-indexes -[⬅️ Go back](#multiple-pid-sup) - -## SMTP Settings: +### SMTP Settings: - dataverse.mail.system-email - dataverse.mail.mta.host @@ -326,19 +324,17 @@ A new QA Guide is intended mostly for the core development team but may be of in - dataverse.mail.mta.allow-utf8-addresses - Plus many more for advanced usage and special provider requirements. See [configuration guide for a full list](https://guides.dataverse.org/en/6.2/installation/config.html#dataverse-mail-mta). -[⬅️ Go back](#simplified-smtp-configuration) - -## Database Settings: +### Database Settings: - :RateLimitingDefaultCapacityTiers - :RateLimitingCapacityByTierAndAction - :StoreIngestedTabularFilesWithVarHeaders -## Complete List of Changes +## 📋 Complete List of Changes For the complete list of code changes in this release, see the [6.2 Milestone](https://github.com/IQSS/dataverse/issues?q=milestone%3A6.2+is%3Aclosed) in GitHub. -## Getting Help +## 🛟 Getting Help For help with upgrading, installing, or general questions please post to the [Dataverse Community Google Group](https://groups.google.com/forum/#!forum/dataverse-community) or email support@dataverse.org. From 5f7c99f94ad5f9888dcdccf3fa4a43c80ffe9640 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 28 Mar 2024 15:18:07 -0400 Subject: [PATCH 675/689] Table of contents final --- doc/release-notes/6.2-release-notes.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 6becea8d05d..d172ba48c29 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -13,8 +13,8 @@ Thank you to all of the community members who contributed code, suggestions, bug - [⚠️ Backward Incompatibilities](#%EF%B8%8F-backward-incompatibilities) - [📖 Guides](#-guides) - [⚙️ New Settings](#%EF%B8%8F-new-settings) -- [📋 Complete List of Changes](#-upgrade-instructions) -- [🛟 Getting Help](#-upgrade-instructions) +- [📋 Complete List of Changes](#-complete-list-of-changes) +- [🛟 Getting Help](#-getting-help) - [💻 Upgrade instructions](#-upgrade-instructions) ## 💡Release Highlights @@ -127,6 +127,8 @@ pip install -r requirements.txt A new MicroProfile setting called `dataverse.solr.concurrency.max-async-indexes` has been added that controls the maximum number of simultaneously running asynchronous dataset index operations (defaults to 4). +[⬆️](#-table-of-contents) + ## 🪲 Bug fixes ### Publication Status Facet Restored @@ -152,6 +154,8 @@ OAI-PMH error handling has been improved to display a machine-readable error in A bug introduced with the guestbook-at-request, requests are not deleted when granted, they are now given the state granted. +[⬆️](#-table-of-contents) + ## 💾 Persistence ### Missing Database Constraints @@ -259,10 +263,14 @@ The API endpoint `api/datasets/{id}/metadata` has been changed to default to the An experimental Make Data Count processingState API has been added. For now it has been documented in the (developer guide)[https://guides.dataverse.org/en/6.2/developers/make-data-count.html#processing-archived-logs]. +[⬆️](#-table-of-contents) + ## ⚠️ Backward Incompatibilities To view a list of changes that can be impactful to your implementation please visit our detailed [list of changes to the API](https://guides.dataverse.org/en/6.2/develop/api/changelog.html). +[⬆️](#-table-of-contents) + ## 📖 Guides ### Container Guide, Documentation for Faster Redeploy @@ -279,6 +287,8 @@ The Container Guide now containers a tutorial for running Dataverse in container A new QA Guide is intended mostly for the core development team but may be of interest to contributors on: https://guides.dataverse.org/en/6.2/develop/qa +[⬆️](#-table-of-contents) + ## ⚙️ New Settings ### MicroProfile Settings @@ -334,10 +344,14 @@ A new QA Guide is intended mostly for the core development team but may be of in For the complete list of code changes in this release, see the [6.2 Milestone](https://github.com/IQSS/dataverse/issues?q=milestone%3A6.2+is%3Aclosed) in GitHub. +[⬆️](#-table-of-contents) + ## 🛟 Getting Help For help with upgrading, installing, or general questions please post to the [Dataverse Community Google Group](https://groups.google.com/forum/#!forum/dataverse-community) or email support@dataverse.org. +[⬆️](#-table-of-contents) + ## 💻 Upgrade instructions Upgrading requires a maintenance window and downtime. Please plan ahead, create backups of your database, etc. @@ -428,4 +442,4 @@ curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/ta ``` curl http://localhost:8080/api/admin/index ``` - +[⬆️](#-table-of-contents) From f82ebdaaa27f3e183a5c9f43fa100eacf20e2a9d Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 28 Mar 2024 15:20:08 -0400 Subject: [PATCH 676/689] TOC fix --- doc/release-notes/6.2-release-notes.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index d172ba48c29..bbf8d010ebc 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -127,7 +127,7 @@ pip install -r requirements.txt A new MicroProfile setting called `dataverse.solr.concurrency.max-async-indexes` has been added that controls the maximum number of simultaneously running asynchronous dataset index operations (defaults to 4). -[⬆️](#-table-of-contents) +[⬆️](#table-of-contents) ## 🪲 Bug fixes @@ -154,7 +154,7 @@ OAI-PMH error handling has been improved to display a machine-readable error in A bug introduced with the guestbook-at-request, requests are not deleted when granted, they are now given the state granted. -[⬆️](#-table-of-contents) +[⬆️](#table-of-contents) ## 💾 Persistence @@ -263,13 +263,13 @@ The API endpoint `api/datasets/{id}/metadata` has been changed to default to the An experimental Make Data Count processingState API has been added. For now it has been documented in the (developer guide)[https://guides.dataverse.org/en/6.2/developers/make-data-count.html#processing-archived-logs]. -[⬆️](#-table-of-contents) +[⬆️](#table-of-contents) ## ⚠️ Backward Incompatibilities To view a list of changes that can be impactful to your implementation please visit our detailed [list of changes to the API](https://guides.dataverse.org/en/6.2/develop/api/changelog.html). -[⬆️](#-table-of-contents) +[⬆️](#table-of-contents) ## 📖 Guides @@ -287,7 +287,7 @@ The Container Guide now containers a tutorial for running Dataverse in container A new QA Guide is intended mostly for the core development team but may be of interest to contributors on: https://guides.dataverse.org/en/6.2/develop/qa -[⬆️](#-table-of-contents) +[⬆️](#table-of-contents) ## ⚙️ New Settings @@ -344,13 +344,13 @@ A new QA Guide is intended mostly for the core development team but may be of in For the complete list of code changes in this release, see the [6.2 Milestone](https://github.com/IQSS/dataverse/issues?q=milestone%3A6.2+is%3Aclosed) in GitHub. -[⬆️](#-table-of-contents) +[⬆️](#table-of-contents) ## 🛟 Getting Help For help with upgrading, installing, or general questions please post to the [Dataverse Community Google Group](https://groups.google.com/forum/#!forum/dataverse-community) or email support@dataverse.org. -[⬆️](#-table-of-contents) +[⬆️](#table-of-contents) ## 💻 Upgrade instructions Upgrading requires a maintenance window and downtime. Please plan ahead, create backups of your database, etc. @@ -442,4 +442,4 @@ curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/ta ``` curl http://localhost:8080/api/admin/index ``` -[⬆️](#-table-of-contents) +[⬆️](#table-of-contents) From 2035585303925b2cc8ef0fc4d74bc5aee636d67b Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 28 Mar 2024 15:22:44 -0400 Subject: [PATCH 677/689] toc fix 2 --- doc/release-notes/6.2-release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index bbf8d010ebc..494bab39553 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -182,6 +182,7 @@ This release adds install script support for the new permissions model in Postgr PostgreSQL 13 remains the version used with automated testing. +[⬆️](#table-of-contents) ## 🌐 API From 8bbe9744e8945a3318a61f3a23686950d6ca1536 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 28 Mar 2024 15:32:31 -0400 Subject: [PATCH 678/689] more links, tweaks --- doc/release-notes/6.2-release-notes.md | 28 +++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 494bab39553..3c2a116ac7a 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -37,6 +37,8 @@ The Popup for returning to author now allows to type in a message to explain the Please note that this note is mandatory, but that you can still type a creative and meaningful comment such as "The author would like to modify his dataset", "Files are missing", "Nothing to report" or "A curation report with comments and suggestions/instructions will follow in another email" that suits your situation. +For more information, see #10137. + ### Support for Using Multiple PID Providers @@ -92,14 +94,18 @@ For more information, see [#10360](https://github.com/IQSS/dataverse/issues/1036 When a Dataverse installation is configured to use a metadata exporter for the [Croissant](https://github.com/mlcommons/croissant) format, the content of the JSON-LD in the **<head>** of dataset landing pages will be replaced with that format. However, both JSON-LD and Croissant will still be available for download from the dataset page and API. +For more information, see #10382. + ### Harvesting Handle Missing Controlled Values -Allows datasets to be harvested with Controlled Vocabulary Values that existed in the originating Dataverse Project but are not in the harvesting Dataverse Project. For mor information view the changes to the endpoint [here](#harvesting-client-endpoint-extended). +Allows datasets to be harvested with Controlled Vocabulary Values that existed in the originating Dataverse Project but are not in the harvesting Dataverse Project. For more information, view the changes to the endpoint [here](#harvesting-client-endpoint-extended). ### Add .QPJ and .QMD Extensions to Shapefile Handling Support for **.qpj** and **.qmd** files in shapefile uploads has been introduced, ensuring that these files are properly recognized and handled as part of geospatial datasets in Dataverse. +For more information, see #10305. + ### Ingested Tabular Data Files Can Be Stored Without the Variable Name Header Tabular Data Ingest can now save the generated archival files with the list of variable names added as the first tab-delimited line. @@ -110,12 +116,16 @@ This behavior is controlled by the new setting **:StoreIngestedTabularFilesWithV We are planning to add an API for converting existing legacy tabular files in a future release. +For more information, see #10282. + ### Uningest/Reingest Options Available in the File Page Edit Menu New Uningest/Reingest options are available in the File Page Edit menu. Ingest errors can be cleared by users who can published the associated dataset and by superusers, allowing for a successful ingest to be undone or retried (e.g. after a Dataverse version update or if ingest size limits are changed). The /api/files//uningest api also now allows users who can publish the dataset to undo an ingest failure. +For more information, see #10319. + ### Sphinx Guides Now Support Markdown Format and Tabs Our guides now support the Markdown format with the extension **.md**. Additionally, an option to create tabs in the guides using [Sphinx Tabs](https://sphinx-tabs.readthedocs.io) has been added. (You can see the tabs in action in the "dev usage" page of the Container Guide.) To continue building the guides, you will need to install this new dependency by re-running: @@ -123,10 +133,14 @@ Our guides now support the Markdown format with the extension **.md**. Additiona pip install -r requirements.txt ``` +For more information, see #10111. + ### Number of Concurrent Indexing Operations Now Configurable A new MicroProfile setting called `dataverse.solr.concurrency.max-async-indexes` has been added that controls the maximum number of simultaneously running asynchronous dataset index operations (defaults to 4). +For more information, see #10388. + [⬆️](#table-of-contents) ## 🪲 Bug fixes @@ -141,14 +155,14 @@ The permissions required to assign a role have been fixed. It is no longer possi ### Geospatial Metadata Block Fields for North and South Renamed -The Geospatial metadata block fields for north and south were labeled incorrectly as ‘Longitudes,’ as reported in #5645. After updating to this version of Dataverse, users will need to update all the endpoints that used ‘northLongitude’ and ‘southLongitude’ to ‘northLatitude’ and ‘southLatitude,’ respectively. +The Geospatial metadata block fields for north and south were labeled incorrectly as longitudes, as reported in #5645. After updating to this version of Dataverse, users will need to update any API client code used "northLongitude" and "southLongitude" to "northLatitude" and "southLatitude", respectively, as [mentioned](https://groups.google.com/g/dataverse-community/c/5qpOIZUSL6A/m/nlYGEXkYAAAJ) on the mailing list. ### OAI-PMH Error Handling Has Been Improved OAI-PMH error handling has been improved to display a machine-readable error in XML rather than a 500 error with no further information. -> - /oai?foo=bar will show "No argument 'verb' found" -> - /oai?verb=foo&verb=bar will show "Verb must be singular, given: '[foo, bar]'" +- /oai?foo=bar will show "No argument 'verb' found" +- /oai?verb=foo&verb=bar will show "Verb must be singular, given: '[foo, bar]'" ### Granting File Access Without Access Request @@ -233,7 +247,7 @@ It is now possible to retrieve via API the file citation as it appears on the fi This API is not for downloading various citation formats such as EndNote XML, RIS, or BibTeX. -For mor information check the documentation on [this link](https://guides.dataverse.org/en/6.2/api/native-api.html#get-file-citation-as-json) +For more information check the documentation on [this link](https://guides.dataverse.org/en/6.2/api/native-api.html#get-file-citation-as-json) ### Files Endpoint Extended @@ -282,7 +296,7 @@ Also in the context of containers, a new option to skip deployment has been adde ### Evaluation Version Tutorial on the Containers Guide -The Container Guide now containers a tutorial for running Dataverse in containers for demo or evaluation purposes: https://guides.dataverse.org/en/6.2/container +The Container Guide now containers a tutorial for running Dataverse in containers for demo or evaluation purposes: https://guides.dataverse.org/en/6.2/container/running/demo.html ### New QA Guide @@ -353,7 +367,7 @@ For help with upgrading, installing, or general questions please post to the [Da [⬆️](#table-of-contents) -## 💻 Upgrade instructions +## 💻 Upgrade Instructions Upgrading requires a maintenance window and downtime. Please plan ahead, create backups of your database, etc. These instructions assume that you've already upgraded through all the 5.x releases and are now running Dataverse 6.1. From c67b7679c4db714abae0adf31348a232bca2e780 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 28 Mar 2024 15:49:24 -0400 Subject: [PATCH 679/689] ask for the issue number and maybe PR number --- doc/sphinx-guides/source/developers/version-control.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/developers/version-control.rst b/doc/sphinx-guides/source/developers/version-control.rst index a9a60de380c..07922b56b86 100644 --- a/doc/sphinx-guides/source/developers/version-control.rst +++ b/doc/sphinx-guides/source/developers/version-control.rst @@ -119,7 +119,7 @@ Here's how to add a release note snippet to your pull request: - Create a Markdown file under ``doc/release-notes``. You can reuse the name of your branch and append ".md" to it, e.g. ``3728-doc-apipolicy-fix.md`` - Edit the snippet to include anything you think should be mentioned in the release notes. Please include the following if they apply: - - Descriptions of new features or bug fixed, including a link to the HTML preview of the docs you wrote (e.g. https://dataverse-guide--9939.org.readthedocs.build/en/9939/installation/config.html#smtp-email-configuration ) + - Descriptions of new features or bug fixed, including a link to the HTML preview of the docs you wrote (e.g. https://dataverse-guide--9939.org.readthedocs.build/en/9939/installation/config.html#smtp-email-configuration ) and the phrase "For more information, see #3728" (the issue number). If you know the PR number, you can add that too. - New configuration settings - Upgrade instructions - Etc. From ad91b5b62b8aef2adffad15166a565acac83273e Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 28 Mar 2024 16:32:34 -0400 Subject: [PATCH 680/689] Geospatial command fix --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 3c2a116ac7a..143215f1bdb 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -422,7 +422,7 @@ As noted above, deployment of the war file might take several minutes due a data ``` wget https://github.com/IQSS/dataverse/releases/download/v6.2/geospatial.tsv - curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/tab-separated-values" -X POST --upload-file @geospatial.tsv + curl http://localhost:8080/api/admin/datasetfield/load -H "Content-type: text/tab-separated-values" -X POST --upload-file scripts/api/data/metadatablocks/geospatial.tsv wget https://github.com/IQSS/dataverse/releases/download/v6.2/citation.tsv From ade981e2da11b1be68ddbf6ed0c1045f26e107d5 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 28 Mar 2024 17:06:24 -0400 Subject: [PATCH 681/689] Geospatial tooltip updated --- doc/release-notes/10397-geospatial-tooltip-fix.md | 2 ++ scripts/api/data/metadatablocks/geospatial.tsv | 8 ++++---- src/main/java/propertyFiles/geospatial.properties | 8 ++++---- 3 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 doc/release-notes/10397-geospatial-tooltip-fix.md diff --git a/doc/release-notes/10397-geospatial-tooltip-fix.md b/doc/release-notes/10397-geospatial-tooltip-fix.md new file mode 100644 index 00000000000..f3c707ed00a --- /dev/null +++ b/doc/release-notes/10397-geospatial-tooltip-fix.md @@ -0,0 +1,2 @@ +We have updated the tooltip for the Geospatial metadata where previously the use of comas was incorrectly suggested. + diff --git a/scripts/api/data/metadatablocks/geospatial.tsv b/scripts/api/data/metadatablocks/geospatial.tsv index 09d19c608e5..11408317410 100644 --- a/scripts/api/data/metadatablocks/geospatial.tsv +++ b/scripts/api/data/metadatablocks/geospatial.tsv @@ -8,10 +8,10 @@ otherGeographicCoverage Other Other information on the geographic coverage of the data. text 4 #VALUE, FALSE FALSE FALSE TRUE FALSE FALSE geographicCoverage geospatial geographicUnit Geographic Unit Lowest level of geographic aggregation covered by the Dataset, e.g., village, county, region. text 5 TRUE FALSE TRUE TRUE FALSE FALSE geospatial geographicBoundingBox Geographic Bounding Box The fundamental geometric description for any Dataset that models geography is the geographic bounding box. It describes the minimum box, defined by west and east longitudes and north and south latitudes, which includes the largest geographic extent of the Dataset's geographic coverage. This element is used in the first pass of a coordinate-based search. Inclusion of this element in the codebook is recommended, but is required if the bound polygon box is included. none 6 FALSE FALSE TRUE FALSE FALSE FALSE geospatial - westLongitude Westernmost (Left) Longitude Westernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -180,0 <= West Bounding Longitude Value <= 180,0. text 7 FALSE FALSE FALSE FALSE FALSE FALSE geographicBoundingBox geospatial - eastLongitude Easternmost (Right) Longitude Easternmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -180,0 <= East Bounding Longitude Value <= 180,0. text 8 FALSE FALSE FALSE FALSE FALSE FALSE geographicBoundingBox geospatial - northLatitude Northernmost (Top) Latitude Northernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -90,0 <= North Bounding Latitude Value <= 90,0. text 9 FALSE FALSE FALSE FALSE FALSE FALSE geographicBoundingBox geospatial - southLatitude Southernmost (Bottom) Latitude Southernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -90,0 <= South Bounding Latitude Value <= 90,0. text 10 FALSE FALSE FALSE FALSE FALSE FALSE geographicBoundingBox geospatial + westLongitude Westernmost (Left) Longitude Westernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -180.0 <= West Bounding Longitude Value <= 180.0. text 7 FALSE FALSE FALSE FALSE FALSE FALSE geographicBoundingBox geospatial + eastLongitude Easternmost (Right) Longitude Easternmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -180.0 <= East Bounding Longitude Value <= 180.0. text 8 FALSE FALSE FALSE FALSE FALSE FALSE geographicBoundingBox geospatial + northLatitude Northernmost (Top) Latitude Northernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -90.0 <= North Bounding Latitude Value <= 90.0. text 9 FALSE FALSE FALSE FALSE FALSE FALSE geographicBoundingBox geospatial + southLatitude Southernmost (Bottom) Latitude Southernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -90.0 <= South Bounding Latitude Value <= 90.0. text 10 FALSE FALSE FALSE FALSE FALSE FALSE geographicBoundingBox geospatial #controlledVocabulary DatasetField Value identifier displayOrder country Afghanistan 0 country Albania 1 diff --git a/src/main/java/propertyFiles/geospatial.properties b/src/main/java/propertyFiles/geospatial.properties index ce258071c27..2659c2a3cc9 100644 --- a/src/main/java/propertyFiles/geospatial.properties +++ b/src/main/java/propertyFiles/geospatial.properties @@ -19,10 +19,10 @@ datasetfieldtype.city.description=The name of the city that the Dataset is about datasetfieldtype.otherGeographicCoverage.description=Other information on the geographic coverage of the data. datasetfieldtype.geographicUnit.description=Lowest level of geographic aggregation covered by the Dataset, e.g., village, county, region. datasetfieldtype.geographicBoundingBox.description=The fundamental geometric description for any Dataset that models geography is the geographic bounding box. It describes the minimum box, defined by west and east longitudes and north and south latitudes, which includes the largest geographic extent of the Dataset's geographic coverage. This element is used in the first pass of a coordinate-based search. Inclusion of this element in the codebook is recommended, but is required if the bound polygon box is included. -datasetfieldtype.westLongitude.description=Westernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -180,0 <= West Bounding Longitude Value <= 180,0. -datasetfieldtype.eastLongitude.description=Easternmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -180,0 <= East Bounding Longitude Value <= 180,0. -datasetfieldtype.northLatitude.description=Northernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -90,0 <= North Bounding Latitude Value <= 90,0. -datasetfieldtype.southLatitude.description=Southernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -90,0 <= South Bounding Latitude Value <= 90,0. +datasetfieldtype.westLongitude.description=Westernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -180.0 <= West Bounding Longitude Value <= 180.0. +datasetfieldtype.eastLongitude.description=Easternmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -180.0 <= East Bounding Longitude Value <= 180.0. +datasetfieldtype.northLatitude.description=Northernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -90.0 <= North Bounding Latitude Value <= 90.0. +datasetfieldtype.southLatitude.description=Southernmost coordinate delimiting the geographic extent of the Dataset. A valid range of values, expressed in decimal degrees, is -90.0 <= South Bounding Latitude Value <= 90.0. datasetfieldtype.geographicCoverage.watermark= datasetfieldtype.country.watermark= datasetfieldtype.state.watermark= From 6810b593f8e5c346f2af3cbf541c7275eb2affee Mon Sep 17 00:00:00 2001 From: landreev Date: Thu, 28 Mar 2024 17:16:45 -0400 Subject: [PATCH 682/689] Update 10397-geospatial-tooltip-fix.md --- doc/release-notes/10397-geospatial-tooltip-fix.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/10397-geospatial-tooltip-fix.md b/doc/release-notes/10397-geospatial-tooltip-fix.md index f3c707ed00a..cfe2ee283ed 100644 --- a/doc/release-notes/10397-geospatial-tooltip-fix.md +++ b/doc/release-notes/10397-geospatial-tooltip-fix.md @@ -1,2 +1,2 @@ -We have updated the tooltip for the Geospatial metadata where previously the use of comas was incorrectly suggested. +We have updated the tooltips in the Geospatial metadata block, where the use of comas instead of dots in coordinate values was incorrectly suggested. From 25912c6ce08ea62a78d4a056f9e79c1f2f18b29a Mon Sep 17 00:00:00 2001 From: landreev Date: Thu, 28 Mar 2024 17:50:40 -0400 Subject: [PATCH 683/689] Update 10397-geospatial-tooltip-fix.md --- doc/release-notes/10397-geospatial-tooltip-fix.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/10397-geospatial-tooltip-fix.md b/doc/release-notes/10397-geospatial-tooltip-fix.md index cfe2ee283ed..0774dc1860e 100644 --- a/doc/release-notes/10397-geospatial-tooltip-fix.md +++ b/doc/release-notes/10397-geospatial-tooltip-fix.md @@ -1,2 +1,2 @@ -We have updated the tooltips in the Geospatial metadata block, where the use of comas instead of dots in coordinate values was incorrectly suggested. +We have updated the tooltips in the Geospatial metadata block, where the use of commas instead of dots in coordinate values was incorrectly suggested. From ee14b2686acc886924f3758c583edcbad1929aa5 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Fri, 29 Mar 2024 10:04:52 -0400 Subject: [PATCH 684/689] #10422 incorporate tooltip fix --- doc/release-notes/10397-geospatial-tooltip-fix.md | 2 -- doc/release-notes/6.2-release-notes.md | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 doc/release-notes/10397-geospatial-tooltip-fix.md diff --git a/doc/release-notes/10397-geospatial-tooltip-fix.md b/doc/release-notes/10397-geospatial-tooltip-fix.md deleted file mode 100644 index 0774dc1860e..00000000000 --- a/doc/release-notes/10397-geospatial-tooltip-fix.md +++ /dev/null @@ -1,2 +0,0 @@ -We have updated the tooltips in the Geospatial metadata block, where the use of commas instead of dots in coordinate values was incorrectly suggested. - diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 143215f1bdb..cc4b03bbc20 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -156,6 +156,7 @@ The permissions required to assign a role have been fixed. It is no longer possi ### Geospatial Metadata Block Fields for North and South Renamed The Geospatial metadata block fields for north and south were labeled incorrectly as longitudes, as reported in #5645. After updating to this version of Dataverse, users will need to update any API client code used "northLongitude" and "southLongitude" to "northLatitude" and "southLatitude", respectively, as [mentioned](https://groups.google.com/g/dataverse-community/c/5qpOIZUSL6A/m/nlYGEXkYAAAJ) on the mailing list. +Also, we have updated the tooltips in the Geospatial metadata block, where the use of commas instead of dots in coordinate values was incorrectly suggested. ### OAI-PMH Error Handling Has Been Improved From bd333910adbeccd98d372e19b526674a688ae76d Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Fri, 29 Mar 2024 10:13:29 -0400 Subject: [PATCH 685/689] #10442 add note for harvest redirects --- doc/release-notes/10254-fix-harvested-redirects.md | 1 - doc/release-notes/6.2-release-notes.md | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) delete mode 100644 doc/release-notes/10254-fix-harvested-redirects.md diff --git a/doc/release-notes/10254-fix-harvested-redirects.md b/doc/release-notes/10254-fix-harvested-redirects.md deleted file mode 100644 index 02ee5ddaf4d..00000000000 --- a/doc/release-notes/10254-fix-harvested-redirects.md +++ /dev/null @@ -1 +0,0 @@ -Redirects from search cards back to the original source for datasets harvested from "Generic OAI Archives", i.e. non-Dataverse OAI servers, have been fixed. diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index cc4b03bbc20..b14f769f20b 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -169,6 +169,10 @@ OAI-PMH error handling has been improved to display a machine-readable error in A bug introduced with the guestbook-at-request, requests are not deleted when granted, they are now given the state granted. +### Harvesting redirects fixed + +Redirects from search cards back to the original source for datasets harvested from "Generic OAI Archives", i.e. non-Dataverse OAI servers, have been fixed. + [⬆️](#table-of-contents) ## 💾 Persistence From 9d11921f86a23b646972b1ef6775c8cfa63fb612 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Fri, 29 Mar 2024 11:08:52 -0400 Subject: [PATCH 686/689] #10422 fix wording of note --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index b14f769f20b..5ca03e3231c 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -98,7 +98,7 @@ For more information, see #10382. ### Harvesting Handle Missing Controlled Values -Allows datasets to be harvested with Controlled Vocabulary Values that existed in the originating Dataverse Project but are not in the harvesting Dataverse Project. For more information, view the changes to the endpoint [here](#harvesting-client-endpoint-extended). +Allows datasets to be harvested with Controlled Vocabulary Values that existed in the originating Dataverse installation but are not in the harvesting Dataverse installation. For more information, view the changes to the endpoint [here](#harvesting-client-endpoint-extended). ### Add .QPJ and .QMD Extensions to Shapefile Handling From 934bdf128eed12eb2dc1be56ee1b0b9674e39bca Mon Sep 17 00:00:00 2001 From: landreev Date: Fri, 29 Mar 2024 12:20:37 -0400 Subject: [PATCH 687/689] Update 6.2-release-notes.md --- doc/release-notes/6.2-release-notes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 5ca03e3231c..2b95568105a 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -387,14 +387,14 @@ In the following commands we assume that Payara 6 is installed in `/usr/local/pa (or `setenv PAYARA /usr/local/payara6` if you are using a `csh`-like shell) -1\. Usually, when a Solr schema update is released, we recommend deploying the new version of Dataverse, then updating the `schema.xml` on the solr side. With 6.2, we recommend to install the base schema first. Without it Dataverse 6.2 is not going to be able to show any results after the initial deployment. If your instance is using any custom metadata blocks, you will need to further modify the schema, see the laset step of this instruction (step 8). +1\. Usually, when a Solr schema update is released, we recommend deploying the new version of Dataverse, then updating the `schema.xml` on the solr side. With 6.2, we recommend to install the base schema first. Without it Dataverse 6.2 is not going to be able to show any results after the initial deployment. If your instance is using any custom metadata blocks, you will need to further modify the schema, see the last step of this instruction (step 8). - Stop Solr instance (usually `service solr stop`, depending on Solr installation/OS, see the [Installation Guide](https://guides.dataverse.org/en/6.2/installation/prerequisites.html#solr-init-script)) - Replace schema.xml - - `cd /usr/local/solr/solr-9.3.0/server/solr/collection1/conf` - `wget https://raw.githubusercontent.com/IQSS/dataverse/master/conf/solr/9.3.0/schema.xml` + - `cp schema.xml /usr/local/solr/solr-9.3.0/server/solr/collection1/conf` - Start Solr instance (usually `service solr start`, depending on Solr/OS) From 91a9ae95400ae02556ad7dbcef6335b9c2d12e85 Mon Sep 17 00:00:00 2001 From: landreev Date: Fri, 29 Mar 2024 13:44:06 -0400 Subject: [PATCH 688/689] Update 6.2-release-notes.md --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index 2b95568105a..a80ee8d79e9 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -422,7 +422,7 @@ As noted above, deployment of the war file might take several minutes due a data - `service payara stop` - `service payara start` -7\. Update the following Metadata Blocks: +7\. Update the following Metadata Blocks to reflect the incremental improvements made to the handling of core metadata fields: ``` wget https://github.com/IQSS/dataverse/releases/download/v6.2/geospatial.tsv From 58d81648304949fce1f936e1a631c1147f93a3c0 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Fri, 29 Mar 2024 15:27:17 -0400 Subject: [PATCH 689/689] #10422 wording change --- doc/release-notes/6.2-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/6.2-release-notes.md b/doc/release-notes/6.2-release-notes.md index a80ee8d79e9..f694703f0a6 100644 --- a/doc/release-notes/6.2-release-notes.md +++ b/doc/release-notes/6.2-release-notes.md @@ -277,7 +277,7 @@ For more information visit the full native API guide on [this link](https://guid ### Endpoint Fixed: Datasets Metadata -The API endpoint `api/datasets/{id}/metadata` has been changed to default to the latest version of the dataset that the user has access. +The API endpoint `api/datasets/{id}/metadata` has been changed to default to the latest version of the dataset to which the user has access. ### Experimental Make Data Count processingState API