Repository: cayenne
Updated Branches:
  refs/heads/master 8f3ac8b6a -> af5ae7856


http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter_ListenerInducedChangesIT.java
----------------------------------------------------------------------
diff --git 
a/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter_ListenerInducedChangesIT.java
 
b/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter_ListenerInducedChangesIT.java
new file mode 100644
index 0000000..6f162c3
--- /dev/null
+++ 
b/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter_ListenerInducedChangesIT.java
@@ -0,0 +1,256 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.lifecycle.postcommit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.annotation.PrePersist;
+import org.apache.cayenne.annotation.PreUpdate;
+import org.apache.cayenne.configuration.server.ServerRuntimeBuilder;
+import org.apache.cayenne.lifecycle.changemap.AttributeChange;
+import org.apache.cayenne.lifecycle.changemap.ChangeMap;
+import org.apache.cayenne.lifecycle.changemap.ObjectChange;
+import org.apache.cayenne.lifecycle.changemap.ObjectChangeType;
+import org.apache.cayenne.lifecycle.db.Auditable1;
+import org.apache.cayenne.lifecycle.db.AuditableChild1;
+import org.apache.cayenne.lifecycle.unit.AuditableServerCase;
+import org.apache.cayenne.query.SelectById;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+/**
+ * Testing capturing changes introduced by the pre-commit listeners.
+ */
+public class PostCommitFilter_ListenerInducedChangesIT extends 
AuditableServerCase {
+
+       protected ObjectContext context;
+       protected PostCommitListener mockListener;
+
+       @Override
+       protected ServerRuntimeBuilder configureCayenne() {
+               this.mockListener = mock(PostCommitListener.class);
+               return 
super.configureCayenne().addModule(PostCommitModuleBuilder.builder().listener(mockListener).build());
+       }
+
+       @Before
+       public void before() {
+               context = runtime.newContext();
+       }
+
+       @Test
+       public void testPostCommit_Insert() throws SQLException {
+
+               final InsertListener listener = new InsertListener();
+               runtime.getDataDomain().addListener(listener);
+
+               final Auditable1 a1 = context.newObject(Auditable1.class);
+               a1.setCharProperty1("yy");
+
+               doAnswer(new Answer<Object>() {
+                       @Override
+                       public Object answer(InvocationOnMock invocation) 
throws Throwable {
+
+                               assertNotNull(listener.c);
+
+                               List<ObjectChange> sortedChanges = 
sortedChanges(invocation);
+
+                               assertEquals(2, sortedChanges.size());
+
+                               assertEquals(a1.getObjectId(), 
sortedChanges.get(0).getPostCommitId());
+                               assertEquals(ObjectChangeType.INSERT, 
sortedChanges.get(0).getType());
+
+                               assertEquals(listener.c.getObjectId(), 
sortedChanges.get(1).getPostCommitId());
+                               assertEquals(ObjectChangeType.INSERT, 
sortedChanges.get(1).getType());
+
+                               AttributeChange listenerInducedChange = 
sortedChanges.get(1).getAttributeChanges()
+                                               
.get(AuditableChild1.CHAR_PROPERTY1.getName());
+                               assertNotNull(listenerInducedChange);
+                               assertEquals("c1", 
listenerInducedChange.getNewValue());
+
+                               return null;
+                       }
+               }).when(mockListener).onPostCommit(any(ObjectContext.class), 
any(ChangeMap.class));
+
+               context.commitChanges();
+
+               verify(mockListener).onPostCommit(any(ObjectContext.class), 
any(ChangeMap.class));
+       }
+
+       @Test
+       public void testPostCommit_Delete() throws SQLException {
+
+               auditable1.insert(1, "yy");
+               auditableChild1.insert(31, 1, "yyc");
+
+               final DeleteListener listener = new DeleteListener();
+               runtime.getDataDomain().addListener(listener);
+
+               final Auditable1 a1 = SelectById.query(Auditable1.class, 
1).prefetch(Auditable1.CHILDREN1.joint())
+                               .selectFirst(context);
+               a1.setCharProperty1("zz");
+
+               doAnswer(new Answer<Object>() {
+                       @Override
+                       public Object answer(InvocationOnMock invocation) 
throws Throwable {
+
+                               assertNotNull(listener.toDelete);
+                               assertEquals(1, listener.toDelete.size());
+
+                               List<ObjectChange> sortedChanges = 
sortedChanges(invocation);
+
+                               assertEquals(2, sortedChanges.size());
+
+                               assertEquals(ObjectChangeType.UPDATE, 
sortedChanges.get(0).getType());
+                               assertEquals(a1.getObjectId(), 
sortedChanges.get(0).getPostCommitId());
+
+                               assertEquals(ObjectChangeType.DELETE, 
sortedChanges.get(1).getType());
+                               
assertEquals(listener.toDelete.get(0).getObjectId(), 
sortedChanges.get(1).getPostCommitId());
+
+                               AttributeChange listenerInducedChange = 
sortedChanges.get(1).getAttributeChanges()
+                                               
.get(AuditableChild1.CHAR_PROPERTY1.getName());
+                               assertNotNull(listenerInducedChange);
+                               assertEquals("yyc", 
listenerInducedChange.getOldValue());
+
+                               return null;
+                       }
+               }).when(mockListener).onPostCommit(any(ObjectContext.class), 
any(ChangeMap.class));
+
+               context.commitChanges();
+
+               verify(mockListener).onPostCommit(any(ObjectContext.class), 
any(ChangeMap.class));
+       }
+
+       @Test
+       public void testPostCommit_Update() throws SQLException {
+
+               auditable1.insert(1, "yy");
+               auditableChild1.insert(31, 1, "yyc");
+
+               final UpdateListener listener = new UpdateListener();
+               runtime.getDataDomain().addListener(listener);
+
+               final Auditable1 a1 = SelectById.query(Auditable1.class, 
1).prefetch(Auditable1.CHILDREN1.joint())
+                               .selectFirst(context);
+               a1.setCharProperty1("zz");
+
+               doAnswer(new Answer<Object>() {
+                       @Override
+                       public Object answer(InvocationOnMock invocation) 
throws Throwable {
+
+                               assertNotNull(listener.toUpdate);
+                               assertEquals(1, listener.toUpdate.size());
+
+                               List<ObjectChange> sortedChanges = 
sortedChanges(invocation);
+
+                               assertEquals(2, sortedChanges.size());
+
+                               assertEquals(ObjectChangeType.UPDATE, 
sortedChanges.get(0).getType());
+                               assertEquals(a1.getObjectId(), 
sortedChanges.get(0).getPostCommitId());
+
+                               assertEquals(ObjectChangeType.UPDATE, 
sortedChanges.get(1).getType());
+                               
assertEquals(listener.toUpdate.get(0).getObjectId(), 
sortedChanges.get(1).getPostCommitId());
+
+                               AttributeChange listenerInducedChange = 
sortedChanges.get(1).getAttributeChanges()
+                                               
.get(AuditableChild1.CHAR_PROPERTY1.getName());
+                               assertNotNull(listenerInducedChange);
+                               assertEquals("yyc", 
listenerInducedChange.getOldValue());
+                               assertEquals("yyc_", 
listenerInducedChange.getNewValue());
+
+                               return null;
+                       }
+               }).when(mockListener).onPostCommit(any(ObjectContext.class), 
any(ChangeMap.class));
+
+               context.commitChanges();
+
+               verify(mockListener).onPostCommit(any(ObjectContext.class), 
any(ChangeMap.class));
+       }
+
+       private List<ObjectChange> sortedChanges(InvocationOnMock invocation) {
+               assertSame(context, invocation.getArguments()[0]);
+
+               ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
+
+               List<ObjectChange> sortedChanges = new 
ArrayList<>(changes.getUniqueChanges());
+               Collections.sort(sortedChanges, new Comparator<ObjectChange>() {
+                       public int compare(ObjectChange o1, ObjectChange o2) {
+                               return 
o1.getPostCommitId().getEntityName().compareTo(o2.getPostCommitId().getEntityName());
+                       }
+               });
+
+               return sortedChanges;
+       }
+
+       static class InsertListener {
+
+               private AuditableChild1 c;
+
+               @PrePersist(Auditable1.class)
+               public void prePersist(Auditable1 a) {
+
+                       c = 
a.getObjectContext().newObject(AuditableChild1.class);
+                       c.setCharProperty1("c1");
+                       c.setParent(a);
+               }
+       }
+
+       static class DeleteListener {
+
+               private List<AuditableChild1> toDelete;
+
+               @PreUpdate(Auditable1.class)
+               public void prePersist(Auditable1 a) {
+
+                       toDelete = new ArrayList<>(a.getChildren1());
+                       for (AuditableChild1 c : toDelete) {
+                               c.getObjectContext().deleteObject(c);
+                       }
+               }
+       }
+
+       static class UpdateListener {
+
+               private List<AuditableChild1> toUpdate;
+
+               @PreUpdate(Auditable1.class)
+               public void prePersist(Auditable1 a) {
+
+                       toUpdate = new ArrayList<>(a.getChildren1());
+                       for (AuditableChild1 c : toUpdate) {
+                               c.setCharProperty1(c.getCharProperty1() + "_");
+                       }
+               }
+       }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter_OutsideTxIT.java
----------------------------------------------------------------------
diff --git 
a/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter_OutsideTxIT.java
 
b/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter_OutsideTxIT.java
new file mode 100644
index 0000000..b9b3278
--- /dev/null
+++ 
b/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter_OutsideTxIT.java
@@ -0,0 +1,85 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.lifecycle.postcommit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.configuration.server.ServerRuntimeBuilder;
+import org.apache.cayenne.lifecycle.changemap.ChangeMap;
+import org.apache.cayenne.lifecycle.changemap.ObjectChange;
+import org.apache.cayenne.lifecycle.db.AuditLog;
+import org.apache.cayenne.lifecycle.db.Auditable2;
+import org.apache.cayenne.lifecycle.unit.AuditableServerCase;
+import org.apache.cayenne.tx.BaseTransaction;
+import org.junit.Before;
+import org.junit.Test;
+
+public class PostCommitFilter_OutsideTxIT extends AuditableServerCase {
+
+       protected ObjectContext context;
+       protected PostCommitListener listener;
+
+       @Override
+       protected ServerRuntimeBuilder configureCayenne() {
+               this.listener = new PostCommitListener() {
+
+                       @Override
+                       public void onPostCommit(ObjectContext 
originatingContext, ChangeMap changes) {
+
+                               // assert we are inside transaction
+                               
assertNull(BaseTransaction.getThreadTransaction());
+
+                               for (ObjectChange c : 
changes.getUniqueChanges()) {
+                                       AuditLog log = 
runtime.newContext().newObject(AuditLog.class);
+                                       log.setLog("DONE: " + 
c.getPostCommitId());
+                                       log.getObjectContext().commitChanges();
+                               }
+                       }
+               };
+               return super.configureCayenne().addModule(
+                               
PostCommitModuleBuilder.builder().auditableEntitiesOnly().excludeFromTransaction().listener(listener)
+                                               .build());
+       }
+
+       @Before
+       public void before() {
+               this.context = runtime.newContext();
+       }
+
+       @Test
+       public void testCommitLog() throws SQLException {
+               Auditable2 a1 = context.newObject(Auditable2.class);
+               a1.setCharProperty1("yy");
+               a1.setCharProperty2("zz");
+
+               Auditable2 a2 = context.newObject(Auditable2.class);
+               a2.setCharProperty1("yy");
+               a2.setCharProperty2("zz");
+               context.commitChanges();
+
+               List<Object[]> logs = auditLog.selectAll();
+               assertEquals(2, logs.size());
+       }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter_TxIT.java
----------------------------------------------------------------------
diff --git 
a/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter_TxIT.java
 
b/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter_TxIT.java
new file mode 100644
index 0000000..8b62be6
--- /dev/null
+++ 
b/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter_TxIT.java
@@ -0,0 +1,84 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.lifecycle.postcommit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.configuration.server.ServerRuntimeBuilder;
+import org.apache.cayenne.lifecycle.changemap.ChangeMap;
+import org.apache.cayenne.lifecycle.changemap.ObjectChange;
+import org.apache.cayenne.lifecycle.db.AuditLog;
+import org.apache.cayenne.lifecycle.db.Auditable2;
+import org.apache.cayenne.lifecycle.unit.AuditableServerCase;
+import org.apache.cayenne.tx.BaseTransaction;
+import org.junit.Before;
+import org.junit.Test;
+
+public class PostCommitFilter_TxIT extends AuditableServerCase {
+
+       protected ObjectContext context;
+       protected PostCommitListener listener;
+
+       @Override
+       protected ServerRuntimeBuilder configureCayenne() {
+               this.listener = new PostCommitListener() {
+
+                       @Override
+                       public void onPostCommit(ObjectContext 
originatingContext, ChangeMap changes) {
+
+                               // assert we are inside transaction
+                               
assertNotNull(BaseTransaction.getThreadTransaction());
+
+                               for (ObjectChange c : 
changes.getUniqueChanges()) {
+                                       AuditLog log = 
runtime.newContext().newObject(AuditLog.class);
+                                       log.setLog("DONE: " + 
c.getPostCommitId());
+                                       log.getObjectContext().commitChanges();
+                               }
+                       }
+               };
+               return super.configureCayenne().addModule(
+                               
PostCommitModuleBuilder.builder().auditableEntitiesOnly().listener(listener).build());
+       }
+
+       @Before
+       public void before() {
+               this.context = runtime.newContext();
+       }
+
+       @Test
+       public void testCommitLog() throws SQLException {
+               Auditable2 a1 = context.newObject(Auditable2.class);
+               a1.setCharProperty1("yy");
+               a1.setCharProperty2("zz");
+
+               Auditable2 a2 = context.newObject(Auditable2.class);
+               a2.setCharProperty1("yy");
+               a2.setCharProperty2("zz");
+               context.commitChanges();
+
+               List<Object[]> logs = auditLog.selectAll();
+               assertEquals(2, logs.size());
+       }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModuleBuilderTest.java
----------------------------------------------------------------------
diff --git 
a/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModuleBuilderTest.java
 
b/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModuleBuilderTest.java
new file mode 100644
index 0000000..48a9d6c
--- /dev/null
+++ 
b/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModuleBuilderTest.java
@@ -0,0 +1,67 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.lifecycle.postcommit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.di.DIBootstrap;
+import org.apache.cayenne.di.Injector;
+import org.apache.cayenne.di.Key;
+import org.apache.cayenne.di.Module;
+import org.apache.cayenne.lifecycle.changemap.ChangeMap;
+import org.junit.Test;
+
+public class PostCommitModuleBuilderTest {
+
+       @Test
+       public void testListener_Object() {
+
+               L listener = new L();
+               Module m = 
PostCommitModuleBuilder.builder().listener(listener).build();
+
+               Injector i = DIBootstrap.createInjector(m);
+               List<PostCommitListener> listeners = 
i.getInstance(Key.getListOf(PostCommitListener.class));
+               assertEquals(1, listeners.size());
+               assertTrue(listeners.contains(listener));
+       }
+
+       @Test
+       public void testListener_Class() {
+
+               Module m = 
PostCommitModuleBuilder.builder().listener(L.class).build();
+
+               Injector i = DIBootstrap.createInjector(m);
+               List<PostCommitListener> listeners = 
i.getInstance(Key.getListOf(PostCommitListener.class));
+               assertEquals(1, listeners.size());
+               assertTrue(listeners.get(0) instanceof L);
+       }
+
+       public static class L implements PostCommitListener {
+
+               @Override
+               public void onPostCommit(ObjectContext originatingContext, 
ChangeMap changes) {
+                       // do nothing.
+               }
+       }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModuleProviderTest.java
----------------------------------------------------------------------
diff --git 
a/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModuleProviderTest.java
 
b/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModuleProviderTest.java
new file mode 100644
index 0000000..cbf8152
--- /dev/null
+++ 
b/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModuleProviderTest.java
@@ -0,0 +1,32 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.lifecycle.postcommit;
+
+import org.apache.cayenne.configuration.server.CayenneServerModuleProvider;
+import org.apache.cayenne.unit.util.ModuleProviderChecker;
+import org.junit.Test;
+
+public class PostCommitModuleProviderTest {
+
+    @Test
+    public void testAutoLoadable() {
+        
ModuleProviderChecker.testProviderPresent(PostCommitServerModuleProvider.class, 
CayenneServerModuleProvider.class);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/unit/AuditableServerCase.java
----------------------------------------------------------------------
diff --git 
a/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/unit/AuditableServerCase.java
 
b/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/unit/AuditableServerCase.java
new file mode 100644
index 0000000..2fb057b
--- /dev/null
+++ 
b/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/unit/AuditableServerCase.java
@@ -0,0 +1,88 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.lifecycle.unit;
+
+import org.apache.cayenne.configuration.server.ServerRuntime;
+import org.apache.cayenne.configuration.server.ServerRuntimeBuilder;
+import org.apache.cayenne.test.jdbc.DBHelper;
+import org.apache.cayenne.test.jdbc.TableHelper;
+import org.junit.After;
+import org.junit.Before;
+
+/**
+ * A superclass of integration tests for cayenne-lifecycle.
+ */
+public abstract class AuditableServerCase {
+
+       protected ServerRuntime runtime;
+
+       protected TableHelper auditable1;
+       protected TableHelper auditableChild1;
+
+       protected TableHelper auditable2;
+       protected TableHelper auditableChild3;
+
+       protected TableHelper auditable3;
+       protected TableHelper auditable4;
+
+       protected TableHelper auditLog;
+
+       @Before
+       public void startCayenne() throws Exception {
+               this.runtime = configureCayenne().build();
+
+               DBHelper dbHelper = new DBHelper(runtime.getDataSource());
+
+               this.auditLog = new TableHelper(dbHelper, 
"AUDIT_LOG").setColumns("ID", "LOG");
+
+               this.auditable1 = new TableHelper(dbHelper, 
"AUDITABLE1").setColumns("ID", "CHAR_PROPERTY1");
+
+               this.auditableChild1 = new TableHelper(dbHelper, 
"AUDITABLE_CHILD1").setColumns("ID", "AUDITABLE1_ID",
+                               "CHAR_PROPERTY1");
+
+               this.auditable2 = new TableHelper(dbHelper, 
"AUDITABLE2").setColumns("ID", "CHAR_PROPERTY1", "CHAR_PROPERTY2");
+
+               this.auditableChild3 = new TableHelper(dbHelper, 
"AUDITABLE_CHILD3").setColumns("ID", "AUDITABLE2_ID",
+                               "CHAR_PROPERTY1", "CHAR_PROPERTY2");
+
+               this.auditable3 = new TableHelper(dbHelper, 
"AUDITABLE3").setColumns("ID", "CHAR_PROPERTY1", "CHAR_PROPERTY2");
+               this.auditable4 = new TableHelper(dbHelper, 
"AUDITABLE4").setColumns("ID", "CHAR_PROPERTY1", "CHAR_PROPERTY2",
+                               "AUDITABLE3_ID");
+
+               this.auditableChild1.deleteAll();
+               this.auditable1.deleteAll();
+               this.auditableChild3.deleteAll();
+               this.auditable2.deleteAll();
+               this.auditable4.deleteAll();
+               this.auditable3.deleteAll();
+
+               this.auditLog.deleteAll();
+       }
+
+       protected ServerRuntimeBuilder configureCayenne() {
+               return 
ServerRuntime.builder().addConfig("cayenne-lifecycle.xml");
+       }
+
+       @After
+       public void shutdownCayenne() {
+               if (runtime != null) {
+                       runtime.shutdown();
+               }
+       }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/unit/FlattenedServerCase.java
----------------------------------------------------------------------
diff --git 
a/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/unit/FlattenedServerCase.java
 
b/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/unit/FlattenedServerCase.java
new file mode 100644
index 0000000..b9d1b0c
--- /dev/null
+++ 
b/cayenne-postcommit/src/test/java/org/apache/cayenne/lifecycle/unit/FlattenedServerCase.java
@@ -0,0 +1,62 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.lifecycle.unit;
+
+import org.apache.cayenne.configuration.server.ServerRuntime;
+import org.apache.cayenne.configuration.server.ServerRuntimeBuilder;
+import org.apache.cayenne.test.jdbc.DBHelper;
+import org.apache.cayenne.test.jdbc.TableHelper;
+import org.junit.After;
+import org.junit.Before;
+
+public class FlattenedServerCase {
+
+       protected ServerRuntime runtime;
+
+       protected TableHelper e3;
+       protected TableHelper e4;
+       protected TableHelper e34;
+
+       @Before
+       public void startCayenne() throws Exception {
+               this.runtime = configureCayenne().build();
+
+               DBHelper dbHelper = new DBHelper(runtime.getDataSource());
+
+               this.e3 = new TableHelper(dbHelper, "E3").setColumns("ID");
+               this.e4 = new TableHelper(dbHelper, "E4").setColumns("ID");
+               this.e34 = new TableHelper(dbHelper, "E34").setColumns("E3_ID", 
"E4_ID");
+
+               this.e34.deleteAll();
+               this.e3.deleteAll();
+
+       }
+
+       protected ServerRuntimeBuilder configureCayenne() {
+               return 
ServerRuntime.builder().addConfig("cayenne-lifecycle.xml");
+       }
+
+       @After
+       public void shutdownCayenne() {
+               if (runtime != null) {
+                       runtime.shutdown();
+               }
+       }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-postcommit/src/test/resources/cayenne-lifecycle.xml
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/test/resources/cayenne-lifecycle.xml 
b/cayenne-postcommit/src/test/resources/cayenne-lifecycle.xml
new file mode 100644
index 0000000..5b9a83e
--- /dev/null
+++ b/cayenne-postcommit/src/test/resources/cayenne-lifecycle.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<domain project-version="9">
+       <map name="lifecycle-map"/>
+
+       <node name="lifecycle-db"
+                
factory="org.apache.cayenne.configuration.server.XMLPoolingDataSourceFactory"
+                
schema-update-strategy="org.apache.cayenne.access.dbsync.CreateIfNoSchemaStrategy"
+               >
+               <map-ref name="lifecycle-map"/>
+               <data-source>
+                       <driver value="org.hsqldb.jdbcDriver"/>
+                       <url value="jdbc:hsqldb:mem:lifecycle"/>
+                       <connectionPool min="1" max="1"/>
+                       <login userName="sa"/>
+               </data-source>
+       </node>
+</domain>

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-postcommit/src/test/resources/lifecycle-map.map.xml
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/test/resources/lifecycle-map.map.xml 
b/cayenne-postcommit/src/test/resources/lifecycle-map.map.xml
new file mode 100644
index 0000000..d7a4a4a
--- /dev/null
+++ b/cayenne-postcommit/src/test/resources/lifecycle-map.map.xml
@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  ~   Licensed to the Apache Software Foundation (ASF) under one
+  ~  or more contributor license agreements.  See the NOTICE file
+  ~  distributed with this work for additional information
+  ~  regarding copyright ownership.  The ASF licenses this file
+  ~  to you under the Apache License, Version 2.0 (the
+  ~  "License"); you may not use this file except in compliance
+  ~  with the License.  You may obtain a copy of the License at
+  ~
+  ~    http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~  Unless required by applicable law or agreed to in writing,
+  ~  software distributed under the License is distributed on an
+  ~  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~  KIND, either express or implied.  See the License for the
+  ~  specific language governing permissions and limitations
+  ~  under the License.
+  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
+
+<data-map xmlns="http://cayenne.apache.org/schema/9/modelMap";
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+        xsi:schemaLocation="http://cayenne.apache.org/schema/9/modelMap 
http://cayenne.apache.org/schema/9/modelMap.xsd";
+        project-version="9">
+       <property name="defaultPackage" 
value="org.apache.cayenne.lifecycle.db"/>
+       <db-entity name="AUDITABLE1">
+               <db-attribute name="CHAR_PROPERTY1" type="VARCHAR" 
length="200"/>
+               <db-attribute name="ID" type="INTEGER" isPrimaryKey="true" 
isMandatory="true"/>
+       </db-entity>
+       <db-entity name="AUDITABLE2">
+               <db-attribute name="CHAR_PROPERTY1" type="VARCHAR" 
length="200"/>
+               <db-attribute name="CHAR_PROPERTY2" type="VARCHAR" 
length="200"/>
+               <db-attribute name="ID" type="INTEGER" isPrimaryKey="true" 
isMandatory="true"/>
+       </db-entity>
+       <db-entity name="AUDITABLE3">
+               <db-attribute name="CHAR_PROPERTY1" type="VARCHAR" 
length="200"/>
+               <db-attribute name="CHAR_PROPERTY2" type="VARCHAR" 
length="200"/>
+               <db-attribute name="ID" type="INTEGER" isPrimaryKey="true" 
isMandatory="true"/>
+       </db-entity>
+       <db-entity name="AUDITABLE4">
+               <db-attribute name="AUDITABLE3_ID" type="INTEGER"/>
+               <db-attribute name="CHAR_PROPERTY1" type="VARCHAR" 
length="200"/>
+               <db-attribute name="CHAR_PROPERTY2" type="VARCHAR" 
length="200"/>
+               <db-attribute name="ID" type="INTEGER" isPrimaryKey="true" 
isMandatory="true"/>
+       </db-entity>
+       <db-entity name="AUDITABLE_CHILD1">
+               <db-attribute name="AUDITABLE1_ID" type="INTEGER"/>
+               <db-attribute name="CHAR_PROPERTY1" type="VARCHAR" 
length="200"/>
+               <db-attribute name="ID" type="INTEGER" isPrimaryKey="true" 
isMandatory="true"/>
+       </db-entity>
+       <db-entity name="AUDITABLE_CHILD3">
+               <db-attribute name="AUDITABLE2_ID" type="INTEGER"/>
+               <db-attribute name="CHAR_PROPERTY1" type="VARCHAR" 
length="200"/>
+               <db-attribute name="CHAR_PROPERTY2" type="VARCHAR" 
length="200"/>
+               <db-attribute name="ID" type="INTEGER" isPrimaryKey="true" 
isMandatory="true"/>
+       </db-entity>
+       <db-entity name="AUDIT_LOG">
+               <db-attribute name="ID" type="INTEGER" isPrimaryKey="true" 
isMandatory="true"/>
+               <db-attribute name="LOG" type="CLOB"/>
+       </db-entity>
+       <db-entity name="E1">
+               <db-attribute name="ID" type="BIGINT" isPrimaryKey="true" 
isMandatory="true"/>
+       </db-entity>
+       <db-entity name="E2">
+               <db-attribute name="ID" type="BIGINT" isPrimaryKey="true" 
isMandatory="true"/>
+       </db-entity>
+       <db-entity name="E3">
+               <db-attribute name="ID" type="BIGINT" isPrimaryKey="true" 
isMandatory="true"/>
+       </db-entity>
+       <db-entity name="E34">
+               <db-attribute name="E3_ID" type="BIGINT" isPrimaryKey="true" 
isMandatory="true"/>
+               <db-attribute name="E4_ID" type="BIGINT" isPrimaryKey="true" 
isMandatory="true"/>
+       </db-entity>
+       <db-entity name="E4">
+               <db-attribute name="ID" type="BIGINT" isPrimaryKey="true" 
isMandatory="true"/>
+       </db-entity>
+       <obj-entity name="AuditLog" 
className="org.apache.cayenne.lifecycle.db.AuditLog" dbEntityName="AUDIT_LOG">
+               <obj-attribute name="log" type="java.lang.String" 
db-attribute-path="LOG"/>
+       </obj-entity>
+       <obj-entity name="Auditable1" 
className="org.apache.cayenne.lifecycle.db.Auditable1" 
dbEntityName="AUDITABLE1">
+               <obj-attribute name="charProperty1" type="java.lang.String" 
db-attribute-path="CHAR_PROPERTY1"/>
+       </obj-entity>
+       <obj-entity name="Auditable2" 
className="org.apache.cayenne.lifecycle.db.Auditable2" 
dbEntityName="AUDITABLE2">
+               <obj-attribute name="charProperty1" type="java.lang.String" 
db-attribute-path="CHAR_PROPERTY1"/>
+               <obj-attribute name="charProperty2" type="java.lang.String" 
db-attribute-path="CHAR_PROPERTY2"/>
+       </obj-entity>
+       <obj-entity name="Auditable3" 
className="org.apache.cayenne.lifecycle.db.Auditable3" 
dbEntityName="AUDITABLE3">
+               <obj-attribute name="charProperty1" type="java.lang.String" 
db-attribute-path="CHAR_PROPERTY1"/>
+               <obj-attribute name="charProperty2" type="java.lang.String" 
db-attribute-path="CHAR_PROPERTY2"/>
+       </obj-entity>
+       <obj-entity name="Auditable4" 
className="org.apache.cayenne.lifecycle.db.Auditable4" 
dbEntityName="AUDITABLE4">
+               <obj-attribute name="charProperty1" type="java.lang.String" 
db-attribute-path="CHAR_PROPERTY1"/>
+               <obj-attribute name="charProperty2" type="java.lang.String" 
db-attribute-path="CHAR_PROPERTY2"/>
+       </obj-entity>
+       <obj-entity name="AuditableChild1" 
className="org.apache.cayenne.lifecycle.db.AuditableChild1" 
dbEntityName="AUDITABLE_CHILD1">
+               <obj-attribute name="charProperty1" type="java.lang.String" 
db-attribute-path="CHAR_PROPERTY1"/>
+       </obj-entity>
+       <obj-entity name="AuditableChild3" 
className="org.apache.cayenne.lifecycle.db.AuditableChild3" 
dbEntityName="AUDITABLE_CHILD3">
+               <obj-attribute name="charProperty1" type="java.lang.String" 
db-attribute-path="CHAR_PROPERTY1"/>
+               <obj-attribute name="charProperty2" type="java.lang.String" 
db-attribute-path="CHAR_PROPERTY2"/>
+       </obj-entity>
+       <obj-entity name="E1" className="org.apache.cayenne.lifecycle.db.E1" 
dbEntityName="E1">
+       </obj-entity>
+       <obj-entity name="E2" className="org.apache.cayenne.lifecycle.db.E2" 
dbEntityName="E2">
+       </obj-entity>
+       <obj-entity name="E3" className="org.apache.cayenne.lifecycle.db.E3" 
dbEntityName="E3">
+       </obj-entity>
+       <obj-entity name="E4" className="org.apache.cayenne.lifecycle.db.E4" 
dbEntityName="E4">
+       </obj-entity>
+       <db-relationship name="children1" source="AUDITABLE1" 
target="AUDITABLE_CHILD1" toMany="true">
+               <db-attribute-pair source="ID" target="AUDITABLE1_ID"/>
+       </db-relationship>
+       <db-relationship name="children" source="AUDITABLE2" 
target="AUDITABLE_CHILD3" toMany="true">
+               <db-attribute-pair source="ID" target="AUDITABLE2_ID"/>
+       </db-relationship>
+       <db-relationship name="auditable4s" source="AUDITABLE3" 
target="AUDITABLE4" toMany="true">
+               <db-attribute-pair source="ID" target="AUDITABLE3_ID"/>
+       </db-relationship>
+       <db-relationship name="auditable3" source="AUDITABLE4" 
target="AUDITABLE3" toMany="false">
+               <db-attribute-pair source="AUDITABLE3_ID" target="ID"/>
+       </db-relationship>
+       <db-relationship name="parent" source="AUDITABLE_CHILD1" 
target="AUDITABLE1" toMany="false">
+               <db-attribute-pair source="AUDITABLE1_ID" target="ID"/>
+       </db-relationship>
+       <db-relationship name="parent" source="AUDITABLE_CHILD3" 
target="AUDITABLE2" toMany="false">
+               <db-attribute-pair source="AUDITABLE2_ID" target="ID"/>
+       </db-relationship>
+       <db-relationship name="e34s" source="E3" target="E34" 
toDependentPK="true" toMany="true">
+               <db-attribute-pair source="ID" target="E3_ID"/>
+       </db-relationship>
+       <db-relationship name="e3" source="E34" target="E3" toMany="false">
+               <db-attribute-pair source="E3_ID" target="ID"/>
+       </db-relationship>
+       <db-relationship name="e4" source="E34" target="E4" toMany="false">
+               <db-attribute-pair source="E4_ID" target="ID"/>
+       </db-relationship>
+       <db-relationship name="e34s" source="E4" target="E34" 
toDependentPK="true" toMany="true">
+               <db-attribute-pair source="ID" target="E4_ID"/>
+       </db-relationship>
+       <obj-relationship name="children1" source="Auditable1" 
target="AuditableChild1" deleteRule="Deny" db-relationship-path="children1"/>
+       <obj-relationship name="children" source="Auditable2" 
target="AuditableChild3" deleteRule="Deny" db-relationship-path="children"/>
+       <obj-relationship name="auditable4s" source="Auditable3" 
target="Auditable4" deleteRule="Deny" db-relationship-path="auditable4s"/>
+       <obj-relationship name="auditable3" source="Auditable4" 
target="Auditable3" deleteRule="Nullify" db-relationship-path="auditable3"/>
+       <obj-relationship name="parent" source="AuditableChild1" 
target="Auditable1" deleteRule="Nullify" db-relationship-path="parent"/>
+       <obj-relationship name="parent" source="AuditableChild3" 
target="Auditable2" deleteRule="Nullify" db-relationship-path="parent"/>
+       <obj-relationship name="e4s" source="E3" target="E4" deleteRule="Deny" 
db-relationship-path="e34s.e4"/>
+       <obj-relationship name="e3s" source="E4" target="E3" deleteRule="Deny" 
db-relationship-path="e34s.e3"/>
+</data-map>

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-server/src/main/java/org/apache/cayenne/ashwood/SortWeight.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/ashwood/SortWeight.java 
b/cayenne-server/src/main/java/org/apache/cayenne/ashwood/SortWeight.java
new file mode 100644
index 0000000..5719a99
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/ashwood/SortWeight.java
@@ -0,0 +1,45 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.ashwood;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation that defines the insertion sorting "weight" of an entity that 
is used
+ * when sorting DB operations. This annotation allows to override the 
topological sorting
+ * algorithm used by Cayenne by default in special occasions.
+ * 
+ * @since 3.1, since 4.0 moved to cayenne-server from cayenne-lifecycle
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface SortWeight {
+
+    /**
+     * Returns the "weight" of the entity used for the purpose of the DB 
operations
+     * sorting. Entities with lower values will be inserted before entities 
with higher
+     * values. The opposite is true for the delete operations.
+     */
+    int value() default 1;
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-server/src/main/java/org/apache/cayenne/ashwood/WeightedAshwoodEntitySorter.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/main/java/org/apache/cayenne/ashwood/WeightedAshwoodEntitySorter.java
 
b/cayenne-server/src/main/java/org/apache/cayenne/ashwood/WeightedAshwoodEntitySorter.java
new file mode 100644
index 0000000..99df6a6
--- /dev/null
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/ashwood/WeightedAshwoodEntitySorter.java
@@ -0,0 +1,120 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.ashwood;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.commons.collections.comparators.ReverseComparator;
+
+/**
+ * EntitySorter that takes into account entity "weights", and otherwise 
delegating to
+ * another (topological) sorter.
+ * 
+ * @since 3.1, since 4.0 moved to cayenne-server from cayenne-lifecycle
+ */
+public class WeightedAshwoodEntitySorter extends AshwoodEntitySorter {
+
+    private Comparator<DbEntity> weightedDbEntityComparator;
+    private Comparator<ObjEntity> weightedObjEntityComparator;
+
+    protected Map<DbEntity, Integer> entityWeights;
+
+    public WeightedAshwoodEntitySorter() {
+        this.weightedDbEntityComparator = new WeightedDbEntityComparator();
+        this.weightedObjEntityComparator = new WeightedObjEntityComparator();
+        this.entityWeights = Collections.emptyMap();
+    }
+
+    @Override
+    protected void doIndexSorter() {
+        super.doIndexSorter();
+
+        entityWeights = new HashMap<>();
+
+        for (ObjEntity entity : entityResolver.getObjEntities()) {
+            addWeightForEntity(entity);
+        }
+    }
+
+    protected void addWeightForEntity(ObjEntity entity) {
+        Class<?> type = entityResolver
+                .getClassDescriptor(entity.getName())
+                .getObjectClass();
+        SortWeight weight = type.getAnnotation(SortWeight.class);
+        if (weight != null) {
+            entityWeights.put(entity.getDbEntity(), weight.value());
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected Comparator<DbEntity> getDbEntityComparator(boolean 
dependantFirst) {
+        Comparator<DbEntity> c = weightedDbEntityComparator;
+        if (dependantFirst) {
+            c = new ReverseComparator(c);
+        }
+        return c;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected Comparator<ObjEntity> getObjEntityComparator(boolean 
dependantFirst) {
+        Comparator<ObjEntity> c = weightedObjEntityComparator;
+        if (dependantFirst) {
+            c = new ReverseComparator(c);
+        }
+        return c;
+    }
+
+    private int getWeight(DbEntity e) {
+        Integer w = entityWeights.get(e);
+        return w != null ? w : 1;
+    }
+
+    private final class WeightedDbEntityComparator implements 
Comparator<DbEntity> {
+
+        public int compare(DbEntity t1, DbEntity t2) {
+            if (t1 == t2) {
+                return 0;
+            }
+
+            int delta = getWeight(t1) - getWeight(t2);
+            return delta != 0 ? delta : dbEntityComparator.compare(t1, t2);
+        }
+    }
+
+    private final class WeightedObjEntityComparator implements 
Comparator<ObjEntity> {
+
+        public int compare(ObjEntity o1, ObjEntity o2) {
+            if (o1 == o2) {
+                return 0;
+            }
+
+            DbEntity t1 = o1.getDbEntity();
+            DbEntity t2 = o2.getDbEntity();
+
+            return weightedDbEntityComparator.compare(t1, t2);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-server/src/test/java/org/apache/cayenne/ashwood/WeightedAshwoodEntitySorterIT.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/ashwood/WeightedAshwoodEntitySorterIT.java
 
b/cayenne-server/src/test/java/org/apache/cayenne/ashwood/WeightedAshwoodEntitySorterIT.java
new file mode 100644
index 0000000..4f50aea
--- /dev/null
+++ 
b/cayenne-server/src/test/java/org/apache/cayenne/ashwood/WeightedAshwoodEntitySorterIT.java
@@ -0,0 +1,70 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.ashwood;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+@UseServerRuntime(CayenneProjects.WEIGHTED_SORT_PROJECT)
+public class WeightedAshwoodEntitySorterIT extends ServerCase {
+
+    @Inject
+    protected ObjectContext context;
+
+    EntityResolver resolver;
+
+    @Before
+    public void setUp() throws Exception {
+        this.resolver = context.getEntityResolver();
+    }
+
+    @Test
+    public void testSortDbEntities() {
+        // since it is impossible to ensure non-coincidental sort order of 
unrelated
+        // DbEntities (without overriding DbEntity.hashCode()), we'll test on 
2 entities
+        // with a relationship, and reverse the topological order with 
SortWeight annotation.
+
+        List<DbEntity> eSorted = 
Arrays.asList(resolver.getDbEntity("SORT_DEP"), 
resolver.getDbEntity("SORT_ROOT"));
+
+        List<DbEntity> e1 = Arrays.asList(resolver.getDbEntity("SORT_ROOT"), 
resolver.getDbEntity("SORT_DEP"));
+
+        List<DbEntity> e2 = Arrays.asList(resolver.getDbEntity("SORT_DEP"), 
resolver.getDbEntity("SORT_ROOT"));
+
+        WeightedAshwoodEntitySorter sorter = new WeightedAshwoodEntitySorter();
+        sorter.setEntityResolver(resolver);
+
+        sorter.sortDbEntities(e1, false);
+        assertEquals(eSorted, e1);
+
+        sorter.sortDbEntities(e2, false);
+        assertEquals(eSorted, e2);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-server/src/test/java/org/apache/cayenne/testdo/weighted_sort/SortDep.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/testdo/weighted_sort/SortDep.java
 
b/cayenne-server/src/test/java/org/apache/cayenne/testdo/weighted_sort/SortDep.java
new file mode 100644
index 0000000..3d26ace
--- /dev/null
+++ 
b/cayenne-server/src/test/java/org/apache/cayenne/testdo/weighted_sort/SortDep.java
@@ -0,0 +1,9 @@
+package org.apache.cayenne.testdo.weighted_sort;
+
+import org.apache.cayenne.testdo.weighted_sort.auto._SortDep;
+
+public class SortDep extends _SortDep {
+
+    private static final long serialVersionUID = 1L; 
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-server/src/test/java/org/apache/cayenne/testdo/weighted_sort/SortRoot.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/testdo/weighted_sort/SortRoot.java
 
b/cayenne-server/src/test/java/org/apache/cayenne/testdo/weighted_sort/SortRoot.java
new file mode 100644
index 0000000..cb0bdc4
--- /dev/null
+++ 
b/cayenne-server/src/test/java/org/apache/cayenne/testdo/weighted_sort/SortRoot.java
@@ -0,0 +1,11 @@
+package org.apache.cayenne.testdo.weighted_sort;
+
+import org.apache.cayenne.ashwood.SortWeight;
+import org.apache.cayenne.testdo.weighted_sort.auto._SortRoot;
+
+@SortWeight(2)
+public class SortRoot extends _SortRoot {
+
+    private static final long serialVersionUID = 1L; 
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-server/src/test/java/org/apache/cayenne/testdo/weighted_sort/auto/_SortDep.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/testdo/weighted_sort/auto/_SortDep.java
 
b/cayenne-server/src/test/java/org/apache/cayenne/testdo/weighted_sort/auto/_SortDep.java
new file mode 100644
index 0000000..3f6f3ca
--- /dev/null
+++ 
b/cayenne-server/src/test/java/org/apache/cayenne/testdo/weighted_sort/auto/_SortDep.java
@@ -0,0 +1,30 @@
+package org.apache.cayenne.testdo.weighted_sort.auto;
+
+import org.apache.cayenne.CayenneDataObject;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.testdo.weighted_sort.SortRoot;
+
+/**
+ * Class _SortDep was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _SortDep extends CayenneDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String ID_PK_COLUMN = "ID";
+
+    public static final Property<SortRoot> ROOT = Property.create("root", 
SortRoot.class);
+
+    public void setRoot(SortRoot root) {
+        setToOneTarget("root", root, true);
+    }
+
+    public SortRoot getRoot() {
+        return (SortRoot)readProperty("root");
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-server/src/test/java/org/apache/cayenne/testdo/weighted_sort/auto/_SortRoot.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/testdo/weighted_sort/auto/_SortRoot.java
 
b/cayenne-server/src/test/java/org/apache/cayenne/testdo/weighted_sort/auto/_SortRoot.java
new file mode 100644
index 0000000..06d8d60
--- /dev/null
+++ 
b/cayenne-server/src/test/java/org/apache/cayenne/testdo/weighted_sort/auto/_SortRoot.java
@@ -0,0 +1,35 @@
+package org.apache.cayenne.testdo.weighted_sort.auto;
+
+import java.util.List;
+
+import org.apache.cayenne.CayenneDataObject;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.testdo.weighted_sort.SortDep;
+
+/**
+ * Class _SortRoot was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _SortRoot extends CayenneDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String ID_PK_COLUMN = "ID";
+
+    public static final Property<List<SortDep>> DEPS = Property.create("deps", 
List.class);
+
+    public void addToDeps(SortDep obj) {
+        addToManyTarget("deps", obj, true);
+    }
+    public void removeFromDeps(SortDep obj) {
+        removeToManyTarget("deps", obj, true);
+    }
+    @SuppressWarnings("unchecked")
+    public List<SortDep> getDeps() {
+        return (List<SortDep>)readProperty("deps");
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
 
b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
index 5710bcd..0cd6372 100644
--- 
a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
+++ 
b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
@@ -79,5 +79,5 @@ public class CayenneProjects {
     public static final String UNSUPPORTED_DISTINCT_TYPES_PROJECT = 
"cayenne-unsupported-distinct-types.xml";
     public static final String UUID_PROJECT = "cayenne-uuid.xml";
     public static final String CUSTOM_NAME_PROJECT = "custom-name-file.xml";
-
+    public static final String WEIGHTED_SORT_PROJECT = 
"cayenne-weighted-sort.xml";
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
----------------------------------------------------------------------
diff --git 
a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
 
b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
index ec5d332..00121a5 100644
--- 
a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
+++ 
b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
@@ -80,7 +80,7 @@ public class SchemaBuilder {
                        "table-primitives.map.xml", "generic.map.xml", 
"map-db1.map.xml", "map-db2.map.xml", "embeddable.map.xml",
                        "qualified.map.xml", "quoted-identifiers.map.xml", 
"inheritance-single-table1.map.xml",
                        "inheritance-vertical.map.xml", "oneway-rels.map.xml", 
"unsupported-distinct-types.map.xml",
-                       "array-type.map.xml", "cay-2032.map.xml" };
+                       "array-type.map.xml", "cay-2032.map.xml", 
"weighted-sort.map.xml" };
 
        // hardcoded dependent entities that should be excluded
        // if LOBs are not supported
@@ -246,7 +246,7 @@ public class SchemaBuilder {
 
        protected List<DbEntity> dbEntitiesInDeleteOrder(DataMap dataMap) {
                DataMap map = domain.getDataMap(dataMap.getName());
-               List<DbEntity> entities = new 
ArrayList<DbEntity>(map.getDbEntities());
+               List<DbEntity> entities = new ArrayList<>(map.getDbEntities());
 
                dbEntitiesFilter(entities);
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-server/src/test/resources/cayenne-weighted-sort.xml
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/resources/cayenne-weighted-sort.xml 
b/cayenne-server/src/test/resources/cayenne-weighted-sort.xml
new file mode 100644
index 0000000..cc939f7
--- /dev/null
+++ b/cayenne-server/src/test/resources/cayenne-weighted-sort.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<domain project-version="9">
+       <map name="weighted-sort"/>
+</domain>

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-server/src/test/resources/weighted-sort.map.xml
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/resources/weighted-sort.map.xml 
b/cayenne-server/src/test/resources/weighted-sort.map.xml
new file mode 100644
index 0000000..ac01ec5
--- /dev/null
+++ b/cayenne-server/src/test/resources/weighted-sort.map.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<data-map xmlns="http://cayenne.apache.org/schema/9/modelMap";
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+        xsi:schemaLocation="http://cayenne.apache.org/schema/9/modelMap 
http://cayenne.apache.org/schema/9/modelMap.xsd";
+        project-version="9">
+       <property name="defaultPackage" 
value="org.apache.cayenne.testdo.weighted_sort"/>
+       <db-entity name="SORT_DEP">
+               <db-attribute name="ID" type="BIGINT" isPrimaryKey="true" 
isMandatory="true"/>
+               <db-attribute name="ROOT_ID" type="BIGINT" isMandatory="true"/>
+       </db-entity>
+       <db-entity name="SORT_ROOT">
+               <db-attribute name="ID" type="BIGINT" isPrimaryKey="true" 
isMandatory="true"/>
+       </db-entity>
+       <obj-entity name="SortDep" 
className="org.apache.cayenne.testdo.weighted_sort.SortDep" 
dbEntityName="SORT_DEP">
+       </obj-entity>
+       <obj-entity name="SortRoot" 
className="org.apache.cayenne.testdo.weighted_sort.SortRoot" 
dbEntityName="SORT_ROOT">
+       </obj-entity>
+       <db-relationship name="root" source="SORT_DEP" target="SORT_ROOT" 
toMany="false">
+               <db-attribute-pair source="ROOT_ID" target="ID"/>
+       </db-relationship>
+       <db-relationship name="deps" source="SORT_ROOT" target="SORT_DEP" 
toMany="true">
+               <db-attribute-pair source="ID" target="ROOT_ID"/>
+       </db-relationship>
+       <obj-relationship name="root" source="SortDep" target="SortRoot" 
deleteRule="Nullify" db-relationship-path="root"/>
+       <obj-relationship name="deps" source="SortRoot" target="SortDep" 
deleteRule="Deny" db-relationship-path="deps"/>
+</data-map>

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/docs/doc/src/main/resources/RELEASE-NOTES.txt
----------------------------------------------------------------------
diff --git a/docs/doc/src/main/resources/RELEASE-NOTES.txt 
b/docs/doc/src/main/resources/RELEASE-NOTES.txt
index 694e754..64ff8fa 100644
--- a/docs/doc/src/main/resources/RELEASE-NOTES.txt
+++ b/docs/doc/src/main/resources/RELEASE-NOTES.txt
@@ -20,6 +20,7 @@ CAY-2255 ObjectSelect improvement: columns as full entities
 CAY-2258 DI: type-safe binding of List and Map
 CAY-2259 QueryCache: support for referencing type-safe caches
 CAY-2261 Replace NamedQuery with MappedXYZ in *datamap.vm
+CAY-2262 Module auto-loading
 CAY-2266 Move EventBridge implementations into autoloadable modules
 CAY-2267 Contribute lifecycle events listeners via DI
 CAY-2268 DI: Refactor ListBuilder API ambiguities for before() / after() 
bindings
@@ -29,6 +30,7 @@ CAY-2271 ColumnSelect: support for prefetch and limit
 CAY-2272 ColumnSelect: methods to manually control DISTINCT clause
 CAY-2274 Modeler: Validate case when dependent PK is marked as “generated”
 CAY-2277 Create ClientRuntime with ClientRuntimeBuilder just like ServerRuntime
+CAY-2278 Extract cayenne-postcommit module from cayenne-lifecycle
 
 Bug Fixes:
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/docs/doc/src/main/resources/UPGRADE.txt
----------------------------------------------------------------------
diff --git a/docs/doc/src/main/resources/UPGRADE.txt 
b/docs/doc/src/main/resources/UPGRADE.txt
index d73c353..f3c538b 100644
--- a/docs/doc/src/main/resources/UPGRADE.txt
+++ b/docs/doc/src/main/resources/UPGRADE.txt
@@ -7,6 +7,13 @@ IMPORTANT: be sure to read all notes for the intermediate 
releases between your
 
 UPGRADING TO 4.0.M6
 
+* Per CAY-2278 The org.apache.cayenne.lifecycle.audit package (with the 
exception of AuditableChild annotation) and the
+       org.apache.cayenne.lifecycle.changeset package where deprecated.
+       Weighted graph sorter moved to cayenne-server into 
org.apache.cayenne.ashwood package.
+       Packages org.apache.cayenne.lifecycle.changemap, 
org.apache.cayenne.lifecycle.postcommit and Auditable annotation
+       where moved to the new cayenne-postcommit module.
+       Please change your code accordingly.
+
 * Per CAY-2277 ClientRuntime created with ClientRuntimeBuilder, direct 
instantiation of
        ClientRuntime is deprecated. Also whole ClientLocalRuntime class is 
deprecated, use instead
        ClientRuntimeBuilder.local() method.
@@ -19,9 +26,10 @@ UPGRADING TO 4.0.M6
        - cayenne-client-jetty
        - cayenne-protostuff (it also supports auto-loading by 
ServerRuntimeBuilder)
 
-       Also new modules are introduced and should be added to your pom.xml if 
the corresponding
+       Also new modules are extracted from the existing one and should be 
added to your pom.xml if the corresponding
        functionality is used by your project:
-       - cayenne-cache-invalidation (moved from cayenne-lifecycle)
+       - cayenne-cache-invalidation (was part of cayenne-lifecycle)
+       - cayenne-postcommit (was part of cayenne-lifecycle)
 
 * Per CAY-2259 InvalidationFunction returns CacheGroupDescriptor instead of 
simple String with cache group name,
        change your custom functions accordingly.

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index a3d82b6..f938dcb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -58,6 +58,7 @@
                <module>cayenne-ant</module>
                <module>cayenne-project</module>
                <module>cayenne-lifecycle</module>
+               <module>cayenne-postcommit</module>
                <module>cayenne-crypto</module>
                <module>cayenne-joda</module>
                <module>cayenne-dbcp2</module>

Reply via email to