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

DataBiosphere / consent / #5834

02 May 2025 06:55PM UTC coverage: 78.772% (+0.04%) from 78.733%
#5834

push

web-flow
[DT-1601] Add a check that all datasets in a progress report must be approved (#2510)

5 of 5 new or added lines in 1 file covered. (100.0%)

54 existing lines in 6 files now uncovered.

10075 of 12790 relevant lines covered (78.77%)

0.79 hits per line

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

87.2
/src/main/java/org/broadinstitute/consent/http/service/EmailService.java
1
package org.broadinstitute.consent.http.service;
2

3
import static org.broadinstitute.consent.http.service.DataAccessRequestService.EXPIRE_NOTICE_INTERVAL;
4
import static org.broadinstitute.consent.http.service.DataAccessRequestService.EXPIRE_WARN_INTERVAL;
5

6
import com.google.common.annotations.VisibleForTesting;
7
import com.google.common.collect.Streams;
8
import com.google.inject.Inject;
9
import com.sendgrid.Response;
10
import com.sendgrid.helpers.mail.Mail;
11
import com.sendgrid.helpers.mail.objects.Content;
12
import com.sendgrid.helpers.mail.objects.Email;
13
import freemarker.template.Template;
14
import freemarker.template.TemplateException;
15
import jakarta.ws.rs.NotFoundException;
16
import java.io.IOException;
17
import java.io.StringWriter;
18
import java.io.Writer;
19
import java.sql.Timestamp;
20
import java.time.Instant;
21
import java.time.LocalDate;
22
import java.time.LocalTime;
23
import java.time.ZoneOffset;
24
import java.util.Collection;
25
import java.util.Collections;
26
import java.util.Date;
27
import java.util.HashMap;
28
import java.util.List;
29
import java.util.Map;
30
import java.util.Objects;
31
import java.util.Set;
32
import java.util.stream.Collectors;
33
import javax.annotation.Nullable;
34
import org.broadinstitute.consent.http.configurations.ConsentConfiguration;
35
import org.broadinstitute.consent.http.db.DacDAO;
36
import org.broadinstitute.consent.http.db.DarCollectionDAO;
37
import org.broadinstitute.consent.http.db.DataAccessRequestDAO;
38
import org.broadinstitute.consent.http.db.DatasetDAO;
39
import org.broadinstitute.consent.http.db.ElectionDAO;
40
import org.broadinstitute.consent.http.db.MailMessageDAO;
41
import org.broadinstitute.consent.http.db.UserDAO;
42
import org.broadinstitute.consent.http.db.VoteDAO;
43
import org.broadinstitute.consent.http.enumeration.EmailType;
44
import org.broadinstitute.consent.http.enumeration.UserRoles;
45
import org.broadinstitute.consent.http.mail.SendGridAPI;
46
import org.broadinstitute.consent.http.mail.freemarker.FreeMarkerTemplateHelper;
47
import org.broadinstitute.consent.http.mail.message.DaaRequestMessage;
48
import org.broadinstitute.consent.http.mail.message.DarExpirationReminderMessage;
49
import org.broadinstitute.consent.http.mail.message.DarExpiredMessage;
50
import org.broadinstitute.consent.http.mail.message.DataCustodianApprovalMessage;
51
import org.broadinstitute.consent.http.mail.message.DatasetApprovedMessage;
52
import org.broadinstitute.consent.http.mail.message.DatasetDeniedMessage;
53
import org.broadinstitute.consent.http.mail.message.DatasetSubmittedMessage;
54
import org.broadinstitute.consent.http.mail.message.MailMessage;
55
import org.broadinstitute.consent.http.mail.message.NewCaseMessage;
56
import org.broadinstitute.consent.http.mail.message.NewDAAUploadResearcherMessage;
57
import org.broadinstitute.consent.http.mail.message.NewDAAUploadSOMessage;
58
import org.broadinstitute.consent.http.mail.message.NewDARRequestMessage;
59
import org.broadinstitute.consent.http.mail.message.NewResearcherLibraryRequestMessage;
60
import org.broadinstitute.consent.http.mail.message.ReminderMessage;
61
import org.broadinstitute.consent.http.mail.message.ResearcherApprovedMessage;
62
import org.broadinstitute.consent.http.models.Dac;
63
import org.broadinstitute.consent.http.models.DarCollection;
64
import org.broadinstitute.consent.http.models.DataAccessRequest;
65
import org.broadinstitute.consent.http.models.Dataset;
66
import org.broadinstitute.consent.http.models.Election;
67
import org.broadinstitute.consent.http.models.User;
68
import org.broadinstitute.consent.http.models.UserRole;
69
import org.broadinstitute.consent.http.models.Vote;
70
import org.broadinstitute.consent.http.models.dto.DatasetMailDTO;
71
import org.broadinstitute.consent.http.util.ConsentLogger;
72

73
public class EmailService implements ConsentLogger {
74

75
  public static final Timestamp MINIMUM_SUBMITTED_DATE_FOR_DAR_EXPIRATIONS = Timestamp.from(
1✔
76
      Instant.ofEpochSecond(
1✔
77
          LocalDate.of(2024, 9, 30).toEpochSecond(LocalTime.of(0, 0, 0, 0), ZoneOffset.UTC)));
1✔
78
  private final DarCollectionDAO collectionDAO;
79
  private final DataAccessRequestDAO dataAccessRequestDAO;
80
  private final UserDAO userDAO;
81
  private final ElectionDAO electionDAO;
82
  private final MailMessageDAO emailDAO;
83
  private final VoteDAO voteDAO;
84
  private final DatasetDAO datasetDAO;
85
  private final DacDAO dacDAO;
86
  private final FreeMarkerTemplateHelper templateHelper;
87
  private final SendGridAPI sendGridAPI;
88
  private final String fromAccount;
89
  private final String serverUrl;
90

91
  @Inject
92
  public EmailService(
93
      DarCollectionDAO collectionDAO,
94
      VoteDAO voteDAO,
95
      ElectionDAO electionDAO,
96
      UserDAO userDAO,
97
      MailMessageDAO emailDAO,
98
      DatasetDAO datasetDAO,
99
      DacDAO dacDAO,
100
      DataAccessRequestDAO dataAccessRequestDAO,
101
      SendGridAPI sendGridAPI,
102
      FreeMarkerTemplateHelper helper,
103
      ConsentConfiguration config) {
1✔
104
    this.collectionDAO = collectionDAO;
1✔
105
    this.userDAO = userDAO;
1✔
106
    this.electionDAO = electionDAO;
1✔
107
    this.voteDAO = voteDAO;
1✔
108
    this.templateHelper = helper;
1✔
109
    this.emailDAO = emailDAO;
1✔
110
    this.datasetDAO = datasetDAO;
1✔
111
    this.dacDAO = dacDAO;
1✔
112
    this.dataAccessRequestDAO = dataAccessRequestDAO;
1✔
113
    this.sendGridAPI = sendGridAPI;
1✔
114
    this.serverUrl = config.getServicesConfiguration().getLocalURL();
1✔
115
    this.fromAccount = config.getMailConfiguration().getGoogleAccount();
1✔
116
  }
1✔
117

118
  /**
119
   * This method saves an email (either sent or unsent) with all available metadata from the
120
   * SendGrid response.
121
   */
122
  private void saveEmailAndResponse(
123
      @Nullable Response response,
124
      @Nullable String entityReferenceId,
125
      @Nullable Integer voteId,
126
      Integer userId,
127
      EmailType emailType,
128
      String content) {
129
    Instant now = Instant.now();
1✔
130
    Instant dateSent = (Objects.nonNull(response) && response.getStatusCode() < 400) ? now : null;
1✔
131
    emailDAO.insert(
1✔
132
        entityReferenceId,
133
        voteId,
134
        userId,
135
        emailType.getTypeInt(),
1✔
136
        dateSent,
137
        content,
138
        Objects.nonNull(response) ? response.getBody() : null,
1✔
139
        Objects.nonNull(response) ? response.getStatusCode() : null,
1✔
140
        now);
141
  }
1✔
142

143
  @VisibleForTesting
144
  protected void sendMessage(MailMessage mailMessage, Integer userId)
145
      throws IOException, TemplateException {
146
    Writer out = new StringWriter();
1✔
147
    Template template = templateHelper.getTemplate(mailMessage.getTemplateName());
1✔
148
    template.process(mailMessage.createModel(serverUrl), out);
1✔
149
    String content = out.toString();
1✔
150
    Mail message = new Mail(new Email(fromAccount), mailMessage.createSubject(),
1✔
151
        new Email(mailMessage.toUser.getEmail()), new Content("text/html", content));
1✔
152
    Response response = sendGridAPI.sendMessage(message, mailMessage.toUser.getEmail());
1✔
153
    saveEmailAndResponse(
1✔
154
        response,
155
        mailMessage.getEntityReferenceId(),
1✔
156
        mailMessage.getVoteId(),
1✔
157
        userId,
158
        mailMessage.emailType,
159
        content);
160
  }
1✔
161

162
  public List<org.broadinstitute.consent.http.models.mail.MailMessage> fetchEmailMessagesByType(
163
      EmailType emailType, Integer limit,
164
      Integer offset) {
165
    return emailDAO.fetchMessagesByType(emailType.getTypeInt(), limit, offset);
1✔
166
  }
167

168
  public List<org.broadinstitute.consent.http.models.mail.MailMessage> fetchEmailMessagesByCreateDate(
169
      Date start, Date end, Integer limit,
170
      Integer offset) {
171
    return emailDAO.fetchMessagesByCreateDate(start, end, limit, offset);
1✔
172
  }
173

174
  public void sendNewDARCollectionMessage(Integer collectionId)
175
      throws IOException, TemplateException {
176
    DarCollection collection = collectionDAO.findDARCollectionByCollectionId(collectionId);
1✔
177
    if (collection == null) {
1✔
UNCOV
178
      logWarn(
×
179
          "Sending new DAR Collection message: Could not find collection for specified collection id: "
180
              + collectionId);
UNCOV
181
      return;
×
182
    }
183
    List<User> distinctUsers = getDistinctAdminAndChairUsersForCollection(collection);
1✔
184
    User researcher = userDAO.findUserById(collection.getCreateUserId());
1✔
185
    if (researcher == null) {
1✔
UNCOV
186
      logWarn(
×
187
          "Sending new DAR Collection message: Could not find researcher for specified user id: "
UNCOV
188
              + collection.getCreateUserId());
×
189
    }
190
    String researcherName = researcher == null ? "Unknown" : researcher.getDisplayName();
1✔
191
    Collection<Dac> dacsInDAR = dacDAO.findDacsForCollectionId(collectionId);
1✔
192
    List<Integer> datasetIds = collection.getDatasets().stream().map(Dataset::getDatasetId)
1✔
193
        .toList();
1✔
194
    List<Dataset> datasetsInDAR =
195
        datasetIds.isEmpty() ? List.of() : datasetDAO.findDatasetsByIdList(datasetIds);
1✔
196

197
    Map<String, List<String>> sendList = new HashMap<>();
1✔
198
    for (User user : distinctUsers) {
1✔
199
      List<Dac> matchingDacsForUser = getMatchingDacs(user, dacsInDAR);
1✔
200
      for (Dac dac : matchingDacsForUser) {
1✔
201
        List<String> matchingDatasetsForDac = getMatchingDatasets(dac, datasetsInDAR);
1✔
202
        if (matchingDatasetsForDac != null) {
1✔
203
          sendList.put(dac.getName(), matchingDatasetsForDac);
1✔
204
        }
205
      }
1✔
206
      sendNewDARRequestEmail(user, sendList, researcherName, collection.getDarCode());
1✔
207
    }
1✔
208
  }
1✔
209

210
  private List<User> getDistinctAdminAndChairUsersForCollection(DarCollection collection) {
211
    List<User> admins = userDAO.describeUsersByRoleAndEmailPreference(UserRoles.ADMIN.getRoleName(),
1✔
212
        true);
1✔
213
    List<Integer> datasetIds = collection.getDars().values().stream()
1✔
214
        .map(DataAccessRequest::getDatasetIds)
1✔
215
        .flatMap(List::stream)
1✔
216
        .collect(Collectors.toList());
1✔
217
    Set<User> chairPersons = userDAO.findUsersForDatasetsByRole(datasetIds,
1✔
218
        Collections.singletonList(UserRoles.CHAIRPERSON.getRoleName()));
1✔
219
    // Ensure that admins/chairs are not double emailed
220
    // and filter users that don't want to receive email
221
    return Streams.concat(admins.stream(), chairPersons.stream())
1✔
222
        .filter(u -> Boolean.TRUE.equals(u.getEmailPreference()))
1✔
223
        .distinct()
1✔
224
        .toList();
1✔
225
  }
226

227
  private List<Dac> getMatchingDacs(User user, Collection<Dac> dacsInDAR) {
228
    List<Integer> dacIDs = user.getRoles().stream()
1✔
229
        .filter(ur -> ur.getDacId() != null)
1✔
230
        .map(UserRole::getDacId)
1✔
231
        .toList();
1✔
232
    return dacsInDAR.stream()
1✔
233
        .filter(dac -> dacIDs.contains(dac.getDacId()))
1✔
234
        .toList();
1✔
235
  }
236

237
  private List<String> getMatchingDatasets(Dac dac, List<Dataset> datasetsInDAR) {
238
    return datasetsInDAR.stream()
1✔
239
        .filter(dataset -> dataset.getDacId() == dac.getDacId())
1✔
240
        .map(dataset -> dataset.getDatasetIdentifier())
1✔
241
        .toList();
1✔
242
  }
243

244
  private void sendNewDARRequestEmail(
245
      User user, Map<String, List<String>> sendList, String researcherName, String darCode)
246
      throws TemplateException, IOException {
247
    sendMessage(new NewDARRequestMessage(user, darCode, sendList, researcherName),
1✔
248
        user.getUserId());
1✔
249
  }
1✔
250

251
  public void sendExpirationNotices() {
252
    sendDARExpirationReminderNotices();
1✔
253
    sendDARExpirationNotices();
1✔
254
  }
1✔
255

256
  private void sendDARExpirationNotices() {
257
    EmailType emailType = EmailType.DAR_EXPIRED;
1✔
258
    sendDARMessageToList(emailType, EXPIRE_NOTICE_INTERVAL);
1✔
259
  }
1✔
260

261
  private void sendDARExpirationReminderNotices() {
262
    EmailType emailType = EmailType.DAR_EXPIRATION_REMINDER;
1✔
263
    sendDARMessageToList(emailType, EXPIRE_WARN_INTERVAL);
1✔
264
  }
1✔
265

266
  private void sendDARMessageToList(EmailType type, String interval) {
267
    List<DataAccessRequest> expiredDars = dataAccessRequestDAO.findAgedDARsByEmailTypeOlderThanInterval(
1✔
268
        type.getTypeInt(), interval, MINIMUM_SUBMITTED_DATE_FOR_DAR_EXPIRATIONS);
1✔
269
    expiredDars.forEach(expiredDar -> {
1✔
270
      try {
271
        String referenceId = expiredDar.getReferenceId();
1✔
272
        User user = userDAO.findUserById(expiredDar.getUserId());
1✔
273
        String darCode = expiredDar.getDarCode();
1✔
274
        String userName = user.getDisplayName();
1✔
275
        if (user.getEmail() == null) {
1✔
276
          // Do not throw here.  Log information about the DAR since this will continue
277
          // to appear broken until manual intervention is taken to resolve the missing user
278
          // email address
279
          logException(new Exception(String.format(
1✔
280
              "Email address for user %d (%s) not found for expiring warning.  DAR reference id: %s",
281
              expiredDar.getUserId(), userName, referenceId)));
1✔
282
        } else {
283
          switch (type) {
1✔
284
            case DAR_EXPIRATION_REMINDER:
285
              sendDarExpirationReminderMessage(user, darCode, user.getUserId(), referenceId);
1✔
286
              break;
1✔
287
            case DAR_EXPIRED:
288
              sendDarExpiredMessage(user, darCode, user.getUserId(), referenceId);
1✔
289
          }
290
        }
291
      } catch (Exception e) {
1✔
292
        logException(e);
1✔
293
      }
1✔
294
    });
1✔
295
  }
1✔
296

297
  public void sendReminderMessage(Integer voteId) throws IOException, TemplateException {
298
    Vote vote = voteDAO.findVoteById(voteId);
1✔
299
    Election election = electionDAO.findElectionWithFinalVoteById(vote.getElectionId());
1✔
300
    DarCollection collection = collectionDAO.findDARCollectionByReferenceId(
1✔
301
        election.getReferenceId());
1✔
302
    User user = findUserById(vote.getUserId());
1✔
303
    String voteUrl = serverUrl + "dar_collection/%d".formatted(collection.getDarCollectionId());
1✔
304
    sendMessage(new ReminderMessage(user, vote, collection.getDarCode(), election.getElectionType(),
1✔
305
        voteUrl), user.getUserId());
1✔
306
    voteDAO.updateVoteReminderFlag(voteId, true);
1✔
307
  }
1✔
308

309
  public void sendDarNewCollectionElectionMessage(List<User> users, DarCollection darCollection)
310
      throws IOException, TemplateException {
UNCOV
311
    String electionType = "Data Access Request";
×
UNCOV
312
    String darCode = darCollection.getDarCode();
×
UNCOV
313
    for (User user : users) {
×
UNCOV
314
      sendMessage(new NewCaseMessage(user, darCode, electionType), user.getUserId());
×
UNCOV
315
    }
×
UNCOV
316
  }
×
317

318
  public void sendResearcherDarApproved(
319
      String darCode,
320
      Integer researcherId,
321
      List<DatasetMailDTO> datasets,
322
      String dataUseRestriction)
323
      throws TemplateException, IOException {
UNCOV
324
    User user = userDAO.findUserById(researcherId);
×
UNCOV
325
    sendMessage(
×
326
        new ResearcherApprovedMessage(user, darCode, datasets, dataUseRestriction), researcherId);
UNCOV
327
  }
×
328

329
  public void sendDataCustodianApprovalMessage(
330
      User custodian,
331
      String darCode,
332
      List<DatasetMailDTO> datasets,
333
      String dataDepositorName,
334
      String researcherEmail)
335
      throws TemplateException, IOException {
UNCOV
336
    sendMessage(
×
337
        new DataCustodianApprovalMessage(
338
            custodian, darCode, datasets, dataDepositorName, researcherEmail),
UNCOV
339
        custodian.getUserId());
×
UNCOV
340
  }
×
341

342
  public void sendDatasetSubmittedMessage(
343
      User dacChair, User dataSubmitter, String dacName, String datasetName)
344
      throws TemplateException, IOException {
345
    sendMessage(
1✔
346
        new DatasetSubmittedMessage(dacChair, dataSubmitter.getDisplayName(), datasetName, dacName),
1✔
347
        dacChair.getUserId());
1✔
348
  }
1✔
349

350
  public void sendDatasetApprovedMessage(User user, String dacName, String datasetName)
351
      throws TemplateException, IOException {
UNCOV
352
    sendMessage(new DatasetApprovedMessage(user, dacName, datasetName), user.getUserId());
×
UNCOV
353
  }
×
354

355
  public void sendDatasetDeniedMessage(
356
      User user, String dacName, String datasetName, String dacEmail)
357
      throws TemplateException, IOException {
UNCOV
358
    sendMessage(new DatasetDeniedMessage(user, dacName, datasetName, dacEmail), user.getUserId());
×
UNCOV
359
  }
×
360

361
  public void sendNewResearcherMessage(User researcher, User signingOfficial)
362
      throws TemplateException, IOException {
363
    sendMessage(
1✔
364
        new NewResearcherLibraryRequestMessage(signingOfficial, researcher),
365
        researcher.getUserId());
1✔
366
  }
1✔
367

368
  public void sendDaaRequestMessage(
369
      User signingOfficial, User requestUser, String daaName, Integer daaId)
370
      throws TemplateException, IOException {
371
    sendMessage(
1✔
372
        new DaaRequestMessage(signingOfficial, requestUser, daaName, daaId),
373
        requestUser.getUserId());
1✔
374
  }
1✔
375

376
  public void sendNewDAAUploadSOMessage(
377
      User signingOfficial,
378
      String dacName,
379
      String previousDaaName,
380
      String newDaaName,
381
      Integer userId)
382
      throws TemplateException, IOException {
383
    sendMessage(
1✔
384
        new NewDAAUploadSOMessage(signingOfficial, dacName, previousDaaName, newDaaName), userId);
385
  }
1✔
386

387
  public void sendNewDAAUploadResearcherMessage(
388
      User researcher, String dacName, String previousDaaName, String newDaaName, Integer userId)
389
      throws TemplateException, IOException {
390
    sendMessage(
1✔
391
        new NewDAAUploadResearcherMessage(
392
            researcher, dacName, previousDaaName, newDaaName),
393
        userId);
394
  }
1✔
395

396
  private User findUserById(Integer id) throws IllegalArgumentException {
397
    User user = userDAO.findUserById(id);
1✔
398
    if (user == null) {
1✔
UNCOV
399
      throw new NotFoundException("Could not find dacUser for specified id : " + id);
×
400
    }
401
    return user;
1✔
402
  }
403

404
  /**
405
   * Send a message to a researcher that their data access request has expired.
406
   *
407
   * @param researcher  the researcher to send the message to
408
   * @param darCode     the data access request code that's expired
409
   * @param userId      the user id of the person sending the message
410
   * @param referenceId the data access request reference id that's expired
411
   */
412
  public void sendDarExpiredMessage(User researcher, String darCode, Integer userId,
413
      String referenceId)
414
      throws TemplateException, IOException {
415
    sendMessage(new DarExpiredMessage(researcher, darCode, referenceId), userId);
1✔
416
  }
1✔
417

418
  /**
419
   * Remind the user that their data access request is about to expire.
420
   *
421
   * @param user        the user to send the message to
422
   * @param darCode     the data access request code that's about to expire
423
   * @param userId      the user id of the person sending the message
424
   * @param referenceId the data access request reference id that is expiring
425
   */
426
  public void sendDarExpirationReminderMessage(User user, String darCode, Integer userId,
427
      String referenceId)
428
      throws TemplateException, IOException {
429
    sendMessage(new DarExpirationReminderMessage(user, darCode, referenceId), userId);
1✔
430
  }
1✔
431
}
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