From a0b9bc5e2e8a6dc2405db3551d8e132e52aade1b Mon Sep 17 00:00:00 2001 From: veltrup Date: Fri, 23 Sep 2022 08:14:00 +0200 Subject: [PATCH] feat: BulkPurge --- pom.xml | 6 + .../core/domain/entity/BulkOperationKey.java | 19 ++ .../domain/entity/EntityBulkExecution.java | 87 +++++++ .../domain/entity/EntityBulkOperation.java | 77 ++++++ .../core/domain/entity/EntityTree.java | 163 +++++++++++++ .../core/domain/entity/GroupTree.java | 80 ------ .../core/domain/entity/filter/Filter.java | 24 +- .../core/domain/entity/query/Query.java | 12 +- .../domain/entity/query/SubTreeQuery.java | 70 ++++++ .../core/domain/exception/AccessDenied.java | 5 + .../exception/ContentRepositoryException.java | 4 + .../core/port/ContentRepository.java | 2 - .../core/port/EntityBulkExecutor.java | 7 + .../core/port/ExtensionsNotifier.java | 5 - .../core/usecase/BulkPurge.java | 230 +++++++++++++----- .../core/usecase/BulkPurgeInput.java | 56 ++++- .../core/usecase/BulkPurgeOutput.java | 5 - .../core/usecase/GetAllEntities.java | 2 - .../core/usecase/PurgeEntity.java | 18 +- src/main/java/module-info.java | 1 + .../core/domain/entity/EntityTreeTest.java | 28 +++ .../core/domain/entity/query/QueryTest.java | 17 ++ .../domain/entity/query/SubTreeQueryTest.java | 19 ++ .../core/usecase/PurgeEntityTest.java | 12 +- 24 files changed, 746 insertions(+), 203 deletions(-) create mode 100644 src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/BulkOperationKey.java create mode 100644 src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/EntityBulkExecution.java create mode 100644 src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/EntityBulkOperation.java create mode 100644 src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/EntityTree.java delete mode 100644 src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/GroupTree.java create mode 100644 src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/query/SubTreeQuery.java create mode 100644 src/main/java/com/sitepark/ies/contentrepository/core/port/EntityBulkExecutor.java delete mode 100644 src/main/java/com/sitepark/ies/contentrepository/core/usecase/BulkPurgeOutput.java create mode 100644 src/test/java/com/sitepark/ies/contentrepository/core/domain/entity/EntityTreeTest.java create mode 100644 src/test/java/com/sitepark/ies/contentrepository/core/domain/entity/query/QueryTest.java create mode 100644 src/test/java/com/sitepark/ies/contentrepository/core/domain/entity/query/SubTreeQueryTest.java diff --git a/pom.xml b/pom.xml index 5f5cf5b..ebcf4c2 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,12 @@ 1 + + org.apache.logging.log4j + log4j-api + 2.17.2 + + com.fasterxml.jackson.core jackson-databind diff --git a/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/BulkOperationKey.java b/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/BulkOperationKey.java new file mode 100644 index 0000000..9e9fdf2 --- /dev/null +++ b/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/BulkOperationKey.java @@ -0,0 +1,19 @@ +package com.sitepark.ies.contentrepository.core.domain.entity; + +public enum BulkOperationKey { + + PURGE_LOCK("contentrepository.purge.lock"), + PURGE_DEPUBLISH("contentrepository.purge.depublish"), + PURGE_PURGE("contentrepository.purge.purge"), + PURGE_CLEANUP("contentrepository.purge.cleanup"); + + private final String name; + + private BulkOperationKey(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } +} diff --git a/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/EntityBulkExecution.java b/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/EntityBulkExecution.java new file mode 100644 index 0000000..b2b14c1 --- /dev/null +++ b/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/EntityBulkExecution.java @@ -0,0 +1,87 @@ +package com.sitepark.ies.contentrepository.core.domain.entity; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +public class EntityBulkExecution { + + private final String[] topic; + + private final List operations; + + private final EntityBulkOperation finalizer; + + protected EntityBulkExecution(Builder builder) { + this.topic = builder.topic; + this.operations = Collections.unmodifiableList(builder.operations); + this.finalizer = builder.finalizer; + } + + @SuppressFBWarnings("EI_EXPOSE_REP") + public String[] getTopic() { + return this.topic.clone(); + } + + @SuppressFBWarnings("EI_EXPOSE_REP") + public List getOperations() { + return this.operations; + } + + public Optional getFinalizer() { + return Optional.ofNullable(this.finalizer); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String[] topic; + + private final List operations = new ArrayList<>(); + + private EntityBulkOperation finalizer; + + private Builder() { + } + + /** + * Topics are used to display all bulk operations for a specific topic. + * Topics are hierarchical and the path of the topic is specified via + * a string array. Topics are freely definable. + * If e.g. all Topics of level1 are queried, all + * BulkExecutions recursively below level1 are returned. + */ + public Builder topic(String... topic) { + assert topic != null; + this.topic = topic.clone(); + return this; + } + + public Builder operation(EntityBulkOperation... operations) { + assert operations != null; + for (EntityBulkOperation operation : operations) { + assert operation != null; + this.operations.add(operation); + } + return this; + } + + public Builder finalizer(EntityBulkOperation finalizer) { + assert finalizer != null; + this.finalizer = finalizer; + return this; + } + + public EntityBulkExecution build() { + assert this.topic != null; + assert !this.operations.isEmpty(); + return new EntityBulkExecution(this); + } + } +} diff --git a/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/EntityBulkOperation.java b/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/EntityBulkOperation.java new file mode 100644 index 0000000..7dba422 --- /dev/null +++ b/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/EntityBulkOperation.java @@ -0,0 +1,77 @@ +package com.sitepark.ies.contentrepository.core.domain.entity; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +public class EntityBulkOperation { + + private final BulkOperationKey key; + + private final List entityList; + + private final Consumer consumer; + + protected EntityBulkOperation(Builder builder) { + this.key = builder.key; + this.entityList = Collections.unmodifiableList(builder.entityList); + this.consumer = builder.consumer; + } + + public BulkOperationKey getKey() { + return this.key; + } + + @SuppressFBWarnings("EI_EXPOSE_REP") + public List getEntityList() { + return this.entityList; + } + + public Consumer getConsumer() { + return this.consumer; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private BulkOperationKey key; + + private final List entityList = new ArrayList<>(); + + private Consumer consumer; + + private Builder() { + } + + public Builder key(BulkOperationKey key) { + assert key != null; + this.key = key; + return this; + } + + public Builder entityList(List entityList) { + assert entityList != null; + this.entityList.addAll(entityList); + return this; + } + + public Builder consumer(Consumer consumer) { + assert consumer != null; + this.consumer = consumer; + return this; + } + + public EntityBulkOperation build() { + assert this.key != null; + assert !this.entityList.isEmpty(); + assert this.consumer != null; + return new EntityBulkOperation(this); + } + } +} diff --git a/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/EntityTree.java b/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/EntityTree.java new file mode 100644 index 0000000..df5cee4 --- /dev/null +++ b/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/EntityTree.java @@ -0,0 +1,163 @@ +package com.sitepark.ies.contentrepository.core.domain.entity; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + + +@SuppressWarnings("PMD.UseConcurrentHashMap") +public class EntityTree { + + private final Map> children = new HashMap<>(); + + private final Map parents = new HashMap<>(); + + private final Map index = new HashMap<>(); + + /** + * Returns all roots of the tree. + * + * Roots are all entries without a parent + */ + public List getRoots() { + return this.getRootIdList().stream() + .map(id -> this.index.get(id)) + .collect(Collectors.toList()); + } + + private Set getRootIdList() { + Set knownRoots = this.children.get(null); + Set roots = new HashSet<>(); + if (knownRoots != null) { + roots.addAll(knownRoots); + } + for (Long parent : this.parents.values()) { + if (parent != null && !this.parents.containsKey(parent)) { + roots.add(parent); + } + } + return roots; + } + + /** + * Places an element in the tree. + * + * If no parent present, it is set as root element. + */ + public void add(Entity entity) { + + if (entity == null) { + return; + } + + this.parents.put(entity.getId().get(), entity.getParent().orElse(null)); + Set siblings = this.children.get(entity.getParent().orElse(null)); + if (siblings == null) { + siblings = new HashSet<>(); + this.children.put(entity.getParent().orElse(null), siblings); + } + siblings.add(entity.getId().get()); + this.index.put(entity.getId().get(), entity); + } + + public Entity get(Long id) { + return this.index.get(id); + } + + public List getChildren(Long parent) { + Set children = this.children.get(parent); + return children.stream() + .map(id -> this.index.get(id)) + .collect(Collectors.toList()); + } + + /** + * Liefert die Unterelemente des Parent rekusive. + */ + public List getChildrenRecursive(Long root) { + Set children = this.getChildrenIdListRecursive(root); + return children.stream() + .map(id -> this.index.get(id)) + .collect(Collectors.toList()); + } + + private Set getChildrenIdListRecursive(Long root) { + Set list = new LinkedHashSet(); + Set children = this.children.get(root); + if (children != null) { + list.addAll(children); + for (Long id : children) { + Set grandChildren = this.getChildrenIdListRecursive(id); + list.addAll(grandChildren); + } + } + + return list; + } + + /** + * Returns all elements of the tree in hierarchical order + */ + public List getAll() { + Set rootIdList = this.getRootIdList(); + List list = new ArrayList<>(); + for (Long rootId : rootIdList) { + Entity root = this.index.get(rootId); + if (root != null) { + list.add(root); + } + List childrenOrRoot = this.getChildrenRecursive(rootId); + list.addAll(childrenOrRoot); + } + return list; + } + + public boolean hasChildren(Long parent) { + return this.children.containsKey(parent); + } + + @Override + public String toString() { + return this.toString(0); + } + + public String toString(int indent) { + return this.toString(indent, null); + } + + public String toString(int indent, Long parent) { + java.lang.StringBuilder b = new java.lang.StringBuilder(); + this.toString(indent, parent, new java.lang.StringBuilder(), b); + return b.toString(); + } + + private void toString(int indent, Long parent, java.lang.StringBuilder indentPrefix, + java.lang.StringBuilder b) { + + if (parent == null) { + Set roots = this.getRootIdList(); + for (Long child : roots) { + this.toString(indent, child, indentPrefix, b); + } + } else { + b.append(indentPrefix.toString()); + b.append(this.index.get(parent)); + b.append('\n'); + + if (this.hasChildren(parent)) { + for (int i = 0; i < indent; i++) { + indentPrefix.append(' '); + } + for (Long child : this.children.get(parent)) { + this.toString(indent, child, indentPrefix, b); + } + indentPrefix.delete(indentPrefix.length() - indent, indentPrefix.length()); + } + } + } +} diff --git a/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/GroupTree.java b/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/GroupTree.java deleted file mode 100644 index 27efca2..0000000 --- a/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/GroupTree.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.sitepark.ies.contentrepository.core.domain.entity; - -import java.util.List; -import java.util.Set; - -/** - * The hierarchy of groups in the content repository - */ -public interface GroupTree { - - /** - * Returns the root elements of the tree - */ - Set getRoots(); - - /** - * Creates a sub-tree starting from the branch specified with root. - * @param root Node from where the sub-tree should be created. - */ - GroupTree getSubTree(Long node); - - /** - * Liefert true, wenn Object schon enthalten ist. - */ - boolean contains(Long entity); - - /** - * Returns true if Object is already contained. - */ - boolean isEmpty(); - - /** - * Provides the parent elements of the child - */ - Long getParent(Long entity); - - /** - * Returns the subelements of the parent - */ - Set getChildren(Long parent); - - /** - * Returns the subelements of the parent rekusive. - */ - Set getChildrenRecursive(Long root); - - /** - * Returns true if the parent has subelement. - */ - boolean hasChildren(Long parent); - - /** - * Returns a list of all elements that have the same - * parent including the passed object. - */ - Set getSiblings(Long o); - - /** - * Returns all objects contained in this tree - */ - Set getAll(); - - /** - * Returns the path of an object from the root element to the specified element. - * @param o - */ - List getPath(Entity o); - - /** - * Number of elements in the tree - */ - int size(); - - @Override String toString(); - - String toString(int indent); - - String toString(int indent, Long parent); - -} diff --git a/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/filter/Filter.java b/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/filter/Filter.java index d9b044c..66ad68a 100644 --- a/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/filter/Filter.java +++ b/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/filter/Filter.java @@ -3,16 +3,8 @@ @SuppressWarnings("PMD.TooManyMethods") public interface Filter { - public static Or or(Filter... filterList) { - return new Or(filterList); - } - - public static And and(Filter... filterList) { - return new And(filterList); - } - - public static Not not(Filter filter) { - return new Not(filter); + public static IsGroup isGroup(Boolean isGroup) { + return new IsGroup(isGroup); } public static Id id(Long id) { @@ -46,4 +38,16 @@ public static Root root(Long root) { public static RootList rootList(Long... rootList) { return new RootList(rootList); } + + public static Or or(Filter... filterList) { + return new Or(filterList); + } + + public static And and(Filter... filterList) { + return new And(filterList); + } + + public static Not not(Filter filter) { + return new Not(filter); + } } diff --git a/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/query/Query.java b/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/query/Query.java index ede1202..5d5ccb1 100644 --- a/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/query/Query.java +++ b/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/query/Query.java @@ -1,5 +1,7 @@ package com.sitepark.ies.contentrepository.core.domain.entity.query; +import java.util.Optional; + import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import com.sitepark.ies.contentrepository.core.domain.entity.filter.Filter; @@ -17,8 +19,8 @@ protected Query(Builder builder) { this.orderBy = builder.orderBy; } - public Filter getFilterBy() { - return this.filterBy; + public Optional getFilterBy() { + return Optional.ofNullable(this.filterBy); } public OrderBy getOrderBy() { @@ -46,13 +48,12 @@ protected Builder(Query query) { this.orderBy = query.orderBy; } - public Builder filterBy(Filter filterBy) { - assert filterBy != null; + public B filterBy(Filter filterBy) { this.filterBy = filterBy; return this.self(); } - public Builder orderBy(OrderBy orderBy) { + public B orderBy(OrderBy orderBy) { assert orderBy != null; this.orderBy = orderBy; return this.self(); @@ -61,7 +62,6 @@ public Builder orderBy(OrderBy orderBy) { public abstract B self(); public abstract Query build(); - } @JsonPOJOBuilder(withPrefix = "", buildMethodName = "build") diff --git a/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/query/SubTreeQuery.java b/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/query/SubTreeQuery.java new file mode 100644 index 0000000..33b6d28 --- /dev/null +++ b/src/main/java/com/sitepark/ies/contentrepository/core/domain/entity/query/SubTreeQuery.java @@ -0,0 +1,70 @@ +package com.sitepark.ies.contentrepository.core.domain.entity.query; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +public class SubTreeQuery extends Query { + + private final List rootList; + + protected SubTreeQuery(Builder builder) { + super(builder); + this.rootList = Collections.unmodifiableList(builder.rootList); + } + + @SuppressFBWarnings("EI_EXPOSE_REP") + public List getRootList() { + return this.rootList; + } + + public static Builder builder() { + return new Builder(); + } + + @Override public Builder toBuilder() { + return new Builder(this); + } + + @JsonPOJOBuilder(withPrefix = "", buildMethodName = "build") + public static class Builder extends Query.Builder { + + private final List rootList = new ArrayList<>(); + + private Builder() { } + + private Builder(SubTreeQuery curserBasedQuery) { + super(curserBasedQuery); + this.rootList.addAll(curserBasedQuery.rootList); + } + + public Builder root(Long root) { + assert root != null; + this.rootList.add(root); + return this.self(); + } + + public Builder rootList(List rootList) { + assert rootList != null; + for (Long root : rootList) { + this.root(root); + } + return this.self(); + } + + @Override + public SubTreeQuery build() { + assert !this.rootList.isEmpty() : "rootList is empty"; + return new SubTreeQuery(this); + } + + @Override + public Builder self() { + return this; + } + } +} diff --git a/src/main/java/com/sitepark/ies/contentrepository/core/domain/exception/AccessDenied.java b/src/main/java/com/sitepark/ies/contentrepository/core/domain/exception/AccessDenied.java index d959bcd..d6c00eb 100644 --- a/src/main/java/com/sitepark/ies/contentrepository/core/domain/exception/AccessDenied.java +++ b/src/main/java/com/sitepark/ies/contentrepository/core/domain/exception/AccessDenied.java @@ -6,4 +6,9 @@ public class AccessDenied extends ContentRepositoryException { public AccessDenied(String message) { super(message); } + + public AccessDenied(String message, Throwable t) { + super(message, t); + } + } \ No newline at end of file diff --git a/src/main/java/com/sitepark/ies/contentrepository/core/domain/exception/ContentRepositoryException.java b/src/main/java/com/sitepark/ies/contentrepository/core/domain/exception/ContentRepositoryException.java index 1c25fe2..a40cc82 100644 --- a/src/main/java/com/sitepark/ies/contentrepository/core/domain/exception/ContentRepositoryException.java +++ b/src/main/java/com/sitepark/ies/contentrepository/core/domain/exception/ContentRepositoryException.java @@ -10,4 +10,8 @@ public ContentRepositoryException() { public ContentRepositoryException(String message) { super(message); } + + public ContentRepositoryException(String message, Throwable t) { + super(message, t); + } } diff --git a/src/main/java/com/sitepark/ies/contentrepository/core/port/ContentRepository.java b/src/main/java/com/sitepark/ies/contentrepository/core/port/ContentRepository.java index 57439d9..2685370 100644 --- a/src/main/java/com/sitepark/ies/contentrepository/core/port/ContentRepository.java +++ b/src/main/java/com/sitepark/ies/contentrepository/core/port/ContentRepository.java @@ -5,7 +5,6 @@ import com.sitepark.ies.contentrepository.core.domain.entity.Anchor; import com.sitepark.ies.contentrepository.core.domain.entity.Entity; -import com.sitepark.ies.contentrepository.core.domain.entity.GroupTree; import com.sitepark.ies.contentrepository.core.domain.entity.query.Query; public interface ContentRepository { @@ -16,5 +15,4 @@ public interface ContentRepository { void removeEntity(long id); Optional resolveAnchor(Anchor anchor); List getAll(Query query); - GroupTree getGroupTree(); } diff --git a/src/main/java/com/sitepark/ies/contentrepository/core/port/EntityBulkExecutor.java b/src/main/java/com/sitepark/ies/contentrepository/core/port/EntityBulkExecutor.java new file mode 100644 index 0000000..7accf9a --- /dev/null +++ b/src/main/java/com/sitepark/ies/contentrepository/core/port/EntityBulkExecutor.java @@ -0,0 +1,7 @@ +package com.sitepark.ies.contentrepository.core.port; + +import com.sitepark.ies.contentrepository.core.domain.entity.EntityBulkExecution; + +public interface EntityBulkExecutor { + String execute(EntityBulkExecution execution); +} diff --git a/src/main/java/com/sitepark/ies/contentrepository/core/port/ExtensionsNotifier.java b/src/main/java/com/sitepark/ies/contentrepository/core/port/ExtensionsNotifier.java index 549c892..95cabd5 100644 --- a/src/main/java/com/sitepark/ies/contentrepository/core/port/ExtensionsNotifier.java +++ b/src/main/java/com/sitepark/ies/contentrepository/core/port/ExtensionsNotifier.java @@ -1,10 +1,5 @@ package com.sitepark.ies.contentrepository.core.port; -import java.util.List; - public interface ExtensionsNotifier { - void notifyPurge(long id); - - void notifyBulkPurge(List idList); } diff --git a/src/main/java/com/sitepark/ies/contentrepository/core/usecase/BulkPurge.java b/src/main/java/com/sitepark/ies/contentrepository/core/usecase/BulkPurge.java index ca2376f..f9742dd 100644 --- a/src/main/java/com/sitepark/ies/contentrepository/core/usecase/BulkPurge.java +++ b/src/main/java/com/sitepark/ies/contentrepository/core/usecase/BulkPurge.java @@ -1,17 +1,27 @@ package com.sitepark.ies.contentrepository.core.usecase; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.stream.Collectors; import javax.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.sitepark.ies.contentrepository.core.domain.entity.BulkOperationKey; import com.sitepark.ies.contentrepository.core.domain.entity.Entity; -import com.sitepark.ies.contentrepository.core.domain.entity.EntityLock; +import com.sitepark.ies.contentrepository.core.domain.entity.EntityBulkExecution; +import com.sitepark.ies.contentrepository.core.domain.entity.EntityBulkOperation; +import com.sitepark.ies.contentrepository.core.domain.entity.EntityTree; +import com.sitepark.ies.contentrepository.core.domain.entity.query.Query; +import com.sitepark.ies.contentrepository.core.domain.entity.query.SubTreeQuery; import com.sitepark.ies.contentrepository.core.domain.exception.AccessDenied; -import com.sitepark.ies.contentrepository.core.domain.exception.EntityLocked; +import com.sitepark.ies.contentrepository.core.domain.exception.GroupNotEmpty; import com.sitepark.ies.contentrepository.core.port.AccessControl; import com.sitepark.ies.contentrepository.core.port.ContentRepository; +import com.sitepark.ies.contentrepository.core.port.EntityBulkExecutor; import com.sitepark.ies.contentrepository.core.port.EntityLockManager; import com.sitepark.ies.contentrepository.core.port.ExtensionsNotifier; import com.sitepark.ies.contentrepository.core.port.HistoryManager; @@ -43,12 +53,16 @@ public class BulkPurge { private final ExtensionsNotifier extensionsNotifier; + private final EntityBulkExecutor entityBulkExecutor; + + private static Logger LOGGER = LogManager.getLogger(); + @Inject @SuppressWarnings("PMD.ExcessiveParameterList") protected BulkPurge(ContentRepository repository, EntityLockManager lockManager, VersioningManager versioningManager, HistoryManager historyManager, AccessControl accessControl, RecycleBin recycleBin, SearchIndex searchIndex, MediaReferenceManager mediaReferenceManager, - Publisher publisher, ExtensionsNotifier extensionsNotifier) { + Publisher publisher, ExtensionsNotifier extensionsNotifier, EntityBulkExecutor entityBulkExecutor) { this.repository = repository; this.lockManager = lockManager; @@ -60,27 +74,70 @@ protected BulkPurge(ContentRepository repository, EntityLockManager lockManager, this.mediaReferenceManager = mediaReferenceManager; this.publisher = publisher; this.extensionsNotifier = extensionsNotifier; + this.entityBulkExecutor = entityBulkExecutor; } - public void bulkPurge(BulkPurgeInput input, BulkPurgeOutput output) { + /** + * Create a BulkExecution and pass it to the EntityBulkExecutor to execute the purge. + * The return is a BulkExecution ID that can be used to track the progress. + * + * @param input Input argument for the bulk operations + * @return BulkExecution ID that can be used to track the progress + */ + public String bulkPurge(BulkPurgeInput input) { - List entityList = this.repository.getAll(input.getQuery()); + List entityList = this.getEntityList(input); this.accessControl(entityList); - try { + EntityBulkOperation lock = this.buildLockOperation(entityList, false); + EntityBulkOperation depublish = this.buildDepublishOperation(entityList); + EntityBulkOperation purge = this.buildPurgeOperation(entityList); + + EntityBulkOperation unlock = this.buildUnlockOperation(entityList); - this.lockEntityList(entityList, input.isForceLock()); + EntityBulkExecution execution = EntityBulkExecution.builder() + .topic("contentrepository", "purge") + .operation(lock, depublish, purge) + .finalizer(unlock) + .build(); - this.depublishEntityList(entityList); + return this.entityBulkExecutor.execute(execution); + } + + private List getEntityList(BulkPurgeInput input) { - this.purgeEntityList(entityList); + Query query = this.buildQuery(input); + List queryResult = this.repository.getAll(query); + + List entityList = null; + if (input.getRootList().isEmpty()) { + entityList = queryResult; + } else { + List rootList = input.getRootList().stream() + .map(root -> this.repository.get(root)) + .filter(entity -> entity.isPresent()) + .map(entity -> entity.get()) + .collect(Collectors.toList()); + entityList = new ArrayList<>(); + entityList.addAll(rootList); + entityList.addAll(queryResult); + } - this.notifyExtensions(entityList); + return entityList; + } - } finally { - this.unlockEntityList(entityList); + private Query buildQuery(BulkPurgeInput input) { + if (!input.getRootList().isEmpty()) { + return SubTreeQuery.builder() + .rootList(input.getRootList()) + .filterBy(input.getFilter().orElse(null)) + .build(); } + + return Query.builder() + .filterBy(input.getFilter().get()) + .build(); } private void accessControl(List entityList) { @@ -93,79 +150,120 @@ private void accessControl(List entityList) { }); } - private void lockEntityList(List entityList, boolean forceLock) { - entityList.stream().forEach(entity -> this.lockEntity(entity, forceLock)); - } - - private void lockEntity(Entity entity, boolean forceLock) { + private EntityBulkOperation buildLockOperation(List entityList, boolean forceLock) { - long id = entity.getId().get(); - - if (forceLock) { - this.lockManager.forceLock(id); - } else { - Optional lock = this.lockManager.getLock(id); - lock.ifPresent(l -> { - throw new EntityLocked(l); - }); - } - } + return EntityBulkOperation.builder() + .key(BulkOperationKey.PURGE_LOCK) + .entityList(entityList) + .consumer(entity -> { - private void unlockEntityList(List entityList) { - entityList.stream().forEach(entity -> this.unlockEntity(entity)); - } + long id = entity.getId().get(); - private void unlockEntity(Entity entity) { - long id = entity.getId().get(); - this.lockManager.unlock(id); + if (forceLock) { + this.lockManager.forceLock(id); + } else { + this.lockManager.lock(id); + } + }) + .build(); } - private void depublishEntityList(List entityList) { - - List idList = entityList.stream() - .map(entity -> entity.getId().get()) - .collect(Collectors.toList()); + private EntityBulkOperation buildDepublishOperation(List entityList) { - this.publisher.depublish(idList, id -> { - //System.out.println(id) - }); + return EntityBulkOperation.builder() + .key(BulkOperationKey.PURGE_DEPUBLISH) + .entityList(entityList) + .consumer(entity -> { + long id = entity.getId().get(); + this.publisher.depublish(id); + }) + .build(); } - private void purgeEntityList(List entityList) { + private EntityBulkOperation buildPurgeOperation(List entityList) { - - entityList.stream() + List nonGroupList = entityList.stream() .filter(entity -> !entity.isGroup()) - .forEach(entity -> this.purgeEntity(entity)); + .collect(Collectors.toList()); - entityList.stream() + List groupList = entityList.stream() .filter(entity -> entity.isGroup()) - .forEach(entity -> this.purgeEntity(entity)); - } - - private void purgeEntity(Entity entity) { + .collect(Collectors.toList()); - long id = entity.getId().get(); + /* + * Arrange the entityList so that first all group entries are deleted + * and then the groups in the hierarchy from bottom to top. + * This ensures that only empty pools are deleted. + */ + List orderedGroupList = this.orderGroupListHierarchicallyFromBottomToTop(groupList); + List orderedList = new ArrayList<>(); + orderedList.addAll(nonGroupList); + orderedList.addAll(orderedGroupList); + + if (LOGGER.isDebugEnabled()) { + orderedList.stream() + .forEach(entity -> { + LOGGER.debug("purge order: {}", entity); + }); + } - this.searchIndex.remove(id); + return EntityBulkOperation.builder() + .key(BulkOperationKey.PURGE_PURGE) + .entityList(orderedList) + .consumer(entity -> { + long id = entity.getId().get(); + + if (this.repository.isGroup(id) && !this.repository.isEmptyGroup(id)) { + throw new GroupNotEmpty(id); + } + + if (LOGGER.isInfoEnabled()) { + LOGGER.info("purge: {}", entity); + } + + this.searchIndex.remove(id); + this.mediaReferenceManager.removeByReference(id); + this.repository.removeEntity(id); + this.historyManager.purge(id); + this.versioningManager.removeAllVersions(id); + this.recycleBin.removeByObject(id); + this.extensionsNotifier.notifyPurge(id); + }) + .build(); + } - this.mediaReferenceManager.removeByReference(id); + /** + * Arranges the groups according to their hierarchy from bottom to top. + */ + private List orderGroupListHierarchicallyFromBottomToTop(List groupList) { - this.repository.removeEntity(id); + EntityTree tree = new EntityTree(); + groupList.stream().forEach(tree::add); - this.historyManager.purge(id); + List hierarchicalOrder = tree.getAll(); - this.versioningManager.removeAllVersions(id); + Collections.reverse(hierarchicalOrder); - this.recycleBin.removeByObject(id); + return hierarchicalOrder; } - private void notifyExtensions(List entityList) { - - List idList = entityList.stream() - .map(entity -> entity.getId().get()) - .collect(Collectors.toList()); - - this.extensionsNotifier.notifyBulkPurge(idList); + private EntityBulkOperation buildUnlockOperation(List entityList) { + + return EntityBulkOperation.builder() + .key(BulkOperationKey.PURGE_CLEANUP) + .entityList(entityList) + .consumer(entity -> { + long id = entity.getId().get(); + try { + this.lockManager.unlock(id); + } catch (Exception e) { + if (LOGGER.isTraceEnabled()) { + LOGGER.atTrace() + .withThrowable(e) + .log("Unable to unlock {}", entity); + } + } + }) + .build(); } } diff --git a/src/main/java/com/sitepark/ies/contentrepository/core/usecase/BulkPurgeInput.java b/src/main/java/com/sitepark/ies/contentrepository/core/usecase/BulkPurgeInput.java index 0f1e8f3..f329e85 100644 --- a/src/main/java/com/sitepark/ies/contentrepository/core/usecase/BulkPurgeInput.java +++ b/src/main/java/com/sitepark/ies/contentrepository/core/usecase/BulkPurgeInput.java @@ -1,20 +1,35 @@ package com.sitepark.ies.contentrepository.core.usecase; -import com.sitepark.ies.contentrepository.core.domain.entity.query.Query; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import com.sitepark.ies.contentrepository.core.domain.entity.filter.Filter; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; public final class BulkPurgeInput { - private final Query query; + private final List rootList; + + private final Filter filterBy; private final boolean forceLock; private BulkPurgeInput(Builder builder) { - this.query = builder.query; + this.rootList = Collections.unmodifiableList(builder.rootList); + this.filterBy = builder.filterBy; this.forceLock = builder.forceLock; } - public Query getQuery() { - return this.query; + @SuppressFBWarnings("EI_EXPOSE_REP") + public List getRootList() { + return this.rootList; + } + + public Optional getFilter() { + return Optional.ofNullable(this.filterBy); } public boolean isForceLock() { @@ -31,24 +46,43 @@ public Builder toBuilder() { public static class Builder { - private Query query; + private final List rootList = new ArrayList<>(); + + private Filter filterBy; private boolean forceLock; private Builder() { } private Builder(BulkPurgeInput bulkPurgeRequest) { - this.query = bulkPurgeRequest.query; + this.rootList.addAll(bulkPurgeRequest.rootList); + this.filterBy = bulkPurgeRequest.filterBy; this.forceLock = bulkPurgeRequest.forceLock; } - public void query(Query query) { - assert query != null; - this.query = query; + public Builder rootList(List rootList) { + assert rootList != null; + for (Long root : rootList) { + this.root(root); + } + return this; + } + + public Builder root(Long root) { + assert root != null; + this.rootList.add(root); + return this; + } + + public Builder filterBy(Filter filterBy) { + assert filterBy != null; + this.filterBy = filterBy; + return this; } - public void forceLock(boolean forceLock) { + public Builder forceLock(boolean forceLock) { this.forceLock = forceLock; + return this; } public BulkPurgeInput build() { diff --git a/src/main/java/com/sitepark/ies/contentrepository/core/usecase/BulkPurgeOutput.java b/src/main/java/com/sitepark/ies/contentrepository/core/usecase/BulkPurgeOutput.java deleted file mode 100644 index 36d0838..0000000 --- a/src/main/java/com/sitepark/ies/contentrepository/core/usecase/BulkPurgeOutput.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.sitepark.ies.contentrepository.core.usecase; - -public class BulkPurgeOutput { - -} diff --git a/src/main/java/com/sitepark/ies/contentrepository/core/usecase/GetAllEntities.java b/src/main/java/com/sitepark/ies/contentrepository/core/usecase/GetAllEntities.java index 4580161..1c309a2 100644 --- a/src/main/java/com/sitepark/ies/contentrepository/core/usecase/GetAllEntities.java +++ b/src/main/java/com/sitepark/ies/contentrepository/core/usecase/GetAllEntities.java @@ -13,9 +13,7 @@ public final class GetAllEntities { private final ContentRepository repository; @Inject - @SuppressWarnings("PMD.ExcessiveParameterList") protected GetAllEntities(ContentRepository repository) { - this.repository = repository; } diff --git a/src/main/java/com/sitepark/ies/contentrepository/core/usecase/PurgeEntity.java b/src/main/java/com/sitepark/ies/contentrepository/core/usecase/PurgeEntity.java index b7a56b7..9513d48 100644 --- a/src/main/java/com/sitepark/ies/contentrepository/core/usecase/PurgeEntity.java +++ b/src/main/java/com/sitepark/ies/contentrepository/core/usecase/PurgeEntity.java @@ -1,12 +1,11 @@ package com.sitepark.ies.contentrepository.core.usecase; -import java.util.Optional; - import javax.inject.Inject; -import com.sitepark.ies.contentrepository.core.domain.entity.EntityLock; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + import com.sitepark.ies.contentrepository.core.domain.exception.AccessDenied; -import com.sitepark.ies.contentrepository.core.domain.exception.EntityLocked; import com.sitepark.ies.contentrepository.core.domain.exception.GroupNotEmpty; import com.sitepark.ies.contentrepository.core.port.AccessControl; import com.sitepark.ies.contentrepository.core.port.ContentRepository; @@ -41,6 +40,8 @@ public final class PurgeEntity { private final ExtensionsNotifier extensionsNotifier; + private static Logger LOGGER = LogManager.getLogger(); + @Inject @SuppressWarnings("PMD.ExcessiveParameterList") protected PurgeEntity(ContentRepository repository, EntityLockManager lockManager, @@ -71,13 +72,14 @@ public void purgeEntity(long id) { } try { - Optional lock = this.lockManager.getLock(id); - lock.ifPresent(l -> { - throw new EntityLocked(l); - }); + this.lockManager.lock(id); this.publisher.depublish(id); + if (LOGGER.isInfoEnabled()) { + LOGGER.info("purge: {}", this.repository.get(id)); + } + this.searchIndex.remove(id); this.mediaReferenceManager.removeByReference(id); diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 64d6246..2fa1605 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -14,6 +14,7 @@ requires com.fasterxml.jackson.databind; requires com.fasterxml.jackson.annotation; requires com.fasterxml.jackson.datatype.jdk8; + requires org.apache.logging.log4j; opens com.sitepark.ies.contentrepository.core.domain.entity; opens com.sitepark.ies.contentrepository.core.domain.entity.filter; diff --git a/src/test/java/com/sitepark/ies/contentrepository/core/domain/entity/EntityTreeTest.java b/src/test/java/com/sitepark/ies/contentrepository/core/domain/entity/EntityTreeTest.java new file mode 100644 index 0000000..24d3a37 --- /dev/null +++ b/src/test/java/com/sitepark/ies/contentrepository/core/domain/entity/EntityTreeTest.java @@ -0,0 +1,28 @@ +package com.sitepark.ies.contentrepository.core.domain.entity; + +import static org.hamcrest.MatcherAssert.assertThat; + +import java.util.List; +import java.util.stream.Collectors; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +class EntityTreeTest { + + @Test + void testBuildAndGet() { + EntityTree tree = new EntityTree(); + + tree.add(Entity.builder().id(1).build()); + tree.add(Entity.builder().id(10).parent(1).build()); + tree.add(Entity.builder().id(11).parent(1).build()); + tree.add(Entity.builder().id(20).parent(2).build()); + + List all = tree.getAll().stream() + .map(entity -> entity.getId().get()) + .collect(Collectors.toList()); + + assertThat("Unexpected entries", all, Matchers.contains(1L, 10L, 11L, 20L)); + } +} diff --git a/src/test/java/com/sitepark/ies/contentrepository/core/domain/entity/query/QueryTest.java b/src/test/java/com/sitepark/ies/contentrepository/core/domain/entity/query/QueryTest.java new file mode 100644 index 0000000..1036330 --- /dev/null +++ b/src/test/java/com/sitepark/ies/contentrepository/core/domain/entity/query/QueryTest.java @@ -0,0 +1,17 @@ +package com.sitepark.ies.contentrepository.core.domain.entity.query; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class QueryTest { + + @Test + void testWithNullFilter() { + Query query = Query.builder() + .filterBy(null) + .build(); + assertTrue(query.getFilterBy().isEmpty(), "filter optional should be empty"); + } + +} diff --git a/src/test/java/com/sitepark/ies/contentrepository/core/domain/entity/query/SubTreeQueryTest.java b/src/test/java/com/sitepark/ies/contentrepository/core/domain/entity/query/SubTreeQueryTest.java new file mode 100644 index 0000000..8dd470c --- /dev/null +++ b/src/test/java/com/sitepark/ies/contentrepository/core/domain/entity/query/SubTreeQueryTest.java @@ -0,0 +1,19 @@ +package com.sitepark.ies.contentrepository.core.domain.entity.query; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class SubTreeQueryTest { + + @Test + void testWithNullFilter() { + + SubTreeQuery query = SubTreeQuery.builder() + .root(123L) + .filterBy(null) + .build(); + + assertEquals(123L, query.getRootList().get(0), "filter optional should be empty"); + } +} diff --git a/src/test/java/com/sitepark/ies/contentrepository/core/usecase/PurgeEntityTest.java b/src/test/java/com/sitepark/ies/contentrepository/core/usecase/PurgeEntityTest.java index 0232a73..6e6f0f7 100644 --- a/src/test/java/com/sitepark/ies/contentrepository/core/usecase/PurgeEntityTest.java +++ b/src/test/java/com/sitepark/ies/contentrepository/core/usecase/PurgeEntityTest.java @@ -3,12 +3,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import java.util.Optional; - import org.junit.jupiter.api.Test; import org.mockito.InOrder; @@ -59,12 +58,7 @@ void testEntityIsLocked() { when(accessControl.isEntityRemovable(anyLong())).thenReturn(true); EntityLockManager lockManager = mock(EntityLockManager.class); - when(lockManager.getLock(anyLong())) - .thenReturn( - Optional.of( - EntityLock.builder().entity(10L).build() - ) - ); + doThrow(new EntityLocked(EntityLock.builder().entity(10L).build())).when(lockManager).lock(anyLong()); var purgeEntity = new PurgeEntity( repository, @@ -77,6 +71,8 @@ void testEntityIsLocked() { null, null, null); + //purgeEntity.purgeEntity(10L); + EntityLocked entityLocked = assertThrows(EntityLocked.class, () -> { purgeEntity.purgeEntity(10L); }, "entity should be locked");