Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ecdf837
Initial implementation.
otchet-broad Apr 8, 2026
d95f8a5
Add assertions to tests.
otchet-broad Apr 8, 2026
18f6400
Remove unused record class.
otchet-broad Apr 8, 2026
4fca5c5
Add coverage and update condition for empty collection, not a null value
otchet-broad Apr 8, 2026
03a4058
Add swagger docs.
otchet-broad Apr 8, 2026
07e15d9
Allow DAR/PR to submit if user doesn't have DAA and route to SO for a…
otchet-broad Apr 8, 2026
599dcbf
Update tests to cover DAR submission routing when the user doesn't ha…
otchet-broad Apr 9, 2026
c42c7ed
Spotless.
otchet-broad Apr 9, 2026
d368689
Fix argument matchers
otchet-broad Apr 9, 2026
19fe045
Add progress reports to the approval flow, make sure rules don't run …
otchet-broad Apr 9, 2026
3671147
Remove throws from method signature.
otchet-broad Apr 9, 2026
3f2018d
Refactor to use LC DAA information about the user; Add support for ca…
otchet-broad Apr 9, 2026
279c697
Increase coverage by DarCollectionServiceTest
otchet-broad Apr 9, 2026
0a1b210
Increase test coverage.
otchet-broad Apr 9, 2026
e1352ac
After refactoring method it's entirely possible to remove the todo.
otchet-broad Apr 9, 2026
cf04234
Validate two rows were inserted in SQL Batch.
otchet-broad Apr 10, 2026
5458665
Spotless.
otchet-broad Apr 10, 2026
f292f6b
Add basic indexes to table.
otchet-broad Apr 10, 2026
c26a32a
Update swagger api per feedback.
otchet-broad Apr 10, 2026
f7fc58d
Rename method to better describe what it is doing.
otchet-broad Apr 10, 2026
0f3dfcc
When converting a saved DAR to a submitted DAR, delete the old DAA re…
otchet-broad Apr 10, 2026
f7f3a94
Spotless
otchet-broad Apr 10, 2026
c0d7710
Merge branch 'develop' into otchet-dt-3063-find-daas-for-dataset-ids
otchet-broad Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/main/java/org/broadinstitute/consent/http/db/DaaDAO.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.broadinstitute.consent.http.db.mapper.DaaAuditMapper;
import org.broadinstitute.consent.http.db.mapper.DaaDatasetReducer;
import org.broadinstitute.consent.http.db.mapper.DaaMapper;
import org.broadinstitute.consent.http.db.mapper.DataAccessAgreementReducer;
import org.broadinstitute.consent.http.db.mapper.FileStorageObjectMapper;
Expand All @@ -15,6 +17,7 @@
import org.jdbi.v3.sqlobject.customizer.Bind;
import org.jdbi.v3.sqlobject.customizer.BindList;
import org.jdbi.v3.sqlobject.customizer.BindList.EmptyHandling;
import org.jdbi.v3.sqlobject.statement.SqlBatch;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import org.jdbi.v3.sqlobject.statement.UseRowReducer;
Expand Down Expand Up @@ -299,4 +302,26 @@ WHERE dataset.dataset_id IN (<datasetIds>)
Set<Integer> findDaaIdsByDatasetIds(
@BindList(value = "datasetIds", onEmpty = EmptyHandling.NULL_STRING)
List<Integer> datasetIds);

@SqlQuery(
"""
SELECT daa.daa_id, dataset.dataset_id
FROM data_access_agreement daa
INNER JOIN dac_daa ON daa.daa_id = dac_daa.daa_id
INNER JOIN dac ON dac.dac_id = dac_daa.dac_id
INNER JOIN dataset ON dataset.dac_id = dac.dac_id
WHERE dataset.dataset_id IN (<datasetIds>)
GROUP BY daa.daa_id, dataset.dataset_id
ORDER BY daa.daa_id
""")
@UseRowReducer(DaaDatasetReducer.class)
Map<Integer, Set<Integer>> mapDaaIdsToDatasetIds(
@BindList(value = "datasetIds", onEmpty = EmptyHandling.NULL_STRING) Set<Integer> datasetIds);

