cayenne-commits mailing list archives

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

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-lifecycle/src/test/resources/lifecycle-map.map.xml
----------------------------------------------------------------------
diff --git a/cayenne-lifecycle/src/test/resources/lifecycle-map.map.xml b/cayenne-lifecycle/src/test/resources/lifecycle-map.map.xml
index 7b62e06..4a95313 100644
--- a/cayenne-lifecycle/src/test/resources/lifecycle-map.map.xml
+++ b/cayenne-lifecycle/src/test/resources/lifecycle-map.map.xml
@@ -13,17 +13,6 @@
 		<db-attribute name="CHAR_PROPERTY2" type="VARCHAR" length="200"/>
 		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
 	</db-entity>
-	<db-entity name="AUDITABLE3">
-		<db-attribute name="CHAR_PROPERTY1" type="VARCHAR" length="200"/>
-		<db-attribute name="CHAR_PROPERTY2" type="VARCHAR" length="200"/>
-		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
-	</db-entity>
-	<db-entity name="AUDITABLE4">
-		<db-attribute name="AUDITABLE3_ID" type="INTEGER"/>
-		<db-attribute name="CHAR_PROPERTY1" type="VARCHAR" length="200"/>
-		<db-attribute name="CHAR_PROPERTY2" type="VARCHAR" length="200"/>
-		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
-	</db-entity>
 	<db-entity name="AUDITABLE_CHILD1">
 		<db-attribute name="AUDITABLE1_ID" type="INTEGER"/>
 		<db-attribute name="CHAR_PROPERTY1" type="VARCHAR" length="200"/>
@@ -46,10 +35,6 @@
 		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
 		<db-attribute name="UUID" type="VARCHAR" length="200"/>
 	</db-entity>
-	<db-entity name="AUDIT_LOG">
-		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
-		<db-attribute name="LOG" type="CLOB"/>
-	</db-entity>
 	<db-entity name="E1">
 		<db-attribute name="ID" type="BIGINT" isPrimaryKey="true" isMandatory="true"/>
 	</db-entity>
@@ -77,9 +62,6 @@
 		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
 		<db-attribute name="UUID" type="VARCHAR" length="200"/>
 	</db-entity>
-	<obj-entity name="AuditLog" className="org.apache.cayenne.lifecycle.db.AuditLog" dbEntityName="AUDIT_LOG">
-		<obj-attribute name="log" type="java.lang.String" db-attribute-path="LOG"/>
-	</obj-entity>
 	<obj-entity name="Auditable1" className="org.apache.cayenne.lifecycle.db.Auditable1" dbEntityName="AUDITABLE1">
 		<obj-attribute name="charProperty1" type="java.lang.String" db-attribute-path="CHAR_PROPERTY1"/>
 	</obj-entity>
@@ -87,14 +69,6 @@
 		<obj-attribute name="charProperty1" type="java.lang.String" db-attribute-path="CHAR_PROPERTY1"/>
 		<obj-attribute name="charProperty2" type="java.lang.String" db-attribute-path="CHAR_PROPERTY2"/>
 	</obj-entity>
-	<obj-entity name="Auditable3" className="org.apache.cayenne.lifecycle.db.Auditable3" dbEntityName="AUDITABLE3">
-		<obj-attribute name="charProperty1" type="java.lang.String" db-attribute-path="CHAR_PROPERTY1"/>
-		<obj-attribute name="charProperty2" type="java.lang.String" db-attribute-path="CHAR_PROPERTY2"/>
-	</obj-entity>
-	<obj-entity name="Auditable4" className="org.apache.cayenne.lifecycle.db.Auditable4" dbEntityName="AUDITABLE4">
-		<obj-attribute name="charProperty1" type="java.lang.String" db-attribute-path="CHAR_PROPERTY1"/>
-		<obj-attribute name="charProperty2" type="java.lang.String" db-attribute-path="CHAR_PROPERTY2"/>
-	</obj-entity>
 	<obj-entity name="AuditableChild1" className="org.apache.cayenne.lifecycle.db.AuditableChild1" dbEntityName="AUDITABLE_CHILD1">
 		<obj-attribute name="charProperty1" type="java.lang.String" db-attribute-path="CHAR_PROPERTY1"/>
 	</obj-entity>
