diff --git a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java index 6b1a97b7f5..acfc7b4770 100644 --- a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java @@ -217,7 +217,7 @@ public PaginatedResult getProjects(final String name, final boolean excludeInact */ @Override public Project getProject(final String uuid) { - final Project project = getObjectByUuid(Project.class, uuid, Project.FetchGroup.ALL.name()); + final Project project = getObjectByUuid(Project.class, UUID.fromString(uuid), List.of(Project.FetchGroup.ALL.name())); if (project != null) { // set Metrics to minimize the number of round trips a client needs to make project.setMetrics(getMostRecentProjectMetrics(project)); diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 56d45d563e..310bd0d6c8 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -1353,15 +1353,17 @@ public PaginatedResult getTags(String policyUuid) { * @param fetchGroups Fetch groups to use for this operation * @return The object if found, otherwise {@code null} * @param Type of the object - * @throws Exception When closing the query failed * @since 4.6.0 */ - public T getObjectByUuid(final Class clazz, final UUID uuid, final List fetchGroups) throws Exception { - try (final Query query = pm.newQuery(clazz)) { - query.setFilter("uuid == :uuid"); - query.setParameters(uuid); - query.getFetchPlan().setGroups(fetchGroups); + public T getObjectByUuid(final Class clazz, final UUID uuid, final List fetchGroups) { + final Query query = pm.newQuery(clazz); + query.setFilter("uuid == :uuid"); + query.setParameters(uuid); + query.getFetchPlan().setGroups(fetchGroups); + try { return query.executeUnique(); + } finally { + query.closeAll(); } } diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index dd3229bb7a..45377fbba8 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -55,13 +55,16 @@ import javax.json.Json; import javax.json.JsonArray; import javax.json.JsonObject; +import javax.json.JsonObjectBuilder; import javax.ws.rs.HttpMethod; import javax.ws.rs.client.Entity; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.time.Duration; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -1078,4 +1081,76 @@ public void cloneProjectWithAclTest() { Assert.assertTrue(UuidUtil.isValidUUID(json.getString("token"))); } + @Test // https://github.com/DependencyTrack/dependency-track/issues/4048 + public void issue4048RegressionTest() { + final int projectsPerLevel = 10; + final int maxDepth = 5; + + final Map> projectUuidsByLevel = new HashMap<>(); + + // Create multiple parent-child hierarchies of projects. + for (int i = 0; i < maxDepth; i++) { + final List parentUuids = projectUuidsByLevel.get(i - 1); + + for (int j = 0; j < projectsPerLevel; j++) { + final UUID parentUuid = i > 0 ? parentUuids.get(j) : null; + + final JsonObjectBuilder requestBodyBuilder = Json.createObjectBuilder() + .add("name", "project-%d-%d".formatted(i, j)) + .add("version", "%d.%d".formatted(i, j)); + if (parentUuid != null) { + requestBodyBuilder.add("parent", Json.createObjectBuilder() + .add("uuid", parentUuid.toString())); + } + + final Response response = jersey.target(V1_PROJECT) + .request() + .header(X_API_KEY, apiKey) + .put(Entity.json(requestBodyBuilder.build().toString())); + assertThat(response.getStatus()).isEqualTo(201); + final JsonObject jsonResponse = parseJsonObject(response); + + projectUuidsByLevel.compute(i, (ignored, uuids) -> { + final UUID uuid = UUID.fromString(jsonResponse.getString("uuid")); + if (uuids == null) { + return new ArrayList<>(List.of(uuid)); + } + + uuids.add(uuid); + return uuids; + }); + } + } + + // Pick out the UUIDs of projects that should have a parent (i.e. level 1 or above). + final List childUuids = projectUuidsByLevel.entrySet().stream() + .filter(entry -> entry.getKey() > 0) + .map(Map.Entry::getValue) + .flatMap(List::stream) + .toList(); + + // Create a [uuid -> level] mapping for better assertion failure reporting. + final Map levelByChildUuid = projectUuidsByLevel.entrySet().stream() + .filter(entry -> entry.getKey() > 0) + .flatMap(entry -> { + final Integer level = entry.getKey(); + return entry.getValue().stream().map(uuid -> Map.entry(uuid, level)); + }) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + // Request all child projects individually. + // Ensure that the parent field is populated for all of them. + for (final UUID uuid : childUuids) { + final Response response = jersey.target(V1_PROJECT + "/" + uuid) + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + final JsonObject json = parseJsonObject(response); + assertThat(json.getJsonObject("parent")) + .withFailMessage("Parent missing on level: " + levelByChildUuid.get(uuid)) + .isNotEmpty(); + } + } + }