• 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

96.98
/src/main/java/org/broadinstitute/consent/http/service/DataAccessRequestService.java
1
package org.broadinstitute.consent.http.service;
2

3
import com.google.common.annotations.VisibleForTesting;
4
import com.google.inject.Inject;
5
import freemarker.template.TemplateException;
6
import jakarta.ws.rs.BadRequestException;
7
import jakarta.ws.rs.NotAcceptableException;
8
import jakarta.ws.rs.NotFoundException;
9
import java.io.IOException;
10
import java.sql.SQLException;
11
import java.sql.Timestamp;
12
import java.time.Instant;
13
import java.time.LocalDate;
14
import java.time.LocalTime;
15
import java.time.ZoneOffset;
16
import java.util.Collection;
17
import java.util.Date;
18
import java.util.List;
19
import java.util.Objects;
20
import java.util.Set;
21
import java.util.UUID;
22
import org.apache.commons.validator.routines.EmailValidator;
23
import org.broadinstitute.consent.http.configurations.ConsentConfiguration;
24
import org.broadinstitute.consent.http.db.DAOContainer;
25
import org.broadinstitute.consent.http.db.DarCollectionDAO;
26
import org.broadinstitute.consent.http.db.DataAccessRequestDAO;
27
import org.broadinstitute.consent.http.db.ElectionDAO;
28
import org.broadinstitute.consent.http.db.MatchDAO;
29
import org.broadinstitute.consent.http.db.UserDAO;
30
import org.broadinstitute.consent.http.db.VoteDAO;
31
import org.broadinstitute.consent.http.enumeration.EmailType;
32
import org.broadinstitute.consent.http.enumeration.UserRoles;
33
import org.broadinstitute.consent.http.exceptions.InvalidEmailAddressException;
34
import org.broadinstitute.consent.http.exceptions.LibraryCardRequiredException;
35
import org.broadinstitute.consent.http.exceptions.NIHComplianceRuleException;
36
import org.broadinstitute.consent.http.exceptions.SubmittedDARCannotBeEditedException;
37
import org.broadinstitute.consent.http.mail.message.ReminderMessage;
38
import org.broadinstitute.consent.http.models.Collaborator;
39
import org.broadinstitute.consent.http.models.DarCollection;
40
import org.broadinstitute.consent.http.models.DarDataset;
41
import org.broadinstitute.consent.http.models.DataAccessRequest;
42
import org.broadinstitute.consent.http.models.DataAccessRequestData;
43
import org.broadinstitute.consent.http.models.Dataset;
44
import org.broadinstitute.consent.http.models.Election;
45
import org.broadinstitute.consent.http.models.LibraryCard;
46
import org.broadinstitute.consent.http.models.User;
47
import org.broadinstitute.consent.http.models.Vote;
48
import org.broadinstitute.consent.http.service.dao.DataAccessRequestServiceDAO;
49
import org.broadinstitute.consent.http.util.ConsentLogger;
50
import org.jdbi.v3.core.statement.UnableToExecuteStatementException;
51

