From 50c612f8ec75bcd5429bbf1ef681e8502afda8bf Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Wed, 5 Jan 2022 10:26:29 +0100 Subject: [PATCH] NEW: PersistVisitor --- ebean-api/src/main/java/io/ebean/DB.java | 13 ++ .../src/main/java/io/ebean/Database.java | 11 ++ .../main/java/io/ebean/PersistVisitor.java | 29 +++++ .../server/core/DefaultServer.java | 6 + .../server/core/VisitHandler.java | 121 ++++++++++++++++++ .../io/ebeaninternal/api/TDSpiServer.java | 5 + .../java/org/tests/basic/TPersistVisitor.java | 69 ++++++++++ .../org/tests/basic/TestM2MCascadeOne.java | 30 ++++- 8 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 ebean-api/src/main/java/io/ebean/PersistVisitor.java create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/core/VisitHandler.java create mode 100644 ebean-test/src/test/java/org/tests/basic/TPersistVisitor.java diff --git a/ebean-api/src/main/java/io/ebean/DB.java b/ebean-api/src/main/java/io/ebean/DB.java index b5438eb08..f834c0354 100644 --- a/ebean-api/src/main/java/io/ebean/DB.java +++ b/ebean-api/src/main/java/io/ebean/DB.java @@ -477,6 +477,19 @@ public final class DB { public static int saveAll(Object... beans) throws OptimisticLockException { return getDefault().saveAll(beans); } + + /** + * This will visit all beans in the persist graph on a given object + * start. It will call the visitor for each dirty bean that would + * be saved with {@link #save(Object)} or {@link #saveAll(Collection)}. You can + * use this method to implement custom validations. + * + * @param start could be a bean, a list of beans or a map of beans. + * @param visitor the visitor + */ + public static void visitSave(Object start, PersistVisitor visitor) { + getDefault().visitSave(start, visitor); + } /** * This method checks the uniqueness of a bean. I.e. if the save will work. It will return the diff --git a/ebean-api/src/main/java/io/ebean/Database.java b/ebean-api/src/main/java/io/ebean/Database.java index d2344db52..66e1d0cf8 100644 --- a/ebean-api/src/main/java/io/ebean/Database.java +++ b/ebean-api/src/main/java/io/ebean/Database.java @@ -1017,6 +1017,17 @@ public interface Database { * Save all the beans. */ int saveAll(Object... beans) throws OptimisticLockException; + + /** + * This will visit all beans in the persist graph on a given object + * start. It will call the visitor for each dirty bean that would + * be saved with {@link #save(Object)} or {@link #saveAll(Collection)}. You can + * use this method to implement custom validations. + * + * @param start could be a bean, a list of beans or a map of beans. + * @param visitor the visitor + */ + void visitSave(Object start, PersistVisitor visitor); /** * Delete the bean. diff --git a/ebean-api/src/main/java/io/ebean/PersistVisitor.java b/ebean-api/src/main/java/io/ebean/PersistVisitor.java new file mode 100644 index 000000000..b5f1398d0 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/PersistVisitor.java @@ -0,0 +1,29 @@ +package io.ebean; + +import java.util.Collection; +import java.util.Map; + +import io.ebean.bean.EntityBean; +import io.ebean.plugin.Property; + +@FunctionalInterface +public interface PersistVisitor { + + PersistVisitor visitBean(EntityBean bean); + + default PersistVisitor visitProperty(Property prop) { + return this; + }; + + default PersistVisitor visitCollection(Collection collection) { + return this; + }; + + default PersistVisitor visitMap(Map map) { + return this; + }; + + default void visitEnd() { + } + +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java index 84cf05a34..7750de089 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java @@ -18,6 +18,7 @@ import io.ebean.FutureRowCount; import io.ebean.MergeOptions; import io.ebean.MergeOptionsBuilder; import io.ebean.PagedList; +import io.ebean.PersistVisitor; import io.ebean.PersistenceContextScope; import io.ebean.ProfileLocation; import io.ebean.Query; @@ -1628,6 +1629,11 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { persister.save(checkEntityBean(bean), transaction); } + @Override + public void visitSave(Object start, PersistVisitor visitor) { + new VisitHandler(descriptorManager).visit(start, visitor); + } + @Override public void markAsDirty(Object bean) { if (!(bean instanceof EntityBean)) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/VisitHandler.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/VisitHandler.java new file mode 100644 index 000000000..46dcd66ab --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/VisitHandler.java @@ -0,0 +1,121 @@ +package io.ebeaninternal.server.core; + +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; + +import io.ebean.PersistVisitor; +import io.ebean.bean.EntityBean; +import io.ebean.bean.EntityBeanIntercept; +import io.ebeaninternal.server.deploy.BeanDescriptor; +import io.ebeaninternal.server.deploy.BeanDescriptorManager; +import io.ebeaninternal.server.deploy.BeanPropertyAssocMany; +import io.ebeaninternal.server.deploy.BeanPropertyAssocOne; + +/** + * Handler to process persist graphs. It will allow you to visit a persist + * action (insert/update) and get information about all beans that will be + * affected by that action. This allows you to do validation and other things on + * a set of beans. + * + * @author Roland Praml, FOCONIS AG + * + */ +class VisitHandler { + + private final Set seen = Collections.newSetFromMap(new IdentityHashMap()); + private final BeanDescriptorManager beanDescriptorManager; + + public VisitHandler(BeanDescriptorManager beanDescriptorManager) { + this.beanDescriptorManager = beanDescriptorManager; + } + + public void visit(Object start, PersistVisitor visitor) { + if (start != null) { + if (start instanceof EntityBean) { + visitBean((EntityBean) start, visitor); + } else { + visitMany(start, visitor); + } + } + visitor.visitEnd(); + } + + private void visitBean(EntityBean bean, PersistVisitor visitor) { + if (!seen.add(bean)) { + return; + } + visitor = visitor.visitBean(bean); + if (visitor != null) { + BeanDescriptor desc = beanDescriptorManager.descriptor(bean.getClass()); + visitOnes(bean, visitor, desc, desc.propertiesOneImportedSave()); + visitOnes(bean, visitor, desc, desc.propertiesOneExportedSave()); + visitManys(bean, visitor, desc, desc.propertiesManySave()); + visitor.visitEnd(); + } + } + + private void visitManys(EntityBean bean, PersistVisitor visitor, BeanDescriptor desc, + BeanPropertyAssocMany[] manys) { + EntityBeanIntercept ebi = bean._ebean_getIntercept(); + for (BeanPropertyAssocMany many : manys) { + // check that property is loaded and collection should be cascaded to + if (ebi.isLoadedProperty(many.propertyIndex()) && !many.isSkipSaveBeanCollection(bean, ebi.isNew())) { + Object manyValue = many.getValue(bean); + if (manyValue != null) { + PersistVisitor propertyVisitor = visitor.visitProperty(many); + if (propertyVisitor != null) { + visitMany(manyValue, propertyVisitor); + propertyVisitor.visitEnd(); + } + } + } + } + } + + private void visitMany(Object many, PersistVisitor visitor) { + if (!seen.add(many)) { + return; + } + if (many instanceof Collection) { + Collection coll = (Collection) many; + PersistVisitor collectionVisitor = visitor.visitCollection(coll); + if (collectionVisitor != null) { + for (Object elem : coll) { + visitBean((EntityBean) elem, collectionVisitor); + } + collectionVisitor.visitEnd(); + } + } else if (many instanceof Map) { + Map map = (Map) many; + PersistVisitor mapVisitor = visitor.visitMap(map); + if (mapVisitor != null) { + for (Object elem : ((Map) many).values()) { + visitBean((EntityBean) elem, mapVisitor); + } + mapVisitor.visitEnd(); + } + } else { + throw new IllegalArgumentException("Object " + many + " cannot be visited in persist graph"); + } + } + + private void visitOnes(EntityBean bean, PersistVisitor visitor, BeanDescriptor desc, + BeanPropertyAssocOne[] ones) { + EntityBeanIntercept ebi = bean._ebean_getIntercept(); + for (BeanPropertyAssocOne prop : ones) { + if (ebi.isLoadedProperty(prop.propertyIndex())) { + EntityBean detailBean = prop.getValueAsEntityBean(bean); + if (detailBean != null && !prop.isSaveRecurseSkippable(detailBean) && !prop.isReference(detailBean)) { + PersistVisitor propertyVisitor = visitor.visitProperty(prop); + if (propertyVisitor != null) { + visitBean(detailBean, propertyVisitor); + propertyVisitor.visitEnd(); + } + } + } + } + } +} \ No newline at end of file diff --git a/ebean-test/src/test/java/io/ebeaninternal/api/TDSpiServer.java b/ebean-test/src/test/java/io/ebeaninternal/api/TDSpiServer.java index f0d91b935..742428156 100644 --- a/ebean-test/src/test/java/io/ebeaninternal/api/TDSpiServer.java +++ b/ebean-test/src/test/java/io/ebeaninternal/api/TDSpiServer.java @@ -293,6 +293,11 @@ public class TDSpiServer implements SpiServer { return 0; } + @Override + public void visitSave(Object start, PersistVisitor visitor) { + + } + @Override public boolean delete(Object bean) throws OptimisticLockException { return false; diff --git a/ebean-test/src/test/java/org/tests/basic/TPersistVisitor.java b/ebean-test/src/test/java/org/tests/basic/TPersistVisitor.java new file mode 100644 index 000000000..0d61a13f8 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/basic/TPersistVisitor.java @@ -0,0 +1,69 @@ +package org.tests.basic; + +import java.util.Collection; + +import io.ebean.DB; +import io.ebean.PersistVisitor; +import io.ebean.bean.EntityBean; +import io.ebean.plugin.Property; +/** + * Sample persist visitor that converts the visited beans in a XML-like structure + */ +public class TPersistVisitor implements PersistVisitor { + + private final StringBuilder sb; + private final String indent; + private final String tag; + private boolean empty = true; + + public TPersistVisitor() { + this(new StringBuilder(), "", "root"); + } + private TPersistVisitor(StringBuilder sb, String indent, String tag) { + this.sb = sb; + this.indent = indent; + this.tag = tag; + this.sb.append(indent).append('<').append(tag); + } + + TPersistVisitor newVisitor(String tag) { + if (empty) { + sb.append(">\n"); + empty = false; + } + return new TPersistVisitor(sb, indent + " ", tag); + } + + @Override + public void visitEnd() { + if (empty) { + sb.append("/>\n"); + } else { + this.sb.append(indent).append("\n"); + } + } + + TPersistVisitor attr(String attr, Object value) { + sb.append(' ').append(attr).append('=').append('\'').append(value).append('\''); + return this; + } + + public TPersistVisitor visitBean(EntityBean bean) { + return newVisitor("bean").attr("type", bean.getClass().getSimpleName()).attr("newOrDirty", DB.beanState(bean).isNewOrDirty()); + } + + @Override + public PersistVisitor visitProperty(Property prop) { + return newVisitor("property").attr("name", prop.name()); + } + + @Override + public PersistVisitor visitCollection(Collection collection) { + return newVisitor("collection").attr("size", collection.size()); + } + + @Override + public String toString() { + return sb.toString(); + } + } \ No newline at end of file diff --git a/ebean-test/src/test/java/org/tests/basic/TestM2MCascadeOne.java b/ebean-test/src/test/java/org/tests/basic/TestM2MCascadeOne.java index 5722ffbbb..d0a848bc7 100644 --- a/ebean-test/src/test/java/org/tests/basic/TestM2MCascadeOne.java +++ b/ebean-test/src/test/java/org/tests/basic/TestM2MCascadeOne.java @@ -3,12 +3,15 @@ package org.tests.basic; import io.ebean.BaseTestCase; import io.ebean.DB; import io.ebean.Query; + +import static org.assertj.core.api.Assertions.assertThat; + import org.junit.jupiter.api.Test; import org.tests.model.basic.MRole; import org.tests.model.basic.MUser; public class TestM2MCascadeOne extends BaseTestCase { - + @Test public void test() { @@ -30,8 +33,31 @@ public class TestM2MCascadeOne extends BaseTestCase { u1.addRole(r0); u1.addRole(r1); + TPersistVisitor tv = new TPersistVisitor(); + DB.visitSave(u1, tv); + assertThat(tv.toString()).isEqualTo("\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"); + DB.save(u1); - + + u1 = DB.find(MUser.class, u.getUserid()); + + tv = new TPersistVisitor(); + DB.visitSave(u1, tv); + // collection is unloaded + assertThat(tv.toString()).isEqualTo("\n" + + " \n" + + "\n"); + + } @Test