CAY-2030 Capturing a stream of commit changes

Project: http://git-wip-us.apache.org/repos/asf/cayenne/repo
Commit: http://git-wip-us.apache.org/repos/asf/cayenne/commit/58c7c3b3
Tree: http://git-wip-us.apache.org/repos/asf/cayenne/tree/58c7c3b3
Diff: http://git-wip-us.apache.org/repos/asf/cayenne/diff/58c7c3b3

Branch: refs/heads/master
Commit: 58c7c3b37c1d774a794dfe335beb48327de7e08e
Parents: 7decb6c
Author: aadamchik <aadamc...@apache.org>
Authored: Fri Oct 9 15:32:20 2015 -0400
Committer: aadamchik <aadamc...@apache.org>
Committed: Fri Oct 9 15:42:14 2015 -0400

----------------------------------------------------------------------
 cayenne-lifecycle/pom.xml                       | 104 ++++---
 .../cayenne/lifecycle/audit/Auditable.java      |   5 +
 .../lifecycle/changemap/AttributeChange.java    |  32 ++
 .../cayenne/lifecycle/changemap/ChangeMap.java  |  42 +++
 .../changemap/MutableAttributeChange.java       |  46 +++
 .../lifecycle/changemap/MutableChangeMap.java   |  78 +++++
 .../changemap/MutableObjectChange.java          | 178 +++++++++++
 .../MutableToManyRelationshipChange.java        |  64 ++++
 .../MutableToOneRelationshipChange.java         |  48 +++
 .../lifecycle/changemap/ObjectChange.java       |  43 +++
 .../lifecycle/changemap/ObjectChangeType.java   |  29 ++
 .../changemap/ToManyRelationshipChange.java     |  35 +++
 .../changemap/ToOneRelationshipChange.java      |  31 ++
 .../lifecycle/postcommit/Confidential.java      |  23 ++
 .../postcommit/DeletedDiffProcessor.java        | 140 +++++++++
 .../lifecycle/postcommit/DiffFilter.java        |  88 ++++++
 .../lifecycle/postcommit/DiffProcessor.java     | 104 +++++++
 .../lifecycle/postcommit/PostCommitFilter.java  | 118 ++++++++
 .../postcommit/PostCommitListener.java          |  32 ++
 .../postcommit/PostCommitModuleBuilder.java     | 121 ++++++++
 .../meta/AuditablePostCommitEntityFactory.java  | 101 +++++++
 .../meta/DefaultPostCommitEntity.java           |  78 +++++
 .../meta/IncludeAllPostCommitEntityFactory.java |  51 ++++
 .../postcommit/meta/PostCommitEntity.java       |  34 +++
 .../meta/PostCommitEntityFactory.java           |  29 ++
 .../lifecycle/audit/AuditableFilterIT.java      | 249 ++++++++++++++++
 .../audit/AuditableFilter_InRuntime_Test.java   | 294 ------------------
 .../apache/cayenne/lifecycle/db/Auditable2.java |   2 +-
 .../postcommit/PostCommitFilter_AllIT.java      | 295 +++++++++++++++++++
 .../postcommit/PostCommitFilter_FilteredIT.java | 164 +++++++++++
 .../postcommit/PostCommitModuleBuilderTest.java |  67 +++++
 .../lifecycle/unit/LifecycleServerCase.java     |  83 ++++++
 docs/doc/src/main/resources/RELEASE-NOTES.txt   |   1 +
 33 files changed, 2460 insertions(+), 349 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/pom.xml
----------------------------------------------------------------------
diff --git a/cayenne-lifecycle/pom.xml b/cayenne-lifecycle/pom.xml
index d1f8110..e37f38d 100644
--- a/cayenne-lifecycle/pom.xml
+++ b/cayenne-lifecycle/pom.xml
@@ -1,23 +1,16 @@
 <?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.   
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/maven-v4_0_0.xsd";>
+<!-- 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. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+       xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/maven-v4_0_0.xsd";>
        <modelVersion>4.0.0</modelVersion>
        <parent>
                <artifactId>cayenne-parent</artifactId>
@@ -28,7 +21,7 @@
        <name>Cayenne Lifecycle Utilities</name>
        <packaging>jar</packaging>
        <dependencies>
-               
+
                <!-- Compile dependencies -->
                <dependency>
                        <groupId>org.apache.cayenne</groupId>
@@ -59,22 +52,22 @@
                        <scope>test</scope>
                </dependency>
                <dependency>
-               <groupId>org.slf4j</groupId>
-               <artifactId>jcl-over-slf4j</artifactId>
-               <scope>test</scope>
+                       <groupId>org.slf4j</groupId>
+                       <artifactId>jcl-over-slf4j</artifactId>
+                       <scope>test</scope>
                </dependency>
                <dependency>
-               <groupId>org.slf4j</groupId>
-               <artifactId>slf4j-api</artifactId>
-               <scope>test</scope>
+                       <groupId>org.slf4j</groupId>
+                       <artifactId>slf4j-api</artifactId>
+                       <scope>test</scope>
                </dependency>
                <dependency>
-               <groupId>org.slf4j</groupId>
-               <artifactId>slf4j-simple</artifactId>
-               <scope>test</scope>
+                       <groupId>org.slf4j</groupId>
+                       <artifactId>slf4j-simple</artifactId>
+                       <scope>test</scope>
                </dependency>
        </dependencies>
-               <build>
+       <build>
                <plugins>
                        <plugin>
                                
<artifactId>maven-remote-resources-plugin</artifactId>
@@ -86,30 +79,33 @@
                                        </execution>
                                </executions>
                        </plugin>
-        </plugins>
+                       <plugin>
+                               <groupId>org.apache.maven.plugins</groupId>
+                               <artifactId>maven-failsafe-plugin</artifactId>
+                       </plugin>
+               </plugins>
        </build>
-    <profiles>
-        <profile>
-            <id>code-quality</id>
+       <profiles>
+               <profile>
+                       <id>code-quality</id>
 
-            <activation>
-                <property>
-                    <name>!fast-and-dirty</name>
-                </property>
-            </activation>
-            <build>
-                <plugins>
-                    <plugin>
-                        <artifactId>maven-checkstyle-plugin</artifactId>
-                        <!--<configuration>
-                            
<suppressionsLocation>${project.basedir}/cayenne-checkstyle-suppression.xml</suppressionsLocation>
-                        </configuration>-->
-                    </plugin>
-                    <plugin>
-                        <artifactId>maven-pmd-plugin</artifactId>
-                    </plugin>
-                </plugins>
-            </build>
-        </profile>
-    </profiles>
+                       <activation>
+                               <property>
+                                       <name>!fast-and-dirty</name>
+                               </property>
+                       </activation>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               
<artifactId>maven-checkstyle-plugin</artifactId>
+                                               <!--<configuration> 
<suppressionsLocation>${project.basedir}/cayenne-checkstyle-suppression.xml</suppressionsLocation>
 
