• 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

96.62
/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 jakarta.ws.rs.BadRequestException;
6
import jakarta.ws.rs.NotAcceptableException;
7
import jakarta.ws.rs.NotFoundException;
8
import java.sql.SQLException;
9
import java.util.Collection;
10
import java.util.Date;
11
import java.util.HashSet;
12
import java.util.List;
13
import java.util.Objects;
14
import java.util.Set;
15
import java.util.UUID;
16
import org.apache.commons.validator.routines.EmailValidator;
17
import org.broadinstitute.consent.http.db.DAOContainer;
18
import org.broadinstitute.consent.http.db.DarCollectionDAO;
19
import org.broadinstitute.consent.http.db.DataAccessRequestDAO;
20
import org.broadinstitute.consent.http.db.ElectionDAO;
21
import org.broadinstitute.consent.http.db.MatchDAO;
22
import org.broadinstitute.consent.http.db.UserDAO;
23
import org.broadinstitute.consent.http.db.VoteDAO;
24
import org.broadinstitute.consent.http.enumeration.UserRoles;
25
import org.broadinstitute.consent.http.exceptions.LibraryCardRequiredException;
26
import org.broadinstitute.consent.http.exceptions.NIHComplianceRuleException;
27
import org.broadinstitute.consent.http.exceptions.SubmittedDARCannotBeEditedException;
28
import org.broadinstitute.consent.http.models.Collaborator;
29
import org.broadinstitute.consent.http.models.DarDataset;
30
import org.broadinstitute.consent.http.models.DataAccessRequest;
31
import org.broadinstitute.consent.http.models.DataAccessRequestData;
32
import org.broadinstitute.consent.http.models.Dataset;
33
import org.broadinstitute.consent.http.models.Election;
34
import org.broadinstitute.consent.http.models.LibraryCard;
35
import org.broadinstitute.consent.http.models.User;
36
import org.broadinstitute.consent.http.service.dao.DataAccessRequestServiceDAO;
37
import org.broadinstitute.consent.http.util.ConsentLogger;
38
import org.jdbi.v3.core.statement.UnableToExecuteStatementException;
39

40
public class DataAccessRequestService implements ConsentLogger {
41
  public static final String EXPIRE_WARN_INTERVAL = "11 months";
42
  public static final String EXPIRE_NOTICE_INTERVAL = "1 year";
43

44
  private final CounterService counterService;
45
  private final DataAccessRequestDAO dataAccessRequestDAO;
46
  private final DarCollectionDAO darCollectionDAO;
47
  private final ElectionDAO electionDAO;
48
  private final MatchDAO matchDAO;
49
  private final VoteDAO voteDAO;
50
  private final UserDAO userDAO;
51
  private final UserService userService;
52
  private final DataAccessRequestServiceDAO dataAccessRequestServiceDAO;
53

54
  private final DacService dacService;
55

56
  @Inject
57
  public DataAccessRequestService(CounterService counterService, DAOContainer container,
58
      DacService dacService, DataAccessRequestServiceDAO dataAccessRequestServiceDAO, UserService userService) {
1✔
59
    this.counterService = counterService;
1✔
60
    this.dataAccessRequestDAO = container.getDataAccessRequestDAO();
1✔
61
    this.darCollectionDAO = container.getDarCollectionDAO();
1✔
62
    this.electionDAO = container.getElectionDAO();
1✔
63
    this.matchDAO = container.getMatchDAO();
1✔
64
    this.voteDAO = container.getVoteDAO();
1✔
65
    this.userDAO = container.getUserDAO();
1✔
66
    this.dacService = dacService;
1✔
67
    this.dataAccessRequestServiceDAO = dataAccessRequestServiceDAO;
1✔
68
    this.userService = userService;
1✔
69
  }
1✔
70

71
  public List<DataAccessRequest> findAllDraftDataAccessRequests() {
72
    return dataAccessRequestDAO.findAllDraftDataAccessRequests();
1✔
73
  }
74

75
  public List<DataAccessRequest> findAllDraftDataAccessRequestsByUser(Integer userId) {
76
    return dataAccessRequestDAO.findAllDraftsByUserId(userId);
1✔
77
  }
78

79
  public void deleteByReferenceId(User user, String referenceId) throws NotAcceptableException {
80
    List<Election> elections = electionDAO.findElectionsByReferenceId(referenceId);
1✔
81
    if (!elections.isEmpty()) {
1✔
82
      // If the user is an admin, delete all votes and elections
83
      if (user.hasUserRole(UserRoles.ADMIN)) {
1✔
84
        voteDAO.deleteVotesByReferenceId(referenceId);
1✔
85
        List<Integer> electionIds = elections.stream().map(Election::getElectionId).toList();
1✔
86
        electionDAO.deleteElectionsByIds(electionIds);
1✔
87
      } else {
1✔
88
        String message = String.format(
1✔
89
            "Unable to delete DAR: '%s', there are existing elections that reference it.",
90
            referenceId);
91
        logWarn(message);
1✔
92
        throw new NotAcceptableException(message);
1✔
93
      }
94
    }
95
    matchDAO.deleteRationalesByPurposeIds(List.of(referenceId));
1✔
96
    matchDAO.deleteMatchesByPurposeId(referenceId);
1✔
97
    dataAccessRequestDAO.deleteDARDatasetRelationByReferenceId(referenceId);
1✔
98
    dataAccessRequestDAO.deleteByReferenceId(referenceId);
1✔
99
  }
1✔
100

101
  public DataAccessRequest findByReferenceId(String referencedId) {
102
    DataAccessRequest dar = dataAccessRequestDAO.findByReferenceId(referencedId);
1✔
103
    if (Objects.isNull(dar)) {
1✔
UNCOV
104
      throw new NotFoundException("There does not exist a DAR with the given reference Id");
×
105
    }
106
    return dar;
1✔
107
  }
108

109
  //NOTE: rewrite method into new servicedao method on another ticket
110
  public DataAccessRequest insertDraftDataAccessRequest(User user, DataAccessRequest dar) {
111
    if (Objects.isNull(user) || Objects.isNull(dar) || Objects.isNull(
1✔
112
        dar.getReferenceId()) || Objects.isNull(dar.getData())) {
1✔
113
      throw new IllegalArgumentException("User and DataAccessRequest are required");
1✔
114
    }
115

116
    if (user.getLibraryCards().isEmpty()) {
1✔
UNCOV
117
      throw new LibraryCardRequiredException();
×
118
    }
119

120
    Date now = new Date();
1✔
121
    dataAccessRequestDAO.insertDraftDataAccessRequest(
1✔
122
        dar.getReferenceId(),
1✔
123
        user.getUserId(),
1✔
124
        now,
125
        now,
126
        now,
127
        dar.getData()
1✔
128
    );
129
    syncDataAccessRequestDatasets(dar.getDatasetIds(), dar.getReferenceId());
1✔
130

131
    return findByReferenceId(dar.getReferenceId());
1✔
132
  }
133

134
  /**
135
   * First delete any rows with the current reference id. This will allow us to keep (referenceId,
136
   * dataset_id) unique Takes in a list of datasetIds and a referenceId and adds them to the
137
   * dar_dataset collection
138
   *
139
   * @param datasetIds  List of Integers that represent the datasetIds
140
   * @param referenceId ReferenceId of the corresponding DAR
141
   */
142
  private void syncDataAccessRequestDatasets(List<Integer> datasetIds, String referenceId) {
143
    List<DarDataset> darDatasets = datasetIds.stream()
1✔
144
        .map(datasetId -> new DarDataset(referenceId, datasetId))
1✔
145
        .toList();
1✔
146
    dataAccessRequestDAO.deleteDARDatasetRelationByReferenceId(referenceId);
1✔
147

148
    if (!darDatasets.isEmpty()) {
1✔
149
      dataAccessRequestDAO.insertAllDarDatasets(darDatasets);
1✔
150
    }
151
  }
1✔
152

153
  /**
154
   * @param user User
155
   * @return List<DataAccessRequest>
156
   */
157
  public List<DataAccessRequest> getDataAccessRequestsByUserRole(User user) {
158
    List<DataAccessRequest> dars = dataAccessRequestDAO.findAllDataAccessRequests();
1✔
159
    return dacService.filterDataAccessRequestsByDac(dars, user);
1✔
160
  }
161

162
  /**
163
   * Generate a DataAccessRequest from the provided DAR. The provided DAR may or may not exist in
164
   * draft form, so it covers both cases of converting an existing draft to submitted and creating a
165
   * brand new DAR from scratch.
166
   *
167
   * @param user              The create User
168
   * @param dataAccessRequest DataAccessRequest with populated DAR data
169
   * @return The created DAR.
170
   */
171
  public DataAccessRequest createDataAccessRequest(User user, DataAccessRequest dataAccessRequest) {
172
    validateDar(user, dataAccessRequest);
1✔
173

174
    Date now = new Date();
1✔
175
    DataAccessRequestData darData = dataAccessRequest.getData();
1✔
176

177
    DataAccessRequest existingDar = dataAccessRequestDAO.findByReferenceId(
1✔
178
        dataAccessRequest.getReferenceId());
1✔
179
    if (existingDar != null && !existingDar.getDraft()) {
1✔
180
      throw new SubmittedDARCannotBeEditedException();
1✔
181
    }
182
    Integer collectionId;
183
    // Only create a new DarCollection if we haven't done so already
184
    if (Objects.nonNull(existingDar) && Objects.nonNull(existingDar.getCollectionId())) {
1✔
UNCOV
185
      collectionId = existingDar.getCollectionId();
×
186
    } else {
187
      String darCodeSequence = "DAR-" + counterService.getNextDarSequence();
1✔
188
      collectionId = darCollectionDAO.insertDarCollection(darCodeSequence, user.getUserId(), now);
1✔
189
      darData.setDarCode(darCodeSequence);
1✔
190
    }
191
    String referenceId;
192
    List<Integer> datasetIds = dataAccessRequest.getDatasetIds();
1✔
193
    if (Objects.nonNull(existingDar)) {
1✔
194
      referenceId = dataAccessRequest.getReferenceId();
1✔
195
      dataAccessRequestDAO.updateDraftToSubmittedForCollection(collectionId,
1✔
196
          referenceId);
197
      dataAccessRequestDAO.updateDataByReferenceId(
1✔
198
          referenceId,
199
          user.getUserId(),
1✔
200
          now,
201
          now,
202
          now,
203
          darData);
204
    } else {
205
      referenceId = UUID.randomUUID().toString();
1✔
206
      dataAccessRequestDAO.insertDataAccessRequest(
1✔
207
          collectionId,
208
          referenceId,
209
          user.getUserId(),
1✔
210
          now,
211
          now,
212
          now,
213
          now,
214
          darData);
215
    }
216
    syncDataAccessRequestDatasets(datasetIds, referenceId);
1✔
217
    return findByReferenceId(referenceId);
1✔
218
  }
219

220
  /**
221
   * Create a progress report for the given DataAccessRequest.
222
   * The parent DAR is just passed in for validation purposes.
223
   *
224
   * @param user              The User
225
   * @param progressReport    The DataAccessRequest
226
   * @param parentDar         The parent DataAccessRequest
227
   * @return The created progress report.
228
   */
229
  public DataAccessRequest createProgressReport(User user, DataAccessRequest progressReport, DataAccessRequest parentDar) {
230
    validateProgressReport(user, progressReport, parentDar);
1✔
231

232
    String referenceId = progressReport.getReferenceId();
1✔
233
    List<Integer> progressReportDatasetIds = progressReport.getDatasetIds();
1✔
234
    Set<Integer> darDatasetIds = dataAccessRequestDAO.findApprovedDatasetsByDar(parentDar.getReferenceId());
1✔
235
    if (!darDatasetIds.containsAll(progressReportDatasetIds)) {
1✔
236
      throw new BadRequestException("Progress report can only be created for approved datasets in the parent DAR");
1✔
237
    }
238
    dataAccessRequestDAO.insertProgressReport(
1✔
239
          Integer.valueOf(progressReport.getParentId()),
1✔
240
          progressReport.getCollectionId(),
1✔
241
          referenceId,
242
          user.getUserId(),
1✔
243
          progressReport.getData());
1✔
244
    syncDataAccessRequestDatasets(progressReportDatasetIds, referenceId);
1✔
245
    return findByReferenceId(referenceId);
1✔
246
  }
247

248
  public void validateProgressReport(User user, DataAccessRequest progressReport, DataAccessRequest parentDar) {
249
    validateDar(user, progressReport);
1✔
250
    if (parentDar.getDraft()) {
1✔
251
      throw new BadRequestException(
1✔
252
          "Cannot create a progress report for a draft Data Access Request");
253
    }
254
    if (progressReport.getDatasetIds() == null || progressReport.getDatasetIds().isEmpty() ) {
1✔
255
      throw new BadRequestException("At least one dataset is required");
1✔
256
    }
257
    if (!parentDar.getDatasetIds().containsAll(progressReport.getDatasetIds())) {
1✔
258
      throw new BadRequestException("Progress report can only be created for datasets in the parent DAR");
1✔
259
    }
260
    if (progressReport.getData().getProgressReportSummary() == null ||
1✔
261
        progressReport.getData().getProgressReportSummary().isEmpty()) {
1✔
262
      throw new BadRequestException("Progress report summary is required");
1✔
263
    }
264
    if (progressReport.getData().getIntellectualPropertySummary() == null ||
1✔
265
        progressReport.getData().getIntellectualPropertySummary().isEmpty()) {
1✔
266
      throw new BadRequestException("Intellectual Property Summary is required");
1✔
267
    }
268
  }
1✔
269

270
  public void validateDar(User user, DataAccessRequest dar) {
271
    if (Objects.isNull(user) || Objects.isNull(dar) || Objects.isNull(
1✔
272
        dar.getReferenceId()) || Objects.isNull(dar.getData())) {
1✔
273
      throw new IllegalArgumentException("User and DataAccessRequest are required");
1✔
274
    }
275

276
    if (user.getLibraryCards().isEmpty()) {
1✔
277
      throw new NIHComplianceRuleException();
1✔
278
    }
279

280
    userService.hasValidActiveERACredentials(user);
1✔
281

282
    validateInternalCollaborators(dar, user);
1✔
283
    validateNoKeyPersonnelDuplicates(dar.getData());
1✔
284
  }
1✔
285

286
  @VisibleForTesting
287
  public void validateInternalCollaborators(DataAccessRequest payload, User requestingUser) {
288
    Integer institution = requestingUser.getInstitutionId();
1✔
289
    List<Collaborator> internalCollaborators = payload.getData().getInternalCollaborators();
1✔
290
    for (Collaborator collaborator : internalCollaborators) {
1✔
291
      User collabUser = userDAO.findUserByEmail(collaborator.getEmail());
1✔
292
      if (collabUser == null) {
1✔
293
        throw new NotFoundException(
1✔
294
            "Unable to find User with the provided email: " + collaborator.getEmail());
1✔
295
      }
296
      if (!Objects.equals(collabUser.getInstitutionId(), institution)) {
1✔
297
        throw new BadRequestException(
1✔
298
            "Collaborator " + collaborator.getEmail() + " is not part of the same institution, "
1✔
299
                + requestingUser.getInstitution().getName());
1✔
300
      }
301
      List<LibraryCard> libraryCards = collabUser.getLibraryCards();
1✔
302
      if (libraryCards.isEmpty()) {
1✔
303
        throw new BadRequestException(
1✔
304
            "Collaborator " + collaborator.getEmail() + " does not have a library card.");
1✔
305
      }
306
    }
1✔
307
  }
1✔
308

309
  /**
310
   * Update an existing DataAccessRequest. Replaces DataAccessRequestData.
311
   *
312
   * @param user The User
313
   * @param dar  The DataAccessRequest
314
   * @return The updated DataAccessRequest
315
   */
316
  public DataAccessRequest updateByReferenceId(User user, DataAccessRequest dar) {
317
    if (!dar.getDraft()) {
1✔
318
      throw new SubmittedDARCannotBeEditedException();
1✔
319
    }
320
    try {
321
      return dataAccessRequestServiceDAO.updateByReferenceId(user, dar);
1✔
UNCOV
322
    } catch (SQLException e) {
×
323
      // If I simply rethrow the error then I'll have to redefine any method that
324
      // calls this function to "throw SQLException"
325
      //Instead I'm going to throw an UnableToExecuteStatementException
326
      //Response class will catch it, log it, and throw a 500 through the "unableToExecuteExceptionHandler"
327
      //on the Resource class, just like it would with a SQLException
UNCOV
328
      throw new UnableToExecuteStatementException(e.getMessage());
×
329
    }
330
  }
331

332
  /**
333
   * Validates that PI email is not duplicated with SO or IT Director emails
334
   *
335
   * @param darData The data access request data to validate
336
   * @throws IllegalArgumentException if duplicate emails are found
337
   */
338
  public void validateNoKeyPersonnelDuplicates(DataAccessRequestData darData) {
339
    EmailValidator emailValidator = EmailValidator.getInstance();
1✔
340

341
    String piEmail = darData.getPiEmail();
1✔
342
    String soEmail = darData.getSigningOfficialEmail();
1✔
343
    String itEmail = darData.getItDirectorEmail();
1✔
344

345
    if (!emailValidator.isValid(piEmail) || !emailValidator.isValid(soEmail)
1✔
346
        || !emailValidator.isValid(itEmail)) {
1✔
347
      throw new IllegalArgumentException(
1✔
348
          "Principal Investigator, Signing Official, and IT Director emails must be valid");
349
    }
350

351
    if (piEmail.equalsIgnoreCase(soEmail)) {
1✔
352
      throw new IllegalArgumentException(
1✔
353
          "Principal Investigator email cannot be the same as Signing Official email");
354
    }
355

356
    if (piEmail.equalsIgnoreCase(itEmail)) {
1✔
357
      throw new IllegalArgumentException(
1✔
358
          "Principal Investigator email cannot be the same as IT Director email");
359
    }
360
  }
1✔
361

362
  public Collection<DataAccessRequest> getApprovedDARsForDataset(Dataset dataset) {
363
    return dataAccessRequestDAO.findApprovedDARsByDatasetId(dataset.getDatasetId());
1✔
364
  }
365

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