@@ -134,12 +108,6 @@
 	<db-relationship name="children" source="AUDITABLE2" target="AUDITABLE_CHILD3" toMany="true">
 		<db-attribute-pair source="ID" target="AUDITABLE2_ID"/>
 	</db-relationship>
-	<db-relationship name="auditable4s" source="AUDITABLE3" target="AUDITABLE4" toMany="true">
-		<db-attribute-pair source="ID" target="AUDITABLE3_ID"/>
-	</db-relationship>
-	<db-relationship name="auditable3" source="AUDITABLE4" target="AUDITABLE3" toMany="false">
-		<db-attribute-pair source="AUDITABLE3_ID" target="ID"/>
-	</db-relationship>
 	<db-relationship name="parent" source="AUDITABLE_CHILD1" target="AUDITABLE1" toMany="false">
 		<db-attribute-pair source="AUDITABLE1_ID" target="ID"/>
 	</db-relationship>
@@ -169,8 +137,6 @@
 	</db-relationship>
 	<obj-relationship name="children1" source="Auditable1" target="AuditableChild1" deleteRule="Deny" db-relationship-path="children1"/>
 	<obj-relationship name="children" source="Auditable2" target="AuditableChild3" deleteRule="Deny" db-relationship-path="children"/>
-	<obj-relationship name="auditable4s" source="Auditable3" target="Auditable4" deleteRule="Deny" db-relationship-path="auditable4s"/>
-	<obj-relationship name="auditable3" source="Auditable4" target="Auditable3" deleteRule="Nullify" db-relationship-path="auditable3"/>
 	<obj-relationship name="parent" source="AuditableChild1" target="Auditable1" deleteRule="Nullify" db-relationship-path="parent"/>
 	<obj-relationship name="parent" source="AuditableChild2" target="Auditable1" deleteRule="Nullify" db-relationship-path="parent"/>
 	<obj-relationship name="parent" source="AuditableChild3" target="Auditable2" deleteRule="Nullify" db-relationship-path="parent"/>

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-postcommit/pom.xml
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/pom.xml b/cayenne-postcommit/pom.xml
new file mode 100644
index 0000000..318a252
--- /dev/null
+++ b/cayenne-postcommit/pom.xml
@@ -0,0 +1,90 @@
+<?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/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>cayenne-parent</artifactId>
+        <groupId>org.apache.cayenne</groupId>
+        <version>4.0.M6-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>cayenne-postcommit</artifactId>
+    <name>cayenne-postcommit: Cayenne Postcommit Module</name>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <!-- Compile dependencies -->
+        <dependency>
+            <groupId>org.apache.cayenne</groupId>
+            <artifactId>cayenne-server</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <!-- Test dependencies -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cayenne.build-tools</groupId>
+            <artifactId>cayenne-test-utilities</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cayenne</groupId>
+            <artifactId>cayenne-server</artifactId>
+            <version>${project.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.hsqldb</groupId>
+            <artifactId>hsqldb</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <!-- This ensures LICENSE and NOTICE inclusion in all jars -->
+            <plugin>
+                <artifactId>maven-remote-resources-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>process</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/audit/Auditable.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/audit/Auditable.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/audit/Auditable.java
new file mode 100644
index 0000000..a826bd8
--- /dev/null
+++ b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/audit/Auditable.java
@@ -0,0 +1,76 @@
+/*****************************************************************
+ *   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 java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.apache.cayenne.lifecycle.postcommit.Confidential;
+
+/**
+ * An annotation that adds auditing behavior to DataObjects.
+ * 
+ * @since 3.1
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface Auditable {
+
+	/**
+	 * Returns an array of entity properties that should be excluded from audit.
+	 */
+	String[] ignoredProperties() default {};
+
+	/**
+	 * Returns whether all attributes should be excluded from audit.
+	 * 
+	 * @since 4.0
+	 */
+	boolean ignoreAttributes() default false;
+
+	/**
+	 * Returns whether all to-one relationships should be excluded from audit.
+	 * 
+	 * @since 4.0
+	 */
+	boolean ignoreToOneRelationships() default false;
+
+	/**
+	 * Returns whether all to-many relationships should be excluded from audit.
+	 * 
+	 * @since 4.0
+	 */
+	boolean ignoreToManyRelationships() default false;
+
+	/**
+	 * Returns an array of properties that should be treated as confidential.
+	 * I.e. their change should be recorded, but their values should be hidden
+	 * from listeners. In practice both old and new values will be set to an
+	 * instance of {@link Confidential}.
+	 * 
+	 * @since 4.0
+	 */
+	String[] confidential() default {};
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/AttributeChange.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/AttributeChange.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/AttributeChange.java
new file mode 100644
index 0000000..c70786c
--- /dev/null
+++ b/cayenne-postcommit/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 extends PropertyChange {
+
+	Object getOldValue();
+
+	Object getNewValue();
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/ChangeMap.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/ChangeMap.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/ChangeMap.java
new file mode 100644
index 0000000..ec1c8e5
--- /dev/null
+++ b/cayenne-postcommit/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/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableAttributeChange.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableAttributeChange.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableAttributeChange.java
new file mode 100644
index 0000000..6d6cdc0
--- /dev/null
+++ b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableAttributeChange.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.changemap;
+
+/**
+ * @since 4.0
+ */
+public class MutableAttributeChange implements AttributeChange {
+
+	private Object oldValue;
+	private Object newValue;
+
+	@Override
+	public <T> T accept(PropertyChangeVisitor<T> visitor) {
+		return visitor.visitAttribute(this);
+	}
+
+	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/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableChangeMap.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableChangeMap.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableChangeMap.java
new file mode 100644
index 0000000..3df3043
--- /dev/null
+++ b/cayenne-postcommit/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/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableObjectChange.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableObjectChange.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableObjectChange.java
new file mode 100644
index 0000000..47fa7b2
--- /dev/null
+++ b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableObjectChange.java
@@ -0,0 +1,197 @@
+/*****************************************************************
+ *   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 PropertyChange> getChanges() {
+		Map<String, PropertyChange> allChanges = new HashMap<>();
+
+		if (attributeChanges != null) {
+			allChanges.putAll(attributeChanges);
+		}
+
+		if (toOneRelationshipChanges != null) {
+			allChanges.putAll(toOneRelationshipChanges);
+		}
+
+		if (toManyRelationshipChanges != null) {
+			allChanges.putAll(toManyRelationshipChanges);
+		}
+
+		return allChanges;
+	}
+
+	@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/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToManyRelationshipChange.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToManyRelationshipChange.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToManyRelationshipChange.java
new file mode 100644
index 0000000..b49f618
--- /dev/null
+++ b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToManyRelationshipChange.java
@@ -0,0 +1,69 @@
+/*****************************************************************
+ *   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 <T> T accept(PropertyChangeVisitor<T> visitor) {
+		return visitor.visitToManyRelationship(this);
+	}
+
+	@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/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToOneRelationshipChange.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToOneRelationshipChange.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToOneRelationshipChange.java
new file mode 100644
index 0000000..674938e
--- /dev/null
+++ b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToOneRelationshipChange.java
@@ -0,0 +1,53 @@
+/*****************************************************************
+ *   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 <T> T accept(PropertyChangeVisitor<T> visitor) {
+		return visitor.visitToOneRelationship(this);
+	}
+
+	@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/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChange.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChange.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChange.java
new file mode 100644
index 0000000..b7ed806
--- /dev/null
+++ b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChange.java
@@ -0,0 +1,45 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.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 PropertyChange> getChanges();
+
+	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/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChangeType.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChangeType.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChangeType.java
new file mode 100644
index 0000000..77d9895
--- /dev/null
+++ b/cayenne-postcommit/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/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/PropertyChange.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/PropertyChange.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/PropertyChange.java
new file mode 100644
index 0000000..47e7323
--- /dev/null
+++ b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/PropertyChange.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;
+
+/**
+ * A base interface for various types of property changes.
+ * 
+ * @since 4.0
+ */
+public interface PropertyChange {
+
+	<T> T accept(PropertyChangeVisitor<T> visitor);
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/PropertyChangeVisitor.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/PropertyChangeVisitor.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/PropertyChangeVisitor.java
new file mode 100644
index 0000000..134ba98
--- /dev/null
+++ b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/PropertyChangeVisitor.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;
+
+/**
+ * @since 4.0
+ */
+public interface PropertyChangeVisitor<T> {
+
+	T visitAttribute(AttributeChange change);
+
+	T visitToOneRelationship(ToOneRelationshipChange change);
+
+	T visitToManyRelationship(ToManyRelationshipChange change);
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/ToManyRelationshipChange.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/ToManyRelationshipChange.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/ToManyRelationshipChange.java
new file mode 100644
index 0000000..95fb394
--- /dev/null
+++ b/cayenne-postcommit/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 extends PropertyChange {
+
+	Collection<ObjectId> getAdded();
+
+	Collection<ObjectId> getRemoved();
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/ToOneRelationshipChange.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/ToOneRelationshipChange.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/changemap/ToOneRelationshipChange.java
new file mode 100644
index 0000000..7c5cc47
--- /dev/null
+++ b/cayenne-postcommit/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 extends PropertyChange {
+
+	ObjectId getOldValue();
+
+	ObjectId getNewValue();
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/Confidential.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/Confidential.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/Confidential.java
new file mode 100644
index 0000000..b13fb67
--- /dev/null
+++ b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/Confidential.java
@@ -0,0 +1,41 @@
+/*****************************************************************
+ *   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;
+
+/**
+ * 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/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/DeletedDiffProcessor.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/DeletedDiffProcessor.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/DeletedDiffProcessor.java
new file mode 100644
index 0000000..5f6042a
--- /dev/null
+++ b/cayenne-postcommit/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/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffFilter.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffFilter.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffFilter.java
new file mode 100644
index 0000000..a8494d5
--- /dev/null
+++ b/cayenne-postcommit/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/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffProcessor.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffProcessor.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffProcessor.java
new file mode 100644
index 0000000..7daf6e1
--- /dev/null
+++ b/cayenne-postcommit/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/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter.java
new file mode 100644
index 0000000..d0343b4
--- /dev/null
+++ b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter.java
@@ -0,0 +1,114 @@
+/*****************************************************************
+ *   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 captures commit changes, delegating their
+ * processing to an underlying collection of listeners.
+ * 
+ * @since 4.0
+ */
+public class PostCommitFilter implements DataChannelFilter {
+
+	private PostCommitEntityFactory entityFactory;
+	private Collection<PostCommitListener> listeners;
+
+	public PostCommitFilter(@Inject PostCommitEntityFactory entityFactory,
+			@Inject List<PostCommitListener> listeners) {
+		this.entityFactory = entityFactory;
+		this.listeners = listeners;
+	}
+
+	@Override
+	public void init(DataChannel channel) {
+		// do nothing...
+	}
+
+	@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/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitListener.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitListener.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitListener.java
new file mode 100644
index 0000000..fc9412b
--- /dev/null
+++ b/cayenne-postcommit/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/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModule.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModule.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModule.java
new file mode 100644
index 0000000..458c7ce
--- /dev/null
+++ b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModule.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.postcommit;
+
+import org.apache.cayenne.di.Binder;
+import org.apache.cayenne.di.ListBuilder;
+import org.apache.cayenne.di.Module;
+import org.apache.cayenne.lifecycle.postcommit.meta.IncludeAllPostCommitEntityFactory;
+import org.apache.cayenne.lifecycle.postcommit.meta.PostCommitEntityFactory;
+
+/**
+ * @since 4.0
+ */
+public class PostCommitModule implements Module{
+
+    public static ListBuilder<PostCommitListener> contributeListeners(Binder binder) {
+        return binder.bindList(PostCommitListener.class);
+    }
+
+    @Override
+    public void configure(Binder binder) {
+        contributeListeners(binder);
+        binder.bind(PostCommitEntityFactory.class).to(IncludeAllPostCommitEntityFactory.class);
+        binder.bind(PostCommitFilter.class).to(PostCommitFilter.class);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModuleBuilder.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModuleBuilder.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModuleBuilder.java
new file mode 100644
index 0000000..ae3f691
--- /dev/null
+++ b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModuleBuilder.java
@@ -0,0 +1,137 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.lifecycle.postcommit;
+
+import org.apache.cayenne.configuration.server.ServerModule;
+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;
+import org.apache.cayenne.tx.TransactionFilter;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.util.Collection;
+import java.util.HashSet;
+
+/**
+ * A builder of a module that integrates {@link PostCommitFilter} and
+ * {@link PostCommitListener} in Cayenne.
+ * 
+ * @since 4.0
+ */
+public class PostCommitModuleBuilder {
+
+	private static final Log LOGGER = LogFactory.getLog(PostCommitModuleBuilder.class);
+
+	public static PostCommitModuleBuilder builder() {
+		return new PostCommitModuleBuilder();
+	}
+
+	private Class<? extends PostCommitEntityFactory> entityFactoryType;
+	private Collection<Class<? extends PostCommitListener>> listenerTypes;
+	private Collection<PostCommitListener> listenerInstances;
+	private boolean excludeFromTransaction;
+
+	PostCommitModuleBuilder() {
+		entityFactory(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;
+	}
+
+	/**
+	 * If called, events will be dispatched outside of the main commit
+	 * transaction. By default events are dispatched within the transaction, so
+	 * listeners can commit their code together with the main commit.
+	 */
+	public PostCommitModuleBuilder excludeFromTransaction() {
+		this.excludeFromTransaction = true;
+		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() {
+		return entityFactory(AuditablePostCommitEntityFactory.class);
+	}
+
+	/**
+	 * 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) {
+
+				if (listenerTypes.isEmpty() && listenerInstances.isEmpty()) {
+					LOGGER.info("No listeners configured. Skipping PostCommitFilter registration");
+					return;
+				}
+
+				binder.bind(PostCommitEntityFactory.class).to(entityFactoryType);
+
+				ListBuilder<PostCommitListener> listeners = PostCommitModule.contributeListeners(binder)
+						.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);
+				}
+
+				if (excludeFromTransaction) {
+					ServerModule.contributeDomainFilters(binder).addAfter(PostCommitFilter.class, TransactionFilter.class);
+				} else {
+					ServerModule.contributeDomainFilters(binder).insertBefore(PostCommitFilter.class, TransactionFilter.class);
+				}
+			}
+		};
+	}
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitServerModuleProvider.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitServerModuleProvider.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitServerModuleProvider.java
new file mode 100644
index 0000000..207af35
--- /dev/null
+++ b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitServerModuleProvider.java
@@ -0,0 +1,50 @@
+/*****************************************************************
+ *   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.Collections;
+
+import org.apache.cayenne.configuration.server.CayenneServerModuleProvider;
+import org.apache.cayenne.configuration.server.ServerModule;
+import org.apache.cayenne.di.Module;
+
+/**
+ * @since 4.0
+ */
+public class PostCommitServerModuleProvider implements CayenneServerModuleProvider {
+
+    @Override
+    public Module module() {
+        return new PostCommitModule();
+    }
+
+    @Override
+    public Class<? extends Module> moduleType() {
+        return PostCommitModule.class;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Collection<Class<? extends Module>> overrides() {
+        Collection modules = Collections.singletonList(ServerModule.class);
+        return modules;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/AuditablePostCommitEntityFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/AuditablePostCommitEntityFactory.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/AuditablePostCommitEntityFactory.java
new file mode 100644
index 0000000..424c7d2
--- /dev/null
+++ b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/AuditablePostCommitEntityFactory.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.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 a = classDescriptor.getObjectClass().getAnnotation(Auditable.class);
+		if (a == null) {
+			return BLOCKED_ENTITY;
+		}
+
+		ObjEntity entity = entityResolver.getObjEntity(entityName);
+		return new MutablePostCommitEntity(entity).setConfidential(a.confidential())
+				.setIgnoreProperties(a.ignoredProperties()).setIgnoreAttributes(a.ignoreAttributes())
+				.setIgnoreToOneRelationships(a.ignoreToOneRelationships())
+				.setIgnoreToManyRelationships(a.ignoreToManyRelationships());
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/af5ae785/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/IncludeAllPostCommitEntityFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/IncludeAllPostCommitEntityFactory.java b/cayenne-postcommit/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/IncludeAllPostCommitEntityFactory.java
new file mode 100644
index 0000000..2064885
--- /dev/null
+++ b/cayenne-postcommit/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;
+
+	}
+}


Mime
View raw message