Skip to content

Commit 0d324a9

Browse files
committed
Merge branch 'develop' into 11323-includeDeaccesioned-compare-ds-versions
2 parents f26758b + feb1c02 commit 0d324a9

File tree

9 files changed

+263
-74
lines changed

9 files changed

+263
-74
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
## Bug fix to Search API. Now includes all type totals (Dataverses, Dataset, and Files) regardless of the list of types requested
2+
3+
None requested types were returned with total count set to 0.
4+
&type=dataverse&type=dataset would result in "Files" : 0 since type=file was not requested
5+
6+
Now all counts show the correct totals.
7+
Note: This is only true for the first page requested. Subsequent pages could have 0 counts and should not be used. This is due to the need for speed. Getting the totals is an additional search call in the background.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
### Extend Restrict API to include new attributes
2+
3+
Original /restrict API only allowed for a boolean to update the restricted attribute of a file.
4+
The extended API still allows for the single boolean for backward compatibility.
5+
This change also allows for a JSON object to be passed which allows for the required `restrict` flag as well as optional attributes: `enableAccessRequest` and `termsOfAccess`.
6+
If `enableAccessRequest` is false then the `termsOfAccess` text must also be included.
7+
8+
See [the guides](https://dataverse-guide--11349.org.readthedocs.build/en/11349/api/native-api.html#restrict-files), #11299, and #11349.

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4039,6 +4039,8 @@ Restrict Files
40394039
~~~~~~~~~~~~~~
40404040
40414041
Restrict or unrestrict an existing file where ``id`` is the database id of the file or ``pid`` is the persistent id (DOI or Handle) of the file to restrict. Note that some Dataverse installations do not allow the ability to restrict files (see :ref:`:PublicInstall`).
4042+
Restricting or Unrestricting a file, not in a draft version of the Dataset, will result in a new Draft version being created.
4043+
Optionally the API can receive a JSON string with additional parameters related to the ability to request access to the file and the terms of that access.
40424044
40434045
A curl example using an ``id``
40444046
@@ -4072,6 +4074,33 @@ The fully expanded example above (without environment variables) looks like this
40724074
40734075
curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X PUT -d true "https://demo.dataverse.org/api/files/:persistentId/restrict?persistentId=doi:10.5072/FK2/AAA000"
40744076
4077+
Optional JSON string with additional attributes:
4078+
4079+
.. code-block:: bash
4080+
4081+
export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
4082+
export SERVER_URL=https://demo.dataverse.org
4083+
export ID=24
4084+
4085+
curl -H "X-Dataverse-key:$API_TOKEN" -X PUT "$SERVER_URL/api/files/$ID/restrict" \
4086+
-H "Content-Type: application/json" \
4087+
-d '{"restrict": true, "enableAccessRequest":false, "termsOfAccess": "Reason for the restricted access"}'
4088+
4089+
Note the behavior of the optional parameters:
4090+
4091+
- If restrict is false then enableAccessRequest and termsOfAccess are ignored
4092+
- If restrict is true and enableAccessRequest is false then termsOfAccess is required. A status of CONFLICT (409) will be returned if the termsOfAccess is missing
4093+
4094+
The enableAccessRequest and termsOfAccess are applied to the Draft version of the Dataset and affect all of the restricted files in said Draft version.
4095+
4096+
The fully expanded example above (without environment variables) looks like this:
4097+
4098+
.. code-block:: bash
4099+
4100+
curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X PUT "https://demo.dataverse.org/api/files/:persistentId/restrict?persistentId=doi:10.5072/FK2/AAA000" \
4101+
-H "Content-Type: application/json" \
4102+
-d '{"restrict": true, "enableAccessRequest":false, "termsOfAccess": "Reason for the restricted access"}'
4103+
40754104
.. _file-uningest:
40764105
40774106
Uningest a File

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

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,43 @@ public Response restrictFileInDataset(@Context ContainerRequestContext crc, @Pat
141141
return error(BAD_REQUEST, "Could not find datafile with id " + fileToRestrictId);
142142
}
143143

144-
boolean restrict = Boolean.valueOf(restrictStr);
144+
Boolean restrict = null;
145+
Boolean enableAccessRequest = null;
146+
String termsOfAccess = null;
147+
String returnMessage = " ";
148+
// Backward comparability - allow true/false in string(old) or json(new)
149+
if (restrictStr != null && restrictStr.trim().startsWith("{")) {
150+
// process as json
151+
jakarta.json.JsonObject jsonObject;
152+
try (StringReader stringReader = new StringReader(restrictStr)) {
153+
jsonObject = Json.createReader(stringReader).readObject();
154+
if (jsonObject.containsKey("restrict")) {
155+
restrict = Boolean.valueOf(jsonObject.getBoolean("restrict"));
156+
returnMessage += restrict ? "restricted." : "unrestricted.";
157+
} else {
158+
return badRequest("Error parsing Json: 'restrict' is required.");
159+
}
160+
if (jsonObject.containsKey("enableAccessRequest")) {
161+
enableAccessRequest = Boolean.valueOf(jsonObject.getBoolean("enableAccessRequest"));
162+
returnMessage += " Access Request is " + (enableAccessRequest ? "enabled." : "disabled.");
163+
}
164+
if (jsonObject.containsKey("termsOfAccess")) {
165+
termsOfAccess = jsonObject.getString("termsOfAccess");
166+
returnMessage += " Terms of Access for restricted files: " + termsOfAccess;
167+
}
168+
} catch (JsonParsingException jpe) {
169+
return badRequest("Error parsing Json: " + jpe.getMessage());
170+
}
171+
} else {
172+
restrict = Boolean.valueOf(restrictStr);
173+
returnMessage += restrict ? "restricted." : "unrestricted.";
174+
}
145175

146176
dataverseRequest = createDataverseRequest(getRequestUser(crc));
147177

148178
// try to restrict the datafile
149179
try {
150-
engineSvc.submit(new RestrictFileCommand(dataFile, dataverseRequest, restrict));
180+
engineSvc.submit(new RestrictFileCommand(dataFile, dataverseRequest, restrict, enableAccessRequest, termsOfAccess));
151181
} catch (CommandException ex) {
152182
return error(BAD_REQUEST, "Problem trying to update restriction status on " + dataFile.getDisplayName() + ": " + ex.getLocalizedMessage());
153183
}
@@ -165,8 +195,7 @@ public Response restrictFileInDataset(@Context ContainerRequestContext crc, @Pat
165195
return error(BAD_REQUEST, "Problem saving datafile " + dataFile.getDisplayName() + ": " + ex.getLocalizedMessage());
166196
}
167197

168-
String text = restrict ? "restricted." : "unrestricted.";
169-
return ok("File " + dataFile.getDisplayName() + " " + text);
198+
return ok("File " + dataFile.getDisplayName() + returnMessage);
170199
}
171200