52
public class DataAccessRequestService implements ConsentLogger {
53
  public static final String EXPIRE_WARN_INTERVAL = "11 months";
54
  public static final String EXPIRE_NOTICE_INTERVAL = "1 year";
55
  protected static final Timestamp MINIMUM_SUBMITTED_DATE_FOR_DAR_EXPIRATIONS = Timestamp.from(
1✔
56
      Instant.ofEpochSecond(
1✔
57
          LocalDate.of(2024, 9, 30).toEpochSecond(LocalTime.of(0, 0, 0, 0), ZoneOffset.UTC)));
1✔
58
  private final CounterService counterService;
59
  private final DataAccessRequestDAO dataAccessRequestDAO;
60
  private final DarCollectionDAO darCollectionDAO;
61
  private final ElectionDAO electionDAO;
62
  private final EmailService emailService;
63
  private final MatchDAO matchDAO;
64
  private final VoteDAO voteDAO;
65
  private final UserDAO userDAO;
66
  private final UserService userService;
67
  private final DataAccessRequestServiceDAO dataAccessRequestServiceDAO;
68

69
  private final DacService dacService;
70
  private final String serverUrl;
71

72
  @Inject
73
  public DataAccessRequestService(CounterService counterService, DAOContainer container,
74
      DacService dacService, DataAccessRequestServiceDAO dataAccessRequestServiceDAO, UserService userService, EmailService emailService, ConsentConfiguration config) {
1✔
75
    this.counterService = counterService;
1✔
76
    this.dataAccessRequestDAO = container.getDataAccessRequestDAO();
1✔
77
    this.darCollectionDAO = container.getDarCollectionDAO();
1✔
78
    this.electionDAO = container.getElectionDAO();
1✔
79
    this.matchDAO = container.getMatchDAO();
1✔
80
    this.voteDAO = container.getVoteDAO();
1✔
81
    this.userDAO = container.getUserDAO();
1✔
82
    this.dacService = dacService;
1✔
83
    this.dataAccessRequestServiceDAO = dataAccessRequestServiceDAO;
1✔
84
    this.userService = userService;
1✔
85
    this.emailService = emailService;
1✔
86
    this.serverUrl = config.getServicesConfiguration().getLocalURL();
1✔
87
  }
1✔
88

89
  public List<DataAccessRequest> findAllDraftDataAccessRequests() {
90
    return dataAccessRequestDAO.findAllDraftDataAccessRequests();
1✔
91
  }
92

93
  public List<DataAccessRequest> findAllDraftDataAccessRequestsByUser(Integer userId) {
94
    return dataAccessRequestDAO.findAllDraftsByUserId(userId);
1✔
95
  }
96

97
  public void deleteByReferenceId(User user, String referenceId) throws NotAcceptableException {
98
    List<Election> elections = electionDAO.findElectionsByReferenceId(referenceId);
1✔
99
    if (!elections.isEmpty()) {
1✔
100
      // If the user is an admin, delete all votes and elections
101
      if (user.hasUserRole(UserRoles.ADMIN)) {
1✔
102
        voteDAO.deleteVotesByReferenceId(referenceId);
1✔
103
        List<Integer> electionIds = elections.stream().map(Election::getElectionId).toList();
1✔
104
        electionDAO.deleteElectionsByIds(electionIds);
1✔
105
      } else {
1✔
106
        String message = String.format(
1✔
107
            "Unable to delete DAR: '%s', there are existing elections that reference it.",
108
            referenceId);
109
        logWarn(message);
1✔
110
        throw new NotAcceptableException(message);
1✔
111
      }
112
    }
113
    matchDAO.deleteRationalesByPurposeIds(List.of(referenceId));
1✔
114
    matchDAO.deleteMatchesByPurposeId(referenceId);
1✔
115
    dataAccessRequestDAO.deleteDARDatasetRelationByReferenceId(referenceId);
1✔
116
    dataAccessRequestDAO.deleteByReferenceId(referenceId);
1✔
117
  }
1✔
118

119
  public DataAccessRequest findByReferenceId(String referencedId) {
120
    DataAccessRequest dar = dataAccessRequestDAO.findByReferenceId(referencedId);
1✔
121
    if (Objects.isNull(dar)) {
1✔
122
      throw new NotFoundException("There does not exist a DAR with the given reference Id");
×
123
    }
124
    return dar;
1✔
125
  }
126

127
  //NOTE: rewrite method into new servicedao method on another ticket
128
  public DataAccessRequest insertDraftDataAccessRequest(User user, DataAccessRequest dar) {
129
    if (Objects.isNull(user) || Objects.isNull(dar) || Objects.isNull(
1✔
130
        dar.getReferenceId()) || Objects.isNull(dar.getData())) {
1✔
131
      throw new IllegalArgumentException("User and DataAccessRequest are required");
1✔
132
    }
133

134
    if (user.getLibraryCards().isEmpty()) {
1✔
135
      throw new LibraryCardRequiredException();
×
136
    }
137

138
    Date now = new Date();
1✔
139
    dataAccessRequestDAO.insertDraftDataAccessRequest(
1✔
140
        dar.getReferenceId(),
1✔
141
        user.getUserId(),
1✔
142
        now,
143
        now,
144
        now,
145
        dar.getData()
1✔
146
    );
147
    syncDataAccessRequestDatasets(dar.getDatasetIds(), dar.getReferenceId());
1✔
148

149
    return findByReferenceId(dar.getReferenceId());
1✔
150
  }
151

152
  /**
153
   * First delete any rows with the current reference id. This will allow us to keep (referenceId,
154
   * dataset_id) unique Takes in a list of datasetIds and a referenceId and adds them to the
155
   * dar_dataset collection
156
   *
157
   * @param datasetIds  List of Integers that represent the datasetIds
158
   * @param referenceId ReferenceId of the corresponding DAR
159
   */
160
  private void syncDataAccessRequestDatasets(List<Integer> datasetIds, String referenceId) {
161
    List<DarDataset> darDatasets = datasetIds.stream()
1✔
162
        .map(datasetId -> new DarDataset(referenceId, datasetId))
1✔
163
        .toList();
1✔
164
    dataAccessRequestDAO.deleteDARDatasetRelationByReferenceId(referenceId);
1✔
165

166
    if (!darDatasets.isEmpty()) {
1✔
167
      dataAccessRequestDAO.insertAllDarDatasets(darDatasets);
1✔
168
    }
169
  }
1✔
170

171
  /**
172
   * @param user User
173
   * @return List<DataAccessRequest>
174
   */
175
  public List<DataAccessRequest> getDataAccessRequestsByUserRole(User user) {
176
    List<DataAccessRequest> dars = dataAccessRequestDAO.findAllDataAccessRequests();
1✔
177
    return dacService.filterDataAccessRequestsByDac(dars, user);
1✔
178
  }
179

180
  /**
181
   * Generate a DataAccessRequest from the provided DAR. The provided DAR may or may not exist in
182
   * draft form, so it covers both cases of converting an existing draft to submitted and creating a
183
   * brand new DAR from scratch.
184
   *
185
   * @param user              The create User
186
   * @param dataAccessRequest DataAccessRequest with populated DAR data
187
   * @return The created DAR.
188
   */
189
  public DataAccessRequest createDataAccessRequest(User user, DataAccessRequest dataAccessRequest) {
190
    validateDar(user, dataAccessRequest);
1✔
191

192
    Date now = new Date();
1✔
193
    DataAccessRequestData darData = dataAccessRequest.getData();
1✔
194

195
    DataAccessRequest existingDar = dataAccessRequestDAO.findByReferenceId(
1✔
196
        dataAccessRequest.getReferenceId());
1✔
197
    if (existingDar != null && !existingDar.getDraft()) {
1✔
198
      throw new SubmittedDARCannotBeEditedException();
1✔
199
    }
200
    Integer collectionId;
201
    // Only create a new DarCollection if we haven't done so already
202
    if (Objects.nonNull(existingDar) && Objects.nonNull(existingDar.getCollectionId())) {
1✔
203
      collectionId = existingDar.getCollectionId();
×
204
    } else {
205
      String darCodeSequence = "DAR-" + counterService.getNextDarSequence();
1✔
206
      collectionId = darCollectionDAO.insertDarCollection(darCodeSequence, user.getUserId(), now);
1✔
207
    }
208
    String referenceId;
209
    List<Integer> datasetIds = dataAccessRequest.getDatasetIds();
1✔
210
    if (Objects.nonNull(existingDar)) {
1✔
211
      referenceId = dataAccessRequest.getReferenceId();
1✔
212
      dataAccessRequestDAO.updateDraftToSubmittedForCollection(collectionId,
1✔
213
          referenceId);
214
      dataAccessRequestDAO.updateDataByReferenceId(
1✔
215
          referenceId,
216
          user.getUserId(),
1✔
217
          now,
218
          now,
219
          now,
220
          darData,
221
          user.getEraCommonsId());
1✔
222
    } else {
223
      referenceId = UUID.randomUUID().toString();
1✔
224
      dataAccessRequestDAO.insertDataAccessRequest(
1✔
225
          collectionId,
226
          referenceId,
227
          user.getUserId(),
1✔
228
          now,
229
          now,
230
          now,
231
          now,
232
          darData,
233
          user.getEraCommonsId());
1✔
234
    }
235
    syncDataAccessRequestDatasets(datasetIds, referenceId);
1✔
236
    return findByReferenceId(referenceId);
1✔
237
  }
238

239
  /**
240
   * Create a progress report for the given DataAccessRequest.
241
   * The parent DAR is just passed in for validation purposes.
242
   *
243
   * @param user              The User
244
   * @param progressReport    The DataAccessRequest
245
   * @param parentDar         The parent DataAccessRequest
246
   * @return The created progress report.
247
   */
248
  public DataAccessRequest createProgressReport(User user, DataAccessRequest progressReport, DataAccessRequest parentDar) {
249
    validateProgressReport(user, progressReport, parentDar);
1✔
250

251
    String referenceId = progressReport.getReferenceId();
1✔
252
    List<Integer> progressReportDatasetIds = progressReport.getDatasetIds();
1✔
253
    Set<Integer> darDatasetIds = dataAccessRequestDAO.findDatasetApprovalsByDars(List.of(parentDar.getReferenceId()));
1✔
254
    if (!darDatasetIds.containsAll(progressReportDatasetIds)) {
1✔
255
      throw new BadRequestException("Progress report can only be created for approved datasets in the parent DAR");
1✔
256
    }
257
    dataAccessRequestDAO.insertProgressReport(
1✔
258
          progressReport.getParentId(),
1✔
259
          progressReport.getCollectionId(),
1✔
260
          referenceId,
261
          user.getUserId(),
1✔
262
          progressReport.getData());
1✔
263
    syncDataAccessRequestDatasets(progressReportDatasetIds, referenceId);
1✔
264
    return findByReferenceId(referenceId);
1✔
265
  }
266

267
  public void validateProgressReport(User user, DataAccessRequest progressReport, DataAccessRequest parentDar) {
268
    validateDar(user, progressReport);
1✔
269
    if (parentDar.getDraft()) {
1✔
270
      throw new BadRequestException(
1✔
271
          "Cannot create a progress report for a draft Data Access Request");
272
    }
273
    if (progressReport.getDatasetIds() == null || progressReport.getDatasetIds().isEmpty() ) {
1✔
274
      throw new BadRequestException("At least one dataset is required");
1✔
275
    }
276
    if (!parentDar.getDatasetIds().containsAll(progressReport.getDatasetIds())) {
1✔
277
      throw new BadRequestException("Progress report can only be created for datasets in the parent DAR");
1✔
278
    }
279
    if (progressReport.getData().getProgressReportSummary() == null ||
1✔
280
        progressReport.getData().getProgressReportSummary().isEmpty()) {
1✔
281
      throw new BadRequestException("Progress report summary is required");
1✔
282
    }
283
    if (progressReport.getData().getIntellectualPropertySummary() == null ||
1✔
284
        progressReport.getData().getIntellectualPropertySummary().isEmpty()) {
1✔
285
      throw new BadRequestException("Intellectual Property Summary is required");
1✔
286
    }
287
  }
1✔
288

289
  public void validateDar(User user, DataAccessRequest dar) {
290
    if (Objects.isNull(user) || Objects.isNull(dar) || Objects.isNull(
1✔
291
        dar.getReferenceId()) || Objects.isNull(dar.getData())) {
1✔
292
      throw new IllegalArgumentException("User and DataAccessRequest are required");
1✔
293
    }
294

295
    if (user.getLibraryCards().isEmpty()) {
1✔
296
      throw new NIHComplianceRuleException();
1✔
297
    }
298

299
    userService.hasValidActiveERACredentials(user);
1✔
300

301
    validateInternalCollaborators(dar, user);
1✔
302
    validateNoKeyPersonnelDuplicates(dar.getData());
1✔
303
  }
1✔
304

305
  @VisibleForTesting
306
  protected void validateInternalCollaborators(DataAccessRequest payload, User requestingUser) {
307
    Integer institution = requestingUser.getInstitutionId();
1✔
308
    List<Collaborator> internalCollaborators = payload.getData().getInternalCollaborators();
1✔
309
    for (Collaborator collaborator : internalCollaborators) {
1✔
310
      User collabUser = userDAO.findUserByEmail(collaborator.getEmail());
1✔
311
      if (collabUser == null) {
1✔
312
        throw new NotFoundException(
1✔
313
            "Unable to find User with the provided email: " + collaborator.getEmail());
1✔
314
      }
315
      if (!Objects.equals(collabUser.getInstitutionId(), institution)) {
1✔
316
        throw new BadRequestException(
1✔
317
            "Collaborator " + collaborator.getEmail() + " is not part of the same institution, "
1✔
318
                + requestingUser.getInstitution().getName());
1✔
319
      }
320
      List<LibraryCard> libraryCards = collabUser.getLibraryCards();
1✔
321
      if (libraryCards.isEmpty()) {
1✔
322
        throw new BadRequestException(
1✔
323
            "Collaborator " + collaborator.getEmail() + " does not have a library card.");
1✔
324
      }
325
    }
1✔
326
  }
1✔
327

328
  /**
329
   * Update an existing DataAccessRequest. Replaces DataAccessRequestData.
330
   *
331
   * @param user The User
332
   * @param dar  The DataAccessRequest
333
   * @return The updated DataAccessRequest
334
   */
335
  public DataAccessRequest updateByReferenceId(User user, DataAccessRequest dar) {
336
    if (!dar.getDraft()) {
1✔
337
      throw new SubmittedDARCannotBeEditedException();
1✔
338
    }
339
    try {
340
      return dataAccessRequestServiceDAO.updateByReferenceId(user, dar);
1✔
341
    } catch (SQLException e) {
×
342
      // If I simply rethrow the error then I'll have to redefine any method that
343
      // calls this function to "throw SQLException"
344
      //Instead I'm going to throw an UnableToExecuteStatementException
345
      //Response class will catch it, log it, and throw a 500 through the "unableToExecuteExceptionHandler"
346
      //on the Resource class, just like it would with a SQLException
347
      throw new UnableToExecuteStatementException(e.getMessage());
×
348
    }
349
  }
350

351
  /**
352
   * Validates that PI email is not duplicated with SO or IT Director emails
353
   *
354
   * @param darData The data access request data to validate
355
   * @throws IllegalArgumentException if duplicate emails are found
356
   */
357
  public void validateNoKeyPersonnelDuplicates(DataAccessRequestData darData) {
358
    EmailValidator emailValidator = EmailValidator.getInstance();
1✔
359

360
    String piEmail = darData.getPiEmail();
1✔
361
    String soEmail = darData.getSigningOfficialEmail();
1✔
362
    String itEmail = darData.getItDirectorEmail();
1✔
363

364
    if (!emailValidator.isValid(piEmail) || !emailValidator.isValid(soEmail)
1✔
365
        || !emailValidator.isValid(itEmail)) {
1✔
366
      throw new IllegalArgumentException(
1✔
367
          "Principal Investigator, Signing Official, and IT Director emails must be valid");
368
    }
369

370
    if (piEmail.equalsIgnoreCase(soEmail)) {
1✔
371
      throw new IllegalArgumentException(
1✔
372
          "Principal Investigator email cannot be the same as Signing Official email");
373
    }
374

375
    if (piEmail.equalsIgnoreCase(itEmail)) {
1✔
376
      throw new IllegalArgumentException(
1✔
377
          "Principal Investigator email cannot be the same as IT Director email");
378
    }
379
  }
1✔
380

381
  public Collection<DataAccessRequest> getApprovedDARsForDataset(Dataset dataset) {
382
    return dataAccessRequestDAO.findApprovedDARsByDatasetId(dataset.getDatasetId());
1✔
383
  }
384

385
  public void sendExpirationNotices() {
386
    sendDARExpirationReminderNotices();
1✔
387
    sendDARExpirationNotices();
1✔
388
  }
1✔
389

390
  private void sendDARExpirationNotices() {
391
    EmailType emailType = EmailType.DAR_EXPIRED;
1✔
392
    sendDARMessageToList(emailType, EXPIRE_NOTICE_INTERVAL);
1✔
393
  }
1✔
394

395
  private void sendDARExpirationReminderNotices() {
396
    EmailType emailType = EmailType.DAR_EXPIRATION_REMINDER;
1✔
397
    sendDARMessageToList(emailType, EXPIRE_WARN_INTERVAL);
1✔
398
  }
1✔
399

400
  private void sendDARMessageToList(EmailType type, String interval) {
401
    List<DataAccessRequest> expiredDars =
1✔
402
        dataAccessRequestDAO.findAgedDARsByEmailTypeOlderThanInterval(
1✔
403
            type.getTypeInt(), interval, MINIMUM_SUBMITTED_DATE_FOR_DAR_EXPIRATIONS);
1✔
404
    expiredDars.forEach(
1✔
405
        expiredDar -> {
406
          try {
407
            String referenceId = expiredDar.getReferenceId();
1✔
408
            User user = userDAO.findUserById(expiredDar.getUserId());
1✔
409
            String darCode = expiredDar.getDarCode();
1✔
410
            String userName = user.getDisplayName();
1✔
411
            if (user.getEmail() == null) {
1✔
412
              throw new InvalidEmailAddressException(
1✔
413
                  String.format(
1✔
414
                      "Email address for user %d (%s) not found for expiring warning.  DAR reference id: %s",
415
                      expiredDar.getUserId(), userName, referenceId));
1✔
416
            }
417
            switch (type) {
1✔
418
              case DAR_EXPIRATION_REMINDER:
419
                emailService.sendDarExpirationReminderMessage(
1✔
420
                    user, darCode, user.getUserId(), referenceId);
1✔
421
                break;
1✔
422
              case DAR_EXPIRED:
423
                emailService.sendDarExpiredMessage(user, darCode, user.getUserId(), referenceId);
1✔
424
                break;
1✔
425
              default:
426
                break;
427
            }
428
          } catch (Exception e) {
1✔
429
            logException(e);
1✔
430
          }
1✔
431
        });
1✔
432
  }
1✔
433

434
  public void sendReminderMessage(Integer voteId) throws IOException, TemplateException {
435
    Vote vote = voteDAO.findVoteById(voteId);
1✔
436
    Election election = electionDAO.findElectionWithFinalVoteById(vote.getElectionId());
1✔
437
    DarCollection collection = darCollectionDAO.findDARCollectionByReferenceId(
1✔
438
        election.getReferenceId());
1✔
439
    User user = findUserById(vote.getUserId());
1✔
440
    String voteUrl = serverUrl + "dar_collection/%d".formatted(collection.getDarCollectionId());
1✔
441
    emailService.sendReminderMessage(user, vote, collection.getDarCode(), election.getElectionType(), voteUrl);
1✔
442
    voteDAO.updateVoteReminderFlag(voteId, true);
1✔
443
  }
1✔
444

445
  private User findUserById(Integer id) throws IllegalArgumentException {
446
    User user = userDAO.findUserById(id);
1✔
447
    if (user == null) {
1✔
NEW
448
      throw new NotFoundException("Could not find dacUser for specified id : " + id);
×
449
    }
450
    return user;
1✔
451
  }
452

453
}
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