• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DataBiosphere / consent / #5929

19 May 2025 12:36PM UTC coverage: 78.551% (-0.2%) from 78.727%
#5929

push

web-flow
DT-1564, Progress Report DAC flow (#2523)

175 of 228 new or added lines in 17 files covered. (76.75%)

5 existing lines in 3 files now uncovered.

10027 of 12765 relevant lines covered (78.55%)

0.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.71
/src/main/java/org/broadinstitute/consent/http/resources/DataAccessRequestResource.java
1
package org.broadinstitute.consent.http.resources;
2

3
import com.google.api.client.http.HttpStatusCodes;
4
import com.google.cloud.storage.BlobId;
5
import com.google.common.base.Strings;
6
import com.google.inject.Inject;
7
import io.dropwizard.auth.Auth;
8
import jakarta.annotation.security.PermitAll;
9
import jakarta.annotation.security.RolesAllowed;
10
import jakarta.ws.rs.BadRequestException;
11
import jakarta.ws.rs.Consumes;
12
import jakarta.ws.rs.DELETE;
13
import jakarta.ws.rs.ForbiddenException;
14
import jakarta.ws.rs.GET;
15
import jakarta.ws.rs.NotFoundException;
16
import jakarta.ws.rs.POST;
17
import jakarta.ws.rs.PUT;
18
import jakarta.ws.rs.Path;
19
import jakarta.ws.rs.PathParam;
20
import jakarta.ws.rs.Produces;
21
import jakarta.ws.rs.core.Context;
22
import jakarta.ws.rs.core.MediaType;
23
import jakarta.ws.rs.core.Request;
24
import jakarta.ws.rs.core.Response;
25
import jakarta.ws.rs.core.StreamingOutput;
26
import jakarta.ws.rs.core.UriInfo;
27
import java.io.IOException;
28
import java.io.InputStream;
29
import java.net.URI;
30
import java.util.Collections;
31
import java.util.List;
32
import java.util.Objects;
33
import java.util.UUID;
34
import java.util.stream.Collectors;
35
import java.util.stream.Stream;
36
import org.apache.commons.lang3.StringUtils;
37
import org.broadinstitute.consent.http.cloudstore.GCSService;
38
import org.broadinstitute.consent.http.enumeration.DarDocumentType;
39
import org.broadinstitute.consent.http.enumeration.UserRoles;
40
import org.broadinstitute.consent.http.exceptions.LibraryCardRequiredException;
41
import org.broadinstitute.consent.http.exceptions.SubmittedDARCannotBeEditedException;
42
import org.broadinstitute.consent.http.models.AuthUser;
43
import org.broadinstitute.consent.http.models.DataAccessAgreement;
44
import org.broadinstitute.consent.http.models.DataAccessRequest;
45
import org.broadinstitute.consent.http.models.DataAccessRequestData;
46
import org.broadinstitute.consent.http.models.DataUse;
47
import org.broadinstitute.consent.http.models.Dataset;
48
import org.broadinstitute.consent.http.models.Error;
49
import org.broadinstitute.consent.http.models.User;
50
import org.broadinstitute.consent.http.service.DaaService;
51
import org.broadinstitute.consent.http.service.DarCollectionService;
52
import org.broadinstitute.consent.http.service.DataAccessRequestService;
53
import org.broadinstitute.consent.http.service.DatasetService;
54
import org.broadinstitute.consent.http.service.EmailService;
55
import org.broadinstitute.consent.http.service.MatchService;
56
import org.broadinstitute.consent.http.service.UserService;
57
import org.broadinstitute.consent.http.util.ComplianceLogger;
58
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
59
import org.glassfish.jersey.media.multipart.FormDataParam;
60
import org.glassfish.jersey.server.ContainerRequest;
61

62
@Path("api/dar")
63
public class DataAccessRequestResource extends Resource {
64

65
  private final DaaService daaService;
66
  private final DataAccessRequestService dataAccessRequestService;
67
  private final DarCollectionService darCollectionService;
68
  private final EmailService emailService;
69
  private final GCSService gcsService;
70
  private final MatchService matchService;
71
  private final UserService userService;
72
  private final DatasetService datasetService;
73

74
  @Inject
75
  public DataAccessRequestResource(
76
      DaaService daaService,
77
      DataAccessRequestService dataAccessRequestService,
78
      EmailService emailService,
79
      GCSService gcsService,
80
      UserService userService,
81
      DatasetService datasetService,
82
      MatchService matchService,
83
      DarCollectionService darCollectionService
84
  ) {
1✔
85
    this.daaService = daaService;
1✔
86
    this.dataAccessRequestService = dataAccessRequestService;
1✔
87
    this.emailService = emailService;
1✔
88
    this.gcsService = gcsService;
1✔
89
    this.userService = userService;
1✔
90
    this.datasetService = datasetService;
1✔
91
    this.matchService = matchService;
1✔
92
    this.darCollectionService = darCollectionService;
1✔
93
  }
1✔
94

95
  @GET
96
  @Produces("application/json")
97
  @PermitAll
98
  @Path("/v2")
99
  public Response getDataAccessRequests(@Auth AuthUser authUser) {
100
    try {
101
      User user = userService.findUserByEmail(authUser.getEmail());
1✔
102
      List<DataAccessRequest> dars = dataAccessRequestService.getDataAccessRequestsByUserRole(user);
1✔
103
      return Response.ok().entity(dars).build();
1✔
104
    } catch (Exception e) {
×
105
      return createExceptionResponse(e);
×
106
    }
107
  }
108

109
  @POST
110
  @Consumes("application/json")
111
  @Produces("application/json")
112
  @RolesAllowed(RESEARCHER)
113
  @Path("/v2")
114
  public Response createDataAccessRequest(
115
      @Auth AuthUser authUser,
116
      @Context Request request,
117
      @Context UriInfo info,
118
      String dar) {
119
    try {
120
      User user = findUserByEmail(authUser.getEmail());
1✔
121

122
      DataAccessRequest payload = populateDarFromJsonString(user, dar);
1✔
123
      DataAccessRequest newDar = dataAccessRequestService.createDataAccessRequest(user, payload);
1✔
124
      sendNewDarCollectionMessage(newDar.getCollectionId());
1✔
125
      URI uri = info.getRequestUriBuilder().build();
1✔
126
      matchService.reprocessMatchesForPurpose(newDar.getReferenceId());
1✔
127
      List<Dataset> datasets = datasetService.findDatasetsByIds(newDar.getDatasetIds());
1✔
128
      ComplianceLogger.logDARSubmission(user, datasets, ((ContainerRequest) request), HttpStatusCodes.STATUS_CODE_CREATED);
1✔
129
      return Response.created(uri).entity(newDar.convertToSimplifiedDar()).build();
1✔
130
    } catch (Exception e) {
1✔
131
      return createExceptionResponse(e);
1✔
132
    }
133
  }
134

135
  @POST
136
  @Consumes("application/json")
137
  @Produces("application/json")
138
  @RolesAllowed(RESEARCHER)
139
  @Path("/v3")
140
  public Response createDataAccessRequestWithDAARestrictions(
141
      @Auth AuthUser authUser, @Context UriInfo info, String dar) {
142
    try {
143
      User user = findUserByEmail(authUser.getEmail());
1✔
144
      DataAccessRequest payload = populateDarFromJsonString(user, dar);
1✔
145
      // DAA Enforcement
146
      datasetService.enforceDAARestrictions(user, payload.getDatasetIds());
1✔
147
      DataAccessRequest newDar = dataAccessRequestService.createDataAccessRequest(user, payload);
1✔
148
      sendNewDarCollectionMessage(newDar.getCollectionId());
1✔
149
      URI uri = info.getRequestUriBuilder().build();
1✔
150
      matchService.reprocessMatchesForPurpose(newDar.getReferenceId());
1✔
151
      return Response.created(uri).entity(newDar.convertToSimplifiedDar()).build();
1✔
152
    } catch (Exception e) {
1✔
153
      return createExceptionResponse(e);
1✔
154
    }
155
  }
156

157
  @GET
158
  @Path("/v2/{referenceId}")
159
  @Produces("application/json")
160
  @PermitAll
161
  public Response getByReferenceId(
162
      @Auth AuthUser authUser, @PathParam("referenceId") String referenceId) {
163
    validateAuthedRoleUser(
1✔
164
        Stream.of(UserRoles.ADMIN, UserRoles.CHAIRPERSON, UserRoles.MEMBER)
1✔
165
            .collect(Collectors.toList()),
1✔
166
        authUser, referenceId);
167
    try {
168
      DataAccessRequest dar = dataAccessRequestService.findByReferenceId(referenceId);
1✔
169
      if (Objects.nonNull(dar)) {
1✔
170
        return Response.status(Response.Status.OK).entity(dar.convertToSimplifiedDar()).build();
1✔
171
      }
172
      return Response.status(Response.Status.NOT_FOUND)
×
173
          .entity(
×
174
              new Error(
175
                  "Unable to find Data Access Request with reference id: " + referenceId,
176
                  Response.Status.NOT_FOUND.getStatusCode()))
×
177
          .build();
×
178
    } catch (Exception e) {
×
179
      return createExceptionResponse(e);
×
180
    }
181
  }
182

183
  @GET
184
  @Path("/v2/{referenceId}/daas")
185
  @Produces("application/json")
186
  @PermitAll
187
  public Response getDAAsByReferenceId(
188
      @Auth AuthUser authUser, @PathParam("referenceId") String referenceId) {
189
    try {
190
      validateAuthedRoleUser(
1✔
191
          Stream.of(UserRoles.ADMIN, UserRoles.CHAIRPERSON, UserRoles.MEMBER)
1✔
192
              .collect(Collectors.toList()),
1✔
193
          authUser, referenceId);
194
      List<DataAccessAgreement> dataAccessAgreements = daaService.findByDarReferenceId(referenceId);
1✔
195
      return Response.status(Response.Status.OK).entity(dataAccessAgreements).build();
1✔
196
    } catch (Exception e) {
1✔
197
      return createExceptionResponse(e);
1✔
198
    }
199
  }
200

201
  @PUT
202
  @Path("/v2/{referenceId}")
203
  @Produces("application/json")
204
  @RolesAllowed(RESEARCHER)
205
  public Response updateByReferenceId(
206
      @Auth AuthUser authUser, @PathParam("referenceId") String referenceId, String dar) {
207
    try {
208
      User user = findUserByEmail(authUser.getEmail());
1✔
209
      DataAccessRequest originalDar = dataAccessRequestService.findByReferenceId(referenceId);
1✔
210
      checkAuthorizedUpdateUser(user, originalDar);
1✔
211
      DataAccessRequestData data = DataAccessRequestData.fromString(dar);
1✔
212
      // Keep dar data reference id in sync with the dar until we fully deprecate
213
      // it in dar data.
214
      data.setReferenceId(originalDar.getReferenceId());
1✔
215
      originalDar.setData(data);
1✔
216
      DataAccessRequest updatedDar =
1✔
217
          dataAccessRequestService.updateByReferenceId(user, originalDar);
1✔
218
      matchService.reprocessMatchesForPurpose(referenceId);
1✔
219
      return Response.ok().entity(updatedDar.convertToSimplifiedDar()).build();
1✔
220
    } catch (Exception e) {
1✔
221
      return createExceptionResponse(e);
1✔
222
    }
223
  }
224

225
  @GET
226
  @Produces("application/json")
227
  @Path("/v2/draft")
228
  @RolesAllowed(RESEARCHER)
229
  public Response getDraftDataAccessRequests(@Auth AuthUser authUser) {
230
    try {
231
      User user = findUserByEmail(authUser.getEmail());
1✔
232
      List<DataAccessRequest> draftDars = dataAccessRequestService.findAllDraftDataAccessRequestsByUser(
1✔
233
          user.getUserId());
1✔
234
      return Response.ok().entity(draftDars).build();
1✔
235
    } catch (Exception e) {
1✔
236
      return createExceptionResponse(e);
1✔
237
    }
238
  }
239

240
  @GET
241
  @Produces("application/json")
242
  @Path("/v2/draft/{referenceId}")
243
  @RolesAllowed(RESEARCHER)
244
  public Response getDraftDar(@Auth AuthUser authUser, @PathParam("referenceId") String id) {
245
    try {
246
      User user = findUserByEmail(authUser.getEmail());
1✔
247
      DataAccessRequest dar = dataAccessRequestService.findByReferenceId(id);
1✔
248
      if (dar.getUserId().equals(user.getUserId())) {
1✔
249
        return Response.ok().entity(dar).build();
1✔
250
      }
251
      throw new ForbiddenException("User does not have permission");
1✔
252
    } catch (Exception e) {
1✔
253
      return createExceptionResponse(e);
1✔
254
    }
255
  }
256

257
  @POST
258
  @Consumes("application/json")
259
  @Produces("application/json")
260
  @Path("/v2/draft")
261
  @RolesAllowed(RESEARCHER)
262
  public Response createDraftDataAccessRequest(
263
      @Auth AuthUser authUser, @Context UriInfo info, String dar) {
264
    try {
265
      User user = findUserByEmail(authUser.getEmail());
1✔
266
      DataAccessRequest newDar = populateDarFromJsonString(user, dar);
1✔
267
      DataAccessRequest result =
1✔
268
          dataAccessRequestService.insertDraftDataAccessRequest(user, newDar);
1✔
269
      URI uri = info.getRequestUriBuilder().path("/" + result.getReferenceId()).build();
1✔
270
      return Response.created(uri).entity(result.convertToSimplifiedDar()).build();
1✔
271
    } catch (Exception e) {
×
272
      return createExceptionResponse(e);
×
273
    }
274
  }
275

276
  @POST
277
  @Consumes("application/json")
278
  @Produces("application/json")
279
  @Path("/v3/draft")
280
  @RolesAllowed(RESEARCHER)
281
  public Response createDraftDataAccessRequestWithDAARestrictions(
282
      @Auth AuthUser authUser, @Context UriInfo info, String dar) {
283
    try {
284
      User user = findUserByEmail(authUser.getEmail());
1✔
285
      DataAccessRequest newDar = populateDarFromJsonString(user, dar);
1✔
286
      // DAA Enforcement
287
      datasetService.enforceDAARestrictions(user, newDar.getDatasetIds());
1✔
288
      DataAccessRequest result =
1✔
289
          dataAccessRequestService.insertDraftDataAccessRequest(user, newDar);
1✔
290
      URI uri = info.getRequestUriBuilder().path("/" + result.getReferenceId()).build();
1✔
291
      return Response.created(uri).entity(result.convertToSimplifiedDar()).build();
1✔
292
    } catch (Exception e) {
1✔
293
      return createExceptionResponse(e);
1✔
294
    }
295
  }
296

297
  @PUT
298
  @Consumes("application/json")
299
  @Produces("application/json")
300
  @Path("/v2/draft/{referenceId}")
301
  @RolesAllowed(RESEARCHER)
302
  public Response updatePartialDataAccessRequest(
303
      @Auth AuthUser authUser, @PathParam("referenceId") String referenceId, String dar) {
304
    try {
305
      User user = findUserByEmail(authUser.getEmail());
1✔
306
      DataAccessRequest originalDar = dataAccessRequestService.findByReferenceId(referenceId);
1✔
307
      checkAuthorizedUpdateUser(user, originalDar);
1✔
308
      DataAccessRequestData data = DataAccessRequestData.fromString(dar);
1✔
309
      // Keep dar data reference id in sync with the dar until we fully deprecate
310
      // it in dar data.
311
      data.setReferenceId(originalDar.getReferenceId());
1✔
312
      originalDar.setData(data);
1✔
313
      originalDar.setDatasetIds(data.getDatasetIds());
1✔
314
      DataAccessRequest updatedDar =
1✔
315
          dataAccessRequestService.updateByReferenceId(user, originalDar);
1✔
316
      return Response.ok().entity(updatedDar.convertToSimplifiedDar()).build();
1✔
317
    } catch (Exception e) {
1✔
318
      return createExceptionResponse(e);
1✔
319
    }
320
  }
321

322
  @PUT
323
  @Consumes("application/json")
324
  @Produces("application/json")
325
  @Path("/v3/draft/{referenceId}")
326
  @RolesAllowed(RESEARCHER)
327
  public Response updatePartialDataAccessRequestWithDAARestrictions(
328
      @Auth AuthUser authUser, @PathParam("referenceId") String referenceId, String dar) {
329
    try {
330
      User user = findUserByEmail(authUser.getEmail());
1✔
331
      DataAccessRequest originalDar = dataAccessRequestService.findByReferenceId(referenceId);
1✔
332
      checkAuthorizedUpdateUser(user, originalDar);
1✔
333
      DataAccessRequestData data = DataAccessRequestData.fromString(dar);
1✔
334
      // Keep dar data reference id in sync with the dar until we fully deprecate
335
      // it in dar data.
336
      data.setReferenceId(originalDar.getReferenceId());
1✔
337
      originalDar.setData(data);
1✔
338
      originalDar.setDatasetIds(data.getDatasetIds());
1✔
339
      // DAA Enforcement
340
      datasetService.enforceDAARestrictions(user, originalDar.getDatasetIds());
1✔
341
      DataAccessRequest updatedDar =
1✔
342
          dataAccessRequestService.updateByReferenceId(user, originalDar);
1✔
343
      return Response.ok().entity(updatedDar.convertToSimplifiedDar()).build();
1✔
344
    } catch (Exception e) {
1✔
345
      return createExceptionResponse(e);
1✔
346
    }
347
  }
348

349
  @GET
350
  @Produces({MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_JSON})
351
  @Path("/v2/{referenceId}/irbDocument")
352
  @RolesAllowed({ADMIN, CHAIRPERSON, MEMBER, RESEARCHER})
353
  public Response getIrbDocument(
354
      @Auth AuthUser authUser,
355
      @PathParam("referenceId") String referenceId) {
356
    try {
357
      DataAccessRequest dar = getDarById(referenceId);
1✔
358
      validateAuthedRoleUser(
1✔
359
          Stream.of(UserRoles.ADMIN, UserRoles.CHAIRPERSON, UserRoles.MEMBER)
1✔
360
              .collect(Collectors.toList()),
1✔
361
          authUser, referenceId);
362
      if (dar.getData() != null &&
1✔
363
          StringUtils.isNotEmpty(dar.getData().getIrbDocumentLocation()) &&
1✔
364
          StringUtils.isNotEmpty(dar.getData().getIrbDocumentName())
1✔
365
      ) {
366
        String blobIdName = dar.getData().getIrbDocumentLocation();
1✔
367
        String fileName = dar.getData().getIrbDocumentName();
1✔
368
        InputStream is = gcsService.getDocument(blobIdName);
1✔
369
        StreamingOutput stream = createStreamingOutput(is);
1✔
370
        return Response.ok(stream)
1✔
371
            .header("Content-Disposition", "attachment; filename=" + fileName)
1✔
372
            .build();
1✔
373
      }
374
      throw new NotFoundException();
1✔
375
    } catch (Exception e) {
1✔
376
      return createExceptionResponse(e);
1✔
377
    }
378
  }
379

380
  @POST
381
  @Consumes(MediaType.MULTIPART_FORM_DATA)
382
  @Produces(MediaType.APPLICATION_JSON)
383
  @Path("/v2/{referenceId}/irbDocument")
384
  @RolesAllowed({RESEARCHER})
385
  public Response uploadIrbDocument(
386
      @Auth AuthUser authUser,
387
      @PathParam("referenceId") String referenceId,
388
      @FormDataParam("file") InputStream uploadInputStream,
389
      @FormDataParam("file") FormDataContentDisposition fileDetail) {
390
    try {
391
      User user = findUserByEmail(authUser.getEmail());
1✔
392
      DataAccessRequest dar = getDarById(referenceId);
1✔
393
      checkAuthorizedUpdateUser(user, dar);
1✔
394
      DataAccessRequest updatedDar = updateDarWithDocumentContents(DarDocumentType.IRB, user, dar,
1✔
395
          uploadInputStream, fileDetail);
396
      return Response.ok(updatedDar.convertToSimplifiedDar()).build();
1✔
397
    } catch (Exception e) {
1✔
398
      return createExceptionResponse(e);
1✔
399
    }
400
  }
401

402
  @POST
403
  @Consumes(MediaType.MULTIPART_FORM_DATA)
404
  @Produces(MediaType.APPLICATION_JSON)
405
  @Path("/v2/progress_report/{parentReferenceId}")
406
  @RolesAllowed({RESEARCHER})
407
  public Response postProgressReport(
408
      @Auth AuthUser authUser,
409
      @PathParam("parentReferenceId") String parentReferenceId,
410
      @FormDataParam("dar") String dar,
411
      @FormDataParam("collaboratorRequiredFile") InputStream collabInputStream,
412
      @FormDataParam("collaboratorRequiredFile") FormDataContentDisposition collabFileDetails,
413
      @FormDataParam("ethicsApprovalRequiredFile") InputStream ethicsInputStream,
414
      @FormDataParam("ethicsApprovalRequiredFile") FormDataContentDisposition ethicsFileDetails) {
415
    try {
416
      User user = userService.findUserByEmail(authUser.getEmail());
1✔
417
      // added here because other dataAccessRequestServices calls are invoked that do not normally
418
      // require this sequence.  hasValidActiveERACredentials will also check for a LC so no
419
      // additional LC check needed.
420
      userService.hasValidActiveERACredentials(user);
1✔
421
      DataAccessRequest parentDar = dataAccessRequestService.findByReferenceId(parentReferenceId);
1✔
422
      // needs to happen before docs are uploaded
423
      if (!user.getUserId().equals(parentDar.getUserId())) {
1✔
424
        throw new ForbiddenException("User not authorized to update this Data Access Request");
1✔
425
      }
426
      DataAccessRequest payload = DataAccessRequest.populateProgressReportFromJsonString(dar, parentDar);
1✔
427
      populateProgressReportWithDocuments(collabInputStream, collabFileDetails, ethicsInputStream,
1✔
428
          ethicsFileDetails, payload, parentDar);
429
      DataAccessRequest progressReport = dataAccessRequestService.createProgressReport(user,
1✔
430
          payload, parentDar);
431
      sendNewDarCollectionMessage(parentDar.getCollectionId());
1✔
432
      return Response.ok(progressReport.convertToSimplifiedDar()).build();
1✔
433
    } catch (Exception e) {
1✔
434
      return createExceptionResponse(e);
1✔
435
    }
436
  }
437

438
  private void sendNewDarCollectionMessage(Integer collectionId) {
439
    try {
440
      // This service method knows how to distinguish between a DAR and a Progress Report.
441
      darCollectionService.sendNewDARCollectionMessage(collectionId);
1✔
NEW
442
    } catch (Exception e) {
×
443
      // non-fatal exception
NEW
444
      logCaughtEmailException(collectionId, e);
×
445
    }
1✔
446
  }
1✔
447

448
  private void logCaughtEmailException(Integer collectionId, Exception e) {
NEW
449
    logException("Exception sending email for collection id: " + collectionId, e);
×
NEW
450
  }
×
451

452
  public void populateProgressReportWithDocuments(InputStream collabInputStream,
453
      FormDataContentDisposition collabFileDetails, InputStream ethicsInputStream,
454
      FormDataContentDisposition ethicsFileDetails, DataAccessRequest childDar,
455
      DataAccessRequest parentDar) throws IOException {
456
    for (Integer datasetId : childDar.getDatasetIds()) {
1✔
457
      Dataset dataset = datasetService.findDatasetById(datasetId);
1✔
458
      if (dataset == null) {
1✔
459
        throw new NotFoundException("Dataset " + datasetId + " not found");
1✔
460
      }
461
      DataUse dataUse = dataset.getDataUse();
1✔
462
      if (dataUse == null || dataUse.getCollaboratorRequired() == null
1✔
463
          || dataUse.getEthicsApprovalRequired() == null) {
1✔
464
        throw new BadRequestException("Dataset " + datasetId + " is missing data use(s)");
1✔
465
      }
466
      if (dataUse.getCollaboratorRequired()) {
1✔
467
        String parentCollabLocation = parentDar.getData().getCollaborationLetterLocation();
1✔
468
        if ((collabFileDetails == null || collabFileDetails.getSize() <= 0)
1✔
469
            && Strings.isNullOrEmpty(parentCollabLocation)) {
1✔
470
          throw new BadRequestException("Collaboration document is required");
1✔
471
        }
472
        uploadDocumentContents(DarDocumentType.COLLABORATION, childDar,
1✔
473
            collabInputStream, collabFileDetails);
474
      }
475
      if (dataUse.getEthicsApprovalRequired()) {
1✔
476
        String parentEthicsLocation = parentDar.getData().getIrbDocumentLocation();
1✔
477
        if ((ethicsFileDetails == null || ethicsFileDetails.getSize() <= 0)
1✔
478
            && Strings.isNullOrEmpty(parentEthicsLocation)) {
1✔
479
          throw new BadRequestException("Ethics approval document is required");
1✔
480
        }
481
        uploadDocumentContents(DarDocumentType.IRB, childDar,
1✔
482
            ethicsInputStream, ethicsFileDetails);
483
      }
484
    }
1✔
485
  }
1✔
486

487
  @GET
488
  @Produces({MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_JSON})
489
  @Path("/v2/{referenceId}/collaborationDocument")
490
  @RolesAllowed({ADMIN, CHAIRPERSON, MEMBER, RESEARCHER})
491
  public Response getCollaborationDocument(
492
      @Auth AuthUser authUser,
493
      @PathParam("referenceId") String referenceId) {
494
    try {
495
      DataAccessRequest dar = getDarById(referenceId);
1✔
496
      validateAuthedRoleUser(
1✔
497
          Stream.of(UserRoles.ADMIN, UserRoles.CHAIRPERSON, UserRoles.MEMBER)
1✔
498
              .collect(Collectors.toList()),
1✔
499
          authUser, referenceId);
500
      if (dar.getData() != null &&
1✔
501
          StringUtils.isNotEmpty(dar.getData().getCollaborationLetterLocation()) &&
1✔
502
          StringUtils.isNotEmpty(dar.getData().getCollaborationLetterName())
1✔
503
      ) {
504
        String blobIdName = dar.getData().getCollaborationLetterLocation();
1✔
505
        String fileName = dar.getData().getCollaborationLetterName();
1✔
506
        InputStream is = gcsService.getDocument(blobIdName);
1✔
507
        StreamingOutput stream = createStreamingOutput(is);
1✔
508
        return Response.ok(stream)
1✔
509
            .header("Content-Disposition", "attachment; filename=" + fileName)
1✔
510
            .build();
1✔
511
      }
512
      throw new NotFoundException();
1✔
513
    } catch (Exception e) {
1✔
514
      return createExceptionResponse(e);
1✔
515
    }
516
  }
517

518
  @POST
519
  @Consumes(MediaType.MULTIPART_FORM_DATA)
520
  @Produces(MediaType.APPLICATION_JSON)
521
  @Path("/v2/{referenceId}/collaborationDocument")
522
  @RolesAllowed({RESEARCHER})
523
  public Response uploadCollaborationDocument(
524
      @Auth AuthUser authUser,
525
      @PathParam("referenceId") String referenceId,
526
      @FormDataParam("file") InputStream uploadInputStream,
527
      @FormDataParam("file") FormDataContentDisposition fileDetail) {
528
    try {
529
      User user = findUserByEmail(authUser.getEmail());
1✔
530
      DataAccessRequest dar = getDarById(referenceId);
1✔
531
      checkAuthorizedUpdateUser(user, dar);
1✔
532
      DataAccessRequest updatedDar = updateDarWithDocumentContents(DarDocumentType.COLLABORATION,
1✔
533
          user, dar, uploadInputStream, fileDetail);
534
      return Response.ok(updatedDar.convertToSimplifiedDar()).build();
1✔
535
    } catch (Exception e) {
1✔
536
      return createExceptionResponse(e);
1✔
537
    }
538
  }
539

540
  @DELETE
541
  @Path("/v2/{referenceId}")
542
  @Produces("application/json")
543
  @RolesAllowed({ADMIN, RESEARCHER})
544
  public Response deleteDar(@Auth AuthUser authUser, @PathParam("referenceId") String referenceId) {
545
    validateAuthedRoleUser(Collections.singletonList(UserRoles.ADMIN), authUser, referenceId);
×
546
    try {
547
      User user = findUserByEmail(authUser.getEmail());
×
548
      dataAccessRequestService.deleteByReferenceId(user, referenceId);
×
549
      return Response.ok().build();
×
550
    } catch (Exception e) {
×
551
      return createExceptionResponse(e);
×
552
    }
553
  }
554

555
  private User findUserByEmail(String email) {
556
    User user = userService.findUserByEmail(email);
1✔
557
    if (user == null) {
1✔
558
      throw new NotFoundException("Unable to find User with the provided email: " + email);
×
559
    }
560
    return user;
1✔
561
  }
562

563
  private DataAccessRequest populateDarFromJsonString(User user, String json) {
564
    DataAccessRequest newDar = new DataAccessRequest();
1✔
565
    DataAccessRequestData data = DataAccessRequestData.populateDARData(json);
1✔
566
    // When posting a submitted dar, there are two cases:
567
    // 1. those that existed previously as a draft dar
568
    // 2. those that are brand new
569
    // Validate the provided referenceId with the authenticated user and draft status
570
    // Those that do not validate are considered a brand new dar
571
    if (Objects.nonNull(data.getReferenceId())) {
1✔
572
      DataAccessRequest existingDar =
×
573
          dataAccessRequestService.findByReferenceId(data.getReferenceId());
×
574
      if (Objects.nonNull(existingDar)
×
575
          && existingDar.getUserId().equals(user.getUserId())
×
576
          && existingDar.getDraft()) {
×
577
        newDar.setReferenceId(data.getReferenceId());
×
578

579
        // if dar was part of a collection, we should use the same collection.
580
        if (Objects.nonNull(existingDar.getCollectionId())) {
×
581
          newDar.setCollectionId(existingDar.getCollectionId());
×
582
        }
583
      } else {
584
        String referenceId = UUID.randomUUID().toString();
×
585
        newDar.setReferenceId(referenceId);
×
586
        data.setReferenceId(referenceId);
×
587
      }
588
    } else {
×
589
      String referenceId = UUID.randomUUID().toString();
1✔
590
      newDar.setReferenceId(referenceId);
1✔
591
      data.setReferenceId(referenceId);
1✔
592
    }
593
    newDar.setData(data);
1✔
594
    newDar.addDatasetIds(data.getDatasetIds());
1✔
595
    return newDar;
1✔
596
  }
597

598
  /**
599
   * Populate a new Data Access Request from the JSON string and the parent Data Access Request.
600
   * Copies all the data from the parent dar, then overwrites the collaborators and datasets. Adds
601
   * all progress report specific fields.
602
   *
603
   * @param json      The JSON string to populate the new Progress Report.
604
   * @param parentDar The parent Data Access Request to copy data from.
605
   * @return A new Progress Report populated with the provided JSON string and parent DAR data.
606
   */
607
  public DataAccessRequest populateProgressReportFromJsonString(String json,
608
      DataAccessRequest parentDar) {
NEW
609
    DataAccessRequest newDar = new DataAccessRequest();
×
NEW
610
    DataAccessRequestData newData = DataAccessRequestData.populateDARData(json);
×
NEW
611
    DataAccessRequestData originalDataCopy = DataAccessRequestData.fromString(
×
NEW
612
        parentDar.getData().toString());
×
613

NEW
614
    String referenceId = UUID.randomUUID().toString();
×
NEW
615
    newDar.setReferenceId(referenceId);
×
NEW
616
    newDar.setParentId(parentDar.getId());
×
NEW
617
    newDar.setCollectionId(parentDar.getCollectionId());
×
618

NEW
619
    newDar.addDatasetIds(newData.getDatasetIds());
×
NEW
620
    originalDataCopy.setInternalCollaborators(newData.getInternalCollaborators());
×
NEW
621
    originalDataCopy.setExternalCollaborators(newData.getExternalCollaborators());
×
NEW
622
    originalDataCopy.setLabCollaborators(newData.getLabCollaborators());
×
NEW
623
    originalDataCopy.setProgressReportSummary(newData.getProgressReportSummary());
×
NEW
624
    originalDataCopy.setIntellectualPropertySummary(newData.getIntellectualPropertySummary());
×
NEW
625
    originalDataCopy.setPublications(newData.getPublications());
×
NEW
626
    originalDataCopy.setPresentations(newData.getPresentations());
×
NEW
627
    originalDataCopy.setDmi(newData.getDmi());
×
NEW
628
    originalDataCopy.setResearchPlans(newData.getResearchPlans());
×
NEW
629
    originalDataCopy.setCloseoutSupplement(newData.getCloseoutSupplement());
×
630

631
    // These values will be updated in populateProgressReportWithDocuments if documents exist.
632
    // Its important we don't copy over the parent values so those documents are not deleted.
NEW
633
    originalDataCopy.setCollaborationLetterName(null);
×
NEW
634
    originalDataCopy.setCollaborationLetterLocation(null);
×
NEW
635
    originalDataCopy.setIrbDocumentName(null);
×
NEW
636
    originalDataCopy.setIrbDocumentLocation(null);
×
637

NEW
638
    newDar.setData(originalDataCopy);
×
NEW
639
    return newDar;
×
640
  }
641

642
  private void checkAuthorizedUpdateUser(User user, DataAccessRequest dar) {
643
    if (!user.getUserId().equals(dar.getUserId())) {
1✔
644
      throw new ForbiddenException("User not authorized to update this Data Access Request");
1✔
645
    }
646
    if (user.getLibraryCards().isEmpty()) {
1✔
647
      throw new LibraryCardRequiredException();
×
648
    }
649
  }
1✔
650

651
  private DataAccessRequest updateDarWithDocumentContents(
652
      DarDocumentType type,
653
      User user,
654
      DataAccessRequest dar,
655
      InputStream uploadInputStream,
656
      FormDataContentDisposition fileDetail) throws IOException {
657
    // When we move updateDarWithDocumentContents to the service tier, we should incorporate the
658
    // code below into that method
659
    if (!dar.getDraft()) {
1✔
660
      throw new SubmittedDARCannotBeEditedException();
×
661
    }
662
    uploadDocumentContents(type, dar, uploadInputStream, fileDetail);
1✔
663
    return dataAccessRequestService.updateByReferenceId(user, dar);
1✔
664
  }
665

666
  /**
667
   * Uploads the document contents to GCS and mutates the dar data object with the blobId.
668
   *
669
   * @param type              The type of document (IRB or Collaboration)
670
   * @param dar               The Data Access Request
671
   * @param uploadInputStream The input stream of the file to be uploaded
672
   * @param fileDetail        The file details
673
   * @throws IOException if an error occurs during upload
674
   */
675
  public void uploadDocumentContents(DarDocumentType type, DataAccessRequest dar,
676
      InputStream uploadInputStream,
677
      FormDataContentDisposition fileDetail) throws IOException {
678
    // This should be moved to service tier logic and the transactions should be coordinated
679
    validateFileDetails(fileDetail);
1✔
680
    String fileName = fileDetail.getFileName();
1✔
681
    UUID id = UUID.randomUUID();
1✔
682
    BlobId blobId = gcsService.storeDocument(uploadInputStream, fileDetail.getType(), id);
1✔
683
    switch (type) {
1✔
684
      case IRB:
685
        // Delete the current document if it exists
686
        if (Objects.nonNull(dar.getData().getIrbDocumentLocation())) {
1✔
687
          deleteDarDocument(dar, dar.getData().getIrbDocumentLocation());
1✔
688
        }
689
        dar.getData().setIrbDocumentLocation(blobId.getName());
1✔
690
        dar.getData().setIrbDocumentName(fileName);
1✔
691
        break;
1✔
692
      case COLLABORATION:
693
        // Delete the current document if it exists
694
        if (Objects.nonNull(dar.getData().getCollaborationLetterLocation())) {
1✔
695
          deleteDarDocument(dar, dar.getData().getCollaborationLetterLocation());
1✔
696
        }
697
        dar.getData().setCollaborationLetterLocation(blobId.getName());
1✔
698
        dar.getData().setCollaborationLetterName(fileName);
1✔
699
        break;
1✔
700
      default:
701
        break;
702
    }
703
  }
1✔
704

705
  private void deleteDarDocument(DataAccessRequest dar, String blobIdName) {
706
    try {
707
      gcsService.deleteDocument(blobIdName);
1✔
708
    } catch (Exception e) {
×
709
      String message = String.format(
×
710
          "Unable to delete document for DAR ID: %s; dar document location: %s",
711
          dar.getReferenceId(), blobIdName);
×
712
      logWarn(message);
×
713
    }
1✔
714
  }
1✔
715

716
  private DataAccessRequest getDarById(String referenceId) {
717
    DataAccessRequest dar = dataAccessRequestService.findByReferenceId(referenceId);
1✔
718
    if (Objects.isNull(dar)) {
1✔
719
      throw new NotFoundException();
1✔
720
    }
721
    return dar;
1✔
722
  }
723

724
  /**
725
   * Custom handler for validating that a user can access a DAR. User will have access if ANY of
726
   * these conditions are met: If the DAR create user is the same as the Auth User, then the user
727
   * can access the resource. If the user has any of the roles in allowableRoles, then the user can
728
   * access the resource. In practice, pass in allowableRoles for users that are not the create user
729
   * (i.e. Admin) so they can also have access to the DAR.
730
   *
731
   * @param allowableRoles List of roles that would allow the user to access the resource
732
   * @param authUser       The AuthUser
733
   * @param referenceId    The referenceId of the resource.
734
   */
735
  private void validateAuthedRoleUser(final List<UserRoles> allowableRoles, AuthUser authUser,
736
      String referenceId) {
737
    DataAccessRequest dataAccessRequest = getDarById(referenceId);
1✔
738
    User user = findUserByEmail(authUser.getEmail());
1✔
739
    if (Objects.nonNull(dataAccessRequest.getUserId()) && dataAccessRequest.getUserId() > 0) {
1✔
740
      super.validateAuthedRoleUser(allowableRoles, user, dataAccessRequest.getUserId());
1✔
741
    } else {
742
      logWarn("DataAccessRequest '" + referenceId + "' has an invalid userId");
×
743
      super.validateAuthedRoleUser(allowableRoles, user, dataAccessRequest.getUserId());
×
744
    }
745
  }
1✔
746
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc