Skip to content

Commit b95ee38

Browse files
authored
Feature: Optionally make embargo reason required (#12067)
* require embargo reason option * fix validation * update api to also check the optional/not blank case * release note * doc updates * fix test * make content-type explicit - catch date parsing error * comment re parse error handling * refactor validation * add validation tests * fix blank test, update deprecated settings call * api tests * use properties per review
1 parent 8c94b12 commit b95ee38

12 files changed

Lines changed: 577 additions & 12 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
It is now possible to configure Dataverse to require an embargo reason when a user creates an embargo on one or more files.
2+
By default the embargo reason is optional.
3+
4+
In addition, with this release, if an embargo reason is supplied, it must not be blank.
5+
6+
New Feature Flag:
7+
8+
dataverse.feature.require-embargo-reason - default false

doc/sphinx-guides/source/api/native-api.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3697,14 +3697,14 @@ Set an Embargo on Files in a Dataset
36973697

36983698
``/api/datasets/$dataset-id/files/actions/:set-embargo`` can be used to set an embargo on one or more files in a dataset. Embargoes can be set on files that are only in a draft dataset version (and are not in any previously published version) by anyone who can edit the dataset. The same API call can be used by a superuser to add an embargo to files that have already been released as part of a previously published dataset version.
36993699

3700-
The API call requires a Json body that includes the embargo's end date (dateAvailable), a short reason (optional), and a list of the fileIds that the embargo should be set on. The dateAvailable must be after the current date and the duration (dateAvailable - today's date) must be less than the value specified by the :ref:`:MaxEmbargoDurationInMonths` setting. All files listed must be in the specified dataset. For example:
3700+
The API call requires a Json body that includes the embargo's end date (dateAvailable - YYYY-MM-DD format), a short reason (must not consist of whitespace only, optional unless Dataverse is configured to make it required), and a list of the fileIds that the embargo should be set on. The dateAvailable must be after the current date and the duration (dateAvailable - today's date) must be less than the value specified by the :ref:`:MaxEmbargoDurationInMonths` setting. All files listed must be in the specified dataset. For example:
37013701

37023702
.. code-block:: bash
37033703
37043704
export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
37053705
export SERVER_URL=https://demo.dataverse.org
37063706
export PERSISTENT_IDENTIFIER=doi:10.5072/FK2/7U7YBV
3707-
export JSON='{"dateAvailable":"2021-10-20", "reason":"Standard project embargo", "fileIds":[300,301,302]}'
3707+
export JSON='{"dateAvailable":"2021-01-20", "reason":"Standard project embargo", "fileIds":[300,301,302]}'
37083708
37093709
curl -H "X-Dataverse-key: $API_TOKEN" -H "Content-Type:application/json" "$SERVER_URL/api/datasets/:persistentId/files/actions/:set-embargo?persistentId=$PERSISTENT_IDENTIFIER" -d "$JSON"
37103710

doc/sphinx-guides/source/user/dataset-management.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -750,7 +750,7 @@ Note that only one Preview URL (normal or with anonymized access) can be configu
750750
Embargoes
751751
=========
752752

753-
A Dataverse instance may be configured to support file-level embargoes. Embargoes make file content inaccessible after a dataset version is published - until the embargo end date.
753+
A Dataverse instance may be configured to support file-level embargoes. Embargoes make file content inaccessible after a dataset version is published - until the embargo end date. A reason for the embargo may be supplied when creating the embargo. A reason may be required in some Dataverse instances.
754754
This means that file previews and the ability to download files will be blocked. The effect is similar to when a file is restricted except that the embargo will end at the specified date without further action and during the embargo, requests for file access cannot be made.
755755
Embargoes of files in a version 1.0 dataset may also affect the date shown in the dataset and file citations. The recommended practice is for the citation to reflect the date on which all embargoes on files in version 1.0 end. (Since Dataverse creates one persistent identifier per dataset and doesn't create new ones for each version, the publication of later versions, with or without embargoed files, does not affect the citation date.)
756756

src/main/java/edu/harvard/iq/dataverse/DatasetPage.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@
157157
import edu.harvard.iq.dataverse.search.SearchFields;
158158
import edu.harvard.iq.dataverse.search.SearchUtil;
159159
import edu.harvard.iq.dataverse.search.SolrClientService;
160+
import edu.harvard.iq.dataverse.settings.FeatureFlags;
160161
import edu.harvard.iq.dataverse.settings.JvmSettings;
161162
import edu.harvard.iq.dataverse.util.SignpostingResources;
162163
import edu.harvard.iq.dataverse.util.FileMetadataUtil;
@@ -6887,4 +6888,7 @@ public void setRequestedCSL(String requestedCSL) {
68876888
this.requestedCSL = requestedCSL;
68886889
}
68896890

6890-
}
6891+
public void validateEmbargoReason(FacesContext context, UIComponent component, Object value) {
6892+
FileUtil.validateEmbargoReason(context, component, value, removeEmbargo);
6893+
}
6894+
}

