From 8b48693c0aa887bb98d798266b8fb3992d756fa9 Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Wed, 15 Jul 2026 13:55:30 -0400 Subject: [PATCH 01/36] validate any DOI URL sent --- .../COARNotifyRelationshipAnnouncement.java | 42 ++++++++++++++- ...OARNotifyRelationshipAnnouncementTest.java | 52 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/test/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncementTest.java diff --git a/src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java b/src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java index 84f5702cf40..fefeb23edf8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java @@ -9,6 +9,8 @@ import edu.harvard.iq.dataverse.UserNotificationServiceBean; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; +import edu.harvard.iq.dataverse.pidproviders.doi.UnmanagedDOIProvider; import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.util.json.JsonLDNamespace; import edu.harvard.iq.dataverse.util.json.JsonLDTerm; @@ -168,7 +170,11 @@ private ResourceMetadata retrieveResourceMetadata(String subjectId) { // Step 3: Retrieve and parse DataCite XML if (dataciteXmlUrl != null) { - parseDataCiteXml(dataciteXmlUrl, client, metadata); + if (isTrustedDataCiteUrl(dataciteXmlUrl)) { + parseDataCiteXml(dataciteXmlUrl, client, metadata); + } else { + logger.warning("DataCite XML URL is not from a trusted source: " + dataciteXmlUrl); + } } else { logger.fine("No DataCite XML URL found in Signposting links"); } @@ -232,6 +238,40 @@ private String extractDataCiteXmlUrl(CloseableHttpResponse headResponse) { return null; } + /** + * Validate that the URL is a trusted source for DataCite XML. + * Supports standard DOI resolvers and DataCite API. + */ + boolean isTrustedDataCiteUrl(String url) { + if (url == null || url.isBlank()) { + return false; + } + + String doiPart = null; + if (url.startsWith(AbstractDOIProvider.DOI_RESOLVER_URL)) { + doiPart = url.substring(AbstractDOIProvider.DOI_RESOLVER_URL.length()); + } else if (url.startsWith(AbstractDOIProvider.HTTP_DOI_RESOLVER_URL)) { + doiPart = url.substring(AbstractDOIProvider.HTTP_DOI_RESOLVER_URL.length()); + } else if (url.startsWith(AbstractDOIProvider.DXDOI_RESOLVER_URL)) { + doiPart = url.substring(AbstractDOIProvider.DXDOI_RESOLVER_URL.length()); + } else if (url.startsWith(AbstractDOIProvider.HTTP_DXDOI_RESOLVER_URL)) { + doiPart = url.substring(AbstractDOIProvider.HTTP_DXDOI_RESOLVER_URL.length()); + } else if (url.startsWith("https://api.datacite.org/dois/")) { + doiPart = url.substring("https://api.datacite.org/dois/".length()); + } else if (url.startsWith("http://api.datacite.org/dois/")) { + doiPart = url.substring("http://api.datacite.org/dois/".length()); + } else if (url.startsWith("https://api.test.datacite.org/dois/")) { + doiPart = url.substring("https://api.test.datacite.org/dois/".length()); + } else if (url.startsWith("http://api.test.datacite.org/dois/")) { + doiPart = url.substring("http://api.test.datacite.org/dois/".length()); + } + + if (doiPart != null) { + return (new UnmanagedDOIProvider()).parsePersistentId(AbstractDOIProvider.DOI_PROTOCOL, doiPart) != null; + } + return false; + } + /** * Parse DataCite XML to extract title and resource type. */ diff --git a/src/test/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncementTest.java b/src/test/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncementTest.java new file mode 100644 index 00000000000..97aeee4598c --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncementTest.java @@ -0,0 +1,52 @@ +package edu.harvard.iq.dataverse.api.ldn; + +import edu.harvard.iq.dataverse.DatasetServiceBean; +import edu.harvard.iq.dataverse.DataverseRoleServiceBean; +import edu.harvard.iq.dataverse.RoleAssigneeServiceBean; +import edu.harvard.iq.dataverse.UserNotificationServiceBean; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.*; + +public class COARNotifyRelationshipAnnouncementTest { + + private COARNotifyRelationshipAnnouncement handler; + + @BeforeEach + public void setUp() { + DatasetServiceBean datasetService = Mockito.mock(DatasetServiceBean.class); + UserNotificationServiceBean userNotificationService = Mockito.mock(UserNotificationServiceBean.class); + DataverseRoleServiceBean roleService = Mockito.mock(DataverseRoleServiceBean.class); + RoleAssigneeServiceBean roleAssigneeService = Mockito.mock(RoleAssigneeServiceBean.class); + handler = new COARNotifyRelationshipAnnouncement(datasetService, userNotificationService, roleService, roleAssigneeService); + } + + @Test + public void testIsTrustedDataCiteUrl() { + // Trusted DOI resolvers + assertTrue(handler.isTrustedDataCiteUrl("https://doi.org/10.7910/DVN/TJCLKP")); + assertTrue(handler.isTrustedDataCiteUrl("http://doi.org/10.7910/DVN/TJCLKP")); + assertTrue(handler.isTrustedDataCiteUrl("https://dx.doi.org/10.7910/DVN/TJCLKP")); + assertTrue(handler.isTrustedDataCiteUrl("http://dx.doi.org/10.7910/DVN/TJCLKP")); + + // DataCite API + assertTrue(handler.isTrustedDataCiteUrl("https://api.datacite.org/dois/10.7910/DVN/TJCLKP")); + assertTrue(handler.isTrustedDataCiteUrl("http://api.datacite.org/dois/10.7910/DVN/TJCLKP")); + assertTrue(handler.isTrustedDataCiteUrl("https://api.test.datacite.org/dois/10.7910/DVN/TJCLKP")); + assertTrue(handler.isTrustedDataCiteUrl("http://api.test.datacite.org/dois/10.7910/DVN/TJCLKP")); + + // Invalid DOIs + assertFalse(handler.isTrustedDataCiteUrl("https://doi.org/not-a-doi")); + assertFalse(handler.isTrustedDataCiteUrl("https://api.datacite.org/dois/not-a-doi")); + + // Untrusted sources + assertFalse(handler.isTrustedDataCiteUrl("https://example.com/metadata.xml")); + assertFalse(handler.isTrustedDataCiteUrl("https://malicious.org/doi.org/10.1234/5678")); + + // Null and empty + assertFalse(handler.isTrustedDataCiteUrl(null)); + assertFalse(handler.isTrustedDataCiteUrl("")); + } +} From 93f3c95d88a107f62bfea3dd0d9740266dc0ab00 Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Wed, 15 Jul 2026 13:59:16 -0400 Subject: [PATCH 02/36] note * not recommended for prod --- 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 13eed47b6c4..7ba3789d6c8 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -3926,8 +3926,10 @@ Linked Data Notifications (LDN) Allowed Hosts +++++++++++++++++++++++++++++++++++++++++++++ Dataverse supports receiving LDN notifications via the /api/inbox endpoint. The dataverse.ldn.allowed-hosts allows you to specify the list of host IP addresses from which LDN notifications can be received, or ``*`` to receive messages from anywhere. +Note that since the Inbox endpoint does not require authentication, allowing un-trusted hosts via ``*`` is not recommended for production. Example: ``dataverse.ldn.allowed-hosts=*`` +Example: ``dataverse.ldn.allowed-hosts=172.16.234.56,172.16.234.57`` COAR Notify Relationship Announcement Notify Superusers Only ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ From d10c0dcc15f49b31592a0d00d97818888d7c5f3e Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 21 Jul 2026 18:59:07 +0200 Subject: [PATCH 03/36] feat(ci): add SonarQube analysis and coverage reporting to GitHub Actions - Implemented SonarQube job in Maven CI workflow for enhanced code quality analysis. - Updated `pom.xml` and parent POM with SonarQube configuration properties and plugin definitions. - Refined JaCoCo configuration to support integrated coverage reporting. --- .github/workflows/maven_unit_test.yml | 27 ++++++++++++++++++++++++++- modules/dataverse-parent/pom.xml | 15 ++++++++++++++- pom.xml | 5 ++++- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/.github/workflows/maven_unit_test.yml b/.github/workflows/maven_unit_test.yml index a69d5c4ffe3..c1e298c400f 100644 --- a/.github/workflows/maven_unit_test.yml +++ b/.github/workflows/maven_unit_test.yml @@ -162,7 +162,32 @@ jobs: -DrepoToken=${COVERALLS_SECRET} -DpullRequest=${{ github.event.number }} jacoco:report coveralls:report - # NOTE: this may be extended with adding a report to the build output, leave a comment, send to Sonarcloud, ... + sonarqube: + runs-on: ubuntu-latest + needs: integration-test + name: SonarQube Analysis and Coverage + steps: + # TODO: As part of #10618 change to setup-maven custom action + # Basic setup chores + - uses: actions/checkout@v7 + - uses: actions/setup-java@v5 + with: + java-version: '21' + distribution: temurin + cache: maven + + # Get the build output from the integration test job + - uses: actions/download-artifact@v8 + with: + name: java-reportdir + - run: tar -xvf java-reportdir.tar + + # Run Sonarcube analysis + - name: Commit Data to SonarCloud + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: mvn -B org.sonarsource.scanner.maven:sonar-maven-plugin:sonar # TODO: Add a filter step here, that avoids calling the app image release workflow if there are changes to the base image. # Use https://github.com/dorny/paths-filter to solve this. Will require and additional job or adding to integration-test job. diff --git a/modules/dataverse-parent/pom.xml b/modules/dataverse-parent/pom.xml index a31b01328ae..f5ced53eb7c 100644 --- a/modules/dataverse-parent/pom.xml +++ b/modules/dataverse-parent/pom.xml @@ -173,7 +173,6 @@ 3.7.1 5.10.2 5.11.0 - 0.8.11 9.3 @@ -198,9 +197,18 @@ 3.2.2 1.6.13 1.7.0 + 5.7.0.6970 + 0.8.14 0.48.0 + + + https://sonarcloud.io + iqss + IQSS_dataverse + ${project.groupId}:${project.artifactId} + jacoco @@ -318,6 +326,11 @@ pomchecker-maven-plugin ${pomchecker-maven-plugin.version} + + org.sonarsource.scanner.maven + sonar-maven-plugin + ${sonar.version} + org.sonatype.plugins nexus-staging-maven-plugin diff --git a/pom.xml b/pom.xml index 50c44586ca3..6b18322c440 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,9 @@ -Ddummy.jacoco.property=true -Ddummy.jacoco.property=true + ${project.reporting.outputDirectory}/jacoco-merged-test-coverage-report + ${jacoco.merged.outputdir} + war @@ -1031,7 +1034,7 @@ ${project.build.directory}/coverage-reports/merged.exec - ${project.reporting.outputDirectory}/jacoco-merged-test-coverage-report + ${jacoco.merged.outputdir} ${skipIntegrationTests} From 39859008805978f3fe4dc6a788692a74439264fc Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 21 Jul 2026 19:41:58 +0200 Subject: [PATCH 04/36] chore(ci): update Maven CI workflow for SonarQube compatibility - Adjusted artifact handling to include binary class files required by SonarQube. - Set `fetch-depth` to 0 for full Git history to improve Sonar analysis. - Updated JaCoCo XML report path in `pom.xml` for accurate coverage reporting (Sonar needs the full path to the XML file). --- .github/workflows/maven_unit_test.yml | 19 ++++++++++++++----- pom.xml | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/maven_unit_test.yml b/.github/workflows/maven_unit_test.yml index c1e298c400f..7f689bb7b3c 100644 --- a/.github/workflows/maven_unit_test.yml +++ b/.github/workflows/maven_unit_test.yml @@ -147,7 +147,7 @@ jobs: # Get the build output from the integration test job - uses: actions/download-artifact@v8 with: - name: java-reportdir + name: java-reportdir - run: tar -xvf java-reportdir.tar # Deposit Code Coverage @@ -162,7 +162,7 @@ jobs: -DrepoToken=${COVERALLS_SECRET} -DpullRequest=${{ github.event.number }} jacoco:report coveralls:report - sonarqube: + sonarcloud: runs-on: ubuntu-latest needs: integration-test name: SonarQube Analysis and Coverage @@ -170,20 +170,29 @@ jobs: # TODO: As part of #10618 change to setup-maven custom action # Basic setup chores - uses: actions/checkout@v7 + with: + # Sonarcube benefits from a full Git history in it's analysis (assign issues, etc) + fetch-depth: 0 - uses: actions/setup-java@v5 with: java-version: '21' distribution: temurin cache: maven - # Get the build output from the integration test job + # Get the coverage output from the integration test job - uses: actions/download-artifact@v8 with: name: java-reportdir - - run: tar -xvf java-reportdir.tar + # Get the binary class files as Sonar needs them for analysis + - uses: actions/download-artifact@v8 + with: + name: java-artifacts + - run: | + tar -xvf java-builddir.tar + tar -xvf java-reportdir.tar # Run Sonarcube analysis - - name: Commit Data to SonarCloud + - name: Analyze and commit to SonarCloud env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/pom.xml b/pom.xml index 6b18322c440..d182abbc3e2 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ -Ddummy.jacoco.property=true ${project.reporting.outputDirectory}/jacoco-merged-test-coverage-report - ${jacoco.merged.outputdir} + ${jacoco.merged.outputdir}/jacoco.xml From 0f724e913489ae067166eda46b99b6f7c9216135 Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Tue, 21 Jul 2026 17:07:16 -0400 Subject: [PATCH 05/36] new setting to determine create behavior w.r.t. default license assignment --- doc/sphinx-guides/source/api/changelog.rst | 2 + .../source/installation/config.rst | 14 +++ .../iq/dataverse/settings/JvmSettings.java | 4 +- .../iq/dataverse/util/json/JsonParser.java | 99 +++++++++++-------- .../dataverse/util/json/JsonParserTest.java | 49 ++++++++- 5 files changed, 123 insertions(+), 45 deletions(-) diff --git a/doc/sphinx-guides/source/api/changelog.rst b/doc/sphinx-guides/source/api/changelog.rst index 10e368da51f..874df2b2ed1 100644 --- a/doc/sphinx-guides/source/api/changelog.rst +++ b/doc/sphinx-guides/source/api/changelog.rst @@ -15,6 +15,8 @@ v6.12 - **/api/admin/index/perms/{id}** +- Dataset creation API calls may now behave differently when neither a license nor terms are provided, depending on the new :ref:`dataverse.api.assume-default-license-when-not-provided-via-api` setting. + v6.11 ----- - The GET /api/mydata/retrieve, if the search returns no data, now includes the "data" block with 0 results. The message that was returned in "error_message" will be returned in "message" and the "success" will be `true`. All other errors will continue to reply with "success":false and the error message in "error_message". diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 13eed47b6c4..6ff6a4d7f95 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -3367,6 +3367,20 @@ Defaults to ``false``. Can also be set via any `supported MicroProfile Config API source`_, e.g. the environment variable ``DATAVERSE_API_ALLOW_INCOMPLETE_METADATA``. Will accept ``[tT][rR][uU][eE]|1|[oO][nN]`` as "true" expressions. +.. _dataverse.api.assume-default-license-when-not-provided-via-api: + +dataverse.api.assume-default-license-when-not-provided-via-api +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +When creating a dataset via API, if neither a license nor any terms of use are provided, this setting determines whether the default license is assigned. + +- If set to ``true``, the default license is assigned if no license and no terms are provided. +- If set to ``false``, no license is assigned (and no terms). +- If not set (default), the behavior depends on the :ref:`:AllowCustomTermsOfUse` setting: if custom terms are allowed, no license is assigned; if not allowed, the default license is assigned. + +Can also be set via any `supported MicroProfile Config API source`_, e.g. the environment variable +``DATAVERSE_API_ASSUME_DEFAULT_LICENSE_WHEN_NOT_PROVIDED_VIA_API``. Will accept ``[tT][rR][uU][eE]|1|[oO][nN]`` as "true" expressions. + .. _dataverse.api.blocked.endpoints: dataverse.api.blocked.endpoints 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 ed0d796a84a..cbcc42e1041 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java @@ -96,7 +96,7 @@ public enum JvmSettings { // API: MDC Citation updates SCOPE_API_MDC(SCOPE_API, "mdc"), API_MDC_UPDATE_MIN_DELAY_MS(SCOPE_API_MDC, "min-delay-ms"), - + ASSUME_DEFAULT_LICENSE_WHEN_NOT_PROVIDED_VIA_API(SCOPE_API, "assume-default-license-when-not-provided-via-api"), // SIGNPOSTING SETTINGS SCOPE_SIGNPOSTING(PREFIX, "signposting"), @@ -318,7 +318,7 @@ public enum JvmSettings { SCOPE_COARNOTIFY(SCOPE_LINKEDDATANOTIFICATION, "coar-notify"), SCOPE_COARNOTIFY_RELATIONSHIP_ANNOUNCEMENT(SCOPE_COARNOTIFY, "relationship-announcement"), COARNOTIFY_RELATIONSHIP_ANNOUNCEMENT_NOTIFY_SUPERUSERS_ONLY(SCOPE_COARNOTIFY_RELATIONSHIP_ANNOUNCEMENT, "notify-superusers-only"), - ; + ; private static final String SCOPE_SEPARATOR = "."; public static final String PLACEHOLDER_KEY = "%s"; 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 7f3db716456..1fde24f3852 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 @@ -17,6 +17,7 @@ import edu.harvard.iq.dataverse.license.License; import edu.harvard.iq.dataverse.license.LicenseServiceBean; import edu.harvard.iq.dataverse.settings.FeatureFlags; +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.StringUtil; @@ -470,25 +471,27 @@ public DatasetVersion parseDatasetVersion(JsonObject obj, DatasetVersion dsv) th License license = null; - try { - // This method will attempt to parse the license in the format - // in which it appears in our json exports, as a compound - // field, for ex.: - // "license": { - // "name": "CC0 1.0", - // "uri": "http://creativecommons.org/publicdomain/zero/1.0" - // } - license = parseLicense(obj.getJsonObject("license")); - } catch (ClassCastException cce) { - logger.fine("class cast exception parsing the license section (will try parsing as a string)"); - // attempt to parse as string: - // i.e. this is for backward compatibility, after the bug in #9155 - // was fixed, with the old style of encoding the license info - // in input json, for ex.: - // "license" : "CC0 1.0" - license = parseLicense(obj.getString("license", null)); + if (obj.containsKey("license")) { + try { + // This method will attempt to parse the license in the format + // in which it appears in our json exports, as a compound + // field, for ex.: + // "license": { + // "name": "CC0 1.0", + // "uri": "http://creativecommons.org/publicdomain/zero/1.0" + // } + license = parseLicense(obj.getJsonObject("license")); + } catch (ClassCastException cce) { + logger.fine("class cast exception parsing the license section (will try parsing as a string)"); + // attempt to parse as string: + // i.e. this is for backward compatibility, after the bug in #9155 + // was fixed, with the old style of encoding the license info + // in input json, for ex.: + // "license" : "CC0 1.0" + license = parseLicense(obj.getString("license", null)); + } } - + //test to see if license exists in dataset type //if not set it to null - //only test if Dataset has a type and if it has custom available licenses @@ -505,21 +508,43 @@ public DatasetVersion parseDatasetVersion(JsonObject obj, DatasetVersion dsv) th license = null; } } - } + } + + terms.setTermsOfUse(obj.getString("termsOfUse", null)); + terms.setConfidentialityDeclaration(obj.getString("confidentialityDeclaration", null)); + terms.setSpecialPermissions(obj.getString("specialPermissions", null)); + terms.setRestrictions(obj.getString("restrictions", null)); + terms.setCitationRequirements(obj.getString("citationRequirements", null)); + terms.setDepositorRequirements(obj.getString("depositorRequirements", null)); + terms.setConditions(obj.getString("conditions", null)); + terms.setDisclaimer(obj.getString("disclaimer", null)); if (license == null) { - terms.setLicense(license); - terms.setTermsOfUse(obj.getString("termsOfUse", null)); - terms.setConfidentialityDeclaration(obj.getString("confidentialityDeclaration", null)); - terms.setSpecialPermissions(obj.getString("specialPermissions", null)); - terms.setRestrictions(obj.getString("restrictions", null)); - terms.setCitationRequirements(obj.getString("citationRequirements", null)); - terms.setDepositorRequirements(obj.getString("depositorRequirements", null)); - terms.setConditions(obj.getString("conditions", null)); - terms.setDisclaimer(obj.getString("disclaimer", null)); - } else { - terms.setLicense(license); + // If no license was provided or the provided license was invalid, + // we check if terms were provided. + boolean termsProvided = terms.getTermsOfUse() != null + || terms.getConfidentialityDeclaration() != null + || terms.getSpecialPermissions() != null + || terms.getRestrictions() != null + || terms.getCitationRequirements() != null + || terms.getDepositorRequirements() != null + || terms.getConditions() != null + || terms.getDisclaimer() != null; + + Optional assumeDefault = JvmSettings.ASSUME_DEFAULT_LICENSE_WHEN_NOT_PROVIDED_VIA_API.lookupOptional(Boolean.class); + + if (assumeDefault.isPresent()) { + if (assumeDefault.get() && !termsProvided) { + license = licenseService.getDefault(); + } + } else { + // Default behavior (when setting not set) + if (!settingsService.isTrueForKey(SettingsServiceBean.Key.AllowCustomTermsOfUse, true)) { + license = licenseService.getDefault(); + } + } } + terms.setLicense(license); terms.setTermsOfAccess(obj.getString("termsOfAccess", null)); terms.setDataAccessPlace(obj.getString("dataAccessPlace", null)); terms.setOriginalArchive(obj.getString("originalArchive", null)); @@ -700,12 +725,7 @@ public GuestbookResponse parseGuestbookResponse(JsonObject obj, GuestbookRespons private edu.harvard.iq.dataverse.license.License parseLicense(String licenseNameOrUri) throws JsonParseException { if (licenseNameOrUri == null){ - boolean safeDefaultIfKeyNotFound = true; - if (settingsService.isTrueForKey(SettingsServiceBean.Key.AllowCustomTermsOfUse, safeDefaultIfKeyNotFound)){ - return null; - } else { - return licenseService.getDefault(); - } + return null; } License license = licenseService.getByNameOrUri(licenseNameOrUri); if (license == null) throw new JsonParseException("Invalid license: " + licenseNameOrUri); @@ -714,12 +734,7 @@ private edu.harvard.iq.dataverse.license.License parseLicense(String licenseName private edu.harvard.iq.dataverse.license.License parseLicense(JsonObject licenseObj) throws JsonParseException { if (licenseObj == null){ - boolean safeDefaultIfKeyNotFound = true; - if (settingsService.isTrueForKey(SettingsServiceBean.Key.AllowCustomTermsOfUse, safeDefaultIfKeyNotFound)){ - return null; - } else { - return licenseService.getDefault(); - } + return null; } String licenseName = licenseObj.getString("name", null); diff --git a/src/test/java/edu/harvard/iq/dataverse/util/json/JsonParserTest.java b/src/test/java/edu/harvard/iq/dataverse/util/json/JsonParserTest.java index 262aa38d5d0..f696e5fcd4c 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/json/JsonParserTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/json/JsonParserTest.java @@ -19,6 +19,7 @@ import edu.harvard.iq.dataverse.dataset.DatasetType; import edu.harvard.iq.dataverse.dataset.DatasetTypeServiceBean; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.license.License; import edu.harvard.iq.dataverse.license.LicenseServiceBean; import edu.harvard.iq.dataverse.mocks.MockDatasetFieldSvc; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; @@ -758,7 +759,53 @@ public boolean assertFieldsEqual( DatasetField ex, DatasetField act ) { throw new IllegalArgumentException("Unknown dataset field type '" + ex.getDatasetFieldType() + "'"); } + @Test + public void testParseDatasetVersion_LicenseAndTerms() throws JsonParseException { + // Prepare mocks + License defaultLicense = new License(); + defaultLicense.setName("CC0 1.0"); + Mockito.when(licenseService.getDefault()).thenReturn(defaultLicense); + + String baseJson = "{\"metadataBlocks\":{\"citation\":{\"fields\":[]}}}"; + + // Case 1: Setting not set, AllowCustomTermsOfUse = false -> should pick default + settingsSvc.setAllowCustomTermsOfUse(false); + System.clearProperty("dataverse.api.assume-default-license-when-not-provided-via-api"); + DatasetVersion dsv1 = sut.parseDatasetVersion(JsonUtil.getJsonObject(baseJson)); + assertEquals(defaultLicense, dsv1.getTermsOfUseAndAccess().getLicense()); + + // Case 2: Setting not set, AllowCustomTermsOfUse = true -> should NOT pick default + settingsSvc.setAllowCustomTermsOfUse(true); + DatasetVersion dsv2 = sut.parseDatasetVersion(JsonUtil.getJsonObject(baseJson)); + assertNull(dsv2.getTermsOfUseAndAccess().getLicense()); + + // Case 3: assumeDefaultLicenseWhenNotProvidedViaApi = true, terms NOT provided -> should pick default + System.setProperty("dataverse.api.assume-default-license-when-not-provided-via-api", "true"); + DatasetVersion dsv3 = sut.parseDatasetVersion(JsonUtil.getJsonObject(baseJson)); + assertEquals(defaultLicense, dsv3.getTermsOfUseAndAccess().getLicense()); + + // Case 4: assumeDefaultLicenseWhenNotProvidedViaApi = true, terms PROVIDED -> should NOT pick default + String jsonWithTerms = "{\"metadataBlocks\":{\"citation\":{\"fields\":[]}}, \"termsOfUse\":\"Some terms\"}"; + DatasetVersion dsv4 = sut.parseDatasetVersion(JsonUtil.getJsonObject(jsonWithTerms)); + assertNull(dsv4.getTermsOfUseAndAccess().getLicense()); + assertEquals("Some terms", dsv4.getTermsOfUseAndAccess().getTermsOfUse()); + + // Case 5: assumeDefaultLicenseWhenNotProvidedViaApi = false -> should NOT pick default + System.setProperty("dataverse.api.assume-default-license-when-not-provided-via-api", "false"); + DatasetVersion dsv5 = sut.parseDatasetVersion(JsonUtil.getJsonObject(baseJson)); + assertNull(dsv5.getTermsOfUseAndAccess().getLicense()); + + // Cleanup + System.clearProperty("dataverse.api.assume-default-license-when-not-provided-via-api"); + } + private static class MockSettingsSvc extends SettingsServiceBean { + private boolean allowCustomTermsOfUse = false; + + public void setAllowCustomTermsOfUse(boolean allow) { + this.allowCustomTermsOfUse = allow; + } + @Override public String getValueForKey( Key key /*, String defaultValue */) { switch (key) { @@ -775,7 +822,7 @@ public String getValueForKey( Key key /*, String defaultValue */) { @Override public boolean isTrueForKey(Key key, boolean safeDefaultIfKeyNotFound) { if (key == Key.AllowCustomTermsOfUse) { - return false; + return this.allowCustomTermsOfUse; } return safeDefaultIfKeyNotFound; } From e8716e50e0b1483cbdfb3fb034af1d43310d404a Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Tue, 21 Jul 2026 17:16:18 -0400 Subject: [PATCH 06/36] release note --- .../11161-assume-default-license-release-note.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 doc/release-notes/11161-assume-default-license-release-note.md diff --git a/doc/release-notes/11161-assume-default-license-release-note.md b/doc/release-notes/11161-assume-default-license-release-note.md new file mode 100644 index 00000000000..fb14f3874c9 --- /dev/null +++ b/doc/release-notes/11161-assume-default-license-release-note.md @@ -0,0 +1,9 @@ +### New JVM Option: assume-default-license-when-not-provided-via-api + +A new JVM option `dataverse.api.assume-default-license-when-not-provided-via-api` has been added. This option controls the behavior when creating a dataset via API without providing a license or terms of use. + +- When set to `true`, the default license will be automatically assigned if no license and no terms are provided in the input JSON. +- When set to `false`, no license (and no terms) will be assigned if none are provided. +- When not set (the default), the system maintains backward compatibility: the default license is assigned if custom terms are not allowed; if custom terms are allowed, no license is assigned. + +This allows administrators more control over whether datasets created via API must have a license or can be created without one when terms are also missing. From 72a5ad2b1215780a941b75ad0097316b8274aec1 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 22 Jul 2026 11:32:43 -0400 Subject: [PATCH 07/36] IQSS/12483 gbr req question fix (#12501) * fix required textarea in gbr * release note --- doc/release-notes/12483-gbr-req-question-fix.md | 1 + src/main/webapp/guestbook-terms-popup-fragment.xhtml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 doc/release-notes/12483-gbr-req-question-fix.md diff --git a/doc/release-notes/12483-gbr-req-question-fix.md b/doc/release-notes/12483-gbr-req-question-fix.md new file mode 100644 index 00000000000..1353fb93683 --- /dev/null +++ b/doc/release-notes/12483-gbr-req-question-fix.md @@ -0,0 +1 @@ +A bug that caused any required custom questions in guestbooks to still be optional has been fixed in the current UI. \ No newline at end of file diff --git a/src/main/webapp/guestbook-terms-popup-fragment.xhtml b/src/main/webapp/guestbook-terms-popup-fragment.xhtml index a17145a43ce..14e8fb551a6 100644 --- a/src/main/webapp/guestbook-terms-popup-fragment.xhtml +++ b/src/main/webapp/guestbook-terms-popup-fragment.xhtml @@ -232,7 +232,7 @@ From 2b69f8052e6236c9ed92e1c5d5a2cdf6a8697e13 Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Wed, 22 Jul 2026 12:42:09 -0400 Subject: [PATCH 08/36] only support full on/off --- ...161-assume-default-license-release-note.md | 9 ------ .../11161-do-not-assume-default-license.md | 8 +++++ doc/sphinx-guides/source/api/changelog.rst | 2 +- .../source/installation/config.rst | 23 ++++++-------- .../iq/dataverse/settings/FeatureFlags.java | 11 +++++++ .../iq/dataverse/settings/JvmSettings.java | 1 - .../iq/dataverse/util/json/JsonParser.java | 12 ++----- .../dataverse/util/json/JsonParserTest.java | 31 ++++++------------- 8 files changed, 41 insertions(+), 56 deletions(-) delete mode 100644 doc/release-notes/11161-assume-default-license-release-note.md create mode 100644 doc/release-notes/11161-do-not-assume-default-license.md diff --git a/doc/release-notes/11161-assume-default-license-release-note.md b/doc/release-notes/11161-assume-default-license-release-note.md deleted file mode 100644 index fb14f3874c9..00000000000 --- a/doc/release-notes/11161-assume-default-license-release-note.md +++ /dev/null @@ -1,9 +0,0 @@ -### New JVM Option: assume-default-license-when-not-provided-via-api - -A new JVM option `dataverse.api.assume-default-license-when-not-provided-via-api` has been added. This option controls the behavior when creating a dataset via API without providing a license or terms of use. - -- When set to `true`, the default license will be automatically assigned if no license and no terms are provided in the input JSON. -- When set to `false`, no license (and no terms) will be assigned if none are provided. -- When not set (the default), the system maintains backward compatibility: the default license is assigned if custom terms are not allowed; if custom terms are allowed, no license is assigned. - -This allows administrators more control over whether datasets created via API must have a license or can be created without one when terms are also missing. diff --git a/doc/release-notes/11161-do-not-assume-default-license.md b/doc/release-notes/11161-do-not-assume-default-license.md new file mode 100644 index 00000000000..5ac23b11584 --- /dev/null +++ b/doc/release-notes/11161-do-not-assume-default-license.md @@ -0,0 +1,8 @@ +### New Feature Flag: do-not-assume-default-license + +A new feature flag `do-not-assume-default-license` has been added. This flag controls the behavior when creating a dataset via API without providing a license or terms of use. + +- By default (flag disabled), the default license will be automatically assigned if no license and no terms are provided in the input JSON, regardless of whether custom terms are allowed. +- When enabled, no license (and no terms) will be assigned if none are provided. + +**Note:** Previously, if custom terms were allowed, the system would not assign a default license in this case. To retain that behavior, you must now enable this feature flag. diff --git a/doc/sphinx-guides/source/api/changelog.rst b/doc/sphinx-guides/source/api/changelog.rst index 874df2b2ed1..de6b8ed1bdd 100644 --- a/doc/sphinx-guides/source/api/changelog.rst +++ b/doc/sphinx-guides/source/api/changelog.rst @@ -15,7 +15,7 @@ v6.12 - **/api/admin/index/perms/{id}** -- Dataset creation API calls may now behave differently when neither a license nor terms are provided, depending on the new :ref:`dataverse.api.assume-default-license-when-not-provided-via-api` setting. +- Dataset creation API calls may now behave differently when neither a license nor terms are provided, depending on the new :ref:`dataverse.feature.do-not-assume-default-license` feature flag. v6.11 ----- diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 6ff6a4d7f95..facadc7ffbf 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -3367,20 +3367,6 @@ Defaults to ``false``. Can also be set via any `supported MicroProfile Config API source`_, e.g. the environment variable ``DATAVERSE_API_ALLOW_INCOMPLETE_METADATA``. Will accept ``[tT][rR][uU][eE]|1|[oO][nN]`` as "true" expressions. -.. _dataverse.api.assume-default-license-when-not-provided-via-api: - -dataverse.api.assume-default-license-when-not-provided-via-api -++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -When creating a dataset via API, if neither a license nor any terms of use are provided, this setting determines whether the default license is assigned. - -- If set to ``true``, the default license is assigned if no license and no terms are provided. -- If set to ``false``, no license is assigned (and no terms). -- If not set (default), the behavior depends on the :ref:`:AllowCustomTermsOfUse` setting: if custom terms are allowed, no license is assigned; if not allowed, the default license is assigned. - -Can also be set via any `supported MicroProfile Config API source`_, e.g. the environment variable -``DATAVERSE_API_ASSUME_DEFAULT_LICENSE_WHEN_NOT_PROVIDED_VIA_API``. Will accept ``[tT][rR][uU][eE]|1|[oO][nN]`` as "true" expressions. - .. _dataverse.api.blocked.endpoints: dataverse.api.blocked.endpoints @@ -4031,6 +4017,15 @@ dataverse.feature.api-bearer-auth-use-oauth-user-on-id-match Allows the use of an OAuth user account (GitHub, Google, or ORCID) when an identity match is found during API bearer authentication. This feature enables automatic association of an incoming IdP identity with an existing OAuth user account, bypassing the need for additional user registration steps. This feature only works when the feature flag ``api-bearer-auth`` is also enabled. **Caution: Enabling this flag could result in impersonation risks if (and only if) used with a misconfigured IdP.** +.. _dataverse.feature.do-not-assume-default-license: + +dataverse.feature.do-not-assume-default-license ++++++++++++++++++++++++++++++++++++++++++++++++ + +When creating a dataset via API, if neither a license nor any terms of use are provided, the system normally assigns the default license. If this feature flag is enabled, no license is assigned (and no terms) in this case. + +Defaults to ``false``. + .. _dataverse.feature.avoid-expensive-solr-join: dataverse.feature.avoid-expensive-solr-join diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java index 6ae5114015c..a170d24209d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java @@ -98,6 +98,17 @@ public enum FeatureFlags { */ API_BEARER_AUTH_USE_OAUTH_USER_ON_ID_MATCH("api-bearer-auth-use-oauth-user-on-id-match"), + /** + * When creating a dataset via API, if neither a license nor any terms of use are provided, + * the system normally assigns the default license. + * + *

If this feature flag is enabled, no license is assigned (and no terms) in this case.

+ * + * @apiNote Raise flag by setting "dataverse.feature.do-not-assume-default-license" + * @since Dataverse 6.5 + */ + DO_NOT_ASSUME_DEFAULT_LICENSE("do-not-assume-default-license"), + /** * For published (public) objects, don't use a join when searching Solr. * Experimental! Requires a reindex with the following feature flag enabled, 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 cbcc42e1041..4c614ee2233 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java @@ -96,7 +96,6 @@ public enum JvmSettings { // API: MDC Citation updates SCOPE_API_MDC(SCOPE_API, "mdc"), API_MDC_UPDATE_MIN_DELAY_MS(SCOPE_API_MDC, "min-delay-ms"), - ASSUME_DEFAULT_LICENSE_WHEN_NOT_PROVIDED_VIA_API(SCOPE_API, "assume-default-license-when-not-provided-via-api"), // SIGNPOSTING SETTINGS SCOPE_SIGNPOSTING(PREFIX, "signposting"), 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 1fde24f3852..cd29b0b00c5 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 @@ -17,7 +17,6 @@ import edu.harvard.iq.dataverse.license.License; import edu.harvard.iq.dataverse.license.LicenseServiceBean; import edu.harvard.iq.dataverse.settings.FeatureFlags; -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.StringUtil; @@ -531,15 +530,8 @@ public DatasetVersion parseDatasetVersion(JsonObject obj, DatasetVersion dsv) th || terms.getConditions() != null || terms.getDisclaimer() != null; - Optional assumeDefault = JvmSettings.ASSUME_DEFAULT_LICENSE_WHEN_NOT_PROVIDED_VIA_API.lookupOptional(Boolean.class); - - if (assumeDefault.isPresent()) { - if (assumeDefault.get() && !termsProvided) { - license = licenseService.getDefault(); - } - } else { - // Default behavior (when setting not set) - if (!settingsService.isTrueForKey(SettingsServiceBean.Key.AllowCustomTermsOfUse, true)) { + if (!FeatureFlags.DO_NOT_ASSUME_DEFAULT_LICENSE.enabled()) { + if (!termsProvided) { license = licenseService.getDefault(); } } diff --git a/src/test/java/edu/harvard/iq/dataverse/util/json/JsonParserTest.java b/src/test/java/edu/harvard/iq/dataverse/util/json/JsonParserTest.java index f696e5fcd4c..f5d19400462 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/json/JsonParserTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/json/JsonParserTest.java @@ -768,35 +768,24 @@ public void testParseDatasetVersion_LicenseAndTerms() throws JsonParseException String baseJson = "{\"metadataBlocks\":{\"citation\":{\"fields\":[]}}}"; - // Case 1: Setting not set, AllowCustomTermsOfUse = false -> should pick default - settingsSvc.setAllowCustomTermsOfUse(false); - System.clearProperty("dataverse.api.assume-default-license-when-not-provided-via-api"); + // Case 1: Flag false (default), terms NOT provided -> should pick default + System.setProperty("dataverse.feature.do-not-assume-default-license", "false"); DatasetVersion dsv1 = sut.parseDatasetVersion(JsonUtil.getJsonObject(baseJson)); assertEquals(defaultLicense, dsv1.getTermsOfUseAndAccess().getLicense()); - // Case 2: Setting not set, AllowCustomTermsOfUse = true -> should NOT pick default - settingsSvc.setAllowCustomTermsOfUse(true); - DatasetVersion dsv2 = sut.parseDatasetVersion(JsonUtil.getJsonObject(baseJson)); + // Case 2: Flag false (default), terms PROVIDED -> should NOT pick default + String jsonWithTerms = "{\"metadataBlocks\":{\"citation\":{\"fields\":[]}}, \"termsOfUse\":\"Some terms\"}"; + DatasetVersion dsv2 = sut.parseDatasetVersion(JsonUtil.getJsonObject(jsonWithTerms)); assertNull(dsv2.getTermsOfUseAndAccess().getLicense()); + assertEquals("Some terms", dsv2.getTermsOfUseAndAccess().getTermsOfUse()); - // Case 3: assumeDefaultLicenseWhenNotProvidedViaApi = true, terms NOT provided -> should pick default - System.setProperty("dataverse.api.assume-default-license-when-not-provided-via-api", "true"); + // Case 3: Flag true, terms NOT provided -> should NOT pick default + System.setProperty("dataverse.feature.do-not-assume-default-license", "true"); DatasetVersion dsv3 = sut.parseDatasetVersion(JsonUtil.getJsonObject(baseJson)); - assertEquals(defaultLicense, dsv3.getTermsOfUseAndAccess().getLicense()); - - // Case 4: assumeDefaultLicenseWhenNotProvidedViaApi = true, terms PROVIDED -> should NOT pick default - String jsonWithTerms = "{\"metadataBlocks\":{\"citation\":{\"fields\":[]}}, \"termsOfUse\":\"Some terms\"}"; - DatasetVersion dsv4 = sut.parseDatasetVersion(JsonUtil.getJsonObject(jsonWithTerms)); - assertNull(dsv4.getTermsOfUseAndAccess().getLicense()); - assertEquals("Some terms", dsv4.getTermsOfUseAndAccess().getTermsOfUse()); - - // Case 5: assumeDefaultLicenseWhenNotProvidedViaApi = false -> should NOT pick default - System.setProperty("dataverse.api.assume-default-license-when-not-provided-via-api", "false"); - DatasetVersion dsv5 = sut.parseDatasetVersion(JsonUtil.getJsonObject(baseJson)); - assertNull(dsv5.getTermsOfUseAndAccess().getLicense()); + assertNull(dsv3.getTermsOfUseAndAccess().getLicense()); // Cleanup - System.clearProperty("dataverse.api.assume-default-license-when-not-provided-via-api"); + System.clearProperty("dataverse.feature.do-not-assume-default-license"); } private static class MockSettingsSvc extends SettingsServiceBean { From a018fdbc3d8379759d3fb66499bf69d45a9bd266 Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Wed, 22 Jul 2026 15:00:46 -0400 Subject: [PATCH 09/36] fix tests, fully enforce no license and terms in TofU&A --- .../iq/dataverse/TermsOfUseAndAccess.java | 24 +++++++++ .../iq/dataverse/util/json/JsonParser.java | 37 ++++++------- .../iq/dataverse/TermsOfUseAndAccessTest.java | 54 +++++++++++++++++++ 3 files changed, 97 insertions(+), 18 deletions(-) create mode 100644 src/test/java/edu/harvard/iq/dataverse/TermsOfUseAndAccessTest.java diff --git a/src/main/java/edu/harvard/iq/dataverse/TermsOfUseAndAccess.java b/src/main/java/edu/harvard/iq/dataverse/TermsOfUseAndAccess.java index 9e48c6c0165..c91533e8677 100644 --- a/src/main/java/edu/harvard/iq/dataverse/TermsOfUseAndAccess.java +++ b/src/main/java/edu/harvard/iq/dataverse/TermsOfUseAndAccess.java @@ -161,6 +161,9 @@ public String getTermsOfUse() { public void setTermsOfUse(String termsOfUse) { this.termsOfUse = termsOfUse; + if (termsOfUse != null) { + this.license = null; + } } public String getTermsOfAccess() { @@ -177,6 +180,9 @@ public String getConfidentialityDeclaration() { public void setConfidentialityDeclaration(String confidentialityDeclaration) { this.confidentialityDeclaration = confidentialityDeclaration; + if (confidentialityDeclaration != null) { + this.license = null; + } } public String getSpecialPermissions() { @@ -185,6 +191,9 @@ public String getSpecialPermissions() { public void setSpecialPermissions(String specialPermissions) { this.specialPermissions = specialPermissions; + if (specialPermissions != null) { + this.license = null; + } } public String getRestrictions() { @@ -193,6 +202,9 @@ public String getRestrictions() { public void setRestrictions(String restrictions) { this.restrictions = restrictions; + if (restrictions != null) { + this.license = null; + } } public String getCitationRequirements() { @@ -201,6 +213,9 @@ public String getCitationRequirements() { public void setCitationRequirements(String citationRequirements) { this.citationRequirements = citationRequirements; + if (citationRequirements != null) { + this.license = null; + } } public String getDepositorRequirements() { @@ -209,6 +224,9 @@ public String getDepositorRequirements() { public void setDepositorRequirements(String depositorRequirements) { this.depositorRequirements = depositorRequirements; + if (depositorRequirements != null) { + this.license = null; + } } public String getConditions() { @@ -217,6 +235,9 @@ public String getConditions() { public void setConditions(String conditions) { this.conditions = conditions; + if (conditions != null) { + this.license = null; + } } public String getDisclaimer() { @@ -225,6 +246,9 @@ public String getDisclaimer() { public void setDisclaimer(String disclaimer) { this.disclaimer = disclaimer; + if (disclaimer != null) { + this.license = null; + } } public String getDataAccessPlace() { 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 cd29b0b00c5..bcd7839d908 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 @@ -491,24 +491,6 @@ public DatasetVersion parseDatasetVersion(JsonObject obj, DatasetVersion dsv) th } } - //test to see if license exists in dataset type - //if not set it to null - - //only test if Dataset has a type and if it has custom available licenses - if (dsv.getDataset() != null) { - DatasetType dst = dsv.getDataset().getDatasetType(); - if (dst != null && dst.getLicenses() != null && !dst.getLicenses().isEmpty() && license != null) { - boolean invalidLicense = true; - for (License testLicense : dst.getLicenses()) { - if (testLicense.equals(license)) { - invalidLicense = false; - } - } - if (invalidLicense) { - license = null; - } - } - } - terms.setTermsOfUse(obj.getString("termsOfUse", null)); terms.setConfidentialityDeclaration(obj.getString("confidentialityDeclaration", null)); terms.setSpecialPermissions(obj.getString("specialPermissions", null)); @@ -547,6 +529,25 @@ public DatasetVersion parseDatasetVersion(JsonObject obj, DatasetVersion dsv) th terms.setFileAccessRequest(obj.getBoolean("fileAccessRequest", false)); dsv.setTermsOfUseAndAccess(terms); terms.setDatasetVersion(dsv); + + //test to see if license exists in dataset type + //if not set it to null - + //only test if Dataset has a type and if it has custom available licenses + if (dsv.getDataset() != null) { + DatasetType dst = dsv.getDataset().getDatasetType(); + if (dst != null && dst.getLicenses() != null && !dst.getLicenses().isEmpty() && license != null) { + boolean invalidLicense = true; + for (License testLicense : dst.getLicenses()) { + if (testLicense.equals(license)) { + invalidLicense = false; + } + } + if (invalidLicense) { + license = null; + } + } + } + JsonObject metadataBlocks = obj.getJsonObject("metadataBlocks"); if (metadataBlocks == null){ throw new JsonParseException(BundleUtil.getStringFromBundle("jsonparser.error.metadatablocks.not.found")); diff --git a/src/test/java/edu/harvard/iq/dataverse/TermsOfUseAndAccessTest.java b/src/test/java/edu/harvard/iq/dataverse/TermsOfUseAndAccessTest.java new file mode 100644 index 00000000000..cb542db4b3c --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/TermsOfUseAndAccessTest.java @@ -0,0 +1,54 @@ +package edu.harvard.iq.dataverse; + +import org.junit.jupiter.api.Test; +import edu.harvard.iq.dataverse.license.License; +import static org.junit.jupiter.api.Assertions.*; + +public class TermsOfUseAndAccessTest { + + @Test + public void testLicenseAndTermsMutualExclusivity() { + License license = new License(); + license.setName("CC0"); + + TermsOfUseAndAccess terms = new TermsOfUseAndAccess(); + + // 1. Setting license should clear terms + terms.setTermsOfUse("Some terms"); + assertEquals("Some terms", terms.getTermsOfUse()); + assertNull(terms.getLicense()); + + terms.setLicense(license); + assertNull(terms.getTermsOfUse()); + assertEquals(license, terms.getLicense()); + + // 2. Setting terms should clear license + terms.setTermsOfUse("New terms"); + assertNull(terms.getLicense()); + assertEquals("New terms", terms.getTermsOfUse()); + + // 3. Test other fields clear license too + terms.setLicense(license); + terms.setConfidentialityDeclaration("Confidential"); + assertNull(terms.getLicense()); + assertEquals("Confidential", terms.getConfidentialityDeclaration()); + } + + @Test + public void testCopyTermsOfUseAndAccess() { + License license = new License(); + license.setName("CC0"); + + TermsOfUseAndAccess terms = new TermsOfUseAndAccess(); + terms.setLicense(license); + + TermsOfUseAndAccess copy = terms.copyTermsOfUseAndAccess(); + assertEquals(license, copy.getLicense()); + assertNull(copy.getTermsOfUse()); + + terms.setTermsOfUse("Some terms"); + TermsOfUseAndAccess copy2 = terms.copyTermsOfUseAndAccess(); + assertNull(copy2.getLicense()); + assertEquals("Some terms", copy2.getTermsOfUse()); + } +} From f9b46f3b5f508b144bbb39c851edf9d9a89400c6 Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Wed, 22 Jul 2026 16:53:22 -0400 Subject: [PATCH 10/36] move DatasetType logic before setLicense() --- .../iq/dataverse/util/json/JsonParser.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 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 bcd7839d908..e6e0c71c396 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 @@ -518,17 +518,6 @@ public DatasetVersion parseDatasetVersion(JsonObject obj, DatasetVersion dsv) th } } } - terms.setLicense(license); - terms.setTermsOfAccess(obj.getString("termsOfAccess", null)); - terms.setDataAccessPlace(obj.getString("dataAccessPlace", null)); - terms.setOriginalArchive(obj.getString("originalArchive", null)); - terms.setAvailabilityStatus(obj.getString("availabilityStatus", null)); - terms.setContactForAccess(obj.getString("contactForAccess", null)); - terms.setSizeOfCollection(obj.getString("sizeOfCollection", null)); - terms.setStudyCompletion(obj.getString("studyCompletion", null)); - terms.setFileAccessRequest(obj.getBoolean("fileAccessRequest", false)); - dsv.setTermsOfUseAndAccess(terms); - terms.setDatasetVersion(dsv); //test to see if license exists in dataset type //if not set it to null - @@ -548,6 +537,18 @@ public DatasetVersion parseDatasetVersion(JsonObject obj, DatasetVersion dsv) th } } + terms.setLicense(license); + terms.setTermsOfAccess(obj.getString("termsOfAccess", null)); + terms.setDataAccessPlace(obj.getString("dataAccessPlace", null)); + terms.setOriginalArchive(obj.getString("originalArchive", null)); + terms.setAvailabilityStatus(obj.getString("availabilityStatus", null)); + terms.setContactForAccess(obj.getString("contactForAccess", null)); + terms.setSizeOfCollection(obj.getString("sizeOfCollection", null)); + terms.setStudyCompletion(obj.getString("studyCompletion", null)); + terms.setFileAccessRequest(obj.getBoolean("fileAccessRequest", false)); + dsv.setTermsOfUseAndAccess(terms); + terms.setDatasetVersion(dsv); + JsonObject metadataBlocks = obj.getJsonObject("metadataBlocks"); if (metadataBlocks == null){ throw new JsonParseException(BundleUtil.getStringFromBundle("jsonparser.error.metadatablocks.not.found")); From b485249231feb43c70459884089b9af7b4c3beea Mon Sep 17 00:00:00 2001 From: Jo Pol Date: Wed, 22 Jul 2026 22:56:27 +0200 Subject: [PATCH 11/36] NPE for preview of draft file with retention period (#12434) * retention null pointer exception * review --- src/main/webapp/file.xhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/file.xhtml b/src/main/webapp/file.xhtml index 50133aefa17..73379009456 100644 --- a/src/main/webapp/file.xhtml +++ b/src/main/webapp/file.xhtml @@ -567,7 +567,7 @@ #{bundle['file.metadataTab.fileMetadata.publicationDate.label']} - #{(empty FilePage.file.embargo) and !(empty FilePage.file.released) and !(empty FilePage.file.retention) ? ''.concat(FilePage.file.publicationDateFormattedYYYYMMDD).concat('; ') : ''} + #{(empty FilePage.file.embargo) and FilePage.file.released and !(empty FilePage.file.retention) ? ''.concat(FilePage.file.publicationDateFormattedYYYYMMDD).concat('; ') : ''} #{!(empty FilePage.file.embargo) ? ''.concat(FilePage.embargoPhrase).concat(' ').concat(FilePage.file.embargo.dateAvailable) : ''} #{!(empty FilePage.file.embargo) and !(empty FilePage.file.retention) ? '; ': ''} #{!(empty FilePage.file.retention) ? ''.concat(FilePage.retentionPhrase) From c6cfad187a985ebd91d7e9560520f36c14ef9c32 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:40:05 +0000 Subject: [PATCH 12/36] chore(deps): bump org.postgresql:postgresql in /modules/dataverse-parent Bumps [org.postgresql:postgresql](https://github.com/pgjdbc/pgjdbc) from 42.7.11 to 42.7.12. - [Release notes](https://github.com/pgjdbc/pgjdbc/releases) - [Changelog](https://github.com/pgjdbc/pgjdbc/blob/master/CHANGELOG.md) - [Commits](https://github.com/pgjdbc/pgjdbc/compare/REL42.7.11...REL42.7.12) --- updated-dependencies: - dependency-name: org.postgresql:postgresql dependency-version: 42.7.12 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- 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 a31b01328ae..fb0735319c4 100644 --- a/modules/dataverse-parent/pom.xml +++ b/modules/dataverse-parent/pom.xml @@ -150,7 +150,7 @@ 7.2026.2 - 42.7.11 + 42.7.12 9.8.0 16 2.33.0 From 9f1bcd126f0be1ff1da9fb3571d0a57131786907 Mon Sep 17 00:00:00 2001 From: jo-pol Date: Mon, 27 Jul 2026 10:59:05 +0200 Subject: [PATCH 13/36] Release note - about fixed NPE for draft file preview - typo in another file name --- doc/release-notes/12433-NPE-draft-prefiew | 1 + doc/release-notes/{12513-podman-md => 12513-podman.md} | 0 2 files changed, 1 insertion(+) create mode 100644 doc/release-notes/12433-NPE-draft-prefiew rename doc/release-notes/{12513-podman-md => 12513-podman.md} (100%) diff --git a/doc/release-notes/12433-NPE-draft-prefiew b/doc/release-notes/12433-NPE-draft-prefiew new file mode 100644 index 00000000000..76b71932887 --- /dev/null +++ b/doc/release-notes/12433-NPE-draft-prefiew @@ -0,0 +1 @@ +This release fixes a null pointer exception when previewing a draft file with a retention period. \ No newline at end of file diff --git a/doc/release-notes/12513-podman-md b/doc/release-notes/12513-podman.md similarity index 100% rename from doc/release-notes/12513-podman-md rename to doc/release-notes/12513-podman.md From f03a14f14d81c9072c39aeda958d48d8d7b55b31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 15:06:37 +0000 Subject: [PATCH 14/36] chore(deps): bump org.omnifaces:omnifaces from 4.7.5 to 4.7.12 Bumps [org.omnifaces:omnifaces](https://github.com/omnifaces/omnifaces) from 4.7.5 to 4.7.12. - [Commits](https://github.com/omnifaces/omnifaces/commits) --- updated-dependencies: - dependency-name: org.omnifaces:omnifaces dependency-version: 4.7.12 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 50c44586ca3..bc4878332ea 100644 --- a/pom.xml +++ b/pom.xml @@ -357,7 +357,7 @@ org.omnifaces omnifaces - 4.7.5 + 4.7.12 From 85639e5047b79226436620226e050c309daa95e2 Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Mon, 27 Jul 2026 12:42:08 -0400 Subject: [PATCH 15/36] updates per review, dropping support for api urls --- .../ldn/COARNotifyRelationshipAnnouncement.java | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java b/src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java index fefeb23edf8..cfeb034782b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java @@ -12,7 +12,6 @@ import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; import edu.harvard.iq.dataverse.pidproviders.doi.UnmanagedDOIProvider; import edu.harvard.iq.dataverse.settings.JvmSettings; -import edu.harvard.iq.dataverse.util.json.JsonLDNamespace; import edu.harvard.iq.dataverse.util.json.JsonLDTerm; import edu.harvard.iq.dataverse.util.json.JsonUtil; @@ -72,7 +71,6 @@ public COARNotifyRelationshipAnnouncement( * Process a COAR Notify Relationship Announcement message. * * @param msgObject The JSON-LD message object - * @return true if the message was successfully processed, false otherwise */ public void processMessage(JsonObject msgObject) { // Extract subject, object, and relationship from the message @@ -195,10 +193,6 @@ private ResourceMetadata retrieveResourceMetadata(String subjectId) { return metadata; } - /** - * Extract DataCite XML URL from Signposting Link headers. - */ - /** * Extract DataCite XML URL from Signposting Link headers. */ @@ -246,6 +240,7 @@ boolean isTrustedDataCiteUrl(String url) { if (url == null || url.isBlank()) { return false; } + url = url.toLowerCase(); String doiPart = null; if (url.startsWith(AbstractDOIProvider.DOI_RESOLVER_URL)) { @@ -256,14 +251,6 @@ boolean isTrustedDataCiteUrl(String url) { doiPart = url.substring(AbstractDOIProvider.DXDOI_RESOLVER_URL.length()); } else if (url.startsWith(AbstractDOIProvider.HTTP_DXDOI_RESOLVER_URL)) { doiPart = url.substring(AbstractDOIProvider.HTTP_DXDOI_RESOLVER_URL.length()); - } else if (url.startsWith("https://api.datacite.org/dois/")) { - doiPart = url.substring("https://api.datacite.org/dois/".length()); - } else if (url.startsWith("http://api.datacite.org/dois/")) { - doiPart = url.substring("http://api.datacite.org/dois/".length()); - } else if (url.startsWith("https://api.test.datacite.org/dois/")) { - doiPart = url.substring("https://api.test.datacite.org/dois/".length()); - } else if (url.startsWith("http://api.test.datacite.org/dois/")) { - doiPart = url.substring("http://api.test.datacite.org/dois/".length()); } if (doiPart != null) { From 119cf32f6f792b0a9e8f75ed1accf2ad1a977a71 Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Mon, 27 Jul 2026 13:02:35 -0400 Subject: [PATCH 16/36] update test --- .../ldn/COARNotifyRelationshipAnnouncementTest.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncementTest.java b/src/test/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncementTest.java index 97aeee4598c..c0f2de888e8 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncementTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncementTest.java @@ -31,15 +31,9 @@ public void testIsTrustedDataCiteUrl() { assertTrue(handler.isTrustedDataCiteUrl("https://dx.doi.org/10.7910/DVN/TJCLKP")); assertTrue(handler.isTrustedDataCiteUrl("http://dx.doi.org/10.7910/DVN/TJCLKP")); - // DataCite API - assertTrue(handler.isTrustedDataCiteUrl("https://api.datacite.org/dois/10.7910/DVN/TJCLKP")); - assertTrue(handler.isTrustedDataCiteUrl("http://api.datacite.org/dois/10.7910/DVN/TJCLKP")); - assertTrue(handler.isTrustedDataCiteUrl("https://api.test.datacite.org/dois/10.7910/DVN/TJCLKP")); - assertTrue(handler.isTrustedDataCiteUrl("http://api.test.datacite.org/dois/10.7910/DVN/TJCLKP")); - - // Invalid DOIs + // Invalid DOI assertFalse(handler.isTrustedDataCiteUrl("https://doi.org/not-a-doi")); - assertFalse(handler.isTrustedDataCiteUrl("https://api.datacite.org/dois/not-a-doi")); + // Untrusted sources assertFalse(handler.isTrustedDataCiteUrl("https://example.com/metadata.xml")); From 5cfcb494438eea207eb55bf172f5bbc087149aa5 Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Wed, 29 Jul 2026 17:36:41 -0400 Subject: [PATCH 17/36] fix test - was using illegal TofUandA --- .../DatasetVersionDifferenceTest.java | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/DatasetVersionDifferenceTest.java b/src/test/java/edu/harvard/iq/dataverse/DatasetVersionDifferenceTest.java index 4ca4a984c2a..bbe303b470a 100644 --- a/src/test/java/edu/harvard/iq/dataverse/DatasetVersionDifferenceTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/DatasetVersionDifferenceTest.java @@ -77,7 +77,7 @@ public void testDifferencing() { datasetVersion2.setTermsOfUseAndAccess(new TermsOfUseAndAccess()); datasetVersion2.getTermsOfUseAndAccess().setLicense(license); datasetVersion.setFileMetadatas(new ArrayList<>()); - + // Published version's two files DataFile dataFile = new DataFile(); dataFile.setId(1L); @@ -91,7 +91,7 @@ public void testDifferencing() { List fileMetadatas = new ArrayList<>(Arrays.asList(fileMetadata1, fileMetadata2)); datasetVersion.setFileMetadatas(fileMetadatas); - + // Draft version - same two files with one label change FileMetadata fileMetadata3 = fileMetadata1.createCopyInVersion(datasetVersion2); fileMetadata3.setId(30L); @@ -184,37 +184,43 @@ public void testDifferencing() { // Set a term field - datasetVersion2.getTermsOfUseAndAccess().setTermsOfUse("Terms o' Use"); + datasetVersion2.getTermsOfUseAndAccess().setTermsOfAccess("Terms o' Access"); String[] termField = new String[] { - BundleUtil.getStringFromBundle("file.dataFilesTab.terms.list.termsOfUse.header"), "", "Terms o' Use" }; + BundleUtil.getStringFromBundle("file.dataFilesTab.terms.list.termsOfAccess.termsOfsAccess"), "", "Terms o' Access" }; changedTerms.add(termField); compareResults(datasetVersion, datasetVersion2, expectedAddedFiles, expectedRemovedFiles, expectedChangedFileMetadata, expectedChangedVariableMetadata, expectedReplacedFiles, changedTerms); - // Set a term field in the original version - + // Set a term field in the original version that will also remove the license + changedTerms = new ArrayList<>(); datasetVersion.getTermsOfUseAndAccess().setDisclaimer("Not our fault"); String[] termField2 = new String[] { BundleUtil.getStringFromBundle("file.dataFilesTab.terms.list.termsOfUse.addInfo.disclaimer"), "Not our fault", "" }; + + String[] termField3 = new String[] { + BundleUtil.getStringFromBundle("file.dataFilesTab.terms.list.license"), + BundleUtil.getStringFromBundle("license.none.chosen"), license.getName() }; + changedTerms.add(termField3); changedTerms.add(termField2); + changedTerms.add(termField); compareResults(datasetVersion, datasetVersion2, expectedAddedFiles, expectedRemovedFiles, expectedChangedFileMetadata, expectedChangedVariableMetadata, expectedReplacedFiles, changedTerms); - - // Change License in Draft version + + // Change License in Draft version (from no license), which resets Disclaimer to null, but leaves termsOfAccess alone datasetVersion2.getTermsOfUseAndAccess().setLicense(license2); - datasetVersion2.getTermsOfUseAndAccess().setTermsOfUse(""); - datasetVersion.getTermsOfUseAndAccess().setDisclaimer(""); - - String[] termField3 = new String[] { + + String[] termField4 = new String[] { BundleUtil.getStringFromBundle("file.dataFilesTab.terms.list.license"), - "CC0 1.0", "CC BY 4.0" }; + BundleUtil.getStringFromBundle("license.none.chosen"), license2.getName() }; changedTerms = new ArrayList<>(); - changedTerms.add(termField3); - + changedTerms.add(termField4); + changedTerms.add(termField2); + changedTerms.add(termField); + compareResults(datasetVersion, datasetVersion2, expectedAddedFiles, expectedRemovedFiles, expectedChangedFileMetadata, expectedChangedVariableMetadata, expectedReplacedFiles, changedTerms); @@ -238,7 +244,7 @@ private FileMetadata createFileMetadata(long id, DatasetVersion datasetVersion, * correct (i.e. the manually created expected* parameters are set correctly for * each use case), we could drop running the originalCalculateDifference method * and just compare with the expected* results. - * + * * @param changedTerms */ private void compareResults(DatasetVersion datasetVersion, DatasetVersion datasetVersion2, @@ -425,10 +431,10 @@ public void testCompareVersionsAsJson() { assertTrue("true".equalsIgnoreCase(dataFile.getString("fileChanges[0].changed[0].newValue"))); assertTrue("disclaimer".equalsIgnoreCase(dataFile.getString("TermsOfAccess.changed[0].newValue"))); } - + @Test public void testGetSummaryAsJson(){ - + Dataverse dv = new Dataverse(); Dataset ds = new Dataset(); ds.setOwner(dv); @@ -462,23 +468,23 @@ public void testGetSummaryAsJson(){ dv2.getFileMetadatas().remove(1); DatasetVersionDifference dvd = dv2.getDefaultVersionDifference(); - + JsonObjectBuilder json = dvd.getSummaryDifferenceAsJson(); JsonObject obj = json.build(); JsonPath dataFile = JsonPath.from(JsonUtil.prettyPrint(obj)); - + assertTrue("true".equalsIgnoreCase(dataFile.getString("termsAccessChanged"))); assertEquals(1,(Long.parseLong(dataFile.getString("files.changedFileMetaData")))); assertEquals(0,(Long.parseLong(dataFile.getString("testMetadataBlock.deleted")))); assertEquals(1, (int) (Long.parseLong(dataFile.getString("testMetadataBlock.added")))); assertEquals(1,(Long.parseLong(dataFile.getString("files.added")))); - + } - - + + private DatasetVersion initDatasetVersion(Long id, Dataset ds, DatasetVersion.VersionState vs) { DatasetVersion dv = new DatasetVersion(); dv.setDataset(ds); From e7f075121a21d068610de76b63261bc2c0be0779 Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Wed, 29 Jul 2026 17:37:03 -0400 Subject: [PATCH 18/36] remove obsolete test - can't have license and TofU --- .../iq/dataverse/util/FileUtilTest.java | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/util/FileUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/util/FileUtilTest.java index af3ef6694fd..c55d0ddad8b 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/FileUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/FileUtilTest.java @@ -95,24 +95,6 @@ public void testIsDownloadPopupRequiredLicenseCC0() { assertFalse(FileUtil.isDownloadPopupRequired(dsv1)); } - @Test - public void testIsDownloadPopupRequiredHasTermsOfUseAndCc0License() { - DatasetVersion dsv1 = new DatasetVersion(); - dsv1.setVersionState(DatasetVersion.VersionState.RELEASED); - TermsOfUseAndAccess termsOfUseAndAccess = new TermsOfUseAndAccess(); - /** - * @todo Ask if setting the license to CC0 should be enough to not show - * the popup when the are Terms of Use. This feels like a bug since the - * Terms of Use should probably be shown. - */ - License license = new License("CC0", "You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission.", URI.create("http://creativecommons.org/publicdomain/zero/1.0"), URI.create("/resources/images/cc0.png"), true, 2l); - license.setDefault(true); - termsOfUseAndAccess.setLicense(license); - termsOfUseAndAccess.setTermsOfUse("be excellent to each other"); - dsv1.setTermsOfUseAndAccess(termsOfUseAndAccess); - assertFalse(FileUtil.isDownloadPopupRequired(dsv1)); - } - @Test public void testIsDownloadPopupRequiredHasTermsOfUseAndNoneLicense() { DatasetVersion dsv1 = new DatasetVersion(); From c361b33b29ab7685e61b81df8ba35efa31f6603d Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Wed, 29 Jul 2026 17:43:02 -0400 Subject: [PATCH 19/36] note the enforcement of license OR custom terms on setters --- .../edu/harvard/iq/dataverse/TermsOfUseAndAccess.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/TermsOfUseAndAccess.java b/src/main/java/edu/harvard/iq/dataverse/TermsOfUseAndAccess.java index c91533e8677..c1e76aee2ab 100644 --- a/src/main/java/edu/harvard/iq/dataverse/TermsOfUseAndAccess.java +++ b/src/main/java/edu/harvard/iq/dataverse/TermsOfUseAndAccess.java @@ -151,6 +151,7 @@ public License getLicense() { public void setLicense(License license) { this.license = license; if(license!=null) { + //Enforce restriction that customTerms can't be used with a license clearCustomTermsVariables(); } } @@ -162,6 +163,7 @@ public String getTermsOfUse() { public void setTermsOfUse(String termsOfUse) { this.termsOfUse = termsOfUse; if (termsOfUse != null) { + //Enforce restriction that customTerms can't be used with a license this.license = null; } } @@ -181,6 +183,7 @@ public String getConfidentialityDeclaration() { public void setConfidentialityDeclaration(String confidentialityDeclaration) { this.confidentialityDeclaration = confidentialityDeclaration; if (confidentialityDeclaration != null) { + //Enforce restriction that customTerms can't be used with a license this.license = null; } } @@ -192,6 +195,7 @@ public String getSpecialPermissions() { public void setSpecialPermissions(String specialPermissions) { this.specialPermissions = specialPermissions; if (specialPermissions != null) { + //Enforce restriction that customTerms can't be used with a license this.license = null; } } @@ -203,6 +207,7 @@ public String getRestrictions() { public void setRestrictions(String restrictions) { this.restrictions = restrictions; if (restrictions != null) { + //Enforce restriction that customTerms can't be used with a license this.license = null; } } @@ -214,6 +219,7 @@ public String getCitationRequirements() { public void setCitationRequirements(String citationRequirements) { this.citationRequirements = citationRequirements; if (citationRequirements != null) { + //Enforce restriction that customTerms can't be used with a license this.license = null; } } @@ -225,6 +231,7 @@ public String getDepositorRequirements() { public void setDepositorRequirements(String depositorRequirements) { this.depositorRequirements = depositorRequirements; if (depositorRequirements != null) { + //Enforce restriction that customTerms can't be used with a license this.license = null; } } @@ -236,6 +243,7 @@ public String getConditions() { public void setConditions(String conditions) { this.conditions = conditions; if (conditions != null) { + //Enforce restriction that customTerms can't be used with a license this.license = null; } } @@ -247,6 +255,7 @@ public String getDisclaimer() { public void setDisclaimer(String disclaimer) { this.disclaimer = disclaimer; if (disclaimer != null) { + //Enforce restriction that customTerms can't be used with a license this.license = null; } } From 897584456222189efb94d2705dffed8de34944e8 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 29 Jul 2026 17:43:52 -0400 Subject: [PATCH 20/36] Apply suggestion from @pdurbin Co-authored-by: Philip Durbin --- .../java/edu/harvard/iq/dataverse/settings/FeatureFlags.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java index 330b6b1c3f3..fa12e3d69d8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java @@ -105,7 +105,7 @@ public enum FeatureFlags { *

If this feature flag is enabled, no license is assigned (and no terms) in this case.

* * @apiNote Raise flag by setting "dataverse.feature.do-not-assume-default-license" - * @since Dataverse 6.5 + * @since Dataverse 6.12 */ DO_NOT_ASSUME_DEFAULT_LICENSE("do-not-assume-default-license"), From e2dc20a1061d974189ec2762f0e20409842fcb48 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 31 Jul 2026 15:39:54 +0200 Subject: [PATCH 21/36] chore(ci): limit SonarQube job to upstream repository #12570 --- .github/workflows/maven_unit_test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/maven_unit_test.yml b/.github/workflows/maven_unit_test.yml index 7f689bb7b3c..6915d534644 100644 --- a/.github/workflows/maven_unit_test.yml +++ b/.github/workflows/maven_unit_test.yml @@ -166,6 +166,8 @@ jobs: runs-on: ubuntu-latest needs: integration-test name: SonarQube Analysis and Coverage + # Only run in upstream repo - avoid unnecessary runs in forks (they don't have access to SONAR_TOKEN) + if: ${{ github.repository_owner == 'IQSS' }} steps: # TODO: As part of #10618 change to setup-maven custom action # Basic setup chores From d94e01d5ef6329018d823d465d90fbaff5e2ce37 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 31 Jul 2026 15:44:41 +0200 Subject: [PATCH 22/36] style(build): fix minor typos in pom.xml --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 0989fafbc62..297f630ceea 100644 --- a/pom.xml +++ b/pom.xml @@ -143,10 +143,10 @@ + false + + true + + + jdk-non-portable + jdk-internal + + + + + + ${project.basedir}/forbiddenapis.txt + + ${target.java.version} + + + + for-classes + process-classes + + check + + + + for-test-classes + process-test-classes + + testCheck + + + +
From 467025c5fa9cfba8b8b2c0d872e7669bd58b3d40 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 17 Jul 2026 16:35:37 +0200 Subject: [PATCH 29/36] build: update rules to refine JSON-P API restrictions Disallow more methods for object and array builders using copy-style arguments. --- forbiddenapis.txt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/forbiddenapis.txt b/forbiddenapis.txt index 2a9eecf4435..15251a07448 100644 --- a/forbiddenapis.txt +++ b/forbiddenapis.txt @@ -1,3 +1,9 @@ -@defaultMessage Use edu.harvard.iq.dataverse.util.json.JsonUtil.createX() instead +@defaultMessage Use edu.harvard.iq.dataverse.util.json.JsonUtil.createArrayBuilder(...) instead jakarta.json.Json#createArrayBuilder() -jakarta.json.Json#createObjectBuilder() \ No newline at end of file +jakarta.json.Json#createArrayBuilder(java.util.Collection) +jakarta.json.Json#createArrayBuilder(jakarta.json.JsonArray) + +@defaultMessage Use edu.harvard.iq.dataverse.util.json.JsonUtil.createObjectBuilder(...) instead +jakarta.json.Json#createObjectBuilder() +jakarta.json.Json#createObjectBuilder(java.util.Map) +jakarta.json.Json#createObjectBuilder(jakarta.json.JsonObject) \ No newline at end of file From b209941daf4b3ee7efe96218697da5985770dca4 Mon Sep 17 00:00:00 2001 From: "t.jansen" Date: Tue, 21 Jul 2026 13:11:14 +0200 Subject: [PATCH 30/36] perf(json): replaced cached builder factory in JsonUtil with cached JsonProvider. replaced all remaining Json.createObjectBuilder() and Json.createArrayBuilder() calls with the matching JsonUtil methods. --- .../edu/harvard/iq/dataverse/DatasetPage.java | 2 +- .../harvard/iq/dataverse/DatasetVersion.java | 2 +- .../iq/dataverse/api/AbstractApiBean.java | 2 +- .../edu/harvard/iq/dataverse/api/Admin.java | 2 +- .../harvard/iq/dataverse/api/Datasets.java | 2 +- .../api/util/JsonResponseBuilder.java | 2 +- .../impl/DRSSubmitToArchiveCommand.java | 7 +-- .../search/PostExternalSearchServiceBean.java | 3 +- .../iq/dataverse/util/bagit/OREMap.java | 2 +- .../iq/dataverse/util/json/JsonPrinter.java | 2 +- .../iq/dataverse/util/json/JsonUtil.java | 45 +++++++++++++++++-- .../harvard/iq/dataverse/api/DatasetsIT.java | 4 +- .../croissant/CroissantExportUtilTest.java | 2 +- 13 files changed, 58 insertions(+), 19 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index 08a4625d224..2c78873fa71 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -6126,7 +6126,7 @@ public String getCroissant() { reviewsJsonObj = commandEngine.submit(new GetDatasetReviewsCommand(dvRequestService.getDataverseRequest(), dataset)); JsonObjectBuilder reviews = CroissantExportUtil.getReviews(reviewsJsonObj); JsonObject croissantJson = JsonUtil.getJsonObject(croissant); - String updatedContent = Json.createObjectBuilder(croissantJson) + String updatedContent = JsonUtil.createObjectBuilder(croissantJson) .add("reviews", reviews.build().getJsonArray("reviews")).build().toString(); return updatedContent; } catch (CommandException e) { diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java b/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java index 58de0dad113..ef998bf9b30 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java @@ -390,7 +390,7 @@ public void setArchivalCopyLocation(String location) { // Convenience method to just change the status without changing the location public void setArchivalStatusOnly(String status) { populateArchivalStatus(false); - JsonObjectBuilder job = Json.createObjectBuilder(archivalCopyLocationJson); + JsonObjectBuilder job = JsonUtil.createObjectBuilder(archivalCopyLocationJson); job.add(DatasetVersion.ARCHIVAL_STATUS, status); archivalCopyLocationJson = job.build(); archivalCopyLocation = JsonUtil.prettyPrint(archivalCopyLocationJson); 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 c572b7fae93..6efcecb72a1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -1148,7 +1148,7 @@ protected Response badRequest(String msg, Map fieldErrors) { .entity(NullSafeJsonBuilder.jsonObjectBuilder() .add(ApiConstants.STATUS_FIELD, ApiConstants.STATUS_ERROR) .add(ApiConstants.MESSAGE_FIELD, msg) - .add("fieldErrors", Json.createObjectBuilder(fieldErrors).build()) + .add("fieldErrors", JsonUtil.createObjectBuilder(fieldErrors).build()) .build() ) .type(MediaType.APPLICATION_JSON_TYPE) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java index cec4a4dca6b..38209ebdd94 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java @@ -2826,7 +2826,7 @@ public Response getFeatureFlags() { for (FeatureFlags flag : FeatureFlags.values()) { map.put(flag.name(), flag.enabled() ? "enabled" : "disabled"); } - return ok(Json.createObjectBuilder(map)); + return ok(JsonUtil.createObjectBuilder(map)); } @GET 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 ff560f32371..1061eae6f30 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -296,7 +296,7 @@ public Response exportDataset(@Context ContainerRequestContext crc, @Parameter(d .getReviews(commandEngine.submit(new GetDatasetReviewsCommand(req, dataset))); String content = new String(is.readAllBytes(), StandardCharsets.UTF_8); JsonObject croissantJson = JsonUtil.getJsonObject(content); - String updatedContent = Json.createObjectBuilder(croissantJson) + String updatedContent = JsonUtil.createObjectBuilder(croissantJson) .add("reviews", reviews.build().getJsonArray("reviews")).build().toString(); is = new ByteArrayInputStream(updatedContent.getBytes(StandardCharsets.UTF_8)); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/util/JsonResponseBuilder.java b/src/main/java/edu/harvard/iq/dataverse/api/util/JsonResponseBuilder.java index ae84bf89bc0..b4a0715172e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/util/JsonResponseBuilder.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/util/JsonResponseBuilder.java @@ -221,7 +221,7 @@ public JsonResponseBuilder log(Logger logger, Level level, Optional e // This is necessary because we need to build in two places: logging and response creation. // Without cloning the object builder, we'd end up with an empty entity in the response when logging before that. JsonObject entity = this.entityBuilder.build(); - this.entityBuilder = Json.createObjectBuilder(entity); + this.entityBuilder = JsonUtil.createObjectBuilder(entity); StringBuilder metadata = new StringBuilder(); entity.forEach((k,v) -> metadata.append("_").append(k).append("=").append(v.toString()).append(";")); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DRSSubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DRSSubmitToArchiveCommand.java index 4ff38fcfdb0..e55667ee211 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DRSSubmitToArchiveCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DRSSubmitToArchiveCommand.java @@ -42,7 +42,6 @@ import jakarta.ejb.TransactionAttribute; import jakarta.ejb.TransactionAttributeType; -import jakarta.json.Json; import jakarta.json.JsonObject; import jakarta.json.JsonObjectBuilder; import jakarta.json.JsonValue; @@ -64,6 +63,8 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTCreationException; +import org.omnifaces.util.Json; + import static edu.harvard.iq.dataverse.settings.SettingsServiceBean.Key.DRSArchiverConfig; @RequiredPermissions(Permission.PublishDataset) @@ -212,7 +213,7 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, String dat job.add(S3_PATH, spaceName); // We start with the default admin_metadata - JsonObjectBuilder amob = Json.createObjectBuilder(adminMetadata); + JsonObjectBuilder amob = JsonUtil.createObjectBuilder(adminMetadata); // Remove collections and then override any params for the given alias amob.remove(COLLECTIONS); // Allow override of bucket name @@ -295,7 +296,7 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, String dat String status = responseObject.getString(DatasetVersion.ARCHIVAL_STATUS); if (status.equals(DatasetVersion.ARCHIVAL_STATUS_PENDING) || status.equals(DatasetVersion.ARCHIVAL_STATUS_FAILURE) || status.equals(DatasetVersion.ARCHIVAL_STATUS_SUCCESS)) { - statusObject.addAll(Json.createObjectBuilder(responseObject)); + statusObject.addAll(JsonUtil.createObjectBuilder(responseObject)); switch (status) { case DatasetVersion.ARCHIVAL_STATUS_PENDING: logger.info("DRS Ingest successfully started for: " + packageId + " : " diff --git a/src/main/java/edu/harvard/iq/dataverse/search/PostExternalSearchServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/search/PostExternalSearchServiceBean.java index f7e9fe5fb0f..b03941695d5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/PostExternalSearchServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/PostExternalSearchServiceBean.java @@ -1,5 +1,6 @@ package edu.harvard.iq.dataverse.search; +import edu.harvard.iq.dataverse.util.json.JsonUtil; import jakarta.ejb.Stateless; import jakarta.inject.Named; import jakarta.json.Json; @@ -35,7 +36,7 @@ private JsonObject prepareQuery(String query, int paginationStart, int numResult // Create JSON object with search parameters JsonObject searchParams = return NullSafeJsonBuilder.jsonObjectBuilder().add("query", query) - .add("filterQueries", Json.createArrayBuilder(filterQueries)).add("sortField", sortField) + .add("filterQueries", JsonUtil.createArrayBuilder(filterQueries)).add("sortField", sortField) .add("sortOrder", sortOrder).add("paginationStart", paginationStart) .add("onlyDataRelatedToMe", onlyDataRelatedToMe).add("numResultsPerPage", numResultsPerPage) .add("geoPoint", geoPoint).add("geoRadius", geoRadius).build(); diff --git a/src/main/java/edu/harvard/iq/dataverse/util/bagit/OREMap.java b/src/main/java/edu/harvard/iq/dataverse/util/bagit/OREMap.java index b6c57a1edfc..152bfcf5b9b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/bagit/OREMap.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/bagit/OREMap.java @@ -507,7 +507,7 @@ private static void addCvocValue(String val, JsonArrayBuilder vals, JsonObject c } JsonObject cachedValue = datasetFieldService.getExternalVocabularyValue(val); if (cachedValue != null) { - JsonObjectBuilder job = Json.createObjectBuilder(cachedValue); + JsonObjectBuilder job = JsonUtil.createObjectBuilder(cachedValue); job.add("@id", val); JsonObject extVal = job.build(); logger.fine("Adding: " + extVal); 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 619a703e8ea..f2716fe20d2 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 @@ -1888,7 +1888,7 @@ public static JsonObjectBuilder jsonLanguage(String locale, String title) { public static JsonArrayBuilder jsonLanguage(Map langMap) { // returns an array of metadatalanguages - return Json.createArrayBuilder(langMap.entrySet().stream().map(entry -> jsonLanguage(entry.getKey(), entry.getValue())).toList()); + return JsonUtil.createArrayBuilder(langMap.entrySet().stream().map(entry -> jsonLanguage(entry.getKey(), entry.getValue())).toList()); } public static JsonArrayBuilder jsonDatasetVersionSummaries(List summaries) { diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonUtil.java index 0c29334dbc2..5e04ae9442f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonUtil.java @@ -6,6 +6,7 @@ import java.io.InputStream; import java.io.StringReader; import java.io.StringWriter; +import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.logging.Logger; @@ -18,10 +19,10 @@ import jakarta.json.JsonReader; import jakarta.json.JsonException; import jakarta.json.JsonValue; +import jakarta.json.spi.JsonProvider; import jakarta.json.stream.JsonGenerator; import jakarta.json.JsonObjectBuilder; import jakarta.json.JsonArrayBuilder; -import jakarta.json.JsonBuilderFactory; public class JsonUtil { @@ -178,7 +179,7 @@ public static JsonValue getJsonValue(String serializedJson) { *

See also JSON-P #26, * Yasson #698 and others.

*/ - private static final JsonBuilderFactory builderFactory = Json.createBuilderFactory(Map.of()); + private static final JsonProvider provider = JsonProvider.provider(); /** * Create a new {@link JsonObjectBuilder} from a cached provider instance. @@ -186,7 +187,25 @@ public static JsonValue getJsonValue(String serializedJson) { * @return the JSON Object Builder */ public static JsonObjectBuilder createObjectBuilder() { - return builderFactory.createObjectBuilder(); + return provider.createObjectBuilder(); + } + + /** + * Create a new {@link JsonObjectBuilder}, initialized with the specified object from a cached provider instance. + * {@link Json#createObjectBuilder()} drop-in replacement, avoiding classpath-rescan on invocation. + * @return the JSON Object Builder + */ + public static JsonObjectBuilder createObjectBuilder(JsonObject object) { + return provider.createObjectBuilder(object); + } + + /** + * Create a new {@link JsonObjectBuilder}, initialized with the data from specified map from a cached provider instance. + * {@link Json#createObjectBuilder()} drop-in replacement, avoiding classpath-rescan on invocation. + * @return the JSON Object Builder + */ + public static JsonObjectBuilder createObjectBuilder(Map map) { + return provider.createObjectBuilder(map); } /** @@ -195,6 +214,24 @@ public static JsonObjectBuilder createObjectBuilder() { * @return the JSON Array Builder */ public static JsonArrayBuilder createArrayBuilder() { - return builderFactory.createArrayBuilder(); + return provider.createArrayBuilder(); + } + + /** + * Create a new {@link JsonArrayBuilder}, initialized with the specified array from a cached provider instance. + * {@link Json#createArrayBuilder()} drop-in replacement, avoiding classpath-rescan on invocation. + * @return the JSON Array Builder + */ + public static JsonArrayBuilder createArrayBuilder(JsonArray array) { + return provider.createArrayBuilder(array); + } + + /** + * Create a new {@link JsonArrayBuilder}, initialized with the content of specified collection from a cached provider instance. + * {@link Json#createArrayBuilder()} drop-in replacement, avoiding classpath-rescan on invocation. + * @return the JSON Array Builder + */ + public static JsonArrayBuilder createArrayBuilder(Collection collection) { + return provider.createArrayBuilder(collection); } } 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 0c38b8b5fa3..3e870f887cc 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -3672,7 +3672,7 @@ public void testSemanticMetadataAPIs() { String expectedJsonLD = UtilIT.getDatasetJson("scripts/search/tests/data/dataset-finch1.jsonld"); - jo = Json.createObjectBuilder(jo).remove("@id").remove("http://schema.org/dateModified").build(); + jo = JsonUtil.createObjectBuilder(jo).remove("@id").remove("http://schema.org/dateModified").build(); String jsonLD = jo.toString(); // ToDo: Are the static pars as expected @@ -3851,7 +3851,7 @@ public void testReCreateDataset() { JsonObject jsonLD = JSONLDUtil.decontextualizeJsonLD(jsonLDString); JsonObject expectedJsonLD = JSONLDUtil.decontextualizeJsonLD(expectedString); - expectedJsonLD = Json.createObjectBuilder(expectedJsonLD).remove("@id").remove("http://schema.org/dateModified") + expectedJsonLD = JsonUtil.createObjectBuilder(expectedJsonLD).remove("@id").remove("http://schema.org/dateModified") .build(); // ToDo: Assert that the semantic api response is the same (everything in the // expected version is in the new one - deleting the @id and dateModified means diff --git a/src/test/java/edu/harvard/iq/dataverse/export/croissant/CroissantExportUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/export/croissant/CroissantExportUtilTest.java index aa544f7a930..414cef784d7 100644 --- a/src/test/java/edu/harvard/iq/dataverse/export/croissant/CroissantExportUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/export/croissant/CroissantExportUtilTest.java @@ -27,7 +27,7 @@ public class CroissantExportUtilTest { void testGetReviews() throws IOException { String content = Files.readString(Path.of("doc/sphinx-guides/source/_static/api/list-reviews.json"), StandardCharsets.UTF_8); JsonObject apiResponseJson = JsonUtil.getJsonObject(content); - JsonObjectBuilder job = Json.createObjectBuilder(apiResponseJson.getJsonObject("data")); + JsonObjectBuilder job = JsonUtil.createObjectBuilder(apiResponseJson.getJsonObject("data")); JsonObject result = CroissantExportUtil.getReviews(job).build(); System.out.println(prettyPrint(result)); assertTrue(result.getJsonArray("reviews").size() == 1); From a5c2e2482e908f0ac1efd4351b80c213d9f1b023 Mon Sep 17 00:00:00 2001 From: "t.jansen" Date: Tue, 21 Jul 2026 14:30:39 +0200 Subject: [PATCH 31/36] perf(json): added all types of JsonUtil.createValue() use JsonUtil.createValue to reuse JsonUtil.provider and avoid usage of Json.createValue. Replaced all Json.createValue to JsonUtil.createValue. --- forbiddenapis.txt | 11 ++- .../iq/dataverse/api/AbstractApiBean.java | 6 +- .../api/util/JsonResponseBuilder.java | 2 +- .../dataverse/globus/GlobusServiceBean.java | 2 +- .../iq/dataverse/util/URLTokenUtil.java | 4 +- .../iq/dataverse/util/bagit/OREMap.java | 18 ++--- .../iq/dataverse/util/json/JsonUtil.java | 73 ++++++++++++++++++- .../harvard/iq/dataverse/api/DatasetsIT.java | 8 +- 8 files changed, 100 insertions(+), 24 deletions(-) diff --git a/forbiddenapis.txt b/forbiddenapis.txt index 15251a07448..523fbda809f 100644 --- a/forbiddenapis.txt +++ b/forbiddenapis.txt @@ -6,4 +6,13 @@ jakarta.json.Json#createArrayBuilder(jakarta.json.JsonArray) @defaultMessage Use edu.harvard.iq.dataverse.util.json.JsonUtil.createObjectBuilder(...) instead jakarta.json.Json#createObjectBuilder() jakarta.json.Json#createObjectBuilder(java.util.Map) -jakarta.json.Json#createObjectBuilder(jakarta.json.JsonObject) \ No newline at end of file +jakarta.json.Json#createObjectBuilder(jakarta.json.JsonObject) + +@defaultMessage Use edu.harvard.iq.dataverse.util.json.JsonUtil.createValue(...) instead +jakarta.json.Json#createValue(double) +jakarta.json.Json#createValue(int) +jakarta.json.Json#createValue(long) +jakarta.json.Json#createValue(Number) +jakarta.json.Json#createValue(String) +jakarta.json.Json#createValue(java.math.BigDecimal) +jakarta.json.Json#createValue(java.math.BigInteger) \ No newline at end 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 6efcecb72a1..4eccb16f2b3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -1067,7 +1067,7 @@ protected Response ok(String msg) { // This is a highly used response builder, which is why this is an experimental opt-in change! // TODO: This will be removed in a future version. if (FeatureFlags.UNIFY_API_RESPONSE_MESSAGE_STYLE.enabled()) { - return ok(null, Json.createValue(msg), null); + return ok(null, JsonUtil.createValue(msg), null); } else { return ok(JsonUtil.createObjectBuilder().add("message", msg).build(), null, null); } @@ -1080,7 +1080,7 @@ protected Response ok(String msg, JsonObjectBuilder bld) { if (JvmSettings.LEGACY_API_RESPONSE_MESSAGE_STYLE.lookupOptional(Boolean.class).orElse(false)) { return ok(bld.build(), JsonUtil.createObjectBuilder().add(ApiConstants.MESSAGE_FIELD, msg).build(), null); } else { - return ok(bld.build(), Json.createValue(msg), null); + return ok(bld.build(), JsonUtil.createValue(msg), null); } } @@ -1089,7 +1089,7 @@ protected Response ok( boolean value ) { } protected Response ok(long value) { - return ok(Json.createValue(value), null, null); + return ok(JsonUtil.createValue(value), null, null); } /** diff --git a/src/main/java/edu/harvard/iq/dataverse/api/util/JsonResponseBuilder.java b/src/main/java/edu/harvard/iq/dataverse/api/util/JsonResponseBuilder.java index b4a0715172e..f16afdbda06 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/util/JsonResponseBuilder.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/util/JsonResponseBuilder.java @@ -122,7 +122,7 @@ public JsonResponseBuilder request(HttpServletRequest request) { */ public JsonResponseBuilder requestContentType(HttpServletRequest request) { String type = request.getContentType(); - this.entityBuilder.add("requestContentType", ((type==null) ? JsonValue.NULL : Json.createValue(type))); + this.entityBuilder.add("requestContentType", ((type==null) ? JsonValue.NULL : JsonUtil.createValue(type))); return this; } 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 1c1f6bf9ab1..f204dd1f156 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -1158,7 +1158,7 @@ private void processUploadedFiles(JsonArray filesJsonArray, Dataset dataset, Aut Long uploadedFileSize = fileSizeMap.get(fileId); myLogger.info("Found size for file " + fileId + ": " + uploadedFileSize + " bytes"); patch = Json.createPatchBuilder() - .add("/fileSize", Json.createValue(uploadedFileSize)).build(); + .add("/fileSize", JsonUtil.createValue(uploadedFileSize)).build(); fileJsonObject = patch.apply(fileJsonObject); } else { logger.fine("No file size entry found for file "+fileId); diff --git a/src/main/java/edu/harvard/iq/dataverse/util/URLTokenUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/URLTokenUtil.java index a65a7fc098e..5c82cba4875 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/URLTokenUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/URLTokenUtil.java @@ -121,9 +121,9 @@ public JsonValue getParam(String value) { if (tokenValue != null && !tokenValue.isBlank()) { try{ int x =Integer.parseInt(tokenValue); - return Json.createValue(x); + return JsonUtil.createValue(x); } catch (NumberFormatException nfe){ - return Json.createValue(tokenValue); + return JsonUtil.createValue(tokenValue); } } else { return null; diff --git a/src/main/java/edu/harvard/iq/dataverse/util/bagit/OREMap.java b/src/main/java/edu/harvard/iq/dataverse/util/bagit/OREMap.java index 152bfcf5b9b..c29354d63f7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/bagit/OREMap.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/bagit/OREMap.java @@ -90,10 +90,10 @@ public JsonObjectBuilder getOREMapBuilder(boolean aggregationOnly) { // Add namespaces we'll definitely use to Context // Additional namespaces are added as needed below - localContext.putIfAbsent(JsonLDNamespace.ore.getPrefix(), Json.createValue(JsonLDNamespace.ore.getUrl())); - localContext.putIfAbsent(JsonLDNamespace.dcterms.getPrefix(), Json.createValue(JsonLDNamespace.dcterms.getUrl())); - localContext.putIfAbsent(JsonLDNamespace.dvcore.getPrefix(), Json.createValue(JsonLDNamespace.dvcore.getUrl())); - localContext.putIfAbsent(JsonLDNamespace.schema.getPrefix(), Json.createValue(JsonLDNamespace.schema.getUrl())); + localContext.putIfAbsent(JsonLDNamespace.ore.getPrefix(), JsonUtil.createValue(JsonLDNamespace.ore.getUrl())); + localContext.putIfAbsent(JsonLDNamespace.dcterms.getPrefix(), JsonUtil.createValue(JsonLDNamespace.dcterms.getUrl())); + localContext.putIfAbsent(JsonLDNamespace.dvcore.getPrefix(), JsonUtil.createValue(JsonLDNamespace.dvcore.getUrl())); + localContext.putIfAbsent(JsonLDNamespace.schema.getPrefix(), JsonUtil.createValue(JsonLDNamespace.schema.getUrl())); Dataset dataset = version.getDataset(); String id = dataset.getGlobalId().asURL(); @@ -381,7 +381,7 @@ private void addIfNotNull(JsonObjectBuilder builder, JsonLDTerm key, Long value) private void addToContextMap(JsonLDTerm key) { if (!key.inNamespace()) { - localContext.putIfAbsent(key.getLabel(), Json.createValue(key.getUrl())); + localContext.putIfAbsent(key.getLabel(), JsonUtil.createValue(key.getUrl())); } } @@ -426,9 +426,9 @@ public static JsonValue getJsonLDForField(DatasetField field, Boolean excludeEma JsonLDTerm fieldName = dfType.getJsonLDTerm(); if (fieldName.inNamespace()) { - localContext2.putIfAbsent(fieldName.getNamespace().getPrefix(), Json.createValue(fieldName.getNamespace().getUrl())); + localContext2.putIfAbsent(fieldName.getNamespace().getPrefix(), JsonUtil.createValue(fieldName.getNamespace().getUrl())); } else { - localContext2.putIfAbsent(fieldName.getLabel(), Json.createValue(fieldName.getUrl())); + localContext2.putIfAbsent(fieldName.getLabel(), JsonUtil.createValue(fieldName.getUrl())); } JsonArrayBuilder vals = JsonUtil.createArrayBuilder(); if (!dfType.isCompound()) { @@ -462,9 +462,9 @@ public static JsonValue getJsonLDForField(DatasetField field, Boolean excludeEma // ToDo - also needs to recurse here? if (subFieldName.inNamespace()) { localContext2.putIfAbsent(subFieldName.getNamespace().getPrefix(), - Json.createValue(subFieldName.getNamespace().getUrl())); + JsonUtil.createValue(subFieldName.getNamespace().getUrl())); } else { - localContext2.putIfAbsent(subFieldName.getLabel(), Json.createValue(subFieldName.getUrl())); + localContext2.putIfAbsent(subFieldName.getLabel(), JsonUtil.createValue(subFieldName.getUrl())); } List values = dsf.getValues_nondisplay(); diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonUtil.java index 5e04ae9442f..807fc0cfca3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonUtil.java @@ -6,6 +6,8 @@ import java.io.InputStream; import java.io.StringReader; import java.io.StringWriter; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -19,10 +21,12 @@ import jakarta.json.JsonReader; import jakarta.json.JsonException; import jakarta.json.JsonValue; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.JsonNumber; +import jakarta.json.JsonString; import jakarta.json.spi.JsonProvider; import jakarta.json.stream.JsonGenerator; -import jakarta.json.JsonObjectBuilder; -import jakarta.json.JsonArrayBuilder; public class JsonUtil { @@ -169,7 +173,7 @@ public static JsonValue getJsonValue(String serializedJson) { } /** - * A factory to create Jakarta JSON-P Builders such as {@code JsonObjectBuilder} and {@code JsonArrayBuilder}. + * A provider to create Jakarta JSON-P Builders such as {@code JsonObjectBuilder} and {@code JsonArrayBuilder}. * This is a thread-safe, static, and final instance to manage JSON builder creation. * *

Using a one-time initialized factory avoids a classpath-scan on every invocation of @@ -234,4 +238,67 @@ public static JsonArrayBuilder createArrayBuilder(JsonArray array) { public static JsonArrayBuilder createArrayBuilder(Collection collection) { return provider.createArrayBuilder(collection); } + + /** + * Create a new {@link JsonNumber}, initialized with the content of a Double from a cached provider instance. + * {@link Json#createValue(double)} drop-in replacement, avoiding classpath-rescan on invocation. + * @return the JSON Number + */ + public static JsonNumber createValue(double value){ + return provider.createValue(value); + } + + /** + * Create a new {@link JsonNumber}, initialized with the content of an Integer from a cached provider instance. + * {@link Json#createValue(int)} drop-in replacement, avoiding classpath-rescan on invocation. + * @return the JSON Number + */ + public static JsonNumber createValue(int value){ + return provider.createValue(value); + } + + /** + * Create a new {@link JsonNumber}, initialized with the content of a Long from a cached provider instance. + * {@link Json#createValue(long)} drop-in replacement, avoiding classpath-rescan on invocation. + * @return the JSON Number + */ + public static JsonNumber createValue(long value){ + return provider.createValue(value); + } + + /** + * Create a new {@link JsonNumber}, initialized with the content of a Number from a cached provider instance. + * {@link Json#createValue(Number)} drop-in replacement, avoiding classpath-rescan on invocation. + * @return the JSON Number + */ + public static JsonNumber createValue(Number value){ + return provider.createValue(value); + } + + /** + * Create a new {@link JsonString}, initialized with the content of a String from a cached provider instance. + * {@link Json#createValue(String)} drop-in replacement, avoiding classpath-rescan on invocation. + * @return the JSON String + */ + public static JsonString createValue(String value){ + return provider.createValue(value); + } + + /** + * Create a new {@link JsonNumber}, initialized with the content of a BigDecimal from a cached provider instance. + * {@link Json#createValue(BigDecimal)} drop-in replacement, avoiding classpath-rescan on invocation. + * @return the JSON Number + */ + public static JsonNumber createValue(BigDecimal value){ + return provider.createValue(value); + } + + /** + * Create a new {@link JsonNumber}, initialized with the content of a BigInteger from a cached provider instance. + * {@link Json#createValue(BigInteger)} drop-in replacement, avoiding classpath-rescan on invocation. + * @return the JSON Number + */ + public static JsonNumber createValue(BigInteger value){ + return provider.createValue(value); + } } 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 3e870f887cc..d6968546694 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -7096,8 +7096,8 @@ public void testUpdateMultipleFileMetadata() { assertEquals("Updated File 1", file.getString("label")); assertEquals("dir1", file.getString("directoryLabel")); assertEquals("Updated description for File 1", dataFile.getString("description")); - assertTrue(dataFile.getJsonArray("categories").contains(Json.createValue("Category 1"))); - assertTrue(dataFile.getJsonArray("categories").contains(Json.createValue("Category 2"))); + assertTrue(dataFile.getJsonArray("categories").contains(JsonUtil.createValue("Category 1"))); + assertTrue(dataFile.getJsonArray("categories").contains(JsonUtil.createValue("Category 2"))); assertTrue(file.getBoolean("restricted")); // Check provFreeForm for file1 @@ -7109,7 +7109,7 @@ public void testUpdateMultipleFileMetadata() { assertEquals("Updated File 2", file.getString("label")); assertEquals("dir2", file.getString("directoryLabel")); assertEquals("Updated description for File 2", dataFile.getString("description")); - assertTrue(dataFile.getJsonArray("categories").contains(Json.createValue("Category 3"))); + assertTrue(dataFile.getJsonArray("categories").contains(JsonUtil.createValue("Category 3"))); // Check provFreeForm for file2 Response provResponse = UtilIT.getProvFreeForm(file2Id.toString(), apiToken); @@ -7204,7 +7204,7 @@ public void testUpdateMultipleFileMetadata() { JsonObject dataFile = file.getJsonObject("dataFile"); if (dataFile.getInt("id") == file3Id) { JsonArray tabularTags = dataFile.getJsonArray("tabularTags"); - if (tabularTags != null && tabularTags.contains(Json.createValue(DataFileTag.TagType.Survey.toString())) && tabularTags.contains(Json.createValue(DataFileTag.TagType.Survey.toString()))) { + if (tabularTags != null && tabularTags.contains(JsonUtil.createValue(DataFileTag.TagType.Survey.toString())) && tabularTags.contains(JsonUtil.createValue(DataFileTag.TagType.Survey.toString()))) { foundValidTags = true; break; } From 27fbb04ea795cbd6d58493b21110ff1944820a1f Mon Sep 17 00:00:00 2001 From: "t.jansen" Date: Tue, 21 Jul 2026 14:52:12 +0200 Subject: [PATCH 32/36] fix(forbiddenapis): use java.lang. in Json.createValue signature, which fixes the forbiddenapis signature parsing failure. --- forbiddenapis.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/forbiddenapis.txt b/forbiddenapis.txt index 523fbda809f..a092d1f0a6b 100644 --- a/forbiddenapis.txt +++ b/forbiddenapis.txt @@ -12,7 +12,7 @@ jakarta.json.Json#createObjectBuilder(jakarta.json.JsonObject) jakarta.json.Json#createValue(double) jakarta.json.Json#createValue(int) jakarta.json.Json#createValue(long) -jakarta.json.Json#createValue(Number) -jakarta.json.Json#createValue(String) +jakarta.json.Json#createValue(java.lang.Number) +jakarta.json.Json#createValue(java.lang.String) jakarta.json.Json#createValue(java.math.BigDecimal) jakarta.json.Json#createValue(java.math.BigInteger) \ No newline at end of file From 5c50e563e5cbf86ff914fd0079ecaa7a540005d3 Mon Sep 17 00:00:00 2001 From: "t.jansen" Date: Fri, 31 Jul 2026 14:07:35 +0200 Subject: [PATCH 33/36] style(forbiddenapis): move signatures file to dataverse-sca-config --- .../dataverse-sca-config/forbiddenapis.txt | 0 modules/dataverse-sca-config/pom.xml | 27 +++++++++++++++++++ 2 files changed, 27 insertions(+) rename forbiddenapis.txt => modules/dataverse-sca-config/forbiddenapis.txt (100%) create mode 100644 modules/dataverse-sca-config/pom.xml diff --git a/forbiddenapis.txt b/modules/dataverse-sca-config/forbiddenapis.txt similarity index 100% rename from forbiddenapis.txt rename to modules/dataverse-sca-config/forbiddenapis.txt diff --git a/modules/dataverse-sca-config/pom.xml b/modules/dataverse-sca-config/pom.xml new file mode 100644 index 00000000000..cef4625b1ac --- /dev/null +++ b/modules/dataverse-sca-config/pom.xml @@ -0,0 +1,27 @@ + + 4.0.0 + + edu.harvard.iq + dataverse-sca-config + 1.0.0-SNAPSHOT + jar + + dataverse-sca-config + https://dataverse.org + + + + + ${project.basedir} + dataverse-sca-config + + pom.xml + .git*/** + .git* + target/** + + + + + \ No newline at end of file From ada2db1481c58a233a356ab254181ce8bafe2185 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 31 Jul 2026 14:59:00 +0200 Subject: [PATCH 34/36] build: remove dataverse-sca-config module and reorganize forbidden-apis plugin usage While using the source code analysis config files from a Config JAR is a great idea in general, it will not work in our codebase. As we do not use a parent POM at the root of our project, we cannot make the Maven Reactor pull in the dataverse-sca-config module during a build. We'd need every developer to switch to using `mvn -f modules/dataverse-parent` all the time, which seems unlikely. Bad design choices in the past, not easy to resolve. While we could push a built version to Maven Central, that would not help when modifying the signature file. At the same time, using there is no reliable way to have an absolute path injected into a Maven property. We were using project.basedir within the parent POM before. This will be resolved at build time and always points to the directory where the current POM is running from. The reusability of this centralized plugin definition is rather limited, as it would fail to lookup the file in a different module. The only way out of this: 1. Move the plugin into the dataverse module build chain. 2. This way, the resolved path is always correct, as it is model-bound, not half-parent-bound. 3. To still satisfy the request to avoid clutter in the project root, create a src/maven folder, ready to collect all these configurations. --- modules/dataverse-parent/pom.xml | 53 ------------------- modules/dataverse-sca-config/pom.xml | 27 ---------- pom.xml | 53 +++++++++++++++++++ .../maven}/forbiddenapis.txt | 0 4 files changed, 53 insertions(+), 80 deletions(-) delete mode 100644 modules/dataverse-sca-config/pom.xml rename {modules/dataverse-sca-config => src/maven}/forbiddenapis.txt (100%) diff --git a/modules/dataverse-parent/pom.xml b/modules/dataverse-parent/pom.xml index 197e2e06278..c5c746202d7 100644 --- a/modules/dataverse-parent/pom.xml +++ b/modules/dataverse-parent/pom.xml @@ -383,59 +383,6 @@ - - de.thetaphi - forbiddenapis - 3.10 - - - false - - true - - - jdk-non-portable - jdk-internal - - - - - - ${project.basedir}/forbiddenapis.txt - - ${target.java.version} - - - - for-classes - process-classes - - check - - - - for-test-classes - process-test-classes - - testCheck - - - - diff --git a/modules/dataverse-sca-config/pom.xml b/modules/dataverse-sca-config/pom.xml deleted file mode 100644 index cef4625b1ac..00000000000 --- a/modules/dataverse-sca-config/pom.xml +++ /dev/null @@ -1,27 +0,0 @@ - - 4.0.0 - - edu.harvard.iq - dataverse-sca-config - 1.0.0-SNAPSHOT - jar - - dataverse-sca-config - https://dataverse.org - - - - - ${project.basedir} - dataverse-sca-config - - pom.xml - .git*/** - .git* - target/** - - - - - \ No newline at end of file diff --git a/pom.xml b/pom.xml index 297f630ceea..836d70ab0ed 100644 --- a/pom.xml +++ b/pom.xml @@ -886,6 +886,59 @@ + + de.thetaphi + forbiddenapis + 3.10 + + + false + + true + + + jdk-non-portable + jdk-internal + + + + + + ${project.basedir}/src/maven/forbiddenapis.txt + + ${target.java.version} + + + + for-classes + process-classes + + check + + + + for-test-classes + process-test-classes + + testCheck + + + + org.apache.maven.plugins maven-compiler-plugin diff --git a/modules/dataverse-sca-config/forbiddenapis.txt b/src/maven/forbiddenapis.txt similarity index 100% rename from modules/dataverse-sca-config/forbiddenapis.txt rename to src/maven/forbiddenapis.txt From 9ddbf5c75713a30ad28fa11f177df8709fa3d4c2 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 31 Jul 2026 15:02:05 +0200 Subject: [PATCH 35/36] build: move Checkstyle config to subfolder Cleaning up the root directory, a single directory will now contain any configs related to Maven plugins. This will (mostly) be for Static Code Analysis plugins. --- .github/workflows/reviewdog_checkstyle.yml | 2 +- pom.xml | 2 +- checkstyle.xml => src/maven/checkstyle.xml | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename checkstyle.xml => src/maven/checkstyle.xml (100%) diff --git a/.github/workflows/reviewdog_checkstyle.yml b/.github/workflows/reviewdog_checkstyle.yml index 6e82489c2db..e66095b08e1 100644 --- a/.github/workflows/reviewdog_checkstyle.yml +++ b/.github/workflows/reviewdog_checkstyle.yml @@ -16,6 +16,6 @@ jobs: with: fail_on_error: true reporter: github-pr-review - checkstyle_config: checkstyle.xml + checkstyle_config: src/maven/checkstyle.xml github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/pom.xml b/pom.xml index 836d70ab0ed..136db4e76f2 100644 --- a/pom.xml +++ b/pom.xml @@ -1159,7 +1159,7 @@ org.apache.maven.plugins maven-checkstyle-plugin - checkstyle.xml + ${project.basedir}/src/maven/checkstyle.xml UTF-8 true diff --git a/checkstyle.xml b/src/maven/checkstyle.xml similarity index 100% rename from checkstyle.xml rename to src/maven/checkstyle.xml From 6b2954afa5699ae1052539f15898b0918de1f4ca Mon Sep 17 00:00:00 2001 From: "t.jansen" Date: Fri, 7 Aug 2026 10:33:02 +0200 Subject: [PATCH 36/36] doc(releaseNote): add JSON-P performance release note --- doc/release-notes/12362-factory-method-for-JSONbuilders.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 doc/release-notes/12362-factory-method-for-JSONbuilders.md diff --git a/doc/release-notes/12362-factory-method-for-JSONbuilders.md b/doc/release-notes/12362-factory-method-for-JSONbuilders.md new file mode 100644 index 00000000000..0a3cdfdf5a8 --- /dev/null +++ b/doc/release-notes/12362-factory-method-for-JSONbuilders.md @@ -0,0 +1,4 @@ +# Improved JSON Performance + +A Jakarta JSON-P provider is now reused for every builder or value creation. +This improves the performance of large JSON operations, especially large exports and API responses, without changing the existing behavior or the JSON output.