Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3d9b283
Stash: updateVersionLicense endpoint WIP
GPortas Sep 11, 2025
817ddaf
Stash: updateVersionLicense endpoint WIP. Endpoint logic implemented,…
GPortas Sep 12, 2025
0be4b8c
Merge branch 'develop' of github.com:IQSS/dataverse into 11771-set-da…
GPortas Sep 12, 2025
1ad96d3
Refactor: removed unused injected services in Datasets.java
GPortas Sep 12, 2025
f33028e
Refactor: pending TODO refactor implemented. datasetMetricsService mo…
GPortas Sep 12, 2025
ae10cee
Changed: license update implementation with unit tests
GPortas Sep 12, 2025
5f33be4
Refactor: renamed UpdateDatasetVersionLicenseCommand to UpdateDataset…
GPortas Sep 12, 2025
e253f22
Changed: renamed bundle string
GPortas Sep 12, 2025
6fbd2d9
Added: handling license updates where custom terms are sent
GPortas Sep 16, 2025
37573ac
Merge branch 'develop' of github.com:IQSS/dataverse into 11771-set-da…
GPortas Sep 16, 2025
45c3e04
Changed: naming and tweaks
GPortas Sep 16, 2025
8342c0a
Added: integration tests, tweaks and improved error handling
GPortas Sep 17, 2025
86510d8
Added: DatasetsIT test case for updating license with insufficient pe…
GPortas Sep 17, 2025
806bbb3
Added: docs for new datasets updateLicense endpoint
GPortas Sep 17, 2025
c857a15
Added: release notes for #11771
GPortas Sep 17, 2025
7012604
Merge branch 'develop' of github.com:IQSS/dataverse into 11771-set-da…
GPortas Oct 23, 2025
44f31e9
Merge branch 'develop' of github.com:IQSS/dataverse into 11771-set-da…
GPortas Oct 24, 2025
49945f7
Fixed: missing import statement
GPortas Oct 24, 2025
e2ef4a4
Fixed: UpdateDatasetLicenseCommand by not overwriting dataset TermsOf…
GPortas Oct 25, 2025
676f834
Fixed: native api rst format
GPortas Oct 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions doc/release-notes/11771-update-dataset-license-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## New Endpoint: `/datasets/{id}/license`

A new endpoint has been implemented to manage dataset licenses.

### Functionality
- Updates the license of a dataset by applying it to the draft version.
- If no draft exists, a new one is automatically created.

### Usage
This endpoint supports two ways of defining a license:
1. **Predefined License** – Provide the license name (e.g., `CC BY 4.0`).
2. **Custom Terms of Use and Access** – Provide a JSON body with the `customTerms` object.
- All fields are optional **except** `termsOfUse`, which is required.
47 changes: 47 additions & 0 deletions doc/sphinx-guides/source/api/native-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4357,6 +4357,53 @@ The CSV response for this call is the same as for the /api/datasets/{id}/assignm

Note: This feature requires the "role-assignment-history" feature flag to be enabled (see :ref:`feature-flags`).

Update Dataset License
~~~~~~~~~~~~~~~~~~~~~~

Updates the license of a dataset by applying it to the draft version, or by creating a draft if none exists.

The JSON representation of a license can take two forms, depending on whether you want to specify a predefined license or define custom terms of use and access.

To set a predefined license (e.g., CC BY 4.0), provide a JSON body with the license name:

.. code-block:: json

{
"name": "CC BY 4.0"
}

To define custom terms of use and access, provide a JSON body with the following properties. All fields within ``customTerms`` are optional, except for the ``termsOfUse`` field, which is required:

.. code-block:: json

{
"customTerms": {
"termsOfUse": "Your terms of use",
"confidentialityDeclaration": "Your confidentiality declaration",
"specialPermissions": "Your special permissions",
"restrictions": "Your restrictions",
"citationRequirements": "Your citation requirements",
"depositorRequirements": "Your depositor requirements",
"conditions": "Your conditions",
"disclaimer": "Your disclaimer"
}
}

.. code-block:: bash

export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export SERVER_URL=https://demo.dataverse.org
export ID=3
export FILE_PATH=license.json

curl -H "X-Dataverse-key:$API_TOKEN" -X PUT "$SERVER_URL/api/datasets/$ID/license" -H "Content-type:application/json" --upload-file $FILE_PATH

The fully expanded example above (without environment variables) looks like this:

.. code-block:: bash

curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X PUT "https://demo.dataverse.org/api/datasets/3/license" -H "Content-type:application/json" --upload-file license.json

