cayenne-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From aadamc...@apache.org
Subject [2/2] cayenne git commit: CAY-2030 Capturing a stream of commit changes
Date Fri, 09 Oct 2015 19:44:41 GMT
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 <aadamchik@apache.org>
Authored: Fri Oct 9 15:32:20 2015 -0400
Committer: aadamchik <aadamchik@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++;
+		}
+	}
+}


Mime
View raw message