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

DataBiosphere / consent / #5772

29 Apr 2025 01:12PM UTC coverage: 79.983% (+0.5%) from 79.492%
#5772

push

web-flow
DT-1542: Refactor email sending service code (#2499)

172 of 179 new or added lines in 18 files covered. (96.09%)

12 existing lines in 2 files now uncovered.

10257 of 12824 relevant lines covered (79.98%)

0.8 hits per line

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

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

3
import com.google.common.collect.Streams;
4
import com.google.inject.Inject;
5
import com.sendgrid.Response;
6
import com.sendgrid.helpers.mail.Mail;
7
import com.sendgrid.helpers.mail.objects.Content;
8
import com.sendgrid.helpers.mail.objects.Email;
9
import freemarker.template.Template;
10
import freemarker.template.TemplateException;
11
import jakarta.ws.rs.NotFoundException;
12
import java.io.IOException;
13
import java.io.StringWriter;
14
import java.io.Writer;
15
import java.time.Instant;
16
import java.util.Collection;
17
import java.util.Collections;
18
import java.util.Date;
19
import java.util.HashMap;
20
import java.util.List;
21
import java.util.Map;
22
import java.util.Objects;
23
import java.util.Set;
24
import java.util.stream.Collectors;
25
import javax.annotation.Nullable;
26
import org.broadinstitute.consent.http.configurations.ConsentConfiguration;
27
import org.broadinstitute.consent.http.db.DacDAO;
28
import org.broadinstitute.consent.http.db.DarCollectionDAO;
29
import org.broadinstitute.consent.http.db.DatasetDAO;
30
import org.broadinstitute.consent.http.db.ElectionDAO;
31
import org.broadinstitute.consent.http.db.MailMessageDAO;
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.mail.SendGridAPI;
37
import org.broadinstitute.consent.http.mail.freemarker.FreeMarkerTemplateHelper;
38
import org.broadinstitute.consent.http.mail.message.DaaRequestMessage;
39
import org.broadinstitute.consent.http.mail.message.DataCustodianApprovalMessage;
40
import org.broadinstitute.consent.http.mail.message.DatasetApprovedMessage;
41
import org.broadinstitute.consent.http.mail.message.DatasetDeniedMessage;
42
import org.broadinstitute.consent.http.mail.message.DatasetSubmittedMessage;
43
import org.broadinstitute.consent.http.mail.message.MailMessage;
44
import org.broadinstitute.consent.http.mail.message.NewCaseMessage;
45
import org.broadinstitute.consent.http.mail.message.NewDAAUploadResearcherMessage;
46
import org.broadinstitute.consent.http.mail.message.NewDAAUploadSOMessage;
47
import org.broadinstitute.consent.http.mail.message.NewDARRequestMessage;
48
import org.broadinstitute.consent.http.mail.message.NewResearcherLibraryRequestMessage;
49
import org.broadinstitute.consent.http.mail.message.ReminderMessage;
50
import org.broadinstitute.consent.http.mail.message.ResearcherApprovedMessage;
51
import org.broadinstitute.consent.http.models.Dac;
52
import org.broadinstitute.consent.http.models.DarCollection;
53
import org.broadinstitute.consent.http.models.DataAccessRequest;
54
import org.broadinstitute.consent.http.models.Dataset;
55
import org.broadinstitute.consent.http.models.Election;
56
import org.broadinstitute.consent.http.models.User;
57
import org.broadinstitute.consent.http.models.UserRole;
58
import org.broadinstitute.consent.http.models.Vote;
59
import org.broadinstitute.consent.http.models.dto.DatasetMailDTO;
60
import org.broadinstitute.consent.http.util.ConsentLogger;
61

62
public class EmailService implements ConsentLogger {
63

64
  private final DarCollectionDAO collectionDAO;
65
  private final UserDAO userDAO;
66
  private final ElectionDAO electionDAO;
67
  private final MailMessageDAO emailDAO;
68
  private final VoteDAO voteDAO;
69
  private final DatasetDAO datasetDAO;
70
  private final DacDAO dacDAO;
71
  private final FreeMarkerTemplateHelper templateHelper;
72
  private final SendGridAPI sendGridAPI;
73
  private final String fromAccount;
74
  private final String serverUrl;
75

76
  @Inject
77
  public EmailService(
78
      DarCollectionDAO collectionDAO,
79
      VoteDAO voteDAO,
80
      ElectionDAO electionDAO,
81
      UserDAO userDAO,
82
      MailMessageDAO emailDAO,
83
      DatasetDAO datasetDAO,
84
      DacDAO dacDAO,
85
      SendGridAPI sendGridAPI,
86
      FreeMarkerTemplateHelper helper,
87
      ConsentConfiguration config) {
1✔
88
    this.collectionDAO = collectionDAO;
1✔
89
    this.userDAO = userDAO;
1✔
90
    this.electionDAO = electionDAO;
1✔
91
    this.voteDAO = voteDAO;
1✔
92
    this.templateHelper = helper;
1✔
93
    this.emailDAO = emailDAO;
1✔
94
    this.datasetDAO = datasetDAO;
1✔
95
    this.dacDAO = dacDAO;
1✔
96
    this.sendGridAPI = sendGridAPI;
1✔
97
    this.serverUrl = config.getServicesConfiguration().getLocalURL();
1✔
98
    this.fromAccount = config.getMailConfiguration().getGoogleAccount();
1✔
99
  }
1✔
100

101
  /**
102
   * This method saves an email (either sent or unsent) with all available metadata from the
103
   * SendGrid response.
104
   */
105
  private void saveEmailAndResponse(
106
      @Nullable Response response,
107
      @Nullable String entityReferenceId,
108
      @Nullable Integer voteId,
109
      Integer userId,
110
      EmailType emailType,
111
      String content) {
112
    Instant now = Instant.now();
1✔
113
    Instant dateSent = (Objects.nonNull(response) && response.getStatusCode() < 400) ? now : null;
1✔
114
    emailDAO.insert(
1✔
115
        entityReferenceId,
116
        voteId,
117
        userId,
118
        emailType.getTypeInt(),
1✔
119
        dateSent,
120
        content,
121
        Objects.nonNull(response) ? response.getBody() : null,
1✔
122
        Objects.nonNull(response) ? response.getStatusCode() : null,
1✔
123
        now);
124
  }
1✔
125

126
  private void sendMessage(MailMessage mailMessage, Integer userId) throws IOException, TemplateException {
127
    Writer out = new StringWriter();
1✔
128
    Template template = templateHelper.getTemplate(mailMessage.getTemplateName());
1✔
129
    template.process(mailMessage.createModel(serverUrl), out);
1✔
130
    String content = out.toString();
1✔
131
    Mail message = new Mail(new Email(fromAccount), mailMessage.createSubject(),
1✔
132
        new Email(mailMessage.toUser.getEmail()), new Content("text/html", content));
1✔
133
    Response response = sendGridAPI.sendMessage(message, mailMessage.toUser.getEmail());
1✔
134
    saveEmailAndResponse(
1✔
135
        response,
136
        mailMessage.getEntityReferenceId(),
1✔
137
        mailMessage.getVoteId(),
1✔
138
        userId,
139
        mailMessage.emailType,
140
        content);
141
  }
1✔
142

143
  public List<org.broadinstitute.consent.http.models.mail.MailMessage> fetchEmailMessagesByType(EmailType emailType, Integer limit,
144
      Integer offset) {
145
    return emailDAO.fetchMessagesByType(emailType.getTypeInt(), limit, offset);
1✔
146
  }
147

148
  public List<org.broadinstitute.consent.http.models.mail.MailMessage> fetchEmailMessagesByCreateDate(Date start, Date end, Integer limit,
149
      Integer offset) {
150
    return emailDAO.fetchMessagesByCreateDate(start, end, limit, offset);
1✔
151
  }
152

153
  public void sendNewDARCollectionMessage(Integer collectionId)
154
      throws IOException, TemplateException {
155
    DarCollection collection = collectionDAO.findDARCollectionByCollectionId(collectionId);
1✔
156
    if (collection == null) {
1✔
157
      logWarn("Sending new DAR Collection message: Could not find collection for specified collection id: " + collectionId);
×
158
      return;
×
159
    }
160
    List<User> distinctUsers = getDistinctAdminAndChairUsersForCollection(collection);
1✔
161
    User researcher = userDAO.findUserById(collection.getCreateUserId());
1✔
162
    if (researcher == null) {
1✔
163
      logWarn("Sending new DAR Collection message: Could not find researcher for specified user id: " + collection.getCreateUserId());
×
164
    }
165
    String researcherName = researcher == null ? "Unknown" : researcher.getDisplayName();
1✔
166
    Collection<Dac> dacsInDAR = dacDAO.findDacsForCollectionId(collectionId);
1✔
167
    List<Integer> datasetIds = collection.getDatasets().stream().map(Dataset::getDatasetId).toList();
1✔
168
    List<Dataset> datasetsInDAR = datasetIds.isEmpty() ? List.of() : datasetDAO.findDatasetsByIdList(datasetIds);
1✔
169

170
    Map<String, List<String>>  sendList = new HashMap<>();
1✔
171
    for (User user : distinctUsers) {
1✔
172
      List<Dac> matchingDacsForUser = getMatchingDacs(user, dacsInDAR);
1✔
173
      for (Dac dac : matchingDacsForUser) {
1✔
174
        List<String> matchingDatasetsForDac = getMatchingDatasets(dac, datasetsInDAR);
1✔
175
        if (matchingDatasetsForDac != null) {
1✔
176
          sendList.put(dac.getName(), matchingDatasetsForDac);
1✔
177
        }
178
      }
1✔
179
      sendNewDARRequestEmail(user, sendList, researcherName, collection.getDarCode());
1✔
180
    }
1✔
181
  }
1✔
182

183
  private List<User> getDistinctAdminAndChairUsersForCollection(DarCollection collection) {
184
    List<User> admins = userDAO.describeUsersByRoleAndEmailPreference(UserRoles.ADMIN.getRoleName(),
1✔
185
        true);
1✔
186
    List<Integer> datasetIds = collection.getDars().values().stream()
1✔
187
        .map(DataAccessRequest::getDatasetIds)
1✔
188
        .flatMap(List::stream)
1✔
189
        .collect(Collectors.toList());
1✔
190
    Set<User> chairPersons = userDAO.findUsersForDatasetsByRole(datasetIds,
1✔
191
        Collections.singletonList(UserRoles.CHAIRPERSON.getRoleName()));
1✔
192
    // Ensure that admins/chairs are not double emailed
193
    // and filter users that don't want to receive email
194
    return Streams.concat(admins.stream(), chairPersons.stream())
1✔
195
        .filter(u -> Boolean.TRUE.equals(u.getEmailPreference()))
1✔
196
        .distinct()
1✔
197
        .toList();
1✔
198
  }
199

200
  private List<Dac> getMatchingDacs(User user, Collection<Dac> dacsInDAR) {
201
    List<Integer> dacIDs = user.getRoles().stream()
1✔
202
        .filter(ur -> ur.getDacId() != null)
1✔
203
        .map(UserRole::getDacId)
1✔
204
        .toList();
1✔
205
    return dacsInDAR.stream()
1✔
206
        .filter(dac -> dacIDs.contains(dac.getDacId()))
1✔
207
        .toList();
1✔
208
  }
209

210
  private List<String> getMatchingDatasets(Dac dac, List<Dataset> datasetsInDAR) {
211
    return datasetsInDAR.stream()
1✔
212
        .filter(dataset -> dataset.getDacId() == dac.getDacId())
1✔
213
        .map(dataset -> dataset.getDatasetIdentifier())
1✔
214
        .toList();
1✔
215
  }
216

217
  private void sendNewDARRequestEmail(
218
      User user, Map<String, List<String>> sendList, String researcherName, String darCode)
219
      throws TemplateException, IOException {
220
    sendMessage(new NewDARRequestMessage(user, darCode, sendList, researcherName), user.getUserId());
1✔
221
  }
1✔
222

223
  public void sendReminderMessage(Integer voteId) throws IOException, TemplateException {
224
    Vote vote = voteDAO.findVoteById(voteId);
1✔
225
    Election election = electionDAO.findElectionWithFinalVoteById(vote.getElectionId());
1✔
226
    DarCollection collection = collectionDAO.findDARCollectionByReferenceId(election.getReferenceId());
1✔
227
    User user = findUserById(vote.getUserId());
1✔
228
    String voteUrl = serverUrl + "dar_collection/%d".formatted(collection.getDarCollectionId());
1✔
229
    sendMessage(new ReminderMessage(user, vote, collection.getDarCode(), election.getElectionType(), voteUrl), user.getUserId());
1✔
230
    voteDAO.updateVoteReminderFlag(voteId, true);
1✔
231
  }
1✔
232

233
  public void sendDarNewCollectionElectionMessage(List<User> users, DarCollection darCollection)
234
      throws IOException, TemplateException {
235
    String electionType = "Data Access Request";
×
236
    String darCode = darCollection.getDarCode();
×
237
    for (User user : users) {
×
NEW
238
      sendMessage(new NewCaseMessage(user, darCode, electionType), user.getUserId());
×
UNCOV
239
    }
×
240
  }
×
241

242
  public void sendResearcherDarApproved(
243
      String darCode,
244
      Integer researcherId,
245
      List<DatasetMailDTO> datasets,
246
      String dataUseRestriction)
247
      throws Exception {
248
    User user = userDAO.findUserById(researcherId);
×
NEW
249
    sendMessage(
×
250
        new ResearcherApprovedMessage(user, darCode, datasets, dataUseRestriction), researcherId);
UNCOV
251
  }
×
252

253
  public void sendDataCustodianApprovalMessage(
254
      User custodian,
255
      String darCode,
256
      List<DatasetMailDTO> datasets,
257
      String dataDepositorName,
258
      String researcherEmail)
259
      throws Exception {
NEW
260
    sendMessage(
×
261
        new DataCustodianApprovalMessage(
262
            custodian, darCode, datasets, dataDepositorName, researcherEmail),
NEW
263
        custodian.getUserId());
×
UNCOV
264
  }
×
265

266
  public void sendDatasetSubmittedMessage(
267
      User dacChair, User dataSubmitter, String dacName, String datasetName) throws Exception {
268
    sendMessage(
1✔
269
        new DatasetSubmittedMessage(dacChair, dataSubmitter.getDisplayName(), datasetName, dacName),
1✔
270
        dacChair.getUserId());
1✔
271
  }
1✔
272

273
  public void sendDatasetApprovedMessage(User user, String dacName, String datasetName)
274
      throws Exception {
NEW
275
    sendMessage(new DatasetApprovedMessage(user, dacName, datasetName), user.getUserId());
×
UNCOV
276
  }
×
277

278
  public void sendDatasetDeniedMessage(
279
      User user, String dacName, String datasetName, String dacEmail) throws Exception {
NEW
280
    sendMessage(new DatasetDeniedMessage(user, dacName, datasetName, dacEmail), user.getUserId());
×
UNCOV
281
  }
×
282

283
  public void sendNewResearcherMessage(User researcher, User signingOfficial) throws Exception {
284
    sendMessage(new NewResearcherLibraryRequestMessage(signingOfficial, researcher), researcher.getUserId());
1✔
285
  }
1✔
286

287
  public void sendDaaRequestMessage(
288
      User signingOfficial, User requestUser, String daaName, Integer daaId) throws Exception {
289
    sendMessage(
1✔
290
        new DaaRequestMessage(signingOfficial, requestUser, daaName, daaId),
291
        requestUser.getUserId());
1✔
292
  }
1✔
293

294
  public void sendNewDAAUploadSOMessage(
295
      User signingOfficial,
296
      String dacName,
297
      String previousDaaName,
298
      String newDaaName,
299
      Integer userId)
300
      throws Exception {
301
    sendMessage(
1✔
302
        new NewDAAUploadSOMessage(signingOfficial, dacName, previousDaaName, newDaaName), userId);
303
  }
1✔
304

305
  public void sendNewDAAUploadResearcherMessage(
306
      User researcher, String dacName, String previousDaaName, String newDaaName, Integer userId)
307
      throws Exception {
308
    sendMessage(
1✔
309
        new NewDAAUploadResearcherMessage(
310
            researcher, dacName, previousDaaName, newDaaName),
311
        userId);
312
  }
1✔
313

314
  private User findUserById(Integer id) throws IllegalArgumentException {
315
    User user = userDAO.findUserById(id);
1✔
316
    if (user == null) {
1✔
317
      throw new NotFoundException("Could not find dacUser for specified id : " + id);
×
318
    }
319
    return user;
1✔
320
  }
321
}
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