src/main/java/edu/harvard/iq/dataverse/FilePage.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean;
3636
import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean.MakeDataCountEntry;
3737
import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean;
38+
import edu.harvard.iq.dataverse.settings.FeatureFlags;
3839
import edu.harvard.iq.dataverse.settings.JvmSettings;
3940
import edu.harvard.iq.dataverse.settings.SettingsServiceBean;
4041
import edu.harvard.iq.dataverse.util.BundleUtil;
@@ -60,7 +61,9 @@
6061
import jakarta.ejb.EJB;
6162
import jakarta.ejb.EJBException;
6263
import jakarta.faces.application.FacesMessage;
64+
import jakarta.faces.component.UIComponent;
6365
import jakarta.faces.context.FacesContext;
66+
import jakarta.faces.validator.ValidatorException;
6467
import jakarta.faces.view.ViewScoped;
6568
import jakarta.inject.Inject;
6669
import jakarta.inject.Named;
@@ -1489,4 +1492,8 @@ public String editFileMetadata(){
14891492
return "";
14901493
}
14911494

1495+
public void validateEmbargoReason(FacesContext context, UIComponent component, Object value) {
1496+
FileUtil.validateEmbargoReason(context, component, value, removeEmbargo);
1497+
}
1498+
14921499
}

