Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -516,9 +516,16 @@ public Project updateProject(Project transientProject, boolean commitIndex) {
final Project project = getObjectByUuid(Project.class, transientProject.getUuid());

Project oldParent = project.getParent();
// Resolve the new parent from DB before making scheduling decisions.
// transientProject.getParent() is a stub from the API request with only UUID populated,
// so getCollectionLogic() on it always returns NONE.
Project resolvedNewParent = null;
if (transientProject.getParent() != null && transientProject.getParent().getUuid() != null) {
resolvedNewParent = getObjectByUuid(Project.class, transientProject.getParent().getUuid());
}
boolean scheduleProjectMetricsUpdate = this.needScheduleProjectMetricsUpdate(project, transientProject);
boolean scheduleParentMetricsUpdate = this.needScheduleParentMetricsUpdate(transientProject, scheduleProjectMetricsUpdate);
boolean scheduleOldParentMetricsUpdate = this.needScheduleOldParentMetricsUpdate(oldParent, transientProject);
boolean scheduleParentMetricsUpdate = this.needScheduleParentMetricsUpdate(resolvedNewParent, scheduleProjectMetricsUpdate);
boolean scheduleOldParentMetricsUpdate = this.needScheduleOldParentMetricsUpdate(oldParent, resolvedNewParent);

project.setAuthors(transientProject.getAuthors());
project.setPublisher(transientProject.getPublisher());
Expand Down Expand Up @@ -557,20 +564,19 @@ public Project updateProject(Project transientProject, boolean commitIndex) {
}
project.setIsLatest(transientProject.isLatest());

if (transientProject.getParent() != null && transientProject.getParent().getUuid() != null) {
if (project.getUuid().equals(transientProject.getParent().getUuid())){
if (resolvedNewParent != null) {
if (project.getUuid().equals(resolvedNewParent.getUuid())){
throw new IllegalArgumentException("A project cannot select itself as a parent");
}
Project parent = getObjectByUuid(Project.class, transientProject.getParent().getUuid());
if (!Boolean.TRUE.equals(parent.isActive())){
if (!Boolean.TRUE.equals(resolvedNewParent.isActive())){
throw new IllegalArgumentException("An inactive project cannot be selected as a parent");
} else if (isChildOf(parent, transientProject.getUuid())){
} else if (isChildOf(resolvedNewParent, transientProject.getUuid())){
throw new IllegalArgumentException("The new parent project cannot be a child of the current project.");
} else {
project.setParent(parent);
project.setParent(resolvedNewParent);
}
project.setParent(parent);
}else {
project.setParent(resolvedNewParent);
} else {
project.setParent(null);
}

Expand Down Expand Up @@ -621,17 +627,17 @@ private boolean needScheduleProjectMetricsUpdate(Project project, Project transi
/**
* if parent is collection schedule an update, unless this project itself is scheduled already since that will trigger a parent update, too
*/
private boolean needScheduleParentMetricsUpdate(Project transientProject, boolean scheduleProjectMetricsUpdate) {
private boolean needScheduleParentMetricsUpdate(Project newParent, boolean scheduleProjectMetricsUpdate) {
return !scheduleProjectMetricsUpdate
&& transientProject.getParent() != null
&& transientProject.getParent().getCollectionLogic() != ProjectCollectionLogic.NONE;
&& newParent != null
&& newParent.getCollectionLogic() != ProjectCollectionLogic.NONE;
}

/**
* if project gets a new parent and old parent was collection, we need to update old parent's metrics
*/
private boolean needScheduleOldParentMetricsUpdate(Project oldParent, Project transientProject) {
return oldParent != transientProject.getParent()
private boolean needScheduleOldParentMetricsUpdate(Project oldParent, Project newParent) {
return oldParent != newParent
&& oldParent != null
&& oldParent.getCollectionLogic() != ProjectCollectionLogic.NONE;
}
Expand Down Expand Up @@ -949,7 +955,17 @@ private static Set<UUID> parseDirectDependenciesUuids(
*/
@Override
public void recursivelyDelete(final Project project, final boolean commitIndex) {
Project parent = project.getParent();
// Reload the parent with collectionLogic eagerly fetched before any deletions occur.
// collectionLogic is not in the default fetch group, so accessing it after the PM
// state is modified by cascading deletes risks a silent lazy-load failure (returning NONE).
final Project parent;
if (project.getParent() != null) {
try (var ignored = new ScopedCustomization(pm).withFetchGroup(Project.FetchGroup.METRICS_UPDATE.name())) {
parent = pm.getObjectById(Project.class, project.getParent().getId());
}
} else {
parent = null;
}

if (project.getChildren() != null) {
for (final Project child: project.getChildren()) {
Expand Down Expand Up @@ -1019,6 +1035,17 @@ public void deleteProjectsByUUIDs(Collection<UUID> uuids) {
throw ProjectOperationException.forDeletion(errorByUUID);
}

// Collect parent collection projects that need metrics updates after deletion.
// Exclude parents that are themselves being deleted.
final Set<UUID> deleteUuids = uuids instanceof Set ? (Set<UUID>) uuids : Set.copyOf(uuids);
final Set<UUID> collectionParentUuids = projects.stream()
.map(Project::getParent)
.filter(parent -> parent != null
&& parent.getCollectionLogic() != ProjectCollectionLogic.NONE
&& !deleteUuids.contains(parent.getUuid()))
.map(Project::getUuid)
.collect(Collectors.toSet());

Long[] projectIDsArray = accessibleProjectIds.toArray(Long[]::new);
String commaSeparatedProjectIDs = accessibleProjectIds.stream().map(String::valueOf).collect(Collectors.joining(","));
var queryParameter = DbUtil.isMssql() ? commaSeparatedProjectIDs : projectIDsArray;
Expand Down Expand Up @@ -1325,6 +1352,10 @@ WHERE PROJECT.ID IN (SELECT value FROM STRING_SPLIT(?, ','))
executeAndCloseWithArray(sqlQuery, queryParameter);
}
});

for (final UUID parentUuid : collectionParentUuids) {
Event.dispatch(new ProjectMetricsUpdateEvent(parentUuid));
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@

import java.util.Date;
import java.util.List;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.Mockito.times;

Expand Down Expand Up @@ -132,4 +134,62 @@ public void testCloneProjectMetricUpdate() throws Exception {
}
}

@Test
void testRecursivelyDeleteDispatchesMetricsUpdateForCollectionParent() {
final Project collectionParent = qm.createProject("Collection", null, "1.0", null, null, null, true, false);
final Project detachedParent = qm.detach(Project.class, collectionParent.getId());
detachedParent.setCollectionLogic(ProjectCollectionLogic.AGGREGATE_DIRECT_CHILDREN);
qm.updateProject(detachedParent, false);

final Project child = qm.createProject("Child", null, "1.0", null, collectionParent, null, true, false);

try (MockedStatic<Event> mockedEvent = Mockito.mockStatic(Event.class)) {
qm.recursivelyDelete(child, true);

final ArgumentCaptor<Event> eventCaptor = ArgumentCaptor.forClass(Event.class);
mockedEvent.verify(() -> Event.dispatch(eventCaptor.capture()), Mockito.atLeastOnce());

final List<UUID> dispatchedUuids = eventCaptor.getAllValues().stream()
.filter(e -> e instanceof ProjectMetricsUpdateEvent)
.map(e -> ((ProjectMetricsUpdateEvent) e).getUuid())
.toList();
assertThat(dispatchedUuids).contains(collectionParent.getUuid());
}
}

@Test
void testDeleteProjectsByUUIDsDispatchesMetricsUpdateForCollectionParent() {
final Project collectionParent = qm.createProject("Collection", null, "1.0", null, null, null, true, false);
final Project detachedParent = qm.detach(Project.class, collectionParent.getId());
detachedParent.setCollectionLogic(ProjectCollectionLogic.AGGREGATE_DIRECT_CHILDREN);
qm.updateProject(detachedParent, false);

final Project child = qm.createProject("Child", null, "1.0", null, collectionParent, null, true, false);

try (MockedStatic<Event> mockedEvent = Mockito.mockStatic(Event.class)) {
qm.deleteProjectsByUUIDs(List.of(child.getUuid()));

final ArgumentCaptor<ProjectMetricsUpdateEvent> eventCaptor =
ArgumentCaptor.forClass(ProjectMetricsUpdateEvent.class);
mockedEvent.verify(() -> Event.dispatch(eventCaptor.capture()), times(1));

assertThat(eventCaptor.getValue().getUuid()).isEqualTo(collectionParent.getUuid());
}
}

@Test
void testDeleteProjectsByUUIDsDoesNotDispatchMetricsUpdateWhenParentAlsoDeleted() {
final Project collectionParent = qm.createProject("Collection", null, "1.0", null, null, null, true, false);
final Project detachedParent = qm.detach(Project.class, collectionParent.getId());
detachedParent.setCollectionLogic(ProjectCollectionLogic.AGGREGATE_DIRECT_CHILDREN);
qm.updateProject(detachedParent, false);

final Project child = qm.createProject("Child", null, "1.0", null, collectionParent, null, true, false);

try (MockedStatic<Event> mockedEvent = Mockito.mockStatic(Event.class)) {
qm.deleteProjectsByUUIDs(List.of(collectionParent.getUuid(), child.getUuid()));

mockedEvent.verify(() -> Event.dispatch(Mockito.any(ProjectMetricsUpdateEvent.class)), times(0));
}
}
}
Loading