172201

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

Lines changed: 51 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import edu.harvard.iq.dataverse.*;
44
import edu.harvard.iq.dataverse.api.auth.AuthRequired;
5+
import edu.harvard.iq.dataverse.engine.command.DataverseRequest;
56
import edu.harvard.iq.dataverse.search.SearchFields;
67
import edu.harvard.iq.dataverse.search.FacetCategory;
78
import edu.harvard.iq.dataverse.search.FacetLabel;
@@ -17,10 +18,7 @@
1718
import edu.harvard.iq.dataverse.search.SortBy;
1819
import edu.harvard.iq.dataverse.settings.SettingsServiceBean;
1920
import java.io.IOException;
20-
import java.util.ArrayList;
21-
import java.util.Arrays;
22-
import java.util.List;
23-
import java.util.Map;
21+
import java.util.*;
2422
import java.util.logging.Logger;
2523
import jakarta.ejb.EJB;
2624
import jakarta.inject.Inject;
@@ -92,9 +90,50 @@ public Response search(
9290
String geoPoint;
9391
String geoRadius;
9492
List<Dataverse> dataverseSubtrees = new ArrayList<>();
93+
DataverseRequest requestUser = createDataverseRequest(user);
94+
String allTypes = ":(" + SearchConstants.DATAVERSES + " OR " + SearchConstants.DATASETS + " OR " + SearchConstants.FILES + ")";
95+
Map<String, Long> objectTypeCountsMap = new HashMap<>(3);
96+
objectTypeCountsMap.put(SearchConstants.UI_DATAVERSES, 0L);
97+
objectTypeCountsMap.put(SearchConstants.UI_DATASETS, 0L);
98+
objectTypeCountsMap.put(SearchConstants.UI_FILES, 0L);
99+
100+
// users can't change these (yet anyway)
101+
boolean dataRelatedToMe = showMyData; //getDataRelatedToMe();
95102

96103
try {
104+
// we have to add "" (root) otherwise there is no permissions check
105+
if (subtrees.isEmpty()) {
106+
dataverseSubtrees.add(getSubtree(""));
107+
}
108+
else {
109+
for (String subtree : subtrees) {
110+
dataverseSubtrees.add(getSubtree(subtree));
111+
}
112+
}
113+
filterQueries.add(getFilterQueryFromSubtrees(dataverseSubtrees));
114+
97115
if (!types.isEmpty()) {
116+
// Query to get the totals if needed.
117+
// Only needed if the list of types doesn't include all types since missing types will default to count of 0
118+
// Only get the totals for the first page (paginationStart == 0) for speed
119+
if (showTypeCounts && types.size() < objectTypeCountsMap.size() && paginationStart == 0) {
120+
List<String> totalFilterQueries = new ArrayList<>();
121+
totalFilterQueries.addAll(filterQueries);
122+
totalFilterQueries.add(SearchFields.TYPE + allTypes);
123+
try {
124+
SolrQueryResponse resp = searchService.search(requestUser, dataverseSubtrees, query, totalFilterQueries, null, null, 0,
125+
dataRelatedToMe, 1, false, null, null, false, false);
126+
if (resp != null) {
127+
for (FacetCategory facetCategory : resp.getTypeFacetCategories()) {
128+
for (FacetLabel facetLabel : facetCategory.getFacetLabel()) {
129+
objectTypeCountsMap.put(facetLabel.getName(), facetLabel.getCount());
130+
}
131+
}
132+
}
133+
} catch(Exception e) {
134+
logger.info("Search getting total counts: " + e.getMessage());
135+
}
136+
}
98137
filterQueries.add(getFilterQueryFromTypes(types));
99138
} else {
100139
/**
@@ -103,22 +142,11 @@ public Response search(
103142
* SearchServiceBean tries to get SearchFields.TYPE. The GUI
104143
* always seems to add SearchFields.TYPE, even for superusers.
105144
*/
106-
filterQueries.add(SearchFields.TYPE + ":(" + SearchConstants.DATAVERSES + " OR " + SearchConstants.DATASETS + " OR " + SearchConstants.FILES + ")");
145+
filterQueries.add(SearchFields.TYPE + allTypes);
107146
}
108147
sortBy = SearchUtil.getSortBy(sortField, sortOrder);
109148
numResultsPerPage = getNumberOfResultsPerPage(numResultsPerPageRequested);
110149

111-
// we have to add "" (root) otherwise there is no permissions check
112-
if(subtrees.isEmpty()) {
113-
dataverseSubtrees.add(getSubtree(""));
114-
}
115-
else {
116-
for(String subtree : subtrees) {
117-
dataverseSubtrees.add(getSubtree(subtree));
118-
}
119-
}
120-
filterQueries.add(getFilterQueryFromSubtrees(dataverseSubtrees));
121-
122150
if(filterQueries.isEmpty()) { //Extra sanity check just in case someone else touches this
123151
throw new IOException("Filter is empty, which should never happen, as this allows unfettered searching of our index");
124152
}
@@ -137,13 +165,10 @@ public Response search(
137165
} catch (Exception ex) {
138166
return error(Response.Status.BAD_REQUEST, ex.getLocalizedMessage());
139167
}
140-
141-
// users can't change these (yet anyway)
142-
boolean dataRelatedToMe = showMyData; //getDataRelatedToMe();
143168

144169
SolrQueryResponse solrQueryResponse;
145170
try {
146-
solrQueryResponse = searchService.search(createDataverseRequest(user),
171+
solrQueryResponse = searchService.search(requestUser,
147172
dataverseSubtrees,
148173
query,
149174
filterQueries,
@@ -211,50 +236,17 @@ public Response search(
211236
}
212237

213238
value.add("count_in_response", solrSearchResults.size());
214-
215-
// we want to show the missing dvobject types with count = 0
216-
// per https://github.com/IQSS/dataverse/issues/11127
217239

218240
if (showTypeCounts) {
219-
JsonObjectBuilder objectTypeCounts = Json.createObjectBuilder();
220-
if (!solrQueryResponse.getTypeFacetCategories().isEmpty()) {
221-
boolean filesMissing = true;
222-
boolean datasetsMissing = true;
223-
boolean dataversesMissing = true;
224-
for (FacetCategory facetCategory : solrQueryResponse.getTypeFacetCategories()) {
225-
for (FacetLabel facetLabel : facetCategory.getFacetLabel()) {
226-
objectTypeCounts.add(facetLabel.getName(), facetLabel.getCount());
227-
if (facetLabel.getName().equals((SearchConstants.UI_DATAVERSES))) {
228-
dataversesMissing = false;
229-
}
230-
if (facetLabel.getName().equals((SearchConstants.UI_DATASETS))) {
231-
datasetsMissing = false;
232-
}
233-
if (facetLabel.getName().equals((SearchConstants.UI_FILES))) {
234-
filesMissing = false;
235-
}
236-
}
237-
}
238-
239-
if (solrQueryResponse.getTypeFacetCategories().size() < 3) {
240-
if (dataversesMissing) {
241-
objectTypeCounts.add(SearchConstants.UI_DATAVERSES, 0);
242-
}
243-
if (datasetsMissing) {
244-
objectTypeCounts.add(SearchConstants.UI_DATASETS, 0);
245-
}
246-
if (filesMissing) {
247-
objectTypeCounts.add(SearchConstants.UI_FILES, 0);
241+
for (FacetCategory facetCategory : solrQueryResponse.getTypeFacetCategories()) {
242+
for (FacetLabel facetLabel : facetCategory.getFacetLabel()) {
243+
if (facetLabel.getCount() > 0) {
244+
objectTypeCountsMap.put(facetLabel.getName(), facetLabel.getCount());
248245
}
249246
}
250-
251-
}
252-
if (showTypeCounts && solrQueryResponse.getTypeFacetCategories().isEmpty()) {
253-
objectTypeCounts.add(SearchConstants.UI_DATAVERSES, 0);
254-
objectTypeCounts.add(SearchConstants.UI_DATASETS, 0);
255-
objectTypeCounts.add(SearchConstants.UI_FILES, 0);
256247
}
257-
248+
JsonObjectBuilder objectTypeCounts = Json.createObjectBuilder();
249+
objectTypeCountsMap.forEach((k,v) -> objectTypeCounts.add(k,v));
258250
value.add("total_count_per_object_type", objectTypeCounts);
259251
}
260252
/**

src/main/java/edu/harvard/iq/dataverse/engine/command/impl/RestrictFileCommand.java

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@
55
*/
66
package edu.harvard.iq.dataverse.engine.command.impl;
77

8-
import edu.harvard.iq.dataverse.DataFile;
9-
import edu.harvard.iq.dataverse.Dataset;
10-
import edu.harvard.iq.dataverse.DatasetVersion;
11-
import edu.harvard.iq.dataverse.FileMetadata;
8+
import edu.harvard.iq.dataverse.*;
129
import edu.harvard.iq.dataverse.authorization.Permission;
1310
import edu.harvard.iq.dataverse.engine.command.AbstractVoidCommand;
1411
import edu.harvard.iq.dataverse.engine.command.CommandContext;
@@ -34,13 +31,25 @@ public class RestrictFileCommand extends AbstractVoidCommand {
3431

3532
private final DataFile file;
3633
private final boolean restrict;
37-
34+
private final Boolean fileAccessRequest;
35+
private final String termsOfAccess;
36+
37+
public RestrictFileCommand(DataFile file, DataverseRequest aRequest, boolean restrict, Boolean fileAccessRequest, String termsOfAccess) {
38+
super(aRequest, file.getOwner());
39+
this.file = file;
40+
this.restrict = restrict;
41+
this.fileAccessRequest = fileAccessRequest;
42+
this.termsOfAccess = termsOfAccess;
43+
}
44+
3845
public RestrictFileCommand(DataFile file, DataverseRequest aRequest, boolean restrict) {
3946
super(aRequest, file.getOwner());
4047
this.file = file;
4148
this.restrict = restrict;
42-
}
43-
49+
this.fileAccessRequest = null;
50+
this.termsOfAccess = null;
51+
}
52+
4453
@Override
4554
protected void executeImpl(CommandContext ctxt) throws CommandException {
4655
// check if public install & don't allow
@@ -64,6 +73,13 @@ protected void executeImpl(CommandContext ctxt) throws CommandException {
6473
else {
6574
Dataset dataset = file.getOwner();
6675
DatasetVersion workingVersion = dataset.getOrCreateEditVersion();
76+
if (restrict && fileAccessRequest != null) {
77+
if (workingVersion.getTermsOfUseAndAccess() == null) {
78+
workingVersion.setTermsOfUseAndAccess(new TermsOfUseAndAccess());
79+
}
80+
workingVersion.getTermsOfUseAndAccess().setFileAccessRequest(fileAccessRequest);
81+
workingVersion.getTermsOfUseAndAccess().setTermsOfAccess(termsOfAccess);
82+
}
6783
// We need the FileMetadata for the file in the draft dataset version and the
6884
// file we have may still reference the fmd from the prior released version
6985
FileMetadata draftFmd = file.getFileMetadata();

src/test/java/edu/harvard/iq/dataverse/api/FilesIT.java

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,14 +1027,37 @@ public void testRestrictFile() {
10271027
.body("message", equalTo("Problem trying to update restriction status on dataverseproject.png: File dataverseproject.png is already restricted"))
10281028
.statusCode(BAD_REQUEST.getStatusCode());
10291029

1030-
//unrestrict file
1031-
restrict = false;
1032-
Response unrestrictResponse = UtilIT.restrictFile(origFileId.toString(), restrict, apiToken);
1030+
//unrestrict file using json with missing "restrict"
1031+
String restrictJson = "{}";
1032+
Response unrestrictResponse = UtilIT.restrictFile(origFileId.toString(), restrictJson, apiToken);
1033+
unrestrictResponse.prettyPrint();
1034+
unrestrictResponse.then().assertThat()
1035+
.body("message", equalTo("Error parsing Json: 'restrict' is required."))
1036+
.statusCode(BAD_REQUEST.getStatusCode());
1037+
1038+
//unrestrict file using json
1039+
restrictJson = "{\"restrict\":false}";
1040+
unrestrictResponse = UtilIT.restrictFile(origFileId.toString(), restrictJson, apiToken);
10331041
unrestrictResponse.prettyPrint();
10341042
unrestrictResponse.then().assertThat()
10351043
.body("data.message", equalTo("File dataverseproject.png unrestricted."))
10361044
.statusCode(OK.getStatusCode());
10371045

1046+
//restrict file using json with enableAccessRequest false and missing TOA
1047+
restrictJson = "{\"restrict\":true, \"enableAccessRequest\":false}";
1048+
restrictResponse = UtilIT.restrictFile(origFileId.toString(), restrictJson, apiToken);
1049+
restrictResponse.prettyPrint();
1050+
restrictResponse.then().assertThat()
1051+
.body("message", equalTo(BundleUtil.getStringFromBundle("dataset.message.toua.invalid")))
1052+
.statusCode(CONFLICT.getStatusCode());
1053+
//restrict file using json
1054+
restrictJson = "{\"restrict\":true, \"enableAccessRequest\":false, \"termsOfAccess\":\"Testing terms of access\"}";
1055+
restrictResponse = UtilIT.restrictFile(origFileId.toString(), restrictJson, apiToken);
1056+
restrictResponse.prettyPrint();
1057+
restrictResponse.then().assertThat()
1058+
.body("data.message", equalTo("File dataverseproject.png restricted. Access Request is disabled. Terms of Access for restricted files: Testing terms of access"))
1059+
.statusCode(OK.getStatusCode());
1060+
10381061
//reset public install
10391062
UtilIT.setSetting(SettingsServiceBean.Key.PublicInstall, publicInstall);
10401063

0 commit comments

Comments
 (0)