+                                                       </configuration> -->
+                                       </plugin>
+                                       <plugin>
+                                               
<artifactId>maven-pmd-plugin</artifactId>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+       </profiles>
 </project>

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/audit/Auditable.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/audit/Auditable.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/audit/Auditable.java
index 8f9386d..68a2732 100644
--- 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/audit/Auditable.java
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/audit/Auditable.java
@@ -37,4 +37,9 @@ import java.lang.annotation.Target;
 public @interface Auditable {
 
     String[] ignoredProperties() default {};
+    
+    /**
+     * @since 4.0
+     */
+    String[] confidential() default {};
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/AttributeChange.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/AttributeChange.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/AttributeChange.java
new file mode 100644
index 0000000..bdb9cb6
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/AttributeChange.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.changemap;
+
+/**
+ * Represents a change in a "value" property, which is either a scalar property
+ * or a to-one entity relationship.
+ * 
+ * @since 4.0
+ */
+public interface AttributeChange {
+
+       Object getOldValue();
+
+       Object getNewValue();
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ChangeMap.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ChangeMap.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ChangeMap.java
new file mode 100644
index 0000000..ec1c8e5
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ChangeMap.java
@@ -0,0 +1,42 @@
+/*****************************************************************
+ *   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.changemap;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.apache.cayenne.ObjectId;
+
+/**
+ * Represents a map of changes for a graph of persistent objects.
+ * 
+ * @since 4.0
+ */
+public interface ChangeMap {
+
+       /**
+        * Returns a map of changes. Note the same change sometimes can be 
present
+        * in the map twice. If ObjectId of an object has changed during the 
commit,
+        * the change will be accessible by both pre-commit and post-commit ID. 
To
+        * get unique changes, call {@link #getUniqueChanges()}.
+        */
+       Map<ObjectId, ? extends ObjectChange> getChanges();
+
+       Collection<? extends ObjectChange> getUniqueChanges();
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableAttributeChange.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableAttributeChange.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableAttributeChange.java
new file mode 100644
index 0000000..4d4ebbf
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableAttributeChange.java
@@ -0,0 +1,46 @@
+/*****************************************************************
+ *   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.changemap;
+
+/**
+ * @since 4.0
+ */
+public class MutableAttributeChange implements AttributeChange {
+
+       private Object oldValue;
+       private Object newValue;
+
+       public void setOldValue(Object oldValue) {
+               this.oldValue = oldValue;
+       }
+
+       public void setNewValue(Object value) {
+               this.newValue = value;
+       }
+
+       @Override
+       public Object getOldValue() {
+               return oldValue;
+       }
+
+       @Override
+       public Object getNewValue() {
+               return newValue;
+       }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableChangeMap.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableChangeMap.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableChangeMap.java
new file mode 100644
index 0000000..3df3043
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableChangeMap.java
@@ -0,0 +1,78 @@
+/*****************************************************************
+ *   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.changemap;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+import org.apache.cayenne.ObjectId;
+
+/**
+ * A mutable implementation of {@link ChangeMap}.
+ * 
+ * @since 4.0
+ */
+public class MutableChangeMap implements ChangeMap {
+
+       private Map<ObjectId, MutableObjectChange> changes;
+
+       public MutableObjectChange getOrCreate(ObjectId id, ObjectChangeType 
type) {
+               MutableObjectChange changeSet = getOrCreate(id);
+               changeSet.setType(type);
+               return changeSet;
+       }
+
+       private MutableObjectChange getOrCreate(ObjectId id) {
+
+               MutableObjectChange objectChange = changes != null ? 
changes.get(id) : null;
+
+               if (objectChange == null) {
+
+                       if (changes == null) {
+                               changes = new HashMap<>();
+                       }
+
+                       objectChange = new MutableObjectChange(id);
+                       changes.put(id, objectChange);
+               }
+
+               return objectChange;
+       }
+
+       public MutableObjectChange aliasId(ObjectId preCommitId, ObjectId 
postCommitId) {
+               MutableObjectChange changeSet = getOrCreate(preCommitId);
+               changeSet.setPostCommitId(postCommitId);
+               changes.put(postCommitId, changeSet);
+               return changeSet;
+       }
+
+       @Override
+       public Collection<? extends ObjectChange> getUniqueChanges() {
+               // ensure distinct change set
+               return changes == null ? Collections.<ObjectChange> emptySet() 
: new HashSet<>(changes.values());
+       }
+
+       @Override
+       public Map<ObjectId, ? extends ObjectChange> getChanges() {
+               return changes == null ? Collections.<ObjectId, ObjectChange> 
emptyMap() : changes;
+       }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableObjectChange.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableObjectChange.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableObjectChange.java
new file mode 100644
index 0000000..95c81bc
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableObjectChange.java
@@ -0,0 +1,178 @@
+/*****************************************************************
+ *   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.changemap;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.cayenne.ObjectId;
+
+/**
+ * A mutable implementation of {@link ObjectChange}.
+ * 
+ * @since 4.0
+ */
+public class MutableObjectChange implements ObjectChange {
+
+       private static final int[] TYPE_PRECEDENCE;
+
+       static {
+               TYPE_PRECEDENCE = new int[ObjectChangeType.values().length];
+
+               // decreasing precedence of operations when recording audits is 
DELETE,
+               // INSERT, UPDATE
+               TYPE_PRECEDENCE[ObjectChangeType.DELETE.ordinal()] = 3;
+               TYPE_PRECEDENCE[ObjectChangeType.INSERT.ordinal()] = 2;
+               TYPE_PRECEDENCE[ObjectChangeType.UPDATE.ordinal()] = 1;
+       }
+
+       // note that we are tracking DB-level changes for clarity
+
+       private ObjectId preCommitId;
+       private ObjectId postCommitId;
+       private Map<String, MutableAttributeChange> attributeChanges;
+       private Map<String, MutableToManyRelationshipChange> 
toManyRelationshipChanges;
+       private Map<String, MutableToOneRelationshipChange> 
toOneRelationshipChanges;
+
+       private ObjectChangeType type;
+
+       public MutableObjectChange(ObjectId preCommitId) {
+               this.preCommitId = preCommitId;
+       }
+
+       @Override
+       public Map<String, ? extends AttributeChange> getAttributeChanges() {
+               return attributeChanges != null ? attributeChanges : 
Collections.<String, AttributeChange> emptyMap();
+       }
+
+       @Override
+       public Map<String, ? extends ToManyRelationshipChange> 
getToManyRelationshipChanges() {
+               return toManyRelationshipChanges != null ? 
toManyRelationshipChanges
+                               : Collections.<String, 
ToManyRelationshipChange> emptyMap();
+       }
+
+       @Override
+       public Map<String, ? extends ToOneRelationshipChange> 
getToOneRelationshipChanges() {
+               return toOneRelationshipChanges != null ? 
toOneRelationshipChanges
+                               : Collections.<String, ToOneRelationshipChange> 
emptyMap();
+       }
+
+       @Override
+       public ObjectChangeType getType() {
+               return type;
+       }
+
+       @Override
+       public ObjectId getPreCommitId() {
+               return preCommitId;
+       }
+
+       @Override
+       public ObjectId getPostCommitId() {
+               return postCommitId != null ? postCommitId : preCommitId;
+       }
+
+       public void setPostCommitId(ObjectId postCommitId) {
+               this.postCommitId = postCommitId;
+       }
+
+       public void setType(ObjectChangeType changeType) {
+               if (this.type == null || TYPE_PRECEDENCE[changeType.ordinal()] 
> TYPE_PRECEDENCE[this.type.ordinal()]) {
+                       this.type = changeType;
+               }
+       }
+
+       public void toManyRelationshipConnected(String property, ObjectId 
value) {
+               getOrCreateToManyChange(property).connected(value);
+       }
+
+       public void toManyRelationshipDisconnected(String property, ObjectId 
value) {
+               getOrCreateToManyChange(property).disconnected(value);
+       }
+
+       public void toOneRelationshipConnected(String property, ObjectId value) 
{
+               getOrCreateToOneChange(property).connected(value);
+       }
+
+       public void toOneRelationshipDisconnected(String property, ObjectId 
value) {
+               getOrCreateToOneChange(property).disconnected(value);
+       }
+
+       public void attributeChanged(String property, Object oldValue, Object 
newValue) {
+
+               if (type == null) {
+                       throw new IllegalStateException("Null op");
+               }
+
+               MutableAttributeChange c = getOrCreateAttributeChange(property);
+               c.setNewValue(newValue);
+               c.setOldValue(oldValue);
+       }
+
+       private MutableAttributeChange getOrCreateAttributeChange(String 
property) {
+               MutableAttributeChange pChange = attributeChanges != null ? 
attributeChanges.get(property) : null;
+
+               if (pChange == null) {
+
+                       if (attributeChanges == null) {
+                               attributeChanges = new HashMap<>();
+                       }
+
+                       pChange = new MutableAttributeChange();
+                       attributeChanges.put(property, pChange);
+               }
+
+               return pChange;
+       }
+
+       private MutableToOneRelationshipChange getOrCreateToOneChange(String 
property) {
+               MutableToOneRelationshipChange pChange = 
toOneRelationshipChanges != null
+                               ? toOneRelationshipChanges.get(property) : null;
+
+               if (pChange == null) {
+
+                       if (toOneRelationshipChanges == null) {
+                               toOneRelationshipChanges = new HashMap<>();
+                       }
+
+                       pChange = new MutableToOneRelationshipChange();
+                       toOneRelationshipChanges.put(property, pChange);
+               }
+
+               return pChange;
+       }
+       
+       private MutableToManyRelationshipChange getOrCreateToManyChange(String 
property) {
+               MutableToManyRelationshipChange pChange = 
toManyRelationshipChanges != null
+                               ? toManyRelationshipChanges.get(property) : 
null;
+
+               if (pChange == null) {
+
+                       if (toManyRelationshipChanges == null) {
+                               toManyRelationshipChanges = new HashMap<>();
+                       }
+
+                       pChange = new MutableToManyRelationshipChange();
+                       toManyRelationshipChanges.put(property, pChange);
+               }
+
+               return pChange;
+       }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToManyRelationshipChange.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToManyRelationshipChange.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToManyRelationshipChange.java
new file mode 100644
index 0000000..215a542
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToManyRelationshipChange.java
@@ -0,0 +1,64 @@
+/*****************************************************************
+ *   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.changemap;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.apache.cayenne.ObjectId;
+
+/**
+ * @since 4.0
+ */
+public class MutableToManyRelationshipChange implements 
ToManyRelationshipChange {
+
+       private Collection<ObjectId> added;
+       private Collection<ObjectId> removed;
+
+       @Override
+       public Collection<ObjectId> getAdded() {
+               return added == null ? Collections.<ObjectId> emptyList() : 
added;
+       }
+
+       @Override
+       public Collection<ObjectId> getRemoved() {
+               return removed == null ? Collections.<ObjectId> emptyList() : 
removed;
+       }
+
+       public void connected(ObjectId o) {
+
+               // TODO: cancel previously removed ?
+               if (added == null) {
+                       added = new ArrayList<>();
+               }
+
+               added.add(o);
+       }
+
+       public void disconnected(ObjectId o) {
+
+               // TODO: cancel previously added ?
+               if (removed == null) {
+                       removed = new ArrayList<>();
+               }
+
+               removed.add(o);
+       }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToOneRelationshipChange.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToOneRelationshipChange.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToOneRelationshipChange.java
new file mode 100644
index 0000000..d0269d6
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToOneRelationshipChange.java
@@ -0,0 +1,48 @@
+/*****************************************************************
+ *   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.changemap;
+
+import org.apache.cayenne.ObjectId;
+
+/**
+ * @since 4.0
+ */
+public class MutableToOneRelationshipChange implements ToOneRelationshipChange 
{
+
+       private ObjectId oldValue;
+       private ObjectId newValue;
+
+       @Override
+       public ObjectId getOldValue() {
+               return oldValue;
+       }
+
+       @Override
+       public ObjectId getNewValue() {
+               return newValue;
+       }
+
+       public void connected(ObjectId o) {
+               this.newValue = o;
+       }
+
+       public void disconnected(ObjectId o) {
+               this.oldValue = o;
+       }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChange.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChange.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChange.java
new file mode 100644
index 0000000..6f41d5f
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChange.java
@@ -0,0 +1,43 @@
+/*****************************************************************
+ *   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.changemap;
+
+import java.util.Map;
+
+import org.apache.cayenne.ObjectId;
+
+/**
+ * Accumulates changes of a single object with a transaction.
+ * 
+ * @since 4.0
+ */
+public interface ObjectChange {
+
+       ObjectChangeType getType();
+
+       ObjectId getPreCommitId();
+
+       ObjectId getPostCommitId();
+
+       Map<String, ? extends AttributeChange> getAttributeChanges();
+
+       Map<String, ? extends ToOneRelationshipChange> 
getToOneRelationshipChanges();
+
+       Map<String, ? extends ToManyRelationshipChange> 
getToManyRelationshipChanges();
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChangeType.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChangeType.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChangeType.java
new file mode 100644
index 0000000..77d9895
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChangeType.java
@@ -0,0 +1,29 @@
+/*****************************************************************
+ *   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.changemap;
+
+/**
+ * Defines types of tracked object changes.
+ * 
+ * @since 4.0
+ */
+public enum ObjectChangeType {
+
+       INSERT, UPDATE, DELETE;
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ToManyRelationshipChange.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ToManyRelationshipChange.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ToManyRelationshipChange.java
new file mode 100644
index 0000000..8fcdc59
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ToManyRelationshipChange.java
@@ -0,0 +1,35 @@
+/*****************************************************************
+ *   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.changemap;
+
+import java.util.Collection;
+
+import org.apache.cayenne.ObjectId;
+
+/**
+ * Represents a change in a to-many relationship property to another entity.
+ * 
+ * @since 4.0
+ */
+public interface ToManyRelationshipChange {
+
+       Collection<ObjectId> getAdded();
+
+       Collection<ObjectId> getRemoved();
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ToOneRelationshipChange.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ToOneRelationshipChange.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ToOneRelationshipChange.java
new file mode 100644
index 0000000..0b592ea
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ToOneRelationshipChange.java
@@ -0,0 +1,31 @@
+/*****************************************************************
+ *   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.changemap;
+
+import org.apache.cayenne.ObjectId;
+
+/**
+ * @since 4.0
+ */
+public interface ToOneRelationshipChange {
+
+       ObjectId getOldValue();
+
+       ObjectId getNewValue();
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/Confidential.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/Confidential.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/Confidential.java
new file mode 100644
index 0000000..1e02341
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/Confidential.java
@@ -0,0 +1,23 @@
+package org.apache.cayenne.lifecycle.postcommit;
+
+/**
+ * A singleton representing a confidential property value.
+ * 
+ * @since 4.0
+ */
+public class Confidential {
+
+       private static final Confidential instance = new Confidential();
+
+       public static Confidential getInstance() {
+               return instance;
+       }
+
+       private Confidential() {
+       }
+
+       @Override
+       public String toString() {
+               return "*******";
+       }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DeletedDiffProcessor.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DeletedDiffProcessor.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DeletedDiffProcessor.java
new file mode 100644
index 0000000..5f6042a
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DeletedDiffProcessor.java
@@ -0,0 +1,140 @@
+/*****************************************************************
+ *   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 java.util.List;
+
+import org.apache.cayenne.DataChannel;
+import org.apache.cayenne.DataRow;
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.graph.GraphChangeHandler;
+import org.apache.cayenne.lifecycle.changemap.MutableChangeMap;
+import org.apache.cayenne.lifecycle.changemap.MutableObjectChange;
+import org.apache.cayenne.lifecycle.changemap.ObjectChangeType;
+import org.apache.cayenne.lifecycle.postcommit.meta.PostCommitEntity;
+import org.apache.cayenne.lifecycle.postcommit.meta.PostCommitEntityFactory;
+import org.apache.cayenne.query.ObjectIdQuery;
+import org.apache.cayenne.reflect.AttributeProperty;
+import org.apache.cayenne.reflect.ClassDescriptor;
+import org.apache.cayenne.reflect.PropertyVisitor;
+import org.apache.cayenne.reflect.ToManyProperty;
+import org.apache.cayenne.reflect.ToOneProperty;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+class DeletedDiffProcessor implements GraphChangeHandler {
+
+       private static final Log LOGGER = 
LogFactory.getLog(DeletedDiffProcessor.class);
+
+       private PostCommitEntityFactory entityFactory;
+       private MutableChangeMap changeSet;
+       private DataChannel channel;
+
+       DeletedDiffProcessor(MutableChangeMap changeSet, DataChannel channel, 
PostCommitEntityFactory entityFactory) {
+               this.changeSet = changeSet;
+               this.channel = channel;
+               this.entityFactory = entityFactory;
+       }
+
+       @Override
+       public void nodeRemoved(Object nodeId) {
+               ObjectId id = (ObjectId) nodeId;
+
+               final MutableObjectChange objectChangeSet = 
changeSet.getOrCreate(id, ObjectChangeType.DELETE);
+
+               // TODO: rewrite with SelectById query after Cayenne upgrade
+               ObjectIdQuery query = new ObjectIdQuery(id, true, 
ObjectIdQuery.CACHE);
+               QueryResponse result = channel.onQuery(null, query);
+
+               @SuppressWarnings("unchecked")
+               List<DataRow> rows = result.firstList();
+
+               if (rows.isEmpty()) {
+                       LOGGER.warn("No DB snapshot for object to be deleted, 
no changes will be recorded. ID: " + id);
+                       return;
+               }
+
+               final DataRow row = rows.get(0);
+
+               ClassDescriptor descriptor = 
channel.getEntityResolver().getClassDescriptor(id.getEntityName());
+               final PostCommitEntity entity = entityFactory.getEntity(id);
+
+               descriptor.visitProperties(new PropertyVisitor() {
+
+                       @Override
+                       public boolean visitAttribute(AttributeProperty 
property) {
+
+                               if (!entity.isIncluded(property.getName())) {
+                                       return true;
+                               }
+
+                               Object value;
+                               if (entity.isConfidential(property.getName())) {
+                                       value = Confidential.getInstance();
+                               } else {
+                                       String key = 
property.getAttribute().getDbAttributeName();
+                                       value = row.get(key);
+                               }
+
+                               if (value != null) {
+                                       
objectChangeSet.attributeChanged(property.getName(), value, null);
+                               }
+                               return true;
+                       }
+
+                       @Override
+                       public boolean visitToOne(ToOneProperty property) {
+                               // TODO record FK changes?
+                               return true;
+                       }
+
+                       @Override
+                       public boolean visitToMany(ToManyProperty property) {
+                               return true;
+                       }
+
+               });
+       }
+
+       @Override
+       public void nodeIdChanged(Object nodeId, Object newId) {
+               // do nothing
+       }
+
+       @Override
+       public void nodeCreated(Object nodeId) {
+               // do nothing
+       }
+
+       @Override
+       public void nodePropertyChanged(Object nodeId, String property, Object 
oldValue, Object newValue) {
+               // do nothing
+       }
+
+       @Override
+       public void arcCreated(Object nodeId, Object targetNodeId, Object 
arcId) {
+               // do nothing
+       }
+
+       @Override
+       public void arcDeleted(Object nodeId, Object targetNodeId, Object 
arcId) {
+               // do nothing
+       }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffFilter.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffFilter.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffFilter.java
new file mode 100644
index 0000000..a8494d5
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffFilter.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.postcommit;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.graph.GraphChangeHandler;
+import org.apache.cayenne.lifecycle.postcommit.meta.PostCommitEntity;
+import org.apache.cayenne.lifecycle.postcommit.meta.PostCommitEntityFactory;
+
+/**
+ * Filters changes passing only auditable object changes to the underlying
+ * delegate.
+ */
+class DiffFilter implements GraphChangeHandler {
+
+       private PostCommitEntityFactory entityFactory;
+       private GraphChangeHandler delegate;
+
+       DiffFilter(PostCommitEntityFactory entityFactory, GraphChangeHandler 
delegate) {
+               this.entityFactory = entityFactory;
+               this.delegate = delegate;
+       }
+
+       @Override
+       public void nodeIdChanged(Object nodeId, Object newId) {
+               if (entityFactory.getEntity((ObjectId) nodeId).isIncluded()) {
+                       delegate.nodeIdChanged(nodeId, newId);
+               }
+       }
+
+       @Override
+       public void nodeCreated(Object nodeId) {
+               if (entityFactory.getEntity((ObjectId) nodeId).isIncluded()) {
+                       delegate.nodeCreated(nodeId);
+               }
+       }
+
+       @Override
+       public void nodeRemoved(Object nodeId) {
+               if (entityFactory.getEntity((ObjectId) nodeId).isIncluded()) {
+                       delegate.nodeRemoved(nodeId);
+               }
+       }
+
+       @Override
+       public void nodePropertyChanged(Object nodeId, String property, Object 
oldValue, Object newValue) {
+               PostCommitEntity entity = entityFactory.getEntity((ObjectId) 
nodeId);
+               if (entity.isIncluded(property)) {
+
+                       if (entity.isConfidential(property)) {
+                               oldValue = Confidential.getInstance();
+                               newValue = Confidential.getInstance();
+                       }
+
+                       delegate.nodePropertyChanged(nodeId, property, 
oldValue, newValue);
+               }
+       }
+
+       @Override
+       public void arcCreated(Object nodeId, Object targetNodeId, Object 
arcId) {
+               if (entityFactory.getEntity((ObjectId) 
nodeId).isIncluded(arcId.toString())) {
+                       delegate.arcCreated(nodeId, targetNodeId, arcId);
+               }
+       }
+
+       @Override
+       public void arcDeleted(Object nodeId, Object targetNodeId, Object 
arcId) {
+               if (entityFactory.getEntity((ObjectId) 
nodeId).isIncluded(arcId.toString())) {
+                       delegate.arcDeleted(nodeId, targetNodeId, arcId);
+               }
+       }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffProcessor.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffProcessor.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffProcessor.java
new file mode 100644
index 0000000..7daf6e1
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffProcessor.java
@@ -0,0 +1,104 @@
+/*****************************************************************
+ *   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.ObjectId;
+import org.apache.cayenne.graph.GraphChangeHandler;
+import org.apache.cayenne.lifecycle.changemap.MutableChangeMap;
+import org.apache.cayenne.lifecycle.changemap.MutableObjectChange;
+import org.apache.cayenne.lifecycle.changemap.ObjectChangeType;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.map.ObjRelationship;
+
+/**
+ * Records changes in a given transaction to a {@link MutableChangeMap} object.
+ * 
+ * @since 4.0
+ */
+class DiffProcessor implements GraphChangeHandler {
+
+       private EntityResolver entityResolver;
+       private MutableChangeMap changeSet;
+
+       DiffProcessor(MutableChangeMap changeSet, EntityResolver 
entityResolver) {
+               this.changeSet = changeSet;
+               this.entityResolver = entityResolver;
+       }
+
+       @Override
+       public void nodeRemoved(Object nodeId) {
+               // do nothing... deletes are processed pre-commit
+       }
+
+       @Override
+       public void nodePropertyChanged(Object nodeId, String property, Object 
oldValue, Object newValue) {
+               changeSet.getOrCreate((ObjectId) nodeId, 
ObjectChangeType.UPDATE).attributeChanged(property, oldValue,
+                               newValue);
+       }
+
+       @Override
+       public void nodeIdChanged(Object nodeId, Object newId) {
+               changeSet.aliasId((ObjectId) nodeId, (ObjectId) newId);
+       }
+
+       @Override
+       public void nodeCreated(Object nodeId) {
+               changeSet.getOrCreate((ObjectId) nodeId, 
ObjectChangeType.INSERT);
+       }
+
+       @Override
+       public void arcDeleted(Object nodeId, Object targetNodeId, Object 
arcId) {
+               ObjectId id = (ObjectId) nodeId;
+               String relationshipName = arcId.toString();
+
+               ObjEntity entity = 
entityResolver.getObjEntity(id.getEntityName());
+               ObjRelationship relationship = 
entity.getRelationship(relationshipName);
+
+               MutableObjectChange c = changeSet.getOrCreate(id, 
ObjectChangeType.UPDATE);
+
+               ObjectId tid = (ObjectId) targetNodeId;
+
+               if (relationship.isToMany()) {
+                       c.toManyRelationshipDisconnected(relationshipName, tid);
+               } else {
+                       c.toOneRelationshipDisconnected(relationshipName, tid);
+               }
+       }
+
+       @Override
+       public void arcCreated(Object nodeId, Object targetNodeId, Object 
arcId) {
+
+               ObjectId id = (ObjectId) nodeId;
+               String relationshipName = arcId.toString();
+
+               ObjEntity entity = 
entityResolver.getObjEntity(id.getEntityName());
+               ObjRelationship relationship = 
entity.getRelationship(relationshipName);
+
+               MutableObjectChange c = changeSet.getOrCreate(id, 
ObjectChangeType.UPDATE);
+
+               ObjectId tid = (ObjectId) targetNodeId;
+
+               if (relationship.isToMany()) {
+                       c.toManyRelationshipConnected(relationshipName, tid);
+               } else {
+                       c.toOneRelationshipConnected(relationshipName, tid);
+               }
+       }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter.java
new file mode 100644
index 0000000..d8390de
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter.java
@@ -0,0 +1,118 @@
+/*****************************************************************
+ *   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 java.util.Collection;
+import java.util.List;
+
+import org.apache.cayenne.DataChannel;
+import org.apache.cayenne.DataChannelFilter;
+import org.apache.cayenne.DataChannelFilterChain;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.graph.GraphChangeHandler;
+import org.apache.cayenne.graph.GraphDiff;
+import org.apache.cayenne.lifecycle.changemap.ChangeMap;
+import org.apache.cayenne.lifecycle.changemap.MutableChangeMap;
+import org.apache.cayenne.lifecycle.postcommit.meta.PostCommitEntityFactory;
+import org.apache.cayenne.query.Query;
+
+/**
+ * A {@link DataChannelFilter} that organizes commit changes
+ * 
+ * @since 4.0
+ */
+public class PostCommitFilter implements DataChannelFilter {
+
+       static final String POST_COMMIT_LISTENERS_LIST = 
"cayenne.server.post_commit.listeners";
+
+       private PostCommitEntityFactory entityFactory;
+       private Collection<PostCommitListener> listeners;
+
+       public PostCommitFilter(@Inject PostCommitEntityFactory entityFactory,
+                       @Inject(POST_COMMIT_LISTENERS_LIST) 
List<PostCommitListener> listeners) {
+               this.entityFactory = entityFactory;
+               this.listeners = listeners;
+       }
+
+       @Override
+       public void init(DataChannel channel) {
+               // TODO Auto-generated method stub
+
+       }
+
+       @Override
+       public QueryResponse onQuery(ObjectContext originatingContext, Query 
query, DataChannelFilterChain filterChain) {
+               return filterChain.onQuery(originatingContext, query);
+       }
+
+       @Override
+       public GraphDiff onSync(ObjectContext originatingContext, GraphDiff 
beforeDiff, int syncType,
+                       DataChannelFilterChain filterChain) {
+
+               // process commits only; skip rollback
+               if (syncType != DataChannel.FLUSH_CASCADE_SYNC && syncType != 
DataChannel.FLUSH_NOCASCADE_SYNC) {
+                       return filterChain.onSync(originatingContext, 
beforeDiff, syncType);
+               }
+
+               // don't collect changes if there are no listeners
+               if (listeners.isEmpty()) {
+                       return filterChain.onSync(originatingContext, 
beforeDiff, syncType);
+               }
+
+               MutableChangeMap changes = new MutableChangeMap();
+
+               // passing DataDomain, not ObjectContext to speed things up
+               // and avoid capturing changed state when fetching snapshots
+               DataChannel channel = originatingContext.getChannel();
+
+               beforeCommit(changes, channel, beforeDiff);
+               GraphDiff afterDiff = filterChain.onSync(originatingContext, 
beforeDiff, syncType);
+               afterCommit(changes, channel, beforeDiff, afterDiff);
+               notifyListeners(originatingContext, changes);
+
+               return afterDiff;
+       }
+
+       private void beforeCommit(MutableChangeMap changes, DataChannel 
channel, GraphDiff contextDiff) {
+
+               // capture snapshots of deleted objects before they are purged 
from
+               // cache
+
+               GraphChangeHandler handler = new DiffFilter(entityFactory,
+                               new DeletedDiffProcessor(changes, channel, 
entityFactory));
+               contextDiff.apply(handler);
+       }
+
+       private void afterCommit(MutableChangeMap changes, DataChannel channel, 
GraphDiff contextDiff, GraphDiff dbDiff) {
+
+               GraphChangeHandler handler = new DiffFilter(entityFactory,
+                               new DiffProcessor(changes, 
channel.getEntityResolver()));
+               contextDiff.apply(handler);
+               dbDiff.apply(handler);
+       }
+
+       private void notifyListeners(ObjectContext originatingContext, 
ChangeMap changes) {
+               for (PostCommitListener l : listeners) {
+                       l.onPostCommit(originatingContext, changes);
+               }
+       }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitListener.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitListener.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitListener.java
new file mode 100644
index 0000000..fc9412b
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitListener.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.ObjectContext;
+import org.apache.cayenne.lifecycle.changemap.ChangeMap;
+
+/**
+ * An interface of a listener of post-commit events.
+ * 
+ * @since 4.0
+ */
+public interface PostCommitListener {
+
+       void onPostCommit(ObjectContext originatingContext, ChangeMap changes);
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModuleBuilder.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModuleBuilder.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModuleBuilder.java
new file mode 100644
index 0000000..99b3181
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModuleBuilder.java
@@ -0,0 +1,121 @@
+/*****************************************************************
+ *   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 java.util.Collection;
+import java.util.HashSet;
+
+import org.apache.cayenne.configuration.Constants;
+import org.apache.cayenne.di.Binder;
+import org.apache.cayenne.di.ListBuilder;
+import org.apache.cayenne.di.Module;
+import org.apache.cayenne.lifecycle.audit.Auditable;
+import 
org.apache.cayenne.lifecycle.postcommit.meta.AuditablePostCommitEntityFactory;
+import 
org.apache.cayenne.lifecycle.postcommit.meta.IncludeAllPostCommitEntityFactory;
+import org.apache.cayenne.lifecycle.postcommit.meta.PostCommitEntity;
+import org.apache.cayenne.lifecycle.postcommit.meta.PostCommitEntityFactory;
+
+/**
+ * A builder of a module that integrates {@link PostCommitFilter} and
+ * {@link PostCommitListener} in Cayenne.
+ * 
+ * @since 4.0
+ */
+public class PostCommitModuleBuilder {
+
+       public static PostCommitModuleBuilder builder() {
+               return new PostCommitModuleBuilder();
+       }
+
+       private Class<? extends PostCommitEntityFactory> entityFactoryType;
+       private Collection<Class<? extends PostCommitListener>> listenerTypes;
+       private Collection<PostCommitListener> listenerInstances;
+
+       PostCommitModuleBuilder() {
+               this.entityFactoryType = 
IncludeAllPostCommitEntityFactory.class;
+               this.listenerTypes = new HashSet<>();
+               this.listenerInstances = new HashSet<>();
+       }
+
+       public PostCommitModuleBuilder listener(Class<? extends 
PostCommitListener> type) {
+               this.listenerTypes.add(type);
+               return this;
+       }
+
+       public PostCommitModuleBuilder listener(PostCommitListener instance) {
+               this.listenerInstances.add(instance);
+               return this;
+       }
+
+       /**
+        * Installs entity filter that would only include entities annotated 
with
+        * {@link Auditable} on the callbacks. Also {@link 
Auditable#confidential()}
+        * properties will be obfuscated and {@link 
Auditable#ignoredProperties()} -
+        * excluded from the change collection.
+        */
+       public PostCommitModuleBuilder auditableEntitiesOnly() {
+               this.entityFactoryType = AuditablePostCommitEntityFactory.class;
+               return this;
+       }
+
+       /**
+        * Installs a custom factory for {@link PostCommitEntity} objects that
+        * allows implementors to use their own annotations, etc.
+        */
+       public PostCommitModuleBuilder entityFactory(Class<? extends 
PostCommitEntityFactory> entityFactoryType) {
+               this.entityFactoryType = entityFactoryType;
+               return this;
+       }
+
+       /**
+        * Creates a DI module that would install {@link PostCommitFilter} and 
its
+        * listeners in Cayenne.
+        */
+       public Module build() {
+               return new Module() {
+
+                       @SuppressWarnings({ "rawtypes", "unchecked" })
+                       @Override
+                       public void configure(Binder binder) {
+
+                               ListBuilder<PostCommitListener> listeners = 
binder
+                                               .<PostCommitListener> 
bindList(PostCommitFilter.POST_COMMIT_LISTENERS_LIST)
+                                               .addAll(listenerInstances);
+
+                               // types have to be added one-by-one
+                               for (Class type : listenerTypes) {
+
+                                       // TODO: temp hack - need to bind each 
type before adding to
+                                       // collection...
+                                       binder.bind(type).to(type);
+
+                                       listeners.add(type);
+                               }
+
+                               
binder.bind(PostCommitFilter.class).to(PostCommitFilter.class);
+
+                               // TODO: should be ordering the filter to go 
inside transaction
+                               // once the corresponding Jiras are available 
in Cayenne
+                               
binder.bindList(Constants.SERVER_DOMAIN_FILTERS_LIST).add(PostCommitFilter.class);
+
+                               
binder.bind(PostCommitEntityFactory.class).to(entityFactoryType);
+                       }
+               };
+       }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/AuditablePostCommitEntityFactory.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/AuditablePostCommitEntityFactory.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/AuditablePostCommitEntityFactory.java
new file mode 100644
index 0000000..f7ce2fd
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/AuditablePostCommitEntityFactory.java
@@ -0,0 +1,101 @@
+/*****************************************************************
+ *   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.meta;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.cayenne.DataChannel;
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.di.Provider;
+import org.apache.cayenne.lifecycle.audit.Auditable;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.reflect.ClassDescriptor;
+
+/**
+ * Compiles {@link PostCommitEntity}'s based on {@link Auditable} annotation.
+ * 
+ * @since 4.0
+ */
+public class AuditablePostCommitEntityFactory implements 
PostCommitEntityFactory {
+
+       private static final PostCommitEntity BLOCKED_ENTITY = new 
PostCommitEntity() {
+
+               @Override
+               public boolean isIncluded(String property) {
+                       return false;
+               }
+
+               @Override
+               public boolean isConfidential(String property) {
+                       return false;
+               }
+
+               @Override
+               public boolean isIncluded() {
+                       return false;
+               }
+       };
+
+       private Provider<DataChannel> channelProvider;
+       private ConcurrentMap<String, PostCommitEntity> entities;
+
+       public AuditablePostCommitEntityFactory(@Inject Provider<DataChannel> 
channelProvider) {
+               this.entities = new ConcurrentHashMap<>();
+
+               // injecting provider instead of DataChannel, as otherwise we 
end up
+               // with circular dependency.
+               this.channelProvider = channelProvider;
+       }
+
+       @Override
+       public PostCommitEntity getEntity(ObjectId id) {
+               String entityName = id.getEntityName();
+
+               PostCommitEntity descriptor = entities.get(entityName);
+               if (descriptor == null) {
+                       PostCommitEntity newDescriptor = 
createDescriptor(entityName);
+                       PostCommitEntity existingDescriptor = 
entities.putIfAbsent(entityName, newDescriptor);
+                       descriptor = (existingDescriptor != null) ? 
existingDescriptor : newDescriptor;
+               }
+
+               return descriptor;
+
+       }
+
+       private EntityResolver getEntityResolver() {
+               return channelProvider.get().getEntityResolver();
+       }
+
+       private PostCommitEntity createDescriptor(String entityName) {
+               EntityResolver entityResolver = getEntityResolver();
+               ClassDescriptor classDescriptor = 
entityResolver.getClassDescriptor(entityName);
+
+               Auditable annotation = 
classDescriptor.getObjectClass().getAnnotation(Auditable.class);
+               if (annotation == null) {
+                       return BLOCKED_ENTITY;
+               }
+
+               ObjEntity entity = entityResolver.getObjEntity(entityName);
+               return new DefaultPostCommitEntity(entity, 
annotation.ignoredProperties(), annotation.confidential());
+       }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/DefaultPostCommitEntity.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/DefaultPostCommitEntity.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/DefaultPostCommitEntity.java
new file mode 100644
index 0000000..7d6ad8c
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/DefaultPostCommitEntity.java
@@ -0,0 +1,78 @@
+/*****************************************************************
+ *   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.meta;
+
+import java.util.Collection;
+import java.util.HashSet;
+
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.map.ObjRelationship;
+
+/**
+ * @since 4.0
+ */
+public class DefaultPostCommitEntity implements PostCommitEntity {
+       
+       private Collection<String> ignoredProperties;
+       private Collection<String> confidentialProperties;
+
+       public DefaultPostCommitEntity(ObjEntity entity, String[] 
ignoredProperties, String[] confidentialProperties) {
+
+               this.ignoredProperties = new HashSet<>();
+               this.confidentialProperties = new HashSet<>();
+
+               // ignoring to-many (presumably traced via changes to target 
entities)
+               // TODO: M:N relationships will not be tracked as a result...
+
+               for (ObjRelationship relationship : entity.getRelationships()) {
+                       if (relationship.isToMany()) {
+                               
this.ignoredProperties.add(relationship.getName());
+                       }
+               }
+
+               // ignore explicitly specified properties
+               if (ignoredProperties != null) {
+                       for (String property : ignoredProperties) {
+                               this.ignoredProperties.add(property);
+                       }
+               }
+
+               if (confidentialProperties != null) {
+                       for (String property : confidentialProperties) {
+                               this.confidentialProperties.add(property);
+                       }
+               }
+       }
+
+       @Override
+       public boolean isIncluded(String property) {
+               return !ignoredProperties.contains(property);
+       }
+
+       @Override
+       public boolean isIncluded() {
+               return true;
+       }
+
+       @Override
+       public boolean isConfidential(String property) {
+               return confidentialProperties.contains(property);
+       }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/IncludeAllPostCommitEntityFactory.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/IncludeAllPostCommitEntityFactory.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/IncludeAllPostCommitEntityFactory.java
new file mode 100644
index 0000000..2064885
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/IncludeAllPostCommitEntityFactory.java
@@ -0,0 +1,51 @@
+/*****************************************************************
+ *   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.meta;
+
+import org.apache.cayenne.ObjectId;
+
+/**
+ * @since 4.0
+ */
+public class IncludeAllPostCommitEntityFactory implements 
PostCommitEntityFactory {
+
+       private static final PostCommitEntity ALLOWED_ENTITY = new 
PostCommitEntity() {
+
+               @Override
+               public boolean isIncluded(String property) {
+                       return true;
+               }
+
+               @Override
+               public boolean isConfidential(String property) {
+                       return false;
+               }
+
+               @Override
+               public boolean isIncluded() {
+                       return true;
+               }
+       };
+
+       @Override
+       public PostCommitEntity getEntity(ObjectId id) {
+               return ALLOWED_ENTITY;
+
+       }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/PostCommitEntity.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/PostCommitEntity.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/PostCommitEntity.java
new file mode 100644
index 0000000..1f3d8ca
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/PostCommitEntity.java
@@ -0,0 +1,34 @@
+/*****************************************************************
+ *   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.meta;
+
+/**
+ * Describes post-commit behavior for a given Cayenne entity.
+ * 
+ * @since 4.0
+ */
+public interface PostCommitEntity {
+
+       boolean isIncluded();
+
+       boolean isConfidential(String property);
+
+       boolean isIncluded(String property);
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/PostCommitEntityFactory.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/PostCommitEntityFactory.java
 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/PostCommitEntityFactory.java
new file mode 100644
index 0000000..f8f7e0b
--- /dev/null
+++ 
b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/PostCommitEntityFactory.java
@@ -0,0 +1,29 @@
+/*****************************************************************
+ *   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.meta;
+
+import org.apache.cayenne.ObjectId;
+
+/**
+ * @since 4.0
+ */
+public interface PostCommitEntityFactory {
+
+       PostCommitEntity getEntity(ObjectId id);
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/audit/AuditableFilterIT.java
----------------------------------------------------------------------
diff --git 
a/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/audit/AuditableFilterIT.java
 
b/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/audit/AuditableFilterIT.java
new file mode 100644
index 0000000..717bbca
--- /dev/null
+++ 
b/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/audit/AuditableFilterIT.java
@@ -0,0 +1,249 @@
+/*****************************************************************
+ *   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.audit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.Map;
+
+import org.apache.cayenne.Cayenne;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.access.DataDomain;
+import org.apache.cayenne.lifecycle.changeset.ChangeSetFilter;
+import org.apache.cayenne.lifecycle.db.Auditable1;
+import org.apache.cayenne.lifecycle.db.Auditable2;
+import org.apache.cayenne.lifecycle.db.AuditableChild1;
+import org.apache.cayenne.lifecycle.db.AuditableChild2;
+import org.apache.cayenne.lifecycle.db.AuditableChild3;
+import org.apache.cayenne.lifecycle.db.AuditableChildUuid;
+import org.apache.cayenne.lifecycle.id.IdCoder;
+import org.apache.cayenne.lifecycle.relationship.ObjectIdRelationshipHandler;
+import org.apache.cayenne.lifecycle.unit.LifecycleServerCase;
+import org.junit.Test;
+
+public class AuditableFilterIT extends LifecycleServerCase {
+
+       @Test
+       public void testAudit_IgnoreRuntimeRelationships() throws Exception {
+
+               auditable1.insert(1, "xx");
+               auditable1.insert(2, "yy");
+               auditable1.insert(3, "aa");
+               auditableChild2.insert(1, 1, "zz");
+
+               DataDomain domain = runtime.getDataDomain();
+
+               Processor processor = new Processor();
+
+               AuditableFilter filter = new 
AuditableFilter(domain.getEntityResolver(), processor);
+               domain.addFilter(filter);
+
+               // prerequisite for BaseAuditableProcessor use
+               ChangeSetFilter changeSetFilter = new ChangeSetFilter();
+               domain.addFilter(changeSetFilter);
+
+               ObjectContext context = runtime.newContext();
+
+               Auditable1 a2 = Cayenne.objectForPK(context, Auditable1.class, 
2);
+               AuditableChild2 a21 = Cayenne.objectForPK(context, 
AuditableChild2.class, 1);
+
+               a21.setParent(a2);
+               a21.setCharProperty1("XYZA");
+               context.commitChanges();
+
+               assertEquals(0, processor.size);
+
+               processor.reset();
+
+               Auditable1 a3 = Cayenne.objectForPK(context, Auditable1.class, 
3);
+               a21.setParent(a3);
+               a3.setCharProperty1("12");
+
+               context.commitChanges();
+               assertEquals(1, processor.size);
+               
assertTrue(processor.audited.get(AuditableOperation.UPDATE).contains(a3));
+       }
+
+       @Test
+       public void testAudit_IncludeToManyRelationships() throws Exception {
+
+               auditable1.insert(1, "xx");
+               auditable1.insert(2, "yy");
+               auditableChild1.insert(1, 1, "zz");
+
+               DataDomain domain = runtime.getDataDomain();
+
+               Processor processor = new Processor();
+
+               AuditableFilter filter = new 
AuditableFilter(domain.getEntityResolver(), processor);
+               domain.addFilter(filter);
+
+               // prerequisite for BaseAuditableProcessor use
+               ChangeSetFilter changeSetFilter = new ChangeSetFilter();
+               domain.addFilter(changeSetFilter);
+
+               ObjectContext context = runtime.newContext();
+
+               Auditable1 a2 = Cayenne.objectForPK(context, Auditable1.class, 
2);
+               AuditableChild1 a21 = Cayenne.objectForPK(context, 
AuditableChild1.class, 1);
+
+               a21.setParent(a2);
+               context.commitChanges();
+
+               assertEquals(2, processor.size);
+
+               
assertTrue(processor.audited.get(AuditableOperation.UPDATE).contains(a2));
+               assertTrue(processor.audited.get(AuditableOperation.UPDATE)
+                               .contains(Cayenne.objectForPK(context, 
Auditable1.class, 1)));
+       }
+
+       @Test
+       public void testAudit_IgnoreProperties() throws Exception {
+
+               auditable2.insert(1, "P1_1", "P2_1");
+               auditable2.insert(2, "P1_2", "P2_2");
+               auditable2.insert(3, "P1_3", "P2_3");
+
+               DataDomain domain = runtime.getDataDomain();
+
+               Processor processor = new Processor();
+
+               AuditableFilter filter = new 
AuditableFilter(domain.getEntityResolver(), processor);
+               domain.addFilter(filter);
+
+               // prerequisite for BaseAuditableProcessor use
+               ChangeSetFilter changeSetFilter = new ChangeSetFilter();
+               domain.addFilter(changeSetFilter);
+
+               ObjectContext context = runtime.newContext();
+
+               Auditable2 a1 = Cayenne.objectForPK(context, Auditable2.class, 
1);
+               Auditable2 a2 = Cayenne.objectForPK(context, Auditable2.class, 
2);
+               Auditable2 a3 = Cayenne.objectForPK(context, Auditable2.class, 
3);
+
+               a1.setCharProperty1("__");
+               a2.setCharProperty2("__");
+               a3.setCharProperty1("__");
+               a3.setCharProperty2("__");
+
+               context.commitChanges();
+
+               assertEquals(2, processor.size);
+               
assertTrue(processor.audited.get(AuditableOperation.UPDATE).contains(a2));
+               
assertTrue(processor.audited.get(AuditableOperation.UPDATE).contains(a3));
+       }
+
+       @Test
+       public void testAuditableChild_IgnoreProperties() throws Exception {
+
+               auditable2.insert(1, "P1_1", "P2_1");
+               auditable2.insert(2, "P1_2", "P2_2");
+               auditableChild3.insert(1, 1, "C", "D");
+
+               DataDomain domain = runtime.getDataDomain();
+
+               Processor processor = new Processor();
+
+               AuditableFilter filter = new 
AuditableFilter(domain.getEntityResolver(), processor);
+               domain.addFilter(filter);
+
+               // prerequisite for BaseAuditableProcessor use
+               ChangeSetFilter changeSetFilter = new ChangeSetFilter();
+               domain.addFilter(changeSetFilter);
+
+               ObjectContext context = runtime.newContext();
+
+               AuditableChild3 ac1 = Cayenne.objectForPK(context, 
AuditableChild3.class, 1);
+
+               // a change to ignored property should not cause an audit event
+               ac1.setCharProperty1("X_X");
+
+               context.commitChanges();
+               assertEquals(0, processor.size);
+
+               processor.reset();
+               ac1.setCharProperty2("XXXXX");
+               context.commitChanges();
+               assertEquals(1, processor.size);
+       }
+
+       @Test
+       public void testAuditableChild_objectIdRelationship() throws Exception {
+               auditable1.insert(1, "xx");
+               auditableChildUuid.insert(1, "Auditable1:1", "xxx", "yyy");
+
+               DataDomain domain = runtime.getDataDomain();
+               Processor processor = new Processor();
+
+               AuditableFilter filter = new 
AuditableFilter(domain.getEntityResolver(), processor);
+               domain.addFilter(filter);
+
+               // prerequisite for BaseAuditableProcessor use
+               ChangeSetFilter changeSetFilter = new ChangeSetFilter();
+               domain.addFilter(changeSetFilter);
+
+               ObjectContext context = runtime.newContext();
+               AuditableChildUuid ac = Cayenne.objectForPK(context, 
AuditableChildUuid.class, 1);
+               Auditable1 a1 = Cayenne.objectForPK(context, Auditable1.class, 
1);
+               IdCoder refHandler = new IdCoder(domain.getEntityResolver());
+               ObjectIdRelationshipHandler handler = new 
ObjectIdRelationshipHandler(refHandler);
+               handler.relate(ac, a1);
+
+               ac.setCharProperty1("xxxx");
+               context.commitChanges();
+               assertEquals(1, processor.size);
+               Collection<Object> auditables = 
processor.audited.get(AuditableOperation.UPDATE);
+               assertSame(a1, auditables.toArray()[0]);
+
+               ac.setCharProperty2("yyyy");
+               context.commitChanges();
+               assertEquals(2, processor.size);
+               assertSame(a1, auditables.toArray()[1]);
+       }
+
+       private final class Processor implements AuditableProcessor {
+
+               Map<AuditableOperation, Collection<Object>> audited;
+               int size;
+
+               Processor() {
+                       reset();
+               }
+
+               void reset() {
+
+                       audited = new EnumMap<AuditableOperation, 
Collection<Object>>(AuditableOperation.class);
+
+                       for (AuditableOperation op : 
AuditableOperation.values()) {
+                               audited.put(op, new ArrayList<Object>());
+                       }
+               }
+
+               public void audit(Persistent object, AuditableOperation 
operation) {
+                       audited.get(operation).add(object);
+                       size++;
+               }
+       }
+}

Reply via email to