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

DataBiosphere / consent / #6151

26 Jun 2025 11:10PM UTC coverage: 79.39% (+0.001%) from 79.389%
#6151

push

web-flow
DT-1849, DT-1875: Prevent PR Creation for DARs with open elections (#2587)

6 of 7 new or added lines in 3 files covered. (85.71%)

10404 of 13105 relevant lines covered (79.39%)

0.79 hits per line

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

95.24
/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.InternalServerErrorException;
8
import jakarta.ws.rs.NotAcceptableException;
9
import jakarta.ws.rs.NotFoundException;
10
import java.io.IOException;
11
import java.util.ArrayList;
12
import java.sql.SQLException;
13
import java.sql.Timestamp;
14
import java.time.Instant;
15
import java.time.LocalDate;
16
import java.time.LocalTime;
17
import java.time.ZoneOffset;
18
import java.util.Collection;
19
import java.util.Date;
20
import java.util.HashSet;
21
import java.util.List;
22
import java.util.Objects;
23
import java.util.Set;
24
import java.util.UUID;
25
import org.apache.commons.validator.routines.EmailValidator;
26
import org.broadinstitute.consent.http.configurations.ConsentConfiguration;
27
import org.broadinstitute.consent.http.db.DAOContainer;
28
import org.broadinstitute.consent.http.db.DarCollectionDAO;
29
import org.broadinstitute.consent.http.db.DataAccessRequestDAO;
30
import org.broadinstitute.consent.http.db.ElectionDAO;
31
import org.broadinstitute.consent.http.db.MatchDAO;
32
import org.broadinstitute.consent.http.db.UserDAO;
33
import org.broadinstitute.consent.http.db.VoteDAO;
34
import org.broadinstitute.consent.http.enumeration.EmailType;
35
import org.broadinstitute.consent.http.enumeration.UserRoles;
36
import org.broadinstitute.consent.http.exceptions.InvalidEmailAddressException;
37
import org.broadinstitute.consent.http.exceptions.LibraryCardRequiredException;
38
import org.broadinstitute.consent.http.exceptions.NIHComplianceRuleException;
39
import org.broadinstitute.consent.http.exceptions.SubmittedDARCannotBeEditedException;
40
import org.broadinstitute.consent.http.models.Collaborator;
41
import org.broadinstitute.consent.http.models.Dac;
42
import org.broadinstitute.consent.http.models.DarCollection;
43
import org.broadinstitute.consent.http.models.DarDataset;
44
import org.broadinstitute.consent.http.models.DataAccessRequest;
45
import org.broadinstitute.consent.http.models.DataAccessRequestData;
46
import org.broadinstitute.consent.http.models.Dataset;
47
import org.broadinstitute.consent.http.models.Election;
48
import org.broadinstitute.consent.http.models.Institution;
49
import org.broadinstitute.consent.http.models.User;
50
import org.broadinstitute.consent.http.models.Vote;
51
import org.broadinstitute.consent.http.service.dao.DataAccessRequestServiceDAO;
52
import org.broadinstitute.consent.http.util.ConsentLogger;
53
import org.broadinstitute.consent.http.util.CountryValidator;
54
import org.jdbi.v3.core.JdbiException;
55
import org.jdbi.v3.core.statement.UnableToExecuteStatementException;
56

57
public class DataAccessRequestService implements ConsentLogger {
58
  public static final String EXPIRE_WARN_INTERVAL = "11 months";
59
  public static final String EXPIRE_NOTICE_INTERVAL = "1 year";
60
  protected static final Timestamp MINIMUM_SUBMITTED_DATE_FOR_DAR_EXPIRATIONS = Timestamp.from(
1✔
61
      Instant.ofEpochSecond(
1✔
62
          LocalDate.of(2024, 9, 30).toEpochSecond(LocalTime.of(0, 0, 0, 0), ZoneOffset.UTC)));
1✔
63
  private static final String MEMBER = "member";
64
  private static final String MEMBERS = MEMBER + "s: ";
65
  public static final String ALL_LISTED_PERSONNEL_MUST_SHARE_THE_SAME_INSTITUTION =
66
  """
67
  All listed personnel must share the same institutional affiliation and have a library card.  The following list of \
68
  roles and members must have email addresses associated with your institution or library cards issued:\s""";
69
  private static final String INTERNAL_COLLABORATOR = "Internal Collaborator";
70
  private static final String LAB_STAFF = "Lab staff";
71
  private final CounterService counterService;
72
  private final DataAccessRequestDAO dataAccessRequestDAO;
73
  private final DarCollectionDAO darCollectionDAO;
74
  private final ElectionDAO electionDAO;
75
  private final InstitutionService institutionService;
76
  private final EmailService emailService;
77
  private final MatchDAO matchDAO;
78
  private final VoteDAO voteDAO;
79
  private final UserDAO userDAO;
80
  private final UserService userService;
81
  private final DataAccessRequestServiceDAO dataAccessRequestServiceDAO;
82
  private final CountryValidator countryValidator;
83

84
  private final DacService dacService;
85
  private final String serverUrl;
86

87
  @Inject
88
  public DataAccessRequestService(CounterService counterService, DAOContainer container,
89
      DacService dacService, DataAccessRequestServiceDAO dataAccessRequestServiceDAO, UserService userService, InstitutionService institutionService, EmailService emailService, ConsentConfiguration config) {
1✔
90
    this.counterService = counterService;
1✔
91
    this.dataAccessRequestDAO = container.getDataAccessRequestDAO();
1✔
92
    this.darCollectionDAO = container.getDarCollectionDAO();
1✔
93
    this.electionDAO = container.getElectionDAO();
1✔
94
    this.matchDAO = container.getMatchDAO();
1✔
95
    this.voteDAO = container.getVoteDAO();
1✔
96
    this.userDAO = container.getUserDAO();
1✔
97
    this.dacService = dacService;
1✔
98
    this.dataAccessRequestServiceDAO = dataAccessRequestServiceDAO;
1✔
99
    this.userService = userService;
1✔
100
    this.institutionService = institutionService;
1✔
101
    this.emailService = emailService;
1✔
102
    this.serverUrl = config.getServicesConfiguration().getLocalURL();
1✔
103
    this.countryValidator = new CountryValidator();
1✔
104
  }
1✔
105

106
  public List<DataAccessRequest> findAllDraftDataAccessRequests() {
107
    return dataAccessRequestDAO.findAllDraftDataAccessRequests();
1✔
108
  }
109

110
  public List<DataAccessRequest> findAllDraftDataAccessRequestsByUser(Integer userId) {
111
    return dataAccessRequestDAO.findAllDraftsByUserId(userId);
1✔
112
  }
113

114
  public void deleteDataAccessRequest(DataAccessRequest dataAccessRequest) throws NotAcceptableException {
115
    String referenceId = dataAccessRequest.getReferenceId();
1✔
116
    if (!dataAccessRequest.getDraft()) {
1✔
117
      throw new BadRequestException("Only draft data access requests can be deleted");
1✔
118
    }
119
    List<Election> elections = electionDAO.findElectionsByReferenceId(referenceId);
1✔
120
    if (!elections.isEmpty()) {
1✔
121
        String message = String.format(
1✔
122
            "Unable to delete DAR: '%s', there are existing elections that reference it.",
123
            referenceId);
124
        logWarn(message);
1✔
125
        throw new NotAcceptableException(message);
1✔
126
    }
127
    matchDAO.deleteRationalesByPurposeIds(List.of(referenceId));
1✔
128
    matchDAO.deleteMatchesByPurposeId(referenceId);
1✔
129
    dataAccessRequestDAO.deleteDARDatasetRelationByReferenceId(referenceId);
1✔
130
    dataAccessRequestDAO.deleteByReferenceId(referenceId);
1✔
131
  }
1✔
132

133
  public DataAccessRequest findByReferenceId(String referencedId) {
134
    DataAccessRequest dar = dataAccessRequestDAO.findByReferenceId(referencedId);
1✔
135
    if (Objects.isNull(dar)) {
1✔
136
      throw new NotFoundException("No data access request found for this reference Id");
×
137
    }
138
    return dar;
1✔
139
  }
140

141
  //NOTE: rewrite method into new service DAO method on another ticket
142
  public DataAccessRequest insertDraftDataAccessRequest(User user, DataAccessRequest dar) {
143
    if (Objects.isNull(user) || Objects.isNull(dar) || Objects.isNull(
1✔
144
        dar.getReferenceId()) || Objects.isNull(dar.getData())) {
1✔
145
      throw new IllegalArgumentException("User and DataAccessRequest are required");
1✔
146
    }
147

148
    if (user.getLibraryCard() == null) {
1✔
149
      throw new LibraryCardRequiredException();
×
150
    }
151

152
    Date now = new Date();
1✔
153
    dataAccessRequestDAO.insertDraftDataAccessRequest(
1✔
154
        dar.getReferenceId(),
1✔
155
        user.getUserId(),
1✔
156
        now,
157
        now,
158
        now,
159
        dar.getData()
1✔
160
    );
161
    syncDataAccessRequestDatasets(dar.getDatasetIds(), dar.getReferenceId());
1✔
162

163
    return findByReferenceId(dar.getReferenceId());
1✔
164
  }
165

166
  /**
167
   * First delete any rows with the current reference id. This will allow us to keep (referenceId,
168
   * dataset_id) unique Takes in a list of datasetIds and a referenceId and adds them to the
169
   * dar_dataset collection
170
   *
171
   * @param datasetIds  List of Integers that represent the datasetIds
172
   * @param referenceId ReferenceId of the corresponding DAR
173
   */
174
  private void syncDataAccessRequestDatasets(List<Integer> datasetIds, String referenceId) {
175
    List<DarDataset> darDatasets = datasetIds.stream()
1✔
176
        .map(datasetId -> new DarDataset(referenceId, datasetId))
1✔
177
        .toList();
1✔
178
    dataAccessRequestDAO.deleteDARDatasetRelationByReferenceId(referenceId);
1✔
179

180
    if (!darDatasets.isEmpty()) {
1✔
181
      dataAccessRequestDAO.insertAllDarDatasets(darDatasets);
1✔
182
    }
183
  }
1✔
184

185
  /**
186
   * @param user User
187
   * @return List<DataAccessRequest>
188
   */
189
  public List<DataAccessRequest> getDataAccessRequestsByUserRole(User user) {
190
    List<DataAccessRequest> dars = dataAccessRequestDAO.findAllDataAccessRequests();
1✔
191
    return dacService.filterDataAccessRequestsByDac(dars, user);
1✔
192
  }
193

194
  /**
195
   * Generate a DataAccessRequest from the provided DAR. The provided DAR may or may not exist in
196
   * draft form, so it covers both cases of converting an existing draft to submitted and creating a
197
   * brand-new DAR from scratch.
198
   *
199
   * @param user              The creating User
200
   * @param dataAccessRequest DataAccessRequest with populated DAR data
201
   * @return The created DAR.
202
   */
203
  public DataAccessRequest createDataAccessRequest(User user, DataAccessRequest dataAccessRequest) {
204
    validateDar(user, dataAccessRequest);
1✔
205

206
    Date now = new Date();
1✔
207
    DataAccessRequestData darData = dataAccessRequest.getData();
1✔
208

209
    DataAccessRequest existingDar = dataAccessRequestDAO.findByReferenceId(
1✔
210
        dataAccessRequest.getReferenceId());
1✔
211
    if (existingDar != null && !existingDar.getDraft()) {
1✔
212
      throw new SubmittedDARCannotBeEditedException();
1✔
213
    }
214
    Integer collectionId;
215
    // Only create a new DarCollection if we haven't done so already
216
    if (Objects.nonNull(existingDar) && Objects.nonNull(existingDar.getCollectionId())) {
1✔
217
      collectionId = existingDar.getCollectionId();
×
218
    } else {
219
      String darCodeSequence = "DAR-" + counterService.getNextDarSequence();
1✔
220
      collectionId = darCollectionDAO.insertDarCollection(darCodeSequence, user.getUserId(), now);
1✔
221
    }
222
    String referenceId;
223
    List<Integer> datasetIds = dataAccessRequest.getDatasetIds();
1✔
224
    if (Objects.nonNull(existingDar)) {
1✔
225
      referenceId = dataAccessRequest.getReferenceId();
1✔
226
      dataAccessRequestDAO.updateDraftToSubmittedForCollection(collectionId,
1✔
227
          referenceId);
228
      dataAccessRequestDAO.updateDataByReferenceId(
1✔
229
          referenceId,
230
          user.getUserId(),
1✔
231
          now,
232
          now,
233
          now,
234
          darData,
235
          user.getEraCommonsId());
1✔
236
    } else {
237
      referenceId = UUID.randomUUID().toString();
1✔
238
      dataAccessRequestDAO.insertDataAccessRequest(
1✔
239
          collectionId,
240
          referenceId,
241
          user.getUserId(),
1✔
242
          now,
243
          now,
244
          now,
245
          now,
246
          darData,
247
          user.getEraCommonsId());
1✔
248
    }
249
    syncDataAccessRequestDatasets(datasetIds, referenceId);
1✔
250
    return findByReferenceId(referenceId);
1✔
251
  }
252

253
  /**
254
   * Create a progress report for the given DataAccessRequest.
255
   * The parent DAR is just passed in for validation purposes.
256
   *
257
   * @param user              The User
258
   * @param progressReport    The DataAccessRequest
259
   * @param parentDar         The parent DataAccessRequest
260
   * @return The created progress report.
261
   */
262
  public DataAccessRequest createProgressReport(User user, DataAccessRequest progressReport, DataAccessRequest parentDar) {
263
    validateProgressReport(user, progressReport, parentDar);
1✔
264

265
    String referenceId = progressReport.getReferenceId();
1✔
266
    List<Integer> progressReportDatasetIds = progressReport.getDatasetIds();
1✔
267
    Set<Integer> darDatasetIds = dataAccessRequestDAO.findDatasetApprovalsByDar(parentDar.getReferenceId());
1✔
268
    if (!darDatasetIds.containsAll(progressReportDatasetIds)) {
1✔
269
      throw new BadRequestException("Progress report can only be created for approved datasets in the parent DAR");
1✔
270
    }
271
    try {
272
      dataAccessRequestDAO.insertProgressReport(
1✔
273
          progressReport.getParentId(),
1✔
274
          progressReport.getCollectionId(),
1✔
275
          referenceId,
276
          user.getUserId(),
1✔
277
          progressReport.getData());
1✔
278
    } catch (JdbiException e) {
1✔
279
      throw new BadRequestException(
1✔
280
          "Unable to create progress report for Data Access Request " + parentDar.getReferenceId());
1✔
281
    }
1✔
282

283
    if (progressReport.getIsCloseoutProgressReport()) {
1✔
284
      try {
285
        User signingOfficialUser =
1✔
286
            userService.findUserById(
1✔
287
                progressReport.getData().getCloseoutSupplement().signingOfficialId());
1✔
288
        emailService.sendSubmittedCloseoutMessage(
1✔
289
            signingOfficialUser, parentDar.getDarCode(), referenceId, serverUrl + "dar_application_review/%d".formatted(parentDar.getCollectionId()));
1✔
290
      } catch (TemplateException | IOException e) {
×
291
        throw new InternalServerErrorException(e);
×
292
      }
1✔
293
    }
294

295
    syncDataAccessRequestDatasets(progressReportDatasetIds, referenceId);
1✔
296
    return findByReferenceId(referenceId);
1✔
297
  }
298

299
  public void approveDataAccessRequestCloseout(User signingOfficial, String referenceId) {
300
    DataAccessRequest dar = dataAccessRequestDAO.findByReferenceId(referenceId);
1✔
301
    validateCloseoutApproval(signingOfficial, dar);
1✔
302
    dataAccessRequestDAO.updateDarCloseoutSO(signingOfficial.getUserId(), referenceId);
1✔
303
    Set<User> chairs = new HashSet<>();
1✔
304
    Set<Dac> dacs = dacService.findByDatasetId(dar.getDatasetIds());
1✔
305
    dacs.forEach(dac -> chairs.addAll(dac.getChairpersons()));
1✔
306
    chairs.forEach(
1✔
307
        chairperson -> {
308
          try {
309
            emailService.sendSubmittedCloseoutMessage(
1✔
310
                chairperson,
311
                dar.getDarCode(),
1✔
312
                dar.getReferenceId(),
1✔
313
                serverUrl + "dar_application_review/%d".formatted(dar.getCollectionId()));
1✔
314
          } catch (Exception e) {
×
315
            logWarn("Unable to send close out message for Data Access Request " + referenceId, e);
×
316
          }
1✔
317
        });
1✔
318
  }
1✔
319

320
  @VisibleForTesting
321
  protected void validateCloseoutApproval(User signingOfficial, DataAccessRequest dataAccessRequest) {
322
    // Note: we will allow a signing official to approve their own closeout.
323

324
    if (!dataAccessRequest.getIsCloseoutProgressReport()) {
1✔
325
      throw new BadRequestException("Signing officials can only approve closeout progress reports.");
1✔
326
    }
327

328
    if (dataAccessRequest.getHasSOCloseoutApproval()) {
1✔
329
      throw new BadRequestException("This progress report closeout has already been approved by a signing official.");
1✔
330
    }
331

332
    if (!signingOfficial.getUserId().equals(dataAccessRequest.getData().getCloseoutSupplement().signingOfficialId())) {
1✔
333
      throw new BadRequestException("This request can only be approved by the signing official selected in the closeout request.");
1✔
334
    }
335

336
    try {
337
      User submitter = userService.findUserById(dataAccessRequest.getUserId());
1✔
338
      if (!submitter.getInstitutionId().equals(signingOfficial.getInstitutionId())) {
1✔
339
        throw new BadRequestException("Signing Officials must be in the same institution as the creator of the closeout request.");
1✔
340
      }
341

342
    } catch (NotFoundException e) {
×
343
      // log the state.  we'll allow the SO to process a closeout even if the  user can't be found.
344
      logWarn(
×
345
          String.format(
×
346
              "Signing Official approving closeout %s for non-existent user %d",
347
              dataAccessRequest.getReferenceId(), dataAccessRequest.getUserId()));
×
348
    }
1✔
349
  }
1✔
350

351
  public void validateProgressReport(User user, DataAccessRequest progressReport, DataAccessRequest parentDar) {
352
    validateCommonDarAndProgressReportElements(user, progressReport);
1✔
353
    validateInternalCollaborators(user, progressReport);
1✔
354
    validateCountryOfOperation(progressReport.data, true);
1✔
355

356
    if (parentDar.getDraft()) {
1✔
357
      throw new BadRequestException(
1✔
358
          "Cannot create a progress report for a draft Data Access Request");
359
    }
360
    if (progressReport.getDatasetIds() == null || progressReport.getDatasetIds().isEmpty() ) {
1✔
361
      throw new BadRequestException("At least one dataset is required");
1✔
362
    }
363
    if (!Set.copyOf(parentDar.getDatasetIds()).containsAll(progressReport.getDatasetIds())) {
1✔
364
      throw new BadRequestException("Progress report can only be created for datasets in the parent DAR");
1✔
365
    }
366
    if (progressReport.getData().getProgressReportSummary() == null ||
1✔
367
        progressReport.getData().getProgressReportSummary().isEmpty()) {
1✔
368
      throw new BadRequestException("Progress report summary is required");
1✔
369
    }
370

371
    if (progressReport.getIsCloseoutProgressReport()) {
1✔
372
      Integer providedSigningOfficial =
1✔
373
          progressReport.getData().getCloseoutSupplement().signingOfficialId();
1✔
374
      try {
375
        User selectedSigningOfficial = userService.findUserById(providedSigningOfficial);
1✔
376
        if (!selectedSigningOfficial.getInstitutionId().equals(user.getInstitutionId())) {
1✔
377
          throw new BadRequestException(
1✔
378
              "The signing official selected in the closeout is not in the same institution as the submitter.");
379
        }
380
        if (!selectedSigningOfficial.hasUserRole(UserRoles.SIGNINGOFFICIAL)) {
1✔
381
          throw new BadRequestException("The selected signing official is not a signing official");
1✔
382
        }
383
      } catch (NotFoundException nfe) {
1✔
384
        throw new BadRequestException("The selected signing official in the closeout was not found.");
1✔
385
      }
1✔
386
    }
387
  }
1✔
388

389
  @VisibleForTesting
390
  protected void validateCommonDarAndProgressReportElements(User user, DataAccessRequest dar) {
391
    if (Objects.isNull(user) || Objects.isNull(dar) || Objects.isNull(
1✔
392
        dar.getReferenceId()) || Objects.isNull(dar.getData())) {
1✔
393
      throw new IllegalArgumentException("User and DataAccessRequest are required");
1✔
394
    }
395
    if (user.getLibraryCard() == null) {
1✔
396
      throw new NIHComplianceRuleException();
1✔
397
    }
398

399
    userService.validateActiveERACredentials(user);
1✔
400
  }
1✔
401

402
  public void validateDar(User user, DataAccessRequest dar) {
403
    validateCommonDarAndProgressReportElements(user, dar);
1✔
404

405
    if (!Objects.equals(user.getEmail(), dar.getData().getPiEmail()) || !Objects.equals(user.getDisplayName(), dar.getData().getPiName())) {
1✔
406
      throw new BadRequestException("The PI in the DAR must have the same name and email as the user submitting the DAR.");
1✔
407
    }
408

409
    validateNoKeyPersonnelDuplicates(dar.getData());
1✔
410
    validatePersonnelInstitutionAndLibraryCardRequirements(user, dar.getData());
1✔
411
    validateCountryOfOperation(dar.getData(), false);
1✔
412
  }
1✔
413

414
  protected void validateCountryOfOperation(DataAccessRequestData darData, boolean skipPI) {
415
    List<String> errorSummary = new ArrayList<>();
1✔
416
    // We will have progress reports that don't have country of operation set for the PI.
417
    if (!skipPI && !countryValidator.isInCountryList(darData.getPiCountryOfOperation())) {
1✔
418
      errorSummary.add(
1✔
419
          "Principal Investigator %s Country of Operation (%s) is not allowed"
420
              .formatted(darData.getPiEmail(), darData.getPiCountryOfOperation()));
1✔
421
    }
422

423
    List<Collaborator> collaborators = darData.getLabAndInternalCollaborators();
1✔
424
    collaborators.forEach(
1✔
425
        collaborator -> {
426
          if (!countryValidator.isInCountryList(collaborator.countryOfOperation())) {
1✔
427
            errorSummary.add(
1✔
428
                "Collaborator or Lab Staff Member %s Country of Operation (%s) is not allowed"
429
                    .formatted(collaborator.email(), collaborator.countryOfOperation()));
1✔
430
          }
431
        });
1✔
432

433
    if (!errorSummary.isEmpty()) {
1✔
434
      throw new BadRequestException(String.join(", ", errorSummary));
1✔
435
    }
436
  }
1✔
437

438
  @VisibleForTesting
439
  protected void validateInternalCollaborators(User user, DataAccessRequest progressReport) {
440
    List<String> errorSummary = getCollaboratorAndLibraryCardErrors(user, progressReport.getData());
1✔
441

442
    if (!errorSummary.isEmpty()) {
1✔
443
      throw new BadRequestException( ALL_LISTED_PERSONNEL_MUST_SHARE_THE_SAME_INSTITUTION
1✔
444
          + String.join(", ", errorSummary));
1✔
445
    }
446
  }
1✔
447

448
  private List<String> getCollaboratorAndLibraryCardErrors(User user, DataAccessRequestData darData) {
449
    List<String> errorSummary = new ArrayList<>();
1✔
450
    getErrorSummary(
1✔
451
        darData.getInternalCollaborators().stream().map(Collaborator::email).toList(), user.getInstitution(),
1✔
452
        INTERNAL_COLLABORATOR + " " + MEMBER + ": ", INTERNAL_COLLABORATOR + "  " + MEMBERS, errorSummary);
453
    getErrorSummary(
1✔
454
        darData.getLabCollaborators().stream().map(Collaborator::email).toList(), user.getInstitution(),
1✔
455
        LAB_STAFF + " " + MEMBER + ": ", LAB_STAFF + " " + MEMBERS, errorSummary);
456
    return errorSummary;
1✔
457
  }
458

459
  /**
460
   * Update an existing DataAccessRequest. Replaces DataAccessRequestData.
461
   *
462
   * @param user The User
463
   * @param dar  The DataAccessRequest
464
   * @return The updated DataAccessRequest
465
   */
466
  public DataAccessRequest updateByReferenceId(User user, DataAccessRequest dar) {
467
    if (!dar.getDraft()) {
1✔
468
      throw new SubmittedDARCannotBeEditedException();
1✔
469
    }
470
    try {
471
      return dataAccessRequestServiceDAO.updateByReferenceId(user, dar);
1✔
472
    } catch (SQLException e) {
×
473
      // If I simply rethrow the error then I'll have to redefine any method that
474
      // calls this function to "throw SQLException"
475
      //Instead I'm going to throw an UnableToExecuteStatementException
476
      //Response class will catch it, log it, and throw a 500 through the "unableToExecuteExceptionHandler"
477
      //on the Resource class, just like it would with a SQLException
478
      throw new UnableToExecuteStatementException(e.getMessage());
×
479
    }
480
  }
481

482
  /**
483
   * Validates that PI email is not duplicated with SO or IT Director emails
484
   *
485
   * @param darData The data access request data to validate
486
   * @throws IllegalArgumentException if duplicate emails are found
487
   */
488
  public void validateNoKeyPersonnelDuplicates(DataAccessRequestData darData) {
489
    EmailValidator emailValidator = EmailValidator.getInstance();
1✔
490

491
    String piEmail = darData.getPiEmail();
1✔
492
    String soEmail = darData.getSigningOfficialEmail();
1✔
493
    String itEmail = darData.getItDirectorEmail();
1✔
494

495
    if (!emailValidator.isValid(piEmail) || !emailValidator.isValid(soEmail)
1✔
496
        || !emailValidator.isValid(itEmail)) {
1✔
497
      throw new IllegalArgumentException(
1✔
498
          "Principal Investigator, Signing Official, and IT Director emails must be valid");
499
    }
500

501
    if (piEmail.equalsIgnoreCase(soEmail)) {
1✔
502
      throw new IllegalArgumentException(
1✔
503
          "Principal Investigator email cannot be the same as Signing Official email");
504
    }
505

506
    if (piEmail.equalsIgnoreCase(itEmail)) {
1✔
507
      throw new IllegalArgumentException(
1✔
508
          "Principal Investigator email cannot be the same as IT Director email");
509
    }
510
  }
1✔
511

512
  @VisibleForTesting
513
  protected void validatePersonnelInstitutionAndLibraryCardRequirements(User user, DataAccessRequestData darData) {
514
    Institution submitterInstitution = user.getInstitution();
1✔
515
    String piEmail = darData.getPiEmail();
1✔
516
    String soEmail = darData.getSigningOfficialEmail();
1✔
517
    String itEmail = darData.getItDirectorEmail();
1✔
518

519
    List<String> invalidMembers = new ArrayList<>();
1✔
520

521
    verifyInstitution(submitterInstitution, piEmail, "Principal Investigator", invalidMembers);
1✔
522
    verifyInstitution(submitterInstitution, soEmail, "Signing Official", invalidMembers);
1✔
523
    verifyInstitution(submitterInstitution, itEmail, "IT Director", invalidMembers);
1✔
524
    invalidMembers.addAll(getCollaboratorAndLibraryCardErrors(user, darData));
1✔
525

526
    if (!invalidMembers.isEmpty()) {
1✔
527
      throw new IllegalArgumentException(
1✔
528
          ALL_LISTED_PERSONNEL_MUST_SHARE_THE_SAME_INSTITUTION
529
              + String.join(", ", invalidMembers));
1✔
530
    }
531
  }
1✔
532

533
  private void verifyInstitution(Institution submitterInstitution, String email, String role, List<String> invalidMembers) {
534
    if (emailDoesNotMatchInstitution(submitterInstitution, email)) {
1✔
535
      invalidMembers.add(role + ": " + email);
1✔
536
    }
537
  }
1✔
538

539
  private List<String> findCollaboratorsWithoutLibraryCards(List<String> usersToCheck) {
540
    List<String> usersWithoutLibraryCards = new ArrayList<>();
1✔
541
    usersToCheck.forEach(email -> {
1✔
542
      User collabUser = userDAO.findUserByEmail(email);
1✔
543
      if (collabUser == null || collabUser.getLibraryCard() == null) {
1✔
544
        usersWithoutLibraryCards.add(email);
1✔
545
      }
546
    });
1✔
547
    return usersWithoutLibraryCards;
1✔
548
  }
549

550
  private void getErrorSummary(
551
      List<String> emails,
552
      Institution institution,
553
      String categorySingular,
554
      String categoryPlural,
555
      List<String> invalidMembers) {
556
    List<String> institutionErrors = findEmailAddressesNotInInstitution(emails, institution);
1✔
557
    List<String> libraryCardErrors = findCollaboratorsWithoutLibraryCards(emails);
1✔
558

559
    if (!institutionErrors.isEmpty()) {
1✔
560
      String missingInstitution = " (missing institution) ";
1✔
561
      invalidMembers.add(buildSingleErrorFromErrorList(institutionErrors, categorySingular + missingInstitution, categoryPlural + missingInstitution));
1✔
562
    }
563

564
    if (!libraryCardErrors.isEmpty()) {
1✔
565
      String missingLibraryCard = " (missing library card) ";
1✔
566
      invalidMembers.add(buildSingleErrorFromErrorList(libraryCardErrors, categorySingular + missingLibraryCard, categoryPlural + missingLibraryCard));
1✔
567
    }
568
  }
1✔
569

570
  private List<String> findEmailAddressesNotInInstitution(
571
      List<String> emailAddresses, Institution institution) {
572
    ArrayList<String> errors = new ArrayList<>();
1✔
573
    emailAddresses.forEach(
1✔
574
        collaborator -> {
575
          if (emailDoesNotMatchInstitution(institution, collaborator)) {
1✔
576
            errors.add(collaborator);
1✔
577
          }
578
        });
1✔
579
    return errors;
1✔
580
  }
581

582
  private String buildSingleErrorFromErrorList(
583
      List<String> errors, String categorySingular, String categoryPlural) {
584
    StringBuilder msg = new StringBuilder();
1✔
585
    if (errors.size() == 1) {
1✔
586
      msg.append(categorySingular);
1✔
587
    } else if (errors.size() > 1) {
1✔
588
      msg.append(categoryPlural);
1✔
589
    }
590
    msg.append(String.join(", ", errors));
1✔
591
    return msg.toString();
1✔
592
  }
593

594
  private boolean emailDoesNotMatchInstitution(Institution institution, String email) {
595
    Institution foundInstitution = institutionService.findInstitutionForEmail(email);
1✔
596
    if (foundInstitution == null || institution == null) {
1✔
597
      return true;
1✔
598
    }
599
    return !institution.equals(foundInstitution);
1✔
600
  }
601

602
  public Collection<DataAccessRequest> getApprovedDARsForDataset(Dataset dataset) {
603
    return dataAccessRequestDAO.findApprovedDARsByDatasetId(dataset.getDatasetId());
1✔
604
  }
605

606
  public void sendExpirationNotices() {
607
    sendDARExpirationReminderNotices();
1✔
608
    sendDARExpirationNotices();
1✔
609
  }
1✔
610

611
  private void sendDARExpirationNotices() {
612
    EmailType emailType = EmailType.DAR_EXPIRED;
1✔
613
    sendDARMessageToList(emailType, EXPIRE_NOTICE_INTERVAL);
1✔
614
  }
1✔
615

616
  private void sendDARExpirationReminderNotices() {
617
    EmailType emailType = EmailType.DAR_EXPIRATION_REMINDER;
1✔
618
    sendDARMessageToList(emailType, EXPIRE_WARN_INTERVAL);
1✔
619
  }
1✔
620

621
  private void sendDARMessageToList(EmailType type, String interval) {
622
    List<DataAccessRequest> expiredDars =
1✔
623
        dataAccessRequestDAO.findAgedDARsByEmailTypeOlderThanInterval(
1✔
624
            type.getTypeInt(), interval, MINIMUM_SUBMITTED_DATE_FOR_DAR_EXPIRATIONS);
1✔
625
    expiredDars.forEach(
1✔
626
        expiredDar -> {
627
          try {
628
            String referenceId = expiredDar.getReferenceId();
1✔
629
            User user = userDAO.findUserById(expiredDar.getUserId());
1✔
630
            String darCode = expiredDar.getDarCode();
1✔
631
            String userName = user.getDisplayName();
1✔
632
            if (user.getEmail() == null) {
1✔
633
              throw new InvalidEmailAddressException(
1✔
634
                  String.format(
1✔
635
                      "Email address for user %d (%s) not found for expiring warning.  DAR reference id: %s",
636
                      expiredDar.getUserId(), userName, referenceId));
1✔
637
            }
638
            switch (type) {
1✔
639
              case DAR_EXPIRATION_REMINDER:
640
                emailService.sendDarExpirationReminderMessage(
1✔
641
                    user, darCode, user.getUserId(), referenceId);
1✔
642
                break;
1✔
643
              case DAR_EXPIRED:
644
                emailService.sendDarExpiredMessage(user, darCode, user.getUserId(), referenceId);
1✔
645
                break;
1✔
646
              default:
647
                break;
648
            }
649
          } catch (Exception e) {
1✔
650
            logException(e);
1✔
651
          }
1✔
652
        });
1✔
653
  }
1✔
654

655
  public void sendReminderMessage(Integer voteId) throws IOException, TemplateException {
656
    Vote vote = voteDAO.findVoteById(voteId);
1✔
657
    Election election = electionDAO.findElectionWithFinalVoteById(vote.getElectionId());
1✔
658
    DarCollection collection = darCollectionDAO.findDARCollectionByReferenceId(
1✔
659
        election.getReferenceId());
1✔
660
    User user = findUserById(vote.getUserId());
1✔
661
    String voteUrl = serverUrl + "dar_collection/%d".formatted(collection.getDarCollectionId());
1✔
662
    emailService.sendReminderMessage(user, vote, collection.getDarCode(), election.getElectionType(), voteUrl);
1✔
663
    voteDAO.updateVoteReminderFlag(voteId, true);
1✔
664
  }
1✔
665

666
  private User findUserById(Integer id) throws IllegalArgumentException {
667
    User user = userDAO.findUserById(id);
1✔
668
    if (user == null) {
1✔
669
      throw new NotFoundException("Could not find dacUser for specified id : " + id);
×
670
    }
671
    return user;
1✔
672
  }
673

674
  public List<Election> findOpenElectionsByReferenceId(String referenceId) {
NEW
675
    return electionDAO.findOpenElectionsByReferenceIds(List.of(referenceId));
×
676
  }
677
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc