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

DataBiosphere / consent / #6139

25 Jun 2025 05:44PM UTC coverage: 79.389% (+0.08%) from 79.309%
#6139

push

web-flow
DT-1823: Signing Official Emails (#2572)

Co-authored-by: otchet-broad <111771148+otchet-broad@users.noreply.github.com>

91 of 109 new or added lines in 8 files covered. (83.49%)

10400 of 13100 relevant lines covered (79.39%)

0.79 hits per line

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

91.55
/src/main/java/org/broadinstitute/consent/http/service/DarCollectionService.java
1
package org.broadinstitute.consent.http.service;
2

3
import static java.util.stream.Collectors.toList;
4

5
import com.google.common.annotations.VisibleForTesting;
6
import com.google.common.collect.Streams;
7
import com.google.gson.Gson;
8
import com.google.inject.Inject;
9
import freemarker.template.TemplateException;
10
import jakarta.ws.rs.BadRequestException;
11
import jakarta.ws.rs.NotAcceptableException;
12
import jakarta.ws.rs.NotAuthorizedException;
13
import jakarta.ws.rs.NotFoundException;
14
import java.io.IOException;
15
import java.text.SimpleDateFormat;
16
import java.util.ArrayList;
17
import java.util.Collection;
18
import java.util.Collections;
19
import java.util.Date;
20
import java.util.HashMap;
21
import java.util.List;
22
import java.util.Map;
23
import java.util.Objects;
24
import java.util.Set;
25
import java.util.function.Function;
26
import java.util.function.Predicate;
27
import java.util.stream.Collectors;
28
import java.util.stream.Stream;
29
import org.broadinstitute.consent.http.db.DacDAO;
30
import org.broadinstitute.consent.http.db.DarCollectionDAO;
31
import org.broadinstitute.consent.http.db.DarCollectionSummaryDAO;
32
import org.broadinstitute.consent.http.db.DataAccessRequestDAO;
33
import org.broadinstitute.consent.http.db.DatasetDAO;
34
import org.broadinstitute.consent.http.db.ElectionDAO;
35
import org.broadinstitute.consent.http.db.MatchDAO;
36
import org.broadinstitute.consent.http.db.UserDAO;
37
import org.broadinstitute.consent.http.db.VoteDAO;
38
import org.broadinstitute.consent.http.enumeration.DarCollectionActions;
39
import org.broadinstitute.consent.http.enumeration.DarCollectionStatus;
40
import org.broadinstitute.consent.http.enumeration.DarStatus;
41
import org.broadinstitute.consent.http.enumeration.ElectionStatus;
42
import org.broadinstitute.consent.http.enumeration.UserRoles;
43
import org.broadinstitute.consent.http.enumeration.VoteType;
44
import org.broadinstitute.consent.http.models.Dac;
45
import org.broadinstitute.consent.http.models.DarCollection;
46
import org.broadinstitute.consent.http.models.DarCollectionSummary;
47
import org.broadinstitute.consent.http.models.DataAccessRequest;
48
import org.broadinstitute.consent.http.models.DataAccessRequestData;
49
import org.broadinstitute.consent.http.models.Dataset;
50
import org.broadinstitute.consent.http.models.Election;
51
import org.broadinstitute.consent.http.models.User;
52
import org.broadinstitute.consent.http.models.UserRole;
53
import org.broadinstitute.consent.http.models.Vote;
54
import org.broadinstitute.consent.http.service.dao.DarCollectionServiceDAO;
55
import org.broadinstitute.consent.http.util.ConsentLogger;
56

57
public class DarCollectionService implements ConsentLogger {
58

59
  private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
1✔
60
  private final DarCollectionDAO darCollectionDAO;
61
  private final DarCollectionServiceDAO collectionServiceDAO;
62
  private final DacDAO dacDAO;
63
  private final DarCollectionSummaryDAO darCollectionSummaryDAO;
64
  private final DataAccessRequestDAO dataAccessRequestDAO;
65
  private final DatasetDAO datasetDAO;
66
  private final ElectionDAO electionDAO;
67
  private final EmailService emailService;
68
  private final MatchDAO matchDAO;
69
  private final UserDAO userDAO;
70
  private final VoteDAO voteDAO;
71

72
  @Inject
73
  public DarCollectionService(DarCollectionDAO darCollectionDAO,
74
      DarCollectionServiceDAO collectionServiceDAO, DatasetDAO datasetDAO, ElectionDAO electionDAO,
75
      DataAccessRequestDAO dataAccessRequestDAO, EmailService emailService, VoteDAO voteDAO,
76
      MatchDAO matchDAO, DarCollectionSummaryDAO darCollectionSummaryDAO, UserDAO userDAO,
77
      DacDAO dacDAO) {
1✔
78
    this.darCollectionDAO = darCollectionDAO;
1✔
79
    this.collectionServiceDAO = collectionServiceDAO;
1✔
80
    this.datasetDAO = datasetDAO;
1✔
81
    this.electionDAO = electionDAO;
1✔
82
    this.dataAccessRequestDAO = dataAccessRequestDAO;
1✔
83
    this.emailService = emailService;
1✔
84
    this.voteDAO = voteDAO;
1✔
85
    this.matchDAO = matchDAO;
1✔
86
    this.darCollectionSummaryDAO = darCollectionSummaryDAO;
1✔
87
    this.userDAO = userDAO;
1✔
88
    this.dacDAO = dacDAO;
1✔
89
  }
1✔
90

91
  private void updateStatusCount(Map<String, Integer> statusCount, String status) {
92
    // If the status is null, track it as Undefined to ensure election is accounted for.
93
    statusCount.merge(Objects.requireNonNullElse(status, "Undefined"), 1, Integer::sum);
1✔
94
  }
1✔
95

96
  private void determineCollectionStatus(DarCollectionSummary summary,
97
      Map<String, Integer> statusCount, Integer datasetCount, Integer electionCount) {
98
    //If there are no elections, status is unreviewed
99
    //if there are some elections open, status is in process
100
    //if all elections are closed or canceled and electionCount == datasetCount, status is complete
101
    if (electionCount.equals(0)) {
1✔
102
      summary.setStatus(DarCollectionStatus.SUBMITTED.getValue());
1✔
103
    } else if (electionCount.equals(datasetCount)) {
1✔
104
      Integer openCount = statusCount.get(ElectionStatus.OPEN.getValue());
1✔
105
      if (Objects.isNull(openCount)) {
1✔
106
        summary.setStatus(DarCollectionStatus.COMPLETE.getValue());
1✔
107
      } else {
108
        summary.setStatus(DarCollectionStatus.IN_PROCESS.getValue());
1✔
109
      }
110
    } else {
1✔
111
      summary.setStatus(DarCollectionStatus.IN_PROCESS.getValue());
1✔
112
    }
113
  }
1✔
114

115
  private void processDarCollectionSummariesForAdmin(List<DarCollectionSummary> summaries) {
116
    //if at least one election is open, show cancel
117
    //if at least one non-open/absent election, show open
118
    summaries.forEach(s -> {
1✔
119
      Map<String, Integer> statusCount = new HashMap<>();
1✔
120
      Map<Integer, Election> elections = s.getElections();
1✔
121
      if (elections.isEmpty()) {
1✔
122
        s.addAction(DarCollectionActions.OPEN);
1✔
123
        s.setStatus(DarCollectionStatus.SUBMITTED.getValue());
1✔
124
      } else {
125
        elections.values().forEach(e -> {
1✔
126
          String status = e.getStatus();
1✔
127
          updateStatusCount(statusCount, status);
1✔
128
          if (status.equals(ElectionStatus.OPEN.getValue())) {
1✔
129
            s.addAction(DarCollectionActions.CANCEL);
1✔
130
          } else {
131
            s.addAction(DarCollectionActions.OPEN);
1✔
132
          }
133
        });
1✔
134
        determineCollectionStatus(s, statusCount, s.getDatasetCount(), s.getElections().size());
1✔
135
      }
136
      if (s.getCloseoutSupplement() != null) {
1✔
137
        s.getActions().clear();
1✔
138
      }
139
    });
1✔
140
  }
1✔
141

142
  private DarCollectionSummary processDraftAsSummary(DataAccessRequest d) {
143
    try {
144
      DarCollectionSummary summary = new DarCollectionSummary();
1✔
145
      String darCode = "DRAFT_DAR_" + sdf.format(d.getCreateDate());
1✔
146
      summary.setDarCode(darCode);
1✔
147
      summary.setStatus(DarCollectionStatus.DRAFT.getValue());
1✔
148
      summary.setName(d.getData().getProjectTitle());
1✔
149
      summary.addAction(DarCollectionActions.RESUME);
1✔
150
      summary.addAction(DarCollectionActions.DELETE);
1✔
151
      summary.addReferenceId(d.referenceId);
1✔
152
      return summary;
1✔
153
    } catch (Exception e) {
×
154
      logWarn("Error processing draft with id: %s".formatted(d.getId()), e);
×
155
    }
156
    return null;
×
157
  }
158

159
  private void processDarCollectionSummariesForResearcher(List<DarCollectionSummary> summaries) {
160
    //if an election exists, cancel does not appear
161
    //if there are no elections, review and cancel are present
162
    //if the collection is canceled, revise and review is present
163
    summaries.forEach(s -> {
1✔
164
      Map<String, Integer> statusCount = new HashMap<>();
1✔
165
      Map<Integer, Election> elections = s.getElections();
1✔
166
      int electionCount = elections.size();
1✔
167
      elections.values().forEach(election -> updateStatusCount(statusCount, election.getStatus()));
1✔
168
      s.addAction(DarCollectionActions.REVIEW);
1✔
169
      //if the latest DAR in the collection has at least one approved dataset,
170
      //include the create progress report action
171
      Set<Integer> datasetIds = dataAccessRequestDAO.findDatasetApprovalsByDar(s.getLatestReferenceId());
1✔
172
      // Can only create a progress report if there are approved datasets and no closeout supplement
173
      if (!datasetIds.isEmpty() && s.getCloseoutSupplement() == null) {
1✔
174
          s.addAction(DarCollectionActions.CREATE_PROGRESS_REPORT);
1✔
175
        }
176

177
      //check dar statuses, if they're all canceled show revise (but only if there are no elections)
178
      if (electionCount == 0) {
1✔
179
        Collection<String> darStatuses = s.getDarStatuses().values();
1✔
180
        boolean isCanceled = !darStatuses.isEmpty() && darStatuses.stream()
1✔
181
            .allMatch(st -> st.equalsIgnoreCase(DarStatus.CANCELED.getValue()));
1✔
182
        if (isCanceled) {
1✔
183
          s.addAction(DarCollectionActions.REVISE);
1✔
184
          s.setStatus(DarCollectionStatus.CANCELED.getValue());
1✔
185
        } else {
186
          if (!s.getProgressReport()) {
1✔
187
            s.addAction(DarCollectionActions.CANCEL);
1✔
188
          }
189
          s.setStatus(DarCollectionStatus.SUBMITTED.getValue());
1✔
190
        }
191
      } else {
1✔
192
        determineCollectionStatus(s, statusCount, s.getDatasetCount(), s.getElections().size());
1✔
193
      }
194
    });
1✔
195
  }
1✔
196

197
  private void processDarCollectionSummariesForMember(List<DarCollectionSummary> summaries,
198
      Integer userId) {
199
    summaries.forEach(s -> {
1✔
200
      Collection<Election> elections = s.getElections().values();
1✔
201
      Integer electionCount = elections.size();
1✔
202
      //if there are no elections present, unreviewed
203
      //if there are elections present. in process
204
      if (electionCount == 0) {
1✔
205
        s.setStatus(DarCollectionStatus.SUBMITTED.getValue());
1✔
206
      } else {
207
        boolean isVotable = elections
1✔
208
            .stream()
1✔
209
            .anyMatch(
1✔
210
                election -> election.getStatus().equalsIgnoreCase(ElectionStatus.OPEN.getValue()));
1✔
211

212
        if (isVotable) {
1✔
213
          s.setStatus(DarCollectionStatus.IN_PROCESS.getValue());
1✔
214
          List<Vote> votes = s.getVotes().stream()
1✔
215
              .filter(
1✔
216
                  v -> v.getUserId().equals(userId) && v.getType().equals(VoteType.DAC.getValue()))
1✔
217
              .toList();
1✔
218
          if (!votes.isEmpty()) {
1✔
219
            boolean hasVoted = votes.stream().map(Vote::getVote).allMatch(Objects::nonNull);
1✔
220
            DarCollectionActions targetAction = hasVoted ? DarCollectionActions.UPDATE
1✔
221
                : DarCollectionActions.VOTE;
1✔
222
            s.addAction(targetAction);
1✔
223
          }
224
        } else {
1✔
225
          //non-votable states
226
          //all canceled (complete)
227
          //some datasets do not have elections (in process)
228
          //all voted on (complete)
229
          //no elections
230
          if (electionCount < s.getDatasetCount()) {
1✔
231
            s.setStatus(DarCollectionStatus.IN_PROCESS.getValue());
×
232
          } else {
233
            s.setStatus(DarCollectionStatus.COMPLETE.getValue());
1✔
234
          }
235
        }
236
      }
237
    });
1✔
238
  }
1✔
239

240
  /**
241
   * Process the DarCollectionSummaries for a chairperson. Note that this decorates the raw
242
   * summaries with status and actions based on the elections present in each summary.
243
   *
244
   * @param summaries The list of DarCollectionSummaries to process
245
   */
246
  private void processDarCollectionSummariesForChair(List<DarCollectionSummary> summaries) {
247
    summaries.forEach(s -> {
1✔
248
      Map<String, Integer> statusCount = new HashMap<>();
1✔
249
      Map<Integer, Election> elections = s.getElections();
1✔
250
      if (elections.size() < s.getDatasetCount()) {
1✔
251
        s.addAction(DarCollectionActions.OPEN);
1✔
252
      }
253
      elections.values().forEach(election -> updateStatusCount(statusCount, election.getStatus()));
1✔
254
      Integer closedCount = statusCount.get(ElectionStatus.CLOSED.getValue());
1✔
255
      Integer openCount = statusCount.get(ElectionStatus.OPEN.getValue());
1✔
256
      determineCollectionStatus(s, statusCount, s.getDatasetCount(), s.getElections().size());
1✔
257
      updateSummaryActionsForChair(s, closedCount, openCount);
1✔
258
    });
1✔
259
  }
1✔
260

261
  /**
262
   * Update the summary actions for a chairperson based on the summary and election counts.
263
   *
264
   * @param summary  The DarCollectionSummary to update
265
   * @param closedCount The count of closed elections
266
   * @param openCount The count of open elections
267
   */
268
  private void updateSummaryActionsForChair(
269
      DarCollectionSummary summary,
270
      Integer closedCount,
271
      Integer openCount) {
272

273
    // By default, no actions can be taken on a closeout supplement
274
    if (summary.getCloseoutSupplement() != null) {
1✔
275
      summary.getActions().clear();
1✔
276
      // If the SO has approved the closeout supplement, allow review of the progress report.
277
      if (summary.getCloseoutSigningOfficialApprovalDate() != null) {
1✔
278
        summary.addAction(DarCollectionActions.REVIEW_PROGRESS_REPORT);
1✔
279
      }
280
      return;
1✔
281
    }
282

283
    // If there are no elections, only show open
284
    if (summary.getElections().isEmpty()) {
1✔
285
      summary.addAction(DarCollectionActions.OPEN);
1✔
286
    }
287

288
    // If there are closed or canceled elections, show open
289
    // If there are any open elections, show vote
290
    summary.getElections().values().forEach(election -> {
1✔
291
      ElectionStatus status = ElectionStatus.getStatusFromString(election.getStatus());
1✔
292
      switch (Objects.requireNonNull(status)) {
1✔
293
        case CLOSED, CANCELED:
294
          summary.addAction(DarCollectionActions.OPEN);
1✔
295
          break;
1✔
296
        case OPEN:
297
          summary.addAction(DarCollectionActions.VOTE);
1✔
298
          break;
1✔
299
        default:
300
          break;
301
      }
302
    });
1✔
303

304
    // Add cancel if there are no closed elections and at least one open election
305
    if (Objects.isNull(closedCount) && Objects.nonNull(openCount)) {
1✔
306
      summary.addAction(DarCollectionActions.CANCEL);
1✔
307
    }
308
  }
1✔
309

310
  private void processDarCollectionSummariesForSO(List<DarCollectionSummary> summaries) {
311
    summaries.forEach(s -> {
1✔
312
      Map<String, Integer> statusCount = new HashMap<>();
1✔
313
      s.getElections().values()
1✔
314
          .forEach(election -> updateStatusCount(statusCount, election.getStatus()));
1✔
315
      determineCollectionStatus(s, statusCount, s.getDatasetCount(), s.getElections().size());
1✔
316
      updateSummaryActionsForSO(s);
1✔
317
    });
1✔
318
  }
1✔
319

320
  private void updateSummaryActionsForSO(DarCollectionSummary summary) {
321
    // If the SO has not yet approved the closeout supplement, allow review of the progress report.
322
    if (summary.getCloseoutSupplement() != null && summary.getCloseoutSigningOfficialApprovalDate() == null) {
1✔
323
      summary.addAction(DarCollectionActions.REVIEW_PROGRESS_REPORT);
1✔
324
    }
325
  }
1✔
326

327
  /**
328
   * Find all DarCollectionSummaries for a given role. Admins can see all summaries Chairs and
329
   * Members can see summaries for datasets they have access to Signing Officials can see summaries
330
   * for researchers in their institution Researchers can see only their own summaries
331
   *
332
   * @param user     The user making the request
333
   * @param role The role the user is making the request as
334
   * @return List of DarCollectionSummary objects
335
   */
336
  public List<DarCollectionSummary> getSummariesForRole(User user, UserRoles role) {
337
    final List<DarCollectionSummary> summaries;
338
    Integer userId = user.getUserId();
1✔
339
    List<Integer> datasetIds;
340
    switch (role) {
1✔
341
      case ADMIN:
342
        summaries = darCollectionSummaryDAO.getDarCollectionSummariesForAdmin();
1✔
343
        processDarCollectionSummariesForAdmin(summaries);
1✔
344
        break;
1✔
345
      case SIGNINGOFFICIAL:
346
        summaries = darCollectionSummaryDAO.getDarCollectionSummariesForSO(user.getInstitutionId());
1✔
347
        processDarCollectionSummariesForSO(summaries);
1✔
348
        break;
1✔
349
      case CHAIRPERSON:
350
        datasetIds = getDatasetIdsForUserAndRoleId(user, UserRoles.CHAIRPERSON.getRoleId());
1✔
351
        summaries = darCollectionSummaryDAO.getDarCollectionSummariesForDAC(userId, datasetIds);
1✔
352
        processDarCollectionSummariesForChair(summaries);
1✔
353
        break;
1✔
354
      case MEMBER:
355
        datasetIds = getDatasetIdsForUserAndRoleId(user, UserRoles.MEMBER.getRoleId());
1✔
356
        summaries = darCollectionSummaryDAO.getDarCollectionSummariesForDAC(userId, datasetIds);
1✔
357
        processDarCollectionSummariesForMember(summaries, userId);
1✔
358
        break;
1✔
359
      case RESEARCHER:
360
        var darSummaries = darCollectionSummaryDAO.getDarCollectionSummariesForResearcher(userId);
1✔
361
        processDarCollectionSummariesForResearcher(darSummaries);
1✔
362
        List<DataAccessRequest> drafts = dataAccessRequestDAO.findAllDraftsByUserId(userId);
1✔
363
        summaries =
1✔
364
            Stream.concat(
1✔
365
                    darSummaries.stream(),
1✔
366
                    drafts.stream().map(this::processDraftAsSummary).filter(Objects::nonNull))
1✔
367
                .toList();
1✔
368
        break;
1✔
369
      default:
370
        summaries = List.of();
×
371
        break;
372
    }
373
    return summaries;
1✔
374
  }
375

376
  private List<Integer> getDatasetIdsForUserAndRoleId(User user, Integer roleId) {
377
    List<Integer> roleDacIds = user.getRoles().stream()
1✔
378
        .filter(ur -> Objects.nonNull(ur.getRoleId()))
1✔
379
        .filter(ur -> ur.getRoleId().equals(roleId))
1✔
380
        .map(UserRole::getDacId)
1✔
381
        .filter(Objects::nonNull)
1✔
382
        .toList();
1✔
383
    return Stream.of(roleDacIds)
1✔
384
        .filter(Predicate.not(List::isEmpty))
1✔
385
        .map(datasetDAO::findDatasetListByDacIds)
1✔
386
        .flatMap(List::stream)
1✔
387
        .map(Dataset::getDatasetId)
1✔
388
        .toList();
1✔
389
  }
390

391
  /**
392
   * Finds the DarCollectionSummary for a given darCollectionId, processed by the given role.
393
   *
394
   * @param user         The user making the request
395
   * @param role         The role the user is making the request as
396
   * @param collectionId The darCollectionId of the requested DarCollectionSummary
397
   * @return A DarCollectionSummary object
398
   */
399
  public DarCollectionSummary getSummaryForRoleByCollectionId(User user, UserRoles role,
400
      Integer collectionId) {
401
    DarCollectionSummary summary = null;
1✔
402
    Integer userId = user.getUserId();
1✔
403
    List<Integer> datasetIds;
404
    try {
405
      switch (role) {
1✔
406
        case ADMIN:
407
          summary = darCollectionSummaryDAO.getDarCollectionSummaryByCollectionId(collectionId);
1✔
408
          processDarCollectionSummariesForAdmin(List.of(summary));
1✔
409
          break;
1✔
410
        case SIGNINGOFFICIAL:
411
          summary = darCollectionSummaryDAO.getDarCollectionSummaryByCollectionId(collectionId);
1✔
412
          processDarCollectionSummariesForSO(List.of(summary));
1✔
413
          break;
1✔
414
        case CHAIRPERSON:
415
          datasetIds = getDatasetIdsForUserAndRoleId(user, UserRoles.CHAIRPERSON.getRoleId());
1✔
416
          summary = darCollectionSummaryDAO.getDarCollectionSummaryForDACByCollectionId(userId,
1✔
417
              datasetIds, collectionId);
418
          processDarCollectionSummariesForChair(List.of(summary));
1✔
419
          break;
1✔
420
        case MEMBER:
421
          datasetIds = getDatasetIdsForUserAndRoleId(user, UserRoles.MEMBER.getRoleId());
1✔
422
          summary = darCollectionSummaryDAO.getDarCollectionSummaryForDACByCollectionId(userId,
1✔
423
              datasetIds, collectionId);
424
          processDarCollectionSummariesForMember(List.of(summary), userId);
1✔
425
          break;
1✔
426
        case RESEARCHER:
427
          summary = darCollectionSummaryDAO.getDarCollectionSummaryByCollectionId(collectionId);
1✔
428
          processDarCollectionSummariesForResearcher(List.of(summary));
1✔
429
          break;
1✔
430
        default:
431
          break;
432
      }
433
      return summary;
1✔
434
    } catch (NullPointerException e) {
1✔
435
      throw new NotFoundException(
1✔
436
          "Collection summary with the collection id of " + collectionId + " was not found");
437
    }
438
  }
439

440
  public DarCollectionSummary updateCollectionToDraftStatus(DarCollection sourceCollection) {
441
    sourceCollection.getDars().values().forEach((d) -> {
×
442
      Date now = new Date();
×
443
      DataAccessRequestData newData = new Gson().fromJson(d.getData().toString(),
×
444
          DataAccessRequestData.class);
445
      newData.setDarCode(null);
×
446
      newData.setStatus(null);
×
447
      newData.setReferenceId(d.getReferenceId());
×
448
      newData.setSortDate(now.getTime());
×
449
      dataAccessRequestDAO.updateDataByReferenceId(
×
450
          d.getReferenceId(),
×
451
          d.getUserId(),
×
452
          now,
453
          null,
454
          now,
455
          newData,
456
          null
457
      );
458
    });
×
459

460
    // get updated collection
461
    sourceCollection = this.darCollectionDAO.findDARCollectionByCollectionId(
×
462
        sourceCollection.getDarCollectionId());
×
463

464
    return this.processDraftAsSummary(new ArrayList<>(sourceCollection.getDars().values()).get(0));
×
465
  }
466

467
  /**
468
   * Find all dataset ids by the DAC User. Will return ids for Chairpersons or Members
469
   *
470
   * @param user The DAC User
471
   * @return List of Dataset IDs
472
   */
473
  public List<Integer> findDatasetIdsByDACUser(User user) {
474
    return datasetDAO.findDatasetIdsByDACUserId(user.getUserId());
×
475
  }
476

477
  public void deleteByCollectionId(User user, Integer collectionId)
478
      throws NotAcceptableException, NotAuthorizedException, NotFoundException {
479
    DarCollection coll = darCollectionDAO.findDARCollectionByCollectionId(collectionId);
1✔
480
    if (coll == null) {
1✔
481
      throw new NotFoundException("DAR Collection does not exist at that id.");
1✔
482
    }
483

484
    // ensure the user is capable of deleting the collection
485
    if (!user.hasUserRole(UserRoles.ADMIN) && !coll.getCreateUserId().equals(user.getUserId())) {
1✔
486
      throw new NotAuthorizedException("Not authorized to delete DAR Collection.");
1✔
487
    }
488

489
    // get the reference ids of the dars in the collection
490
    List<String> referenceIds =
1✔
491
        coll.getDars().values().stream().map(DataAccessRequest::getReferenceId).distinct()
1✔
492
            .collect(toList());
1✔
493

494
    // ensure there are no elections; if there are, will attempt to delete (must be admin)
495
    ensureNoElections(user, referenceIds);
1✔
496

497
    // no elections left & user has perms => safe to delete collection
498

499
    // delete DARs
500
    matchDAO.deleteRationalesByPurposeIds(referenceIds);
1✔
501
    matchDAO.deleteMatchesByPurposeIds(referenceIds);
1✔
502
    dataAccessRequestDAO.deleteDARDatasetRelationByReferenceIds(referenceIds);
1✔
503
    dataAccessRequestDAO.deleteByReferenceIds(referenceIds);
1✔
504

505
    // delete collection
506
    darCollectionDAO.deleteByCollectionId(collectionId);
1✔
507
  }
1✔
508

509
  // checks if there are any elections for any of the DARs in the referenceIds; if so,
510
  // will attempt to delete them (must be admin to delete)
511
  private void ensureNoElections(User user, List<String> referenceIds)
512
      throws NotAcceptableException {
513
    // get elections across all reference ids
514
    List<Election> allElections = electionDAO.findElectionsByReferenceIds(referenceIds);
1✔
515

516
    // if there are already no elections, we're done!
517
    if (allElections.isEmpty()) {
1✔
518
      return;
1✔
519
    }
520

521
    // if there are any elections, we need to delete them.
522
    // only admins can delete elections; make sure user is an admin
523
    if (!user.hasUserRole(UserRoles.ADMIN)) {
1✔
524
      throw new NotAcceptableException("Cannot delete DAR with elections.");
1✔
525
    }
526

527
    // delete all votes
528
    voteDAO.deleteVotesByReferenceIds(referenceIds);
1✔
529

530
    // delete all elections
531
    List<Integer> electionIds = allElections.stream().map(Election::getElectionId)
1✔
532
        .collect(toList());
1✔
533

534
    electionDAO.deleteElectionsByIds(electionIds);
1✔
535

536
  }
1✔
537

538
  public DarCollection getByReferenceId(String referenceId) {
539
    DarCollection collection = darCollectionDAO.findDARCollectionByReferenceId(referenceId);
×
540
    if (Objects.isNull(collection)) {
×
541
      throw new NotFoundException(
×
542
          "Collection with the reference id of " + referenceId + " was not found");
543
    }
544
    return addDatasetsToCollection(collection);
×
545
  }
546

547
  public DarCollection getByCollectionId(Integer collectionId) {
548
    DarCollection collection = darCollectionDAO.findDARCollectionByCollectionId(collectionId);
1✔
549
    if (Objects.isNull(collection)) {
1✔
550
      throw new NotFoundException(
×
551
          "Collection with the collection id of " + collectionId + " was not found");
552
    }
553
    return addDatasetsToCollection(collection);
1✔
554
  }
555

556
  /**
557
   * Given a DarCollection, add its relevant datasets.
558
   *
559
   * @param collection      The list of DarCollections to iterate over.
560
   * @return collection with datasets added
561
   */
562
  @VisibleForTesting
563
  protected DarCollection addDatasetsToCollection(DarCollection collection) {
564
    // get datasetIds from each DAR from each collection
565
    List<String> referenceIds = List.copyOf(collection.getDars().keySet());
1✔
566
    List<Integer> datasetIds = referenceIds.isEmpty() ? List.of()
1✔
567
        : dataAccessRequestDAO.findAllDARDatasetRelations(referenceIds);
1✔
568
    if (!datasetIds.isEmpty()) {
1✔
569
      Map<Integer, Dataset> datasetMap = datasetDAO.findDatasetsByIdList(datasetIds)
1✔
570
          .stream()
1✔
571
          .distinct()
1✔
572
          .collect(Collectors.toMap(Dataset::getDatasetId, Function.identity()));
1✔
573

574
        Set<Dataset> collectionDatasets = collection.getDars().values().stream()
1✔
575
            .map(DataAccessRequest::getDatasetIds)
1✔
576
            .flatMap(Collection::stream)
1✔
577
            .map(datasetMap::get)
1✔
578
            .filter(Objects::nonNull) // filtering out nulls which were getting captured by map
1✔
579
            .collect(Collectors.toSet());
1✔
580
        DarCollection copy = collection.deepCopy();
1✔
581
        copy.setDatasets(collectionDatasets);
1✔
582
        return copy;
1✔
583
    }
584
    // There were no datasets to add, so we return the original list
585
    return collection;
1✔
586
  }
587

588
  /**
589
   * Cancel Elections or a dar for a DarCollection, given a user and a role. If the user is a chair,
590
   * or admin, cancel elections. If the user is a researcher, cancel the dar.
591
   *
592
   * @param user       The User initiating the cancel
593
   * @param collection The DarCollection
594
   * @param role       The role of the user, must be one of ADMIN, CHAIRPERSON, or RESEARCHER
595
   * @return The DarCollection that has been canceled
596
   */
597
  public DarCollection cancelDarCollectionByRole(User user, DarCollection collection, UserRoles role) {
598
    Collection<DataAccessRequest> dars = collection.getDars().values();
1✔
599
    if (dars.isEmpty()) {
1✔
600
      logWarn("DAR Collection ID: [%s] does not have any associated DAR ids".formatted(
1✔
601
          collection.getDarCollectionId()));
1✔
602
      return collection;
1✔
603
    }
604

605
    return switch (role) {
1✔
606
      case ADMIN -> cancelDarCollectionElectionsAsAdmin(collection);
1✔
607
      case CHAIRPERSON ->
608
          cancelDarCollectionElectionsAsChair(collection, user);
1✔
609
      default -> cancelDarCollectionAsResearcher(collection, user);
1✔
610
    };
611
  }
612

613
  /**
614
   * Cancel a DarCollection as a researcher.
615
   * <p>
616
   * If an election exists for a DAR within the collection, that DAR cannot be cancelled by the
617
   * researcher. Since it's now under DAC review, it's up to the DAC Chair (or admin) to ultimately
618
   * decline or cancel the elections for the collection.
619
   *
620
   * @param collection The DarCollection
621
   * @param user the researcher requesting the cancel
622
   * @return The canceled DarCollection
623
   */
624
  private DarCollection cancelDarCollectionAsResearcher(DarCollection collection, User user) {
625
    if (!user.getUserId().equals(collection.getCreateUserId())) {
1✔
626
      throw new NotFoundException();
×
627
    }
628
    DarCollectionSummary summary = darCollectionSummaryDAO
1✔
629
        .getDarCollectionSummaryByCollectionId(collection.getDarCollectionId());
1✔
630
    if (summary.getProgressReport()) {
1✔
631
      throw new BadRequestException("Cannot cancel a progress report");
1✔
632
    }
633

634
    Collection<DataAccessRequest> dars = collection.getDars().values();
1✔
635
    List<String> referenceIds = dars.stream().map(DataAccessRequest::getReferenceId).toList();
1✔
636

637
    List<Election> elections = electionDAO.findLastElectionsByReferenceIds(referenceIds);
1✔
638
    if (!elections.isEmpty()) {
1✔
639
      throw new BadRequestException("Elections present on DARs; cannot cancel collection");
1✔
640
    }
641

642
    // Cancel active dars for the researcher
643
    List<String> activeDarIds = dars.stream()
1✔
644
        .filter(d -> !DataAccessRequest.isCanceled(d))
1✔
645
        .map(DataAccessRequest::getReferenceId)
1✔
646
        .toList();
1✔
647
    if (!activeDarIds.isEmpty()) {
1✔
648
      dataAccessRequestDAO.cancelByReferenceIds(activeDarIds);
1✔
649
    }
650

651
    return getByCollectionId(collection.getDarCollectionId());
1✔
652
  }
653

654
  /**
655
   * Cancel Elections for a DarCollection as an admin.
656
   * <p>
657
   * Admins can cancel all elections in a DarCollection
658
   *
659
   * @param collection The DarCollection
660
   * @return The DarCollection whose elections have been canceled
661
   */
662
  private DarCollection cancelDarCollectionElectionsAsAdmin(DarCollection collection) {
663
    Collection<DataAccessRequest> dars = collection.getDars().values();
1✔
664
    List<String> referenceIds = dars.stream().map(DataAccessRequest::getReferenceId).toList();
1✔
665

666
    // Cancel all DAR elections
667
    cancelElectionsForReferenceIds(referenceIds);
1✔
668

669
    return getByCollectionId(collection.getDarCollectionId());
1✔
670
  }
671

672
  /**
673
   * Cancel Elections for a DarCollection as a chairperson.
674
   * <p>
675
   * Chairs can only cancel Elections that reference a dataset the chair is a DAC member for.
676
   *
677
   * @param collection The DarCollection
678
   * @return The DarCollection whose elections have been canceled
679
   */
680
  private DarCollection cancelDarCollectionElectionsAsChair(DarCollection collection, User user) {
681
    // Find dataset ids the chairperson has access to:
682
    Set<Integer> datasetIds = Set.copyOf(datasetDAO.findDatasetIdsByDACUserId(user.getUserId()));
1✔
683

684
    // Filter the list of DARs we can operate on by the datasets accessible to this chairperson
685
    List<String> referenceIds = collection.getDars().values().stream()
1✔
686
        .filter(d -> datasetIds.containsAll(d.getDatasetIds()))
1✔
687
        .map(DataAccessRequest::getReferenceId)
1✔
688
        .toList();
1✔
689

690
    if (referenceIds.isEmpty()) {
1✔
691
      logWarn(
1✔
692
          "DAR Collection ID: [%s] does not have any associated DARs that this chairperson can access".formatted(
1✔
693
              collection.getDarCollectionId()));
1✔
694
      return collection;
1✔
695
    }
696

697
    // Cancel filtered DAR elections
698
    cancelElectionsForReferenceIds(referenceIds);
1✔
699

700
    return getByCollectionId(collection.getDarCollectionId());
1✔
701
  }
702

703
  /**
704
   * DarCollections with no elections, or with previously canceled elections, are valid for
705
   * initiating a new set of elections. Elections in open, closed, pending, or final states are not
706
   * valid.
707
   *
708
   * @param user       The User initiating new elections for a collection
709
   * @param collection The DarCollection
710
   * @return The updated DarCollection
711
   */
712
  public DarCollection createElectionsForDarCollection(User user, DarCollection collection)
713
      throws Exception {
714
    try {
715
      DataAccessRequest dar = collection.getMostRecentDar();
1✔
716
      List<String> createdElectionReferenceIds = collectionServiceDAO.createElectionsForDarByUser(
1✔
717
          user, dar);
718
      if (createdElectionReferenceIds.isEmpty()) {
1✔
719
        var e = new IllegalStateException(
1✔
720
            "No elections were created for DAR Collection: %s %s".formatted(
1✔
721
                collection.getDarCode(), dar.getReferenceId()));
1✔
722
        logException(e);
1✔
723
        throw e;
1✔
724
      }
725
      try {
726
        List<User> voteUsers = voteDAO.findVoteUsersByElectionReferenceIdList(
1✔
727
            createdElectionReferenceIds);
728
        if (dar.getProgressReport()) {
1✔
729
          emailService.sendProgressReportNewCollectionElectionMessage(voteUsers, collection.getDarCode());
1✔
730
        } else {
731
          emailService.sendDarNewCollectionElectionMessage(voteUsers, collection.getDarCode());
1✔
732
        }
733

734
      } catch (Exception e) {
1✔
735
        logException(
1✔
736
            "Unable to send new case message to DAC members for DAR Collection: %s".formatted(
1✔
737
                collection.getDarCode()), e);
1✔
738
      }
1✔
739
    } catch (Exception e) {
1✔
740
      logException("Exception creating elections and votes for collection: %s".formatted(
1✔
741
          collection.getDarCollectionId()), e);
1✔
742
      throw e;
1✔
743
    }
1✔
744
    return darCollectionDAO.findDARCollectionByCollectionId(collection.getDarCollectionId());
1✔
745
  }
746

747
  // Private helper method to mark Elections as 'Canceled'
748
  private void cancelElectionsForReferenceIds(List<String> referenceIds) {
749
    List<Election> elections = electionDAO.findOpenElectionsByReferenceIds(referenceIds);
1✔
750
    elections.forEach(election -> {
1✔
751
      if (!election.getStatus().equals(ElectionStatus.CANCELED.getValue())) {
1✔
752
        electionDAO.updateElectionById(election.getElectionId(), ElectionStatus.CANCELED.getValue(),
1✔
753
            new Date());
754
      }
755
    });
1✔
756
  }
1✔
757

758

759
  public void sendNewDARCollectionMessage(Integer collectionId)
760
      throws IOException, TemplateException {
761
    DarCollection collection = darCollectionDAO.findDARCollectionByCollectionId(collectionId);
1✔
762
    if (collection == null) {
1✔
763
      logWarn(
×
764
          "Sending new DAR Collection message: Could not find collection for specified collection id: "
765
              + collectionId);
766
      return;
×
767
    }
768
    // Do this, but only for a single DAR
769
    DataAccessRequest dar = collection.getMostRecentDar();
1✔
770
    List<User> distinctUsers = getDistinctAdminAndChairUsersForDAR(dar);
1✔
771
    User researcher = userDAO.findUserById(collection.getCreateUserId());
1✔
772
    if (researcher == null) {
1✔
773
      logWarn(
×
774
          "Sending new DAR Collection message: Could not find researcher for specified user id: "
775
              + collection.getCreateUserId());
×
776
    }
777
    String researcherName = researcher == null ? "Unknown" : researcher.getDisplayName();
1✔
778
    // Only do this for the DAR... dacDAO.findDacsForDatasetIds(dar.getDatasetIds())
779
    Collection<Dac> dacsInDAR = dacDAO.findDacsForDatasetIds(dar.getDatasetIds());
1✔
780
    // Use only the datasets from the dar
781
    List<Integer> datasetIds = dar.getDatasetIds();
1✔
782
    List<Dataset> datasetsInDAR =
783
        datasetIds.isEmpty() ? List.of() : datasetDAO.findDatasetsByIdList(datasetIds);
1✔
784

785
    Map<String, List<String>> sendList = new HashMap<>();
1✔
786
    for (User user : distinctUsers) {
1✔
787
      List<Dac> matchingDacsForUser = getMatchingDacs(user, dacsInDAR);
1✔
788
      for (Dac dac : matchingDacsForUser) {
1✔
789
        List<String> matchingDatasetsForDac = getMatchingDatasets(dac, datasetsInDAR);
1✔
790
        if (matchingDatasetsForDac != null) {
1✔
791
          sendList.put(dac.getName(), matchingDatasetsForDac);
1✔
792
        }
793
      }
1✔
794
      // If the dar is not a progress report, use the DAR template else use the PR template.
795
      if (dar.getProgressReport()) {
1✔
796
        // Use the reference ID to link the fact that this progress report will have been noted.
797
        // the DAR Code at this point will be ambiguous.
798
        emailService.sendNewProgressReportRequestEmail(user, sendList, researcherName, collection.getDarCode(), dar.getReferenceId());
×
799
      } else {
800
        emailService.sendNewDARRequestEmail(user, sendList, researcherName, collection.getDarCode());
1✔
801
      }
802
    }
1✔
803
    notifySigningOfficialsOfDARSubmission(dar, researcher, collection.getDarCode());
1✔
804
  }
1✔
805

806
  @VisibleForTesting
807
  protected void notifySigningOfficialsOfDARSubmission(DataAccessRequest dar, User researcher,
808
      String darCode) throws TemplateException, IOException {
809
    if (researcher == null) {
1✔
NEW
810
      logWarn(
×
NEW
811
          "Unable to send new DAR/PR message to Signing Officials: Researcher does not exist: %s".formatted(
×
NEW
812
              dar.getUserId()));
×
NEW
813
      return;
×
814
    }
815
    if (researcher.getInstitutionId() == null) {
1✔
816
      logWarn(
1✔
817
          "Unable to send new DAR/PR message to Signing Officials: Researcher does not have an institution id: %s".formatted(
1✔
818
              dar.getUserId()));
1✔
819
      return;
1✔
820
    }
821
    List<User> signingOfficials = userDAO.getSOsByInstitution(researcher.getInstitutionId());
1✔
822
    List<Dataset> datasets = datasetDAO.findDatasetsByIdList(dar.getDatasetIds());
1✔
823
    for (User so : signingOfficials) {
1✔
824
      if (dar.getProgressReport()) {
1✔
825
        emailService.sendNewSoProgressReportSubmittedEmail(so, darCode, researcher,
1✔
826
            dar.getReferenceId(), datasets);
1✔
827
      } else {
828
        emailService.sendNewSoDARSubmittedEmail(so, darCode, researcher, dar.getReferenceId(),
1✔
829
            datasets);
830
      }
831
    }
1✔
832
  }
1✔
833

834
  private List<User> getDistinctAdminAndChairUsersForDAR(DataAccessRequest dar) {
835
    List<Integer> datasetIds = dar.getDatasetIds();
1✔
836
    return getDistinctAdminAndChairUsersForDatasetIds(datasetIds);
1✔
837
  }
838

839
  private List<User> getDistinctAdminAndChairUsersForDatasetIds(List<Integer> datasetIds) {
840
    List<User> admins = userDAO.describeUsersByRoleAndEmailPreference(UserRoles.ADMIN.getRoleName(),
1✔
841
        true);
1✔
842
    Set<User> chairPersons = userDAO.findUsersForDatasetsByRole(datasetIds,
1✔
843
        Collections.singletonList(UserRoles.CHAIRPERSON.getRoleName()));
1✔
844
    // Ensure that admins/chairs are not double emailed
845
    // and filter users that don't want to receive email
846
    return Streams.concat(admins.stream(), chairPersons.stream())
1✔
847
        .filter(u -> Boolean.TRUE.equals(u.getEmailPreference()))
1✔
848
        .distinct()
1✔
849
        .toList();
1✔
850
  }
851

852
  private List<Dac> getMatchingDacs(User user, Collection<Dac> dacsInDAR) {
853
    List<Integer> dacIDs = user.getRoles().stream()
1✔
854
        .map(UserRole::getDacId)
1✔
855
        .filter(Objects::nonNull)
1✔
856
        .toList();
1✔
857
    return dacsInDAR.stream()
1✔
858
        .filter(dac -> dacIDs.contains(dac.getDacId()))
1✔
859
        .toList();
1✔
860
  }
861

862
  private List<String> getMatchingDatasets(Dac dac, List<Dataset> datasetsInDAR) {
863
    return datasetsInDAR.stream()
1✔
864
        .filter(dataset -> dataset.getDacId().equals(dac.getDacId()))
1✔
865
        .map(Dataset::getDatasetIdentifier)
1✔
866
        .toList();
1✔
867
  }
868

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