src/main/java/edu/harvard/iq/dataverse/api/Datasets.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,6 +1444,8 @@ public Response moveDataset(@Context ContainerRequestContext crc, @PathParam("id
14441444
@POST
14451445
@AuthRequired
14461446
@Path("{id}/files/actions/:set-embargo")
1447+
@Consumes(MediaType.APPLICATION_JSON)
1448+
@Produces(MediaType.APPLICATION_JSON)
14471449
public Response createFileEmbargo(@Context ContainerRequestContext crc, @PathParam("id") String id, String jsonBody){
14481450

14491451
// user is authenticated
@@ -1486,7 +1488,7 @@ public Response createFileEmbargo(@Context ContainerRequestContext crc, @PathPar
14861488
// check if embargoes are allowed(:MaxEmbargoDurationInMonths), gets the :MaxEmbargoDurationInMonths setting variable, if 0 or not set(null) return 400
14871489
long maxEmbargoDurationInMonths = 0;
14881490
try {
1489-
maxEmbargoDurationInMonths = Long.parseLong(settingsService.get(SettingsServiceBean.Key.MaxEmbargoDurationInMonths.toString()));
1491+
maxEmbargoDurationInMonths = Long.parseLong(settingsService.getValueForKey(SettingsServiceBean.Key.MaxEmbargoDurationInMonths));
14901492
} catch (NumberFormatException nfe){
14911493
if (nfe.getMessage().contains("null")) {
14921494
return error(Status.BAD_REQUEST, "No Embargoes allowed");
@@ -1496,13 +1498,19 @@ public Response createFileEmbargo(@Context ContainerRequestContext crc, @PathPar
14961498
return error(Status.BAD_REQUEST, "No Embargoes allowed");
14971499
}
14981500

1501+
//Any parsing error should be handled via the JsonExceptionsHandler
14991502
JsonObject json = JsonUtil.getJsonObject(jsonBody);
15001503

15011504
Embargo embargo = new Embargo();
15021505

15031506

15041507
LocalDate currentDateTime = LocalDate.now();
1505-
LocalDate dateAvailable = LocalDate.parse(json.getString("dateAvailable"));
1508+
LocalDate dateAvailable = null;
1509+
try {
1510+
dateAvailable = LocalDate.parse(json.getString("dateAvailable"));
1511+
} catch (DateTimeParseException e) {
1512+
return error(Status.BAD_REQUEST, "Unable to parse dateAvailable");
1513+
}
15061514

15071515
// check :MaxEmbargoDurationInMonths if -1
15081516
LocalDate maxEmbargoDateTime = maxEmbargoDurationInMonths != -1 ? LocalDate.now().plusMonths(maxEmbargoDurationInMonths) : null;
@@ -1519,8 +1527,16 @@ public Response createFileEmbargo(@Context ContainerRequestContext crc, @PathPar
15191527
return error(Status.BAD_REQUEST, "Date available can not exceed MaxEmbargoDurationInMonths: "+maxEmbargoDurationInMonths);
15201528
}
15211529
}
1522-
1523-
embargo.setReason(json.getString("reason"));
1530+
String reason = null;
1531+
if(json.containsKey("reason")) {
1532+
reason = json.getString("reason");
1533+
}
1534+
if(reason == null && FeatureFlags.REQUIRE_EMBARGO_REASON.enabled()) {
1535+
return error(Status.BAD_REQUEST, BundleUtil.getStringFromBundle("embargo.reason.required"));
1536+
} else if(reason != null && reason.isBlank()) {
1537+
return error(Status.BAD_REQUEST, BundleUtil.getStringFromBundle("embargo.reason.blank"));
1538+
}
1539+
embargo.setReason(reason);
15241540

15251541
List<DataFile> datasetFiles = dataset.getFiles();
15261542
List<DataFile> filesToEmbargo = new LinkedList<>();
@@ -1600,6 +1616,8 @@ public Response createFileEmbargo(@Context ContainerRequestContext crc, @PathPar
16001616
@POST
16011617
@AuthRequired
16021618
@Path("{id}/files/actions/:unset-embargo")
1619+
@Consumes(MediaType.APPLICATION_JSON)
1620+
@Produces(MediaType.APPLICATION_JSON)
16031621
public Response removeFileEmbargo(@Context ContainerRequestContext crc, @PathParam("id") String id, String jsonBody){
16041622

16051623
// user is authenticated

src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,11 @@ public enum FeatureFlags {
249249
* @since Dataverse 6.9
250250
*/
251251
ONLY_UPDATE_DATACITE_WHEN_NEEDED("only-update-datacite-when-needed"),
252-
252+
253+
/** Require Embargo Reason. By default, adding a reason when embargoing is optional. This
254+
* flag makes a reason required, both in the UI and API.
255+
*/
256+
REQUIRE_EMBARGO_REASON("require-embargo-reason"),
253257
;
254258

255259
final String flag;

src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import edu.harvard.iq.dataverse.ingest.IngestableDataChecker;
3838
import edu.harvard.iq.dataverse.license.License;
3939
import edu.harvard.iq.dataverse.settings.ConfigCheckService;
40+
import edu.harvard.iq.dataverse.settings.FeatureFlags;
4041
import edu.harvard.iq.dataverse.settings.JvmSettings;
4142
import edu.harvard.iq.dataverse.util.file.BagItFileHandler;
4243
import edu.harvard.iq.dataverse.util.file.BagItFileHandlerFactory;
@@ -87,6 +88,10 @@
8788
import jakarta.activation.MimetypesFileTypeMap;
8889
import jakarta.ejb.EJBException;
8990
import jakarta.enterprise.inject.spi.CDI;
91+
import jakarta.faces.application.FacesMessage;
92+
import jakarta.faces.component.UIComponent;
93+
import jakarta.faces.context.FacesContext;
94+
import jakarta.faces.validator.ValidatorException;
9095
import jakarta.json.JsonArray;
9196
import jakarta.json.JsonObject;
9297

@@ -1836,6 +1841,47 @@ public static boolean isActivelyEmbargoed(List<FileMetadata> fmdList) {
18361841
}
18371842
return false;
18381843
}
1844+
1845+
/**
1846+
* Validates that an embargo reason is not blank, and exists when required.
1847+
* This method is designed to be called from JSF validator methods.
1848+
*
1849+
* @param context The FacesContext
1850+
* @param component The UIComponent being validated
1851+
* @param value The value to validate (embargo reason)
1852+
* @param removeEmbargo Whether the embargo is being removed (skips validation if true)
1853+
* @param saveButtonId The ID pattern of the save button that should trigger validation
1854+
* @throws ValidatorException if validation fails
1855+
*/
1856+
public static void validateEmbargoReason(FacesContext context, UIComponent component, Object value,
1857+
boolean removeEmbargo) {
1858+
// Skip validation if removing embargo
1859+
if (removeEmbargo) {
1860+
return;
1861+
}
1862+
1863+
// Get the source of the current request
1864+
String source = context.getExternalContext().getRequestParameterMap()
1865+
.get("jakarta.faces.source");
1866+
1867+
// Only validate if the save button triggered this
1868+
if (source == null || !source.contains("fileEmbargoPopupSaveButton")) {
1869+
return;
1870+
}
1871+
1872+
if (value == null && FeatureFlags.REQUIRE_EMBARGO_REASON.enabled()) {
1873+
throw new ValidatorException(
1874+
new FacesMessage(FacesMessage.SEVERITY_ERROR,
1875+
BundleUtil.getStringFromBundle("embargo.reason.required"), null)
1876+
);
1877+
}
1878+
if (value != null && value.toString().trim().isEmpty()) {
1879+
throw new ValidatorException(
1880+
new FacesMessage(FacesMessage.SEVERITY_ERROR,
1881+
BundleUtil.getStringFromBundle("embargo.reason.blank"), null)
1882+
);
1883+
}
1884+
}
18391885

18401886
public static boolean isRetentionExpired(DataFile df) {
18411887
Retention e = df.getRetention();

src/main/java/propertyFiles/Bundle.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ embargoed.wasthrough=Was embargoed until
4141
embargoed.willbeuntil=Draft: will be embargoed until
4242
embargo.date.invalid=Date is outside the allowed range: ({0} to {1})
4343
embargo.date.required=An embargo date is required
44+
embargo.reason.required=An embargo reason is required
45+
embargo.reason.blank=An embargo reason cannot be blank
4446
retention.after=Was retained until
4547
retention.isfrom=Is retained until
4648
retention.willbeafter=Draft: will be retained until

src/main/webapp/file-edit-popup-fragment.xhtml

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,9 @@
123123
maxdate="#{settingsWrapper.maxEmbargoDate}"
124124
disabled="#{bean.removeEmbargo}"
125125
validator="#{settingsWrapper.validateEmbargoDate}" >
126-
<p:ajax process="#{updateElements}"
126+
<p:ajax process="@this embargoCheckbox"
127127
update="#{updateElements}"
128+
partialSubmit="true"
128129
/>
129130
</p:datePicker>
130131
<div>
@@ -133,12 +134,23 @@
133134
</div>
134135
</div>
135136
<div class="p-field p-col-12 p-md-4">
136-
<p class="help-block">#{bundle['file.editEmbargoDialog.reason.tip']}</p>
137+
<o:importConstants type="edu.harvard.iq.dataverse.settings.FeatureFlags" />
138+
<ui:param name="embargoReasonRequired" value="#{FeatureFlags.REQUIRE_EMBARGO_REASON.enabled()}"/>
139+
<label class="help-block" for="datasetForm:fileEmbargoAddReason">
140+
#{bundle['file.editEmbargoDialog.reason.tip']} <span
141+
class="glyphicon glyphicon-asterisk text-danger"
142+
jsf:rendered="#{embargoReasonRequired and !bean.removeEmbargo}" />
143+
</label>
137144
<p:inputText id="fileEmbargoAddReason" styleClass="form-control"
138145
disabled="#{bean.removeEmbargo}" type="text"
139146
value="#{bean.selectionEmbargo.reason}"
147+
validator="#{bean.validateEmbargoReason}"
140148
placeholder="#{bundle['file.editEmbargoDialog.newReason']}"
141149
onkeypress="if (event.keyCode == 13) { return false;}" />
150+
<div>
151+
<h:message for="fileEmbargoAddReason"
152+
styleClass="bg-danger text-danger" />
153+
</div>
142154
</div>
143155
</div>
144156
</div>
@@ -153,8 +165,9 @@
153165
<p:selectBooleanCheckbox value="#{bean.removeEmbargo}" id="embargoCheckbox"
154166
itemLabel="#{bundle['file.editEmbargoDialog.remove']}"
155167
disabled="#{facesContext.validationFailed}">
156-
<p:ajax process="#{updateElements}"
168+
<p:ajax process="@this fileEmbargoDate"
157169
update="#{updateElements}"
170+
partialSubmit="true"
158171
/>
159172
</p:selectBooleanCheckbox>
160173
</div>

0 commit comments

Comments
 (0)