Files
-----

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import edu.harvard.iq.dataverse.engine.command.impl.GetSpecificPublishedDatasetVersionCommand;
import edu.harvard.iq.dataverse.externaltools.ExternalToolServiceBean;
import edu.harvard.iq.dataverse.license.LicenseServiceBean;
import edu.harvard.iq.dataverse.makedatacount.DatasetMetricsServiceBean;
import edu.harvard.iq.dataverse.pidproviders.FailedPIDResolutionLoggingServiceBean;
import edu.harvard.iq.dataverse.pidproviders.PidUtil;
import edu.harvard.iq.dataverse.pidproviders.FailedPIDResolutionLoggingServiceBean.FailedPIDResolutionEntry;
Expand Down Expand Up @@ -224,6 +225,9 @@ String getWrappedMessageWhenJson() {
@EJB
protected ExternalToolServiceBean externalToolService;

@EJB
protected DatasetMetricsServiceBean datasetMetricsService;

@EJB
DataFileServiceBean fileSvc;

Expand Down
62 changes: 38 additions & 24 deletions src/main/java/edu/harvard/iq/dataverse/api/Datasets.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@

import edu.harvard.iq.dataverse.*;
import edu.harvard.iq.dataverse.DatasetLock.Reason;
import edu.harvard.iq.dataverse.DatasetVersion.VersionState;
import edu.harvard.iq.dataverse.DataverseRoleServiceBean.RoleAssignmentHistoryConsolidatedEntry;
import edu.harvard.iq.dataverse.actionlogging.ActionLogRecord;
import edu.harvard.iq.dataverse.api.AbstractApiBean.WrappedResponse;
import edu.harvard.iq.dataverse.api.auth.AuthRequired;
import edu.harvard.iq.dataverse.api.dto.CustomTermsDTO;
import edu.harvard.iq.dataverse.api.dto.LicenseUpdateRequest;
import edu.harvard.iq.dataverse.api.dto.RoleAssignmentDTO;
import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean;
import edu.harvard.iq.dataverse.authorization.DataverseRole;
Expand All @@ -31,7 +30,6 @@
import edu.harvard.iq.dataverse.engine.command.DataverseRequest;
import edu.harvard.iq.dataverse.engine.command.exception.*;
import edu.harvard.iq.dataverse.engine.command.impl.*;
import edu.harvard.iq.dataverse.export.DDIExportServiceBean;
import edu.harvard.iq.dataverse.export.ExportService;
import edu.harvard.iq.dataverse.externaltools.ExternalTool;
import edu.harvard.iq.dataverse.externaltools.ExternalToolHandler;
Expand Down Expand Up @@ -142,9 +140,6 @@ public class Datasets extends AbstractApiBean {
@EJB
AuthenticationServiceBean authenticationServiceBean;

@EJB
DDIExportServiceBean ddiExportService;

@EJB
MetadataBlockServiceBean metadataBlockService;

Expand All @@ -166,10 +161,6 @@ public class Datasets extends AbstractApiBean {
@EJB
SettingsServiceBean settingsService;

// TODO: Move to AbstractApiBean
@EJB
DatasetMetricsServiceBean datasetMetricsSvc;

@EJB
DatasetExternalCitationsServiceBean datasetExternalCitationsService;

Expand Down Expand Up @@ -203,9 +194,6 @@ public class Datasets extends AbstractApiBean {
@Inject
DatasetTypeServiceBean datasetTypeSvc;

@Inject
DatasetFieldsValidator datasetFieldsValidator;

@Inject
DataFileCategoryServiceBean dataFileCategoryService;

Expand Down Expand Up @@ -1155,16 +1143,16 @@ public Response editVersionMetadata(@Context ContainerRequestContext crc, String
return ex.getResponse();
}
}

@PUT
@AuthRequired
@Path("{id}/access")
public Response editVersionTermsOfAccess(@Context ContainerRequestContext crc, String jsonBody, @PathParam("id") String id,
@QueryParam("sourceLastUpdateTime") String sourceLastUpdateTime) {
try {

boolean publicInstall = settingsSvc.isTrueForKey(SettingsServiceBean.Key.PublicInstall, false);

Dataset dataset = findDatasetOrDie(id);

if (sourceLastUpdateTime != null) {
Expand All @@ -1174,11 +1162,11 @@ public Response editVersionTermsOfAccess(@Context ContainerRequestContext crc, S
JsonObject json = JsonUtil.getJsonObject(jsonBody);

TermsOfUseAndAccess toua = jsonParser().parseTermsOfAccess(json);

if (publicInstall && (toua.isFileAccessRequest() || !toua.getTermsOfAccess().isEmpty())){
return error(BAD_REQUEST, "Setting File Access Request or Terms of Access is not permitted on a public installation.");
}

DatasetVersion updatedVersion = execCommand(new UpdateDatasetTermsOfAccessCommand(dataset, toua, createDataverseRequest(getRequestUser(crc)))).getLatestVersion();

return ok(json(updatedVersion, true));
Expand Down Expand Up @@ -3575,7 +3563,7 @@ public Response getMakeDataCountMetric(@PathParam("id") String idSupplied, @Path
return error(Response.Status.BAD_REQUEST, "Country must be one of the ISO 1366 Country Codes");
}
}
DatasetMetrics datasetMetrics = datasetMetricsSvc.getDatasetMetricsByDatasetForDisplay(dataset, monthYear, country);
DatasetMetrics datasetMetrics = datasetMetricsService.getDatasetMetricsByDatasetForDisplay(dataset, monthYear, country);
if (datasetMetrics == null) {
return ok("No metrics available for dataset " + dataset.getId() + " for " + yyyymm + " for country code " + country + ".");
} else if (datasetMetrics.getDownloadsTotal() + datasetMetrics.getViewsTotal() == 0) {
Expand Down Expand Up @@ -6041,7 +6029,7 @@ public Response deleteDatasetFiles(@Context ContainerRequestContext crc, @PathPa
}, getRequestUser(crc));
}

@GET
@GET
@AuthRequired
@Path("{id}/versions/{versionId}/versionNote")
public Response getVersionCreationNote(@Context ContainerRequestContext crc, @PathParam("id") String datasetId, @PathParam("versionId") String versionId, @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse {
Expand Down Expand Up @@ -6110,15 +6098,15 @@ public Response deleteVersionNote(@Context ContainerRequestContext crc, @PathPar
return ok("Note deleted");
}, getRequestUser(crc));
}

@GET
@AuthRequired
@Path("{identifier}/assignments/history")
@Produces({ MediaType.APPLICATION_JSON, "text/csv" })
public Response getRoleAssignmentHistory(@Context ContainerRequestContext crc, @PathParam("identifier") String id, @Context HttpHeaders headers) {
return response(req -> {
Dataset dataset = findDatasetOrDie(id);

// user is authenticated
AuthenticatedUser authenticatedUser = getRequestAuthenticatedUserOrDie(crc);

Expand All @@ -6135,11 +6123,37 @@ public Response getFilesRoleAssignmentHistory(@Context ContainerRequestContext c
@Context HttpHeaders headers) {
return response(req -> {
Dataset dataset = findDatasetOrDie(id);

// user is authenticated
AuthenticatedUser authenticatedUser = getRequestAuthenticatedUserOrDie(crc);

return getRoleAssignmentHistoryResponse(dataset, authenticatedUser, true, headers);
}, getRequestUser(crc));
}

@PUT
@AuthRequired
@Path("{id}/license")
public Response updateLicense(@Context ContainerRequestContext crc,
@PathParam("id") String datasetId,
LicenseUpdateRequest requestBody) {
return response(req -> {
Dataset dataset = findDatasetOrDie(datasetId);
if (requestBody.getName() != null && !requestBody.getName().isEmpty()) {
String licenseName = requestBody.getName();
License license = licenseSvc.getByNameOrUri(licenseName);
if (license == null) {
return notFound(BundleUtil.getStringFromBundle("datasets.api.updateLicense.licenseNotFound", List.of(licenseName)));
}
execCommand(new UpdateDatasetLicenseCommand(req, dataset, license));
return ok(BundleUtil.getStringFromBundle("datasets.api.updateLicense.success"));
} else if (requestBody.getCustomTerms() != null) {
CustomTermsDTO customTerms = requestBody.getCustomTerms();
execCommand(new UpdateDatasetLicenseCommand(req, dataset, customTerms.toTermsOfUseAndAccess()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't do anything to preserve whatever has been entered for File Access Requests or Terms of Access so it will fail if the dataset has any restricted files. In the absence of restricted files would we still want to wipe out whatever may have been entered with respect to data file access?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or the other TermsOfUseAndAccess fields (originalArchive, sizeOfCollection, ...).

return ok(BundleUtil.getStringFromBundle("datasets.api.updateLicense.success"));
} else {
return badRequest(BundleUtil.getStringFromBundle("datasets.api.updateLicense.licenseNameIsEmpty"));
}
}, getRequestUser(crc));
}
}
15 changes: 1 addition & 14 deletions src/main/java/edu/harvard/iq/dataverse/api/MakeDataCountApi.java
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
package edu.harvard.iq.dataverse.api;

import edu.harvard.iq.dataverse.Dataset;
import edu.harvard.iq.dataverse.DatasetServiceBean;
import edu.harvard.iq.dataverse.GlobalId;
import edu.harvard.iq.dataverse.makedatacount.DatasetExternalCitations;
import edu.harvard.iq.dataverse.makedatacount.DatasetExternalCitationsServiceBean;
import edu.harvard.iq.dataverse.makedatacount.DatasetMetrics;
import edu.harvard.iq.dataverse.makedatacount.DatasetMetricsServiceBean;
import edu.harvard.iq.dataverse.makedatacount.MakeDataCountProcessState;
import edu.harvard.iq.dataverse.makedatacount.MakeDataCountProcessStateServiceBean;
import edu.harvard.iq.dataverse.pidproviders.PidProvider;
import edu.harvard.iq.dataverse.pidproviders.PidUtil;
import edu.harvard.iq.dataverse.pidproviders.doi.datacite.DataCiteDOIProvider;
import edu.harvard.iq.dataverse.settings.JvmSettings;
import edu.harvard.iq.dataverse.util.SystemConfig;
import edu.harvard.iq.dataverse.util.json.JsonUtil;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.*;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Future;
Expand Down Expand Up @@ -57,16 +50,10 @@ public class MakeDataCountApi extends AbstractApiBean {

private static final Logger logger = Logger.getLogger(MakeDataCountApi.class.getCanonicalName());

@EJB
DatasetMetricsServiceBean datasetMetricsService;
@EJB
MakeDataCountProcessStateServiceBean makeDataCountProcessStateService;
@EJB
DatasetExternalCitationsServiceBean datasetExternalCitationsService;
@EJB
DatasetServiceBean datasetService;
@EJB
SystemConfig systemConfig;

// Inject the managed executor service provided by the container
@Resource(name = "concurrent/CitationUpdateExecutor")
Expand Down
91 changes: 91 additions & 0 deletions src/main/java/edu/harvard/iq/dataverse/api/dto/CustomTermsDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package edu.harvard.iq.dataverse.api.dto;

import edu.harvard.iq.dataverse.TermsOfUseAndAccess;

public class CustomTermsDTO {
private String termsOfUse;
private String confidentialityDeclaration;
private String specialPermissions;
private String restrictions;
private String citationRequirements;
private String depositorRequirements;
private String conditions;
private String disclaimer;

public String getTermsOfUse() {
return termsOfUse;
}

public void setTermsOfUse(String termsOfUse) {
this.termsOfUse = termsOfUse;
}

public String getConfidentialityDeclaration() {
return confidentialityDeclaration;
}

public void setConfidentialityDeclaration(String confidentialityDeclaration) {
this.confidentialityDeclaration = confidentialityDeclaration;
}

public String getSpecialPermissions() {
return specialPermissions;
}

public void setSpecialPermissions(String specialPermissions) {
this.specialPermissions = specialPermissions;
}

public String getRestrictions() {
return restrictions;
}

public void setRestrictions(String restrictions) {
this.restrictions = restrictions;
}

public String getCitationRequirements() {
return citationRequirements;
}

public void setCitationRequirements(String citationRequirements) {
this.citationRequirements = citationRequirements;
}

public String getDepositorRequirements() {
return depositorRequirements;
}

public void setDepositorRequirements(String depositorRequirements) {
this.depositorRequirements = depositorRequirements;
}

public String getConditions() {
return conditions;
}

public void setConditions(String conditions) {
this.conditions = conditions;
}

public String getDisclaimer() {
return disclaimer;
}

public void setDisclaimer(String disclaimer) {
this.disclaimer = disclaimer;
}

public TermsOfUseAndAccess toTermsOfUseAndAccess() {
TermsOfUseAndAccess termsOfUseAndAccess = new TermsOfUseAndAccess();
termsOfUseAndAccess.setTermsOfUse(termsOfUse);
termsOfUseAndAccess.setConfidentialityDeclaration(confidentialityDeclaration);
termsOfUseAndAccess.setSpecialPermissions(specialPermissions);
termsOfUseAndAccess.setRestrictions(restrictions);
termsOfUseAndAccess.setCitationRequirements(citationRequirements);
termsOfUseAndAccess.setDepositorRequirements(depositorRequirements);
termsOfUseAndAccess.setConditions(conditions);
termsOfUseAndAccess.setDisclaimer(disclaimer);
return termsOfUseAndAccess;
}
}
Loading