@SqlBatch(
"""
INSERT INTO dar_daa (dar_id, daa_id) VALUES (:darId, :daaId)
Comment thread
otchet-broad marked this conversation as resolved.
ON CONFLICT DO NOTHING
""")
void insertDarDAARelationship(@Bind("darId") Integer darId, @Bind("daaId") Set<Integer> daaIds);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot caught this for me - I was unaware of this detail. See current jdbi doc for reference.

In JDBI 3, all @SqlBatch parameters are expected to be iterable. A non-iterable scalar must be annotated with @SingleValue to be held constant per row. Without it, behavior is undefined across JDBI point-releases and the batch will fail if the set has more than one element.

Fix:

void insertDarDAARelationship(@Bind("darId") @SingleValue Integer darId,
                              @Bind("daaId") Set<Integer> daaIds);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to spend some time looking at this because I think I wrote a test that ends up proving this works. See testStoreDarDAARelationshipForPR in DaaDAOTest.java

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the method and added the @SingleValue decoration. No change in the test result. Looking at the docs, they all are using the @SingleValue decoration on Collection parameters. That's different than what I've got in the code. darId is a single integer value. I've added an assertion to the testStoreDarDAARelationshipForPR to confirm multiple rows are being inserted as expected.

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.jdbi.v3.sqlobject.customizer.BindBean;
import org.jdbi.v3.sqlobject.customizer.BindList;
import org.jdbi.v3.sqlobject.customizer.BindList.EmptyHandling;
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
import org.jdbi.v3.sqlobject.statement.SqlBatch;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
Expand Down Expand Up @@ -424,7 +425,8 @@
VALUES (:collectionId, :referenceId, :userId, :createDate,
:submissionDate, :updateDate, regexp_replace(:data, '\\\\u0000', '', 'g')::jsonb, :eraCommonsId)
""")
void insertDataAccessRequest(
@GetGeneratedKeys
Integer insertDataAccessRequest(

Check warning on line 429 in src/main/java/org/broadinstitute/consent/http/db/DataAccessRequestDAO.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Method has 8 parameters, which is greater than 7 authorized.

See more on https://sonarcloud.io/project/issues?id=DataBiosphere_consent&issues=AZ1u_W26dmmi3cCcyM5N&open=AZ1u_W26dmmi3cCcyM5N&pullRequest=2859
@Bind("collectionId") Integer collectionId,
@Bind("referenceId") String referenceId,
@Bind("userId") Integer userId,
Expand All @@ -450,7 +452,8 @@
(parent_id, collection_id, reference_id, user_id, create_date, submission_date, update_date, data, era_commons_id)
VALUES (:parentId, :collectionId, :referenceId, :userId, now(), now(), now(), regexp_replace(:data, '\\\\u0000', '', 'g')::jsonb, :eraCommonsId)
""")
void insertProgressReport(
@GetGeneratedKeys
Integer insertProgressReport(
@Bind("parentId") Integer parentId,
@Bind("collectionId") Integer collectionId,
@Bind("referenceId") String referenceId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.broadinstitute.consent.http.db.mapper;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Stream;
import org.jdbi.v3.core.result.RowReducer;
import org.jdbi.v3.core.result.RowView;

public class DaaDatasetReducer
implements RowReducer<Map<Integer, Set<Integer>>, Map.Entry<Integer, Set<Integer>>> {

@Override
public Map<Integer, Set<Integer>> container() {
return new HashMap<>();
}

@Override
public void accumulate(Map<Integer, Set<Integer>> container, RowView rowView) {
int daaId = rowView.getColumn("daa_id", Integer.class);
Integer datasetId = rowView.getColumn("dataset_id", Integer.class);
container.computeIfAbsent(daaId, k -> new HashSet<>()).add(datasetId);
}

@Override
public Stream<Entry<Integer, Set<Integer>>> stream(Map<Integer, Set<Integer>> container) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks super useful 👍🏽

return container.entrySet().stream();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,11 @@ public static DataAccessRequest populateProgressReportFromJsonString(
// object and not the original DAR.
originalDataCopy.setReferenceId(referenceId);

// We need to set the new DAAs that were in place on the DAR because the DAAs may have been
// updated
// from the original DAR.
originalDataCopy.setDaaIds(newData.getDaaIds());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just clarifying that I think we're planning on the FE to populate these values on submission. If not, these will be empty.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly. It's the next ticket I'm grabbing. :-)


newDar.setData(originalDataCopy);
return newDar;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,9 @@ public void setPiEmail(String piEmail) {
}

public Set<Integer> getDaaIds() {
if (Objects.isNull(daaIds)) {
daaIds = Collections.emptySet();
}
return daaIds;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.broadinstitute.consent.http.resources;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
import io.dropwizard.auth.Auth;
import jakarta.annotation.security.PermitAll;
Expand All @@ -20,10 +22,12 @@
import jakarta.ws.rs.core.StreamingOutput;
import jakarta.ws.rs.core.UriInfo;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import org.broadinstitute.consent.http.enumeration.UserRoles;
import org.broadinstitute.consent.http.models.Dac;
import org.broadinstitute.consent.http.models.DataAccessAgreement;
Expand Down Expand Up @@ -388,4 +392,18 @@ public Response sendNewDaaMessage(
return createExceptionResponse(e);
}
}

@POST
@RolesAllowed({ADMIN, CHAIRPERSON, MEMBER, SIGNINGOFFICIAL, RESEARCHER})
@Path("datasets")
public Response findDaaForDatasets(@Auth DuosUser duosUser, String json) {
try {
Gson gson = new Gson();
Type setType = new TypeToken<Set<Integer>>() {}.getType();
Set<Integer> set = gson.fromJson(json, setType);
return Response.ok(daaService.findDaaIdsByDatasetIds(set)).build();
} catch (Exception e) {
return createExceptionResponse(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
import java.io.InputStream;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.broadinstitute.consent.http.cloudstore.GCSService;
Expand Down Expand Up @@ -221,4 +223,8 @@ public List<DataAccessAgreement> findDAAsInJsonArray(String json, String arrayKe
public List<DataAccessAgreement> findByDarReferenceId(String referenceId) {
return daaDAO.findByDarReferenceId(referenceId);
}

public Map<Integer, Set<Integer>> findDaaIdsByDatasetIds(Set<Integer> datasetIds) {
return daaDAO.mapDaaIdsToDatasetIds(datasetIds);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1232,8 +1232,10 @@ private void approveDataAccessRequestBySigningOfficial(
dataAccessRequestDAO.updateDarApprovalSO(signingOfficial.getUserId(), dar.getReferenceId());
User researcher = userDAO.findUserById(dar.getUserId());
List<Integer> datasetIds = dar.getDatasetIds();
dacAutomationRuleService.triggerDACRuleSettings(
researcher, datasetIds, dar.getReferenceId(), request);
if (!dar.getIsCloseoutProgressReport() && !dar.getHasDMI()) {
dacAutomationRuleService.triggerDACRuleSettings(
researcher, datasetIds, dar.getReferenceId(), request);
}
}

private void validateSigningOfficialApproval(User signingOfficial, DataAccessRequest dar) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.validator.routines.EmailValidator;
import org.broadinstitute.consent.http.configurations.ConsentConfiguration;
import org.broadinstitute.consent.http.db.DAOContainer;
import org.broadinstitute.consent.http.db.DaaDAO;
import org.broadinstitute.consent.http.db.DarCollectionDAO;
import org.broadinstitute.consent.http.db.DataAccessRequestDAO;
import org.broadinstitute.consent.http.db.DatasetDAO;
Expand Down Expand Up @@ -74,6 +76,7 @@ public class DataAccessRequestService implements ConsentLogger {
private static final String LAB_STAFF = "Lab staff";
private final CounterService counterService;
private final DataAccessRequestDAO dataAccessRequestDAO;
private final DaaDAO daaDAO;
private final DarCollectionDAO darCollectionDAO;
private final ElectionDAO electionDAO;
private final InstitutionService institutionService;
Expand Down Expand Up @@ -109,6 +112,7 @@ public DataAccessRequestService(
this.matchDAO = container.getMatchDAO();
this.voteDAO = container.getVoteDAO();
this.userDAO = container.getUserDAO();
this.daaDAO = container.getDaaDAO();
this.dacService = dacService;
this.dataAccessRequestServiceDAO = dataAccessRequestServiceDAO;
this.ruleService = ruleService;
Expand Down Expand Up @@ -243,18 +247,20 @@ public DataAccessRequest createDataAccessRequest(
referenceId, user.getUserId(), now, now, darData, user.getEraCommonsId());
} else {
referenceId = UUID.randomUUID().toString();
dataAccessRequestDAO.insertDataAccessRequest(
collectionId,
referenceId,
user.getUserId(),
now,
now,
now,
darData,
user.getEraCommonsId());
Integer darId =
dataAccessRequestDAO.insertDataAccessRequest(
collectionId,
referenceId,
user.getUserId(),
now,
now,
now,
darData,
user.getEraCommonsId());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if these were both in a single transaction. I have some WITH examples that might serve as a template for this in one of my open PRs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might go with a slightly different approach to see how we like it, but it will be in one transaction.

Copy link
Copy Markdown
Contributor Author

@otchet-broad otchet-broad Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming back to this one. It looks like there were already multiple database updates in this method that should effectively unwind if there's an error here, but because of the number of elements involved, I'm wondering if we should make a ticket for this and come back to it later. Would that be acceptable? There are also other methods in this class (and related services) that need to be linked as well, hence my hesitation to add this to the scope of this ticket.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure thing - it would be nice to tighten this up separately

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DT-3176 created.

daaDAO.insertDarDAARelationship(darId, dataAccessRequest.data.getDaaIds());
}
syncDataAccessRequestDatasets(datasetIds, referenceId);
boolean requiresSOApproval = flagIfSOApprovalIsNeeded(datasetIds, referenceId);
boolean requiresSOApproval = flagIfSOApprovalIsNeeded(user, datasetIds, referenceId);
if (!requiresSOApproval) {
ruleService.triggerDACRuleSettings(user, datasetIds, referenceId, request);
}
Expand Down Expand Up @@ -287,18 +293,23 @@ public DataAccessRequest createProgressReport(
"Progress report can only be created for approved datasets in the parent DAR");
}
try {
dataAccessRequestDAO.insertProgressReport(
progressReport.getParentId(),
progressReport.getCollectionId(),
referenceId,
user.getUserId(),
progressReport.getData(),
user.getEraCommonsId());
Integer id =
dataAccessRequestDAO.insertProgressReport(
progressReport.getParentId(),
progressReport.getCollectionId(),
referenceId,
user.getUserId(),
progressReport.getData(),
user.getEraCommonsId());
if (!progressReport.getIsCloseoutProgressReport()) {
daaDAO.insertDarDAARelationship(id, progressReport.getData().getDaaIds());
}
} catch (JdbiException e) {
throw new BadRequestException(
"Unable to create progress report for Data Access Request " + parentDar.getReferenceId());
}

boolean userIsPreAuthedForDaas =
isUserPreAuthorizedForAllDaas(user, progressReport.getDatasetIds());
if (progressReport.getIsCloseoutProgressReport()) {
try {
User signingOfficialUser =
Expand All @@ -312,11 +323,15 @@ public DataAccessRequest createProgressReport(
} catch (TemplateException | IOException e) {
throw new InternalServerErrorException(e);
}
} else if (!userIsPreAuthedForDaas) {
dataAccessRequestDAO.updateRequiresSOApproval(true, referenceId);
}

syncDataAccessRequestDatasets(progressReportDatasetIds, referenceId);

if (!progressReport.getIsCloseoutProgressReport() && !progressReport.getHasDMI()) {
if (!progressReport.getIsCloseoutProgressReport()
&& !progressReport.getHasDMI()
&& userIsPreAuthedForDaas) {
ruleService.triggerDACRuleSettings(user, progressReportDatasetIds, referenceId, request);
}

Expand Down Expand Up @@ -392,7 +407,7 @@ public void validateProgressReport(
throw new BadRequestException(
"Cannot create a progress report for a draft Data Access Request");
}
if (progressReport.getDatasetIds() == null || progressReport.getDatasetIds().isEmpty()) {
if (progressReport.getDatasetIds().isEmpty()) {
throw new BadRequestException("At least one dataset is required");
}
if (!Set.copyOf(parentDar.getDatasetIds()).containsAll(progressReport.getDatasetIds())) {
Expand Down Expand Up @@ -421,6 +436,9 @@ public void validateProgressReport(
"The selected signing official in the closeout was not found.");
}
}
if (!progressReport.getIsCloseoutProgressReport() && !progressReport.getHasDMI()) {
hasAcknowledgedRequiredDaas(progressReport);
}
}

@VisibleForTesting
Expand All @@ -438,6 +456,29 @@ protected void validateCommonDarAndProgressReportElements(User user, DataAccessR
userService.validateActiveERACredentials(user);
}

@VisibleForTesting
protected void hasAcknowledgedRequiredDaas(DataAccessRequest dar) {
if (dar.getDatasetIds().isEmpty()) {
throw new BadRequestException("At least one dataset is required");
}

Set<Integer> requiredDaas = daaDAO.findDaaIdsByDatasetIds(dar.getDatasetIds());

if (!(requiredDaas.containsAll(dar.getData().getDaaIds())
Comment thread
otchet-broad marked this conversation as resolved.
&& requiredDaas.size() == dar.getData().getDaaIds().size())) {
throw new BadRequestException("All of the DAAs required were not acknowledged.");
}
}

private boolean isUserPreAuthorizedForAllDaas(User user, List<Integer> datasetIds) {
Set<Integer> datasetDaas =
daaDAO.findDaaIdsByDatasetIds(datasetIds).stream().collect(Collectors.toSet());

Set<Integer> userDaas = user.getLibraryCard().getDaaIds().stream().collect(Collectors.toSet());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any potential for NPEs here? Current code paths look clean but there are likely exception cases w're missing. Might be nice to log potential data errors here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sonar is flagging both of these set constructors as simplify-able with a HashSet initialization.


return userDaas.containsAll(datasetDaas);
}

public void validateDar(User user, DataAccessRequest dar) {
validateCommonDarAndProgressReportElements(user, dar);

Expand All @@ -450,6 +491,7 @@ public void validateDar(User user, DataAccessRequest dar) {
validateNoKeyPersonnelDuplicates(dar.getData());
validatePersonnelInstitutionAndLibraryCardRequirements(user, dar.getData());
validateCountryOfOperation(dar.getData(), false);
hasAcknowledgedRequiredDaas(dar);
}

protected void validateCountryOfOperation(DataAccessRequestData darData, boolean skipPI) {
Expand Down Expand Up @@ -736,11 +778,13 @@ public List<Election> findOpenElectionsByReferenceId(String referenceId) {
return electionDAO.findOpenElectionsByReferenceIds(List.of(referenceId));
}

private boolean flagIfSOApprovalIsNeeded(List<Integer> datasetIds, String referenceId) {
private boolean flagIfSOApprovalIsNeeded(
User user, List<Integer> datasetIds, String referenceId) {
if (!datasetDAO
.filterDatasetIdsByAutomationRuleType(
datasetIds, DACAutomationRuleType.REQUIRE_SO_DAR_APPROVAL.name())
.isEmpty()) {
.filterDatasetIdsByAutomationRuleType(
datasetIds, DACAutomationRuleType.REQUIRE_SO_DAR_APPROVAL.name())
.isEmpty()
|| !isUserPreAuthorizedForAllDaas(user, datasetIds)) {
dataAccessRequestDAO.updateRequiresSOApproval(true, referenceId);
return true;
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/assets/api-docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ paths:
$ref: 'paths/daaById.yaml'
/api/daa/{dacId}/updated/{oldDaaId}/{newDaaName}:
$ref: 'paths/daaUpdateByDacId.yaml'
/api/daa/datasets:
$ref: 'paths/daaDatasets.yaml'
/api/dac:
$ref: 'paths/dac.yaml'
/api/dac/rules:
Expand Down
Loading
Loading