CAY-2030 Capturing a stream of commit changes
Project: http://git-wip-us.apache.org/repos/asf/cayenne/repo Commit: http://git-wip-us.apache.org/repos/asf/cayenne/commit/58c7c3b3 Tree: http://git-wip-us.apache.org/repos/asf/cayenne/tree/58c7c3b3 Diff: http://git-wip-us.apache.org/repos/asf/cayenne/diff/58c7c3b3 Branch: refs/heads/master Commit: 58c7c3b37c1d774a794dfe335beb48327de7e08e Parents: 7decb6c Author: aadamchik <aadamc...@apache.org> Authored: Fri Oct 9 15:32:20 2015 -0400 Committer: aadamchik <aadamc...@apache.org> Committed: Fri Oct 9 15:42:14 2015 -0400 ---------------------------------------------------------------------- cayenne-lifecycle/pom.xml | 104 ++++--- .../cayenne/lifecycle/audit/Auditable.java | 5 + .../lifecycle/changemap/AttributeChange.java | 32 ++ .../cayenne/lifecycle/changemap/ChangeMap.java | 42 +++ .../changemap/MutableAttributeChange.java | 46 +++ .../lifecycle/changemap/MutableChangeMap.java | 78 +++++ .../changemap/MutableObjectChange.java | 178 +++++++++++ .../MutableToManyRelationshipChange.java | 64 ++++ .../MutableToOneRelationshipChange.java | 48 +++ .../lifecycle/changemap/ObjectChange.java | 43 +++ .../lifecycle/changemap/ObjectChangeType.java | 29 ++ .../changemap/ToManyRelationshipChange.java | 35 +++ .../changemap/ToOneRelationshipChange.java | 31 ++ .../lifecycle/postcommit/Confidential.java | 23 ++ .../postcommit/DeletedDiffProcessor.java | 140 +++++++++ .../lifecycle/postcommit/DiffFilter.java | 88 ++++++ .../lifecycle/postcommit/DiffProcessor.java | 104 +++++++ .../lifecycle/postcommit/PostCommitFilter.java | 118 ++++++++ .../postcommit/PostCommitListener.java | 32 ++ .../postcommit/PostCommitModuleBuilder.java | 121 ++++++++ .../meta/AuditablePostCommitEntityFactory.java | 101 +++++++ .../meta/DefaultPostCommitEntity.java | 78 +++++ .../meta/IncludeAllPostCommitEntityFactory.java | 51 ++++ .../postcommit/meta/PostCommitEntity.java | 34 +++ .../meta/PostCommitEntityFactory.java | 29 ++ .../lifecycle/audit/AuditableFilterIT.java | 249 ++++++++++++++++ .../audit/AuditableFilter_InRuntime_Test.java | 294 ------------------ .../apache/cayenne/lifecycle/db/Auditable2.java | 2 +- .../postcommit/PostCommitFilter_AllIT.java | 295 +++++++++++++++++++ .../postcommit/PostCommitFilter_FilteredIT.java | 164 +++++++++++ .../postcommit/PostCommitModuleBuilderTest.java | 67 +++++ .../lifecycle/unit/LifecycleServerCase.java | 83 ++++++ docs/doc/src/main/resources/RELEASE-NOTES.txt | 1 + 33 files changed, 2460 insertions(+), 349 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/pom.xml ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/pom.xml b/cayenne-lifecycle/pom.xml index d1f8110..e37f38d 100644 --- a/cayenne-lifecycle/pom.xml +++ b/cayenne-lifecycle/pom.xml @@ -1,23 +1,16 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. ---> -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> +<!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor + license agreements. See the NOTICE file distributed with this work for additional + information regarding copyright ownership. The ASF licenses this file to + you under the Apache License, Version 2.0 (the "License"); you may not use + this file except in compliance with the License. You may obtain a copy of + the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required + by applicable law or agreed to in writing, software distributed under the + License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS + OF ANY KIND, either express or implied. See the License for the specific + language governing permissions and limitations under the License. --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <artifactId>cayenne-parent</artifactId> @@ -28,7 +21,7 @@ <name>Cayenne Lifecycle Utilities</name> <packaging>jar</packaging> <dependencies> - + <!-- Compile dependencies --> <dependency> <groupId>org.apache.cayenne</groupId> @@ -59,22 +52,22 @@ <scope>test</scope> </dependency> <dependency> - <groupId>org.slf4j</groupId> - <artifactId>jcl-over-slf4j</artifactId> - <scope>test</scope> + <groupId>org.slf4j</groupId> + <artifactId>jcl-over-slf4j</artifactId> + <scope>test</scope> </dependency> <dependency> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-api</artifactId> - <scope>test</scope> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <scope>test</scope> </dependency> <dependency> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-simple</artifactId> - <scope>test</scope> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <scope>test</scope> </dependency> </dependencies> - <build> + <build> <plugins> <plugin> <artifactId>maven-remote-resources-plugin</artifactId> @@ -86,30 +79,33 @@ </execution> </executions> </plugin> - </plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-failsafe-plugin</artifactId> + </plugin> + </plugins> </build> - <profiles> - <profile> - <id>code-quality</id> + <profiles> + <profile> + <id>code-quality</id> - <activation> - <property> - <name>!fast-and-dirty</name> - </property> - </activation> - <build> - <plugins> - <plugin> - <artifactId>maven-checkstyle-plugin</artifactId> - <!--<configuration> - <suppressionsLocation>${project.basedir}/cayenne-checkstyle-suppression.xml</suppressionsLocation> - </configuration>--> - </plugin> - <plugin> - <artifactId>maven-pmd-plugin</artifactId> - </plugin> - </plugins> - </build> - </profile> - </profiles> + <activation> + <property> + <name>!fast-and-dirty</name> + </property> + </activation> + <build> + <plugins> + <plugin> + <artifactId>maven-checkstyle-plugin</artifactId> + <!--<configuration> <suppressionsLocation>${project.basedir}/cayenne-checkstyle-suppression.xml</suppressionsLocation> + </configuration> --> + </plugin> + <plugin> + <artifactId>maven-pmd-plugin</artifactId> + </plugin> + </plugins> + </build> + </profile> + </profiles> </project> http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/audit/Auditable.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/audit/Auditable.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/audit/Auditable.java index 8f9386d..68a2732 100644 --- a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/audit/Auditable.java +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/audit/Auditable.java @@ -37,4 +37,9 @@ import java.lang.annotation.Target; public @interface Auditable { String[] ignoredProperties() default {}; + + /** + * @since 4.0 + */ + String[] confidential() default {}; } http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/AttributeChange.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/AttributeChange.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/AttributeChange.java new file mode 100644 index 0000000..bdb9cb6 --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/AttributeChange.java @@ -0,0 +1,32 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.changemap; + +/** + * Represents a change in a "value" property, which is either a scalar property + * or a to-one entity relationship. + * + * @since 4.0 + */ +public interface AttributeChange { + + Object getOldValue(); + + Object getNewValue(); +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ChangeMap.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ChangeMap.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ChangeMap.java new file mode 100644 index 0000000..ec1c8e5 --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ChangeMap.java @@ -0,0 +1,42 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.changemap; + +import java.util.Collection; +import java.util.Map; + +import org.apache.cayenne.ObjectId; + +/** + * Represents a map of changes for a graph of persistent objects. + * + * @since 4.0 + */ +public interface ChangeMap { + + /** + * Returns a map of changes. Note the same change sometimes can be present + * in the map twice. If ObjectId of an object has changed during the commit, + * the change will be accessible by both pre-commit and post-commit ID. To + * get unique changes, call {@link #getUniqueChanges()}. + */ + Map<ObjectId, ? extends ObjectChange> getChanges(); + + Collection<? extends ObjectChange> getUniqueChanges(); +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableAttributeChange.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableAttributeChange.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableAttributeChange.java new file mode 100644 index 0000000..4d4ebbf --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableAttributeChange.java @@ -0,0 +1,46 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.changemap; + +/** + * @since 4.0 + */ +public class MutableAttributeChange implements AttributeChange { + + private Object oldValue; + private Object newValue; + + public void setOldValue(Object oldValue) { + this.oldValue = oldValue; + } + + public void setNewValue(Object value) { + this.newValue = value; + } + + @Override + public Object getOldValue() { + return oldValue; + } + + @Override + public Object getNewValue() { + return newValue; + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableChangeMap.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableChangeMap.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableChangeMap.java new file mode 100644 index 0000000..3df3043 --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableChangeMap.java @@ -0,0 +1,78 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.changemap; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +import org.apache.cayenne.ObjectId; + +/** + * A mutable implementation of {@link ChangeMap}. + * + * @since 4.0 + */ +public class MutableChangeMap implements ChangeMap { + + private Map<ObjectId, MutableObjectChange> changes; + + public MutableObjectChange getOrCreate(ObjectId id, ObjectChangeType type) { + MutableObjectChange changeSet = getOrCreate(id); + changeSet.setType(type); + return changeSet; + } + + private MutableObjectChange getOrCreate(ObjectId id) { + + MutableObjectChange objectChange = changes != null ? changes.get(id) : null; + + if (objectChange == null) { + + if (changes == null) { + changes = new HashMap<>(); + } + + objectChange = new MutableObjectChange(id); + changes.put(id, objectChange); + } + + return objectChange; + } + + public MutableObjectChange aliasId(ObjectId preCommitId, ObjectId postCommitId) { + MutableObjectChange changeSet = getOrCreate(preCommitId); + changeSet.setPostCommitId(postCommitId); + changes.put(postCommitId, changeSet); + return changeSet; + } + + @Override + public Collection<? extends ObjectChange> getUniqueChanges() { + // ensure distinct change set + return changes == null ? Collections.<ObjectChange> emptySet() : new HashSet<>(changes.values()); + } + + @Override + public Map<ObjectId, ? extends ObjectChange> getChanges() { + return changes == null ? Collections.<ObjectId, ObjectChange> emptyMap() : changes; + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableObjectChange.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableObjectChange.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableObjectChange.java new file mode 100644 index 0000000..95c81bc --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableObjectChange.java @@ -0,0 +1,178 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.changemap; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.cayenne.ObjectId; + +/** + * A mutable implementation of {@link ObjectChange}. + * + * @since 4.0 + */ +public class MutableObjectChange implements ObjectChange { + + private static final int[] TYPE_PRECEDENCE; + + static { + TYPE_PRECEDENCE = new int[ObjectChangeType.values().length]; + + // decreasing precedence of operations when recording audits is DELETE, + // INSERT, UPDATE + TYPE_PRECEDENCE[ObjectChangeType.DELETE.ordinal()] = 3; + TYPE_PRECEDENCE[ObjectChangeType.INSERT.ordinal()] = 2; + TYPE_PRECEDENCE[ObjectChangeType.UPDATE.ordinal()] = 1; + } + + // note that we are tracking DB-level changes for clarity + + private ObjectId preCommitId; + private ObjectId postCommitId; + private Map<String, MutableAttributeChange> attributeChanges; + private Map<String, MutableToManyRelationshipChange> toManyRelationshipChanges; + private Map<String, MutableToOneRelationshipChange> toOneRelationshipChanges; + + private ObjectChangeType type; + + public MutableObjectChange(ObjectId preCommitId) { + this.preCommitId = preCommitId; + } + + @Override + public Map<String, ? extends AttributeChange> getAttributeChanges() { + return attributeChanges != null ? attributeChanges : Collections.<String, AttributeChange> emptyMap(); + } + + @Override + public Map<String, ? extends ToManyRelationshipChange> getToManyRelationshipChanges() { + return toManyRelationshipChanges != null ? toManyRelationshipChanges + : Collections.<String, ToManyRelationshipChange> emptyMap(); + } + + @Override + public Map<String, ? extends ToOneRelationshipChange> getToOneRelationshipChanges() { + return toOneRelationshipChanges != null ? toOneRelationshipChanges + : Collections.<String, ToOneRelationshipChange> emptyMap(); + } + + @Override + public ObjectChangeType getType() { + return type; + } + + @Override + public ObjectId getPreCommitId() { + return preCommitId; + } + + @Override + public ObjectId getPostCommitId() { + return postCommitId != null ? postCommitId : preCommitId; + } + + public void setPostCommitId(ObjectId postCommitId) { + this.postCommitId = postCommitId; + } + + public void setType(ObjectChangeType changeType) { + if (this.type == null || TYPE_PRECEDENCE[changeType.ordinal()] > TYPE_PRECEDENCE[this.type.ordinal()]) { + this.type = changeType; + } + } + + public void toManyRelationshipConnected(String property, ObjectId value) { + getOrCreateToManyChange(property).connected(value); + } + + public void toManyRelationshipDisconnected(String property, ObjectId value) { + getOrCreateToManyChange(property).disconnected(value); + } + + public void toOneRelationshipConnected(String property, ObjectId value) { + getOrCreateToOneChange(property).connected(value); + } + + public void toOneRelationshipDisconnected(String property, ObjectId value) { + getOrCreateToOneChange(property).disconnected(value); + } + + public void attributeChanged(String property, Object oldValue, Object newValue) { + + if (type == null) { + throw new IllegalStateException("Null op"); + } + + MutableAttributeChange c = getOrCreateAttributeChange(property); + c.setNewValue(newValue); + c.setOldValue(oldValue); + } + + private MutableAttributeChange getOrCreateAttributeChange(String property) { + MutableAttributeChange pChange = attributeChanges != null ? attributeChanges.get(property) : null; + + if (pChange == null) { + + if (attributeChanges == null) { + attributeChanges = new HashMap<>(); + } + + pChange = new MutableAttributeChange(); + attributeChanges.put(property, pChange); + } + + return pChange; + } + + private MutableToOneRelationshipChange getOrCreateToOneChange(String property) { + MutableToOneRelationshipChange pChange = toOneRelationshipChanges != null + ? toOneRelationshipChanges.get(property) : null; + + if (pChange == null) { + + if (toOneRelationshipChanges == null) { + toOneRelationshipChanges = new HashMap<>(); + } + + pChange = new MutableToOneRelationshipChange(); + toOneRelationshipChanges.put(property, pChange); + } + + return pChange; + } + + private MutableToManyRelationshipChange getOrCreateToManyChange(String property) { + MutableToManyRelationshipChange pChange = toManyRelationshipChanges != null + ? toManyRelationshipChanges.get(property) : null; + + if (pChange == null) { + + if (toManyRelationshipChanges == null) { + toManyRelationshipChanges = new HashMap<>(); + } + + pChange = new MutableToManyRelationshipChange(); + toManyRelationshipChanges.put(property, pChange); + } + + return pChange; + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToManyRelationshipChange.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToManyRelationshipChange.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToManyRelationshipChange.java new file mode 100644 index 0000000..215a542 --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToManyRelationshipChange.java @@ -0,0 +1,64 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.changemap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import org.apache.cayenne.ObjectId; + +/** + * @since 4.0 + */ +public class MutableToManyRelationshipChange implements ToManyRelationshipChange { + + private Collection<ObjectId> added; + private Collection<ObjectId> removed; + + @Override + public Collection<ObjectId> getAdded() { + return added == null ? Collections.<ObjectId> emptyList() : added; + } + + @Override + public Collection<ObjectId> getRemoved() { + return removed == null ? Collections.<ObjectId> emptyList() : removed; + } + + public void connected(ObjectId o) { + + // TODO: cancel previously removed ? + if (added == null) { + added = new ArrayList<>(); + } + + added.add(o); + } + + public void disconnected(ObjectId o) { + + // TODO: cancel previously added ? + if (removed == null) { + removed = new ArrayList<>(); + } + + removed.add(o); + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToOneRelationshipChange.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToOneRelationshipChange.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToOneRelationshipChange.java new file mode 100644 index 0000000..d0269d6 --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/MutableToOneRelationshipChange.java @@ -0,0 +1,48 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.changemap; + +import org.apache.cayenne.ObjectId; + +/** + * @since 4.0 + */ +public class MutableToOneRelationshipChange implements ToOneRelationshipChange { + + private ObjectId oldValue; + private ObjectId newValue; + + @Override + public ObjectId getOldValue() { + return oldValue; + } + + @Override + public ObjectId getNewValue() { + return newValue; + } + + public void connected(ObjectId o) { + this.newValue = o; + } + + public void disconnected(ObjectId o) { + this.oldValue = o; + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChange.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChange.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChange.java new file mode 100644 index 0000000..6f41d5f --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChange.java @@ -0,0 +1,43 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.changemap; + +import java.util.Map; + +import org.apache.cayenne.ObjectId; + +/** + * Accumulates changes of a single object with a transaction. + * + * @since 4.0 + */ +public interface ObjectChange { + + ObjectChangeType getType(); + + ObjectId getPreCommitId(); + + ObjectId getPostCommitId(); + + Map<String, ? extends AttributeChange> getAttributeChanges(); + + Map<String, ? extends ToOneRelationshipChange> getToOneRelationshipChanges(); + + Map<String, ? extends ToManyRelationshipChange> getToManyRelationshipChanges(); +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChangeType.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChangeType.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChangeType.java new file mode 100644 index 0000000..77d9895 --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ObjectChangeType.java @@ -0,0 +1,29 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.changemap; + +/** + * Defines types of tracked object changes. + * + * @since 4.0 + */ +public enum ObjectChangeType { + + INSERT, UPDATE, DELETE; +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ToManyRelationshipChange.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ToManyRelationshipChange.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ToManyRelationshipChange.java new file mode 100644 index 0000000..8fcdc59 --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ToManyRelationshipChange.java @@ -0,0 +1,35 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.changemap; + +import java.util.Collection; + +import org.apache.cayenne.ObjectId; + +/** + * Represents a change in a to-many relationship property to another entity. + * + * @since 4.0 + */ +public interface ToManyRelationshipChange { + + Collection<ObjectId> getAdded(); + + Collection<ObjectId> getRemoved(); +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ToOneRelationshipChange.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ToOneRelationshipChange.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ToOneRelationshipChange.java new file mode 100644 index 0000000..0b592ea --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/changemap/ToOneRelationshipChange.java @@ -0,0 +1,31 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.changemap; + +import org.apache.cayenne.ObjectId; + +/** + * @since 4.0 + */ +public interface ToOneRelationshipChange { + + ObjectId getOldValue(); + + ObjectId getNewValue(); +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/Confidential.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/Confidential.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/Confidential.java new file mode 100644 index 0000000..1e02341 --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/Confidential.java @@ -0,0 +1,23 @@ +package org.apache.cayenne.lifecycle.postcommit; + +/** + * A singleton representing a confidential property value. + * + * @since 4.0 + */ +public class Confidential { + + private static final Confidential instance = new Confidential(); + + public static Confidential getInstance() { + return instance; + } + + private Confidential() { + } + + @Override + public String toString() { + return "*******"; + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DeletedDiffProcessor.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DeletedDiffProcessor.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DeletedDiffProcessor.java new file mode 100644 index 0000000..5f6042a --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DeletedDiffProcessor.java @@ -0,0 +1,140 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.postcommit; + +import java.util.List; + +import org.apache.cayenne.DataChannel; +import org.apache.cayenne.DataRow; +import org.apache.cayenne.ObjectId; +import org.apache.cayenne.QueryResponse; +import org.apache.cayenne.graph.GraphChangeHandler; +import org.apache.cayenne.lifecycle.changemap.MutableChangeMap; +import org.apache.cayenne.lifecycle.changemap.MutableObjectChange; +import org.apache.cayenne.lifecycle.changemap.ObjectChangeType; +import org.apache.cayenne.lifecycle.postcommit.meta.PostCommitEntity; +import org.apache.cayenne.lifecycle.postcommit.meta.PostCommitEntityFactory; +import org.apache.cayenne.query.ObjectIdQuery; +import org.apache.cayenne.reflect.AttributeProperty; +import org.apache.cayenne.reflect.ClassDescriptor; +import org.apache.cayenne.reflect.PropertyVisitor; +import org.apache.cayenne.reflect.ToManyProperty; +import org.apache.cayenne.reflect.ToOneProperty; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +class DeletedDiffProcessor implements GraphChangeHandler { + + private static final Log LOGGER = LogFactory.getLog(DeletedDiffProcessor.class); + + private PostCommitEntityFactory entityFactory; + private MutableChangeMap changeSet; + private DataChannel channel; + + DeletedDiffProcessor(MutableChangeMap changeSet, DataChannel channel, PostCommitEntityFactory entityFactory) { + this.changeSet = changeSet; + this.channel = channel; + this.entityFactory = entityFactory; + } + + @Override + public void nodeRemoved(Object nodeId) { + ObjectId id = (ObjectId) nodeId; + + final MutableObjectChange objectChangeSet = changeSet.getOrCreate(id, ObjectChangeType.DELETE); + + // TODO: rewrite with SelectById query after Cayenne upgrade + ObjectIdQuery query = new ObjectIdQuery(id, true, ObjectIdQuery.CACHE); + QueryResponse result = channel.onQuery(null, query); + + @SuppressWarnings("unchecked") + List<DataRow> rows = result.firstList(); + + if (rows.isEmpty()) { + LOGGER.warn("No DB snapshot for object to be deleted, no changes will be recorded. ID: " + id); + return; + } + + final DataRow row = rows.get(0); + + ClassDescriptor descriptor = channel.getEntityResolver().getClassDescriptor(id.getEntityName()); + final PostCommitEntity entity = entityFactory.getEntity(id); + + descriptor.visitProperties(new PropertyVisitor() { + + @Override + public boolean visitAttribute(AttributeProperty property) { + + if (!entity.isIncluded(property.getName())) { + return true; + } + + Object value; + if (entity.isConfidential(property.getName())) { + value = Confidential.getInstance(); + } else { + String key = property.getAttribute().getDbAttributeName(); + value = row.get(key); + } + + if (value != null) { + objectChangeSet.attributeChanged(property.getName(), value, null); + } + return true; + } + + @Override + public boolean visitToOne(ToOneProperty property) { + // TODO record FK changes? + return true; + } + + @Override + public boolean visitToMany(ToManyProperty property) { + return true; + } + + }); + } + + @Override + public void nodeIdChanged(Object nodeId, Object newId) { + // do nothing + } + + @Override + public void nodeCreated(Object nodeId) { + // do nothing + } + + @Override + public void nodePropertyChanged(Object nodeId, String property, Object oldValue, Object newValue) { + // do nothing + } + + @Override + public void arcCreated(Object nodeId, Object targetNodeId, Object arcId) { + // do nothing + } + + @Override + public void arcDeleted(Object nodeId, Object targetNodeId, Object arcId) { + // do nothing + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffFilter.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffFilter.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffFilter.java new file mode 100644 index 0000000..a8494d5 --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffFilter.java @@ -0,0 +1,88 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.postcommit; + +import org.apache.cayenne.ObjectId; +import org.apache.cayenne.graph.GraphChangeHandler; +import org.apache.cayenne.lifecycle.postcommit.meta.PostCommitEntity; +import org.apache.cayenne.lifecycle.postcommit.meta.PostCommitEntityFactory; + +/** + * Filters changes passing only auditable object changes to the underlying + * delegate. + */ +class DiffFilter implements GraphChangeHandler { + + private PostCommitEntityFactory entityFactory; + private GraphChangeHandler delegate; + + DiffFilter(PostCommitEntityFactory entityFactory, GraphChangeHandler delegate) { + this.entityFactory = entityFactory; + this.delegate = delegate; + } + + @Override + public void nodeIdChanged(Object nodeId, Object newId) { + if (entityFactory.getEntity((ObjectId) nodeId).isIncluded()) { + delegate.nodeIdChanged(nodeId, newId); + } + } + + @Override + public void nodeCreated(Object nodeId) { + if (entityFactory.getEntity((ObjectId) nodeId).isIncluded()) { + delegate.nodeCreated(nodeId); + } + } + + @Override + public void nodeRemoved(Object nodeId) { + if (entityFactory.getEntity((ObjectId) nodeId).isIncluded()) { + delegate.nodeRemoved(nodeId); + } + } + + @Override + public void nodePropertyChanged(Object nodeId, String property, Object oldValue, Object newValue) { + PostCommitEntity entity = entityFactory.getEntity((ObjectId) nodeId); + if (entity.isIncluded(property)) { + + if (entity.isConfidential(property)) { + oldValue = Confidential.getInstance(); + newValue = Confidential.getInstance(); + } + + delegate.nodePropertyChanged(nodeId, property, oldValue, newValue); + } + } + + @Override + public void arcCreated(Object nodeId, Object targetNodeId, Object arcId) { + if (entityFactory.getEntity((ObjectId) nodeId).isIncluded(arcId.toString())) { + delegate.arcCreated(nodeId, targetNodeId, arcId); + } + } + + @Override + public void arcDeleted(Object nodeId, Object targetNodeId, Object arcId) { + if (entityFactory.getEntity((ObjectId) nodeId).isIncluded(arcId.toString())) { + delegate.arcDeleted(nodeId, targetNodeId, arcId); + } + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffProcessor.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffProcessor.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffProcessor.java new file mode 100644 index 0000000..7daf6e1 --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/DiffProcessor.java @@ -0,0 +1,104 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.postcommit; + +import org.apache.cayenne.ObjectId; +import org.apache.cayenne.graph.GraphChangeHandler; +import org.apache.cayenne.lifecycle.changemap.MutableChangeMap; +import org.apache.cayenne.lifecycle.changemap.MutableObjectChange; +import org.apache.cayenne.lifecycle.changemap.ObjectChangeType; +import org.apache.cayenne.map.EntityResolver; +import org.apache.cayenne.map.ObjEntity; +import org.apache.cayenne.map.ObjRelationship; + +/** + * Records changes in a given transaction to a {@link MutableChangeMap} object. + * + * @since 4.0 + */ +class DiffProcessor implements GraphChangeHandler { + + private EntityResolver entityResolver; + private MutableChangeMap changeSet; + + DiffProcessor(MutableChangeMap changeSet, EntityResolver entityResolver) { + this.changeSet = changeSet; + this.entityResolver = entityResolver; + } + + @Override + public void nodeRemoved(Object nodeId) { + // do nothing... deletes are processed pre-commit + } + + @Override + public void nodePropertyChanged(Object nodeId, String property, Object oldValue, Object newValue) { + changeSet.getOrCreate((ObjectId) nodeId, ObjectChangeType.UPDATE).attributeChanged(property, oldValue, + newValue); + } + + @Override + public void nodeIdChanged(Object nodeId, Object newId) { + changeSet.aliasId((ObjectId) nodeId, (ObjectId) newId); + } + + @Override + public void nodeCreated(Object nodeId) { + changeSet.getOrCreate((ObjectId) nodeId, ObjectChangeType.INSERT); + } + + @Override + public void arcDeleted(Object nodeId, Object targetNodeId, Object arcId) { + ObjectId id = (ObjectId) nodeId; + String relationshipName = arcId.toString(); + + ObjEntity entity = entityResolver.getObjEntity(id.getEntityName()); + ObjRelationship relationship = entity.getRelationship(relationshipName); + + MutableObjectChange c = changeSet.getOrCreate(id, ObjectChangeType.UPDATE); + + ObjectId tid = (ObjectId) targetNodeId; + + if (relationship.isToMany()) { + c.toManyRelationshipDisconnected(relationshipName, tid); + } else { + c.toOneRelationshipDisconnected(relationshipName, tid); + } + } + + @Override + public void arcCreated(Object nodeId, Object targetNodeId, Object arcId) { + + ObjectId id = (ObjectId) nodeId; + String relationshipName = arcId.toString(); + + ObjEntity entity = entityResolver.getObjEntity(id.getEntityName()); + ObjRelationship relationship = entity.getRelationship(relationshipName); + + MutableObjectChange c = changeSet.getOrCreate(id, ObjectChangeType.UPDATE); + + ObjectId tid = (ObjectId) targetNodeId; + + if (relationship.isToMany()) { + c.toManyRelationshipConnected(relationshipName, tid); + } else { + c.toOneRelationshipConnected(relationshipName, tid); + } + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter.java new file mode 100644 index 0000000..d8390de --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitFilter.java @@ -0,0 +1,118 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.postcommit; + +import java.util.Collection; +import java.util.List; + +import org.apache.cayenne.DataChannel; +import org.apache.cayenne.DataChannelFilter; +import org.apache.cayenne.DataChannelFilterChain; +import org.apache.cayenne.ObjectContext; +import org.apache.cayenne.QueryResponse; +import org.apache.cayenne.di.Inject; +import org.apache.cayenne.graph.GraphChangeHandler; +import org.apache.cayenne.graph.GraphDiff; +import org.apache.cayenne.lifecycle.changemap.ChangeMap; +import org.apache.cayenne.lifecycle.changemap.MutableChangeMap; +import org.apache.cayenne.lifecycle.postcommit.meta.PostCommitEntityFactory; +import org.apache.cayenne.query.Query; + +/** + * A {@link DataChannelFilter} that organizes commit changes + * + * @since 4.0 + */ +public class PostCommitFilter implements DataChannelFilter { + + static final String POST_COMMIT_LISTENERS_LIST = "cayenne.server.post_commit.listeners"; + + private PostCommitEntityFactory entityFactory; + private Collection<PostCommitListener> listeners; + + public PostCommitFilter(@Inject PostCommitEntityFactory entityFactory, + @Inject(POST_COMMIT_LISTENERS_LIST) List<PostCommitListener> listeners) { + this.entityFactory = entityFactory; + this.listeners = listeners; + } + + @Override + public void init(DataChannel channel) { + // TODO Auto-generated method stub + + } + + @Override + public QueryResponse onQuery(ObjectContext originatingContext, Query query, DataChannelFilterChain filterChain) { + return filterChain.onQuery(originatingContext, query); + } + + @Override + public GraphDiff onSync(ObjectContext originatingContext, GraphDiff beforeDiff, int syncType, + DataChannelFilterChain filterChain) { + + // process commits only; skip rollback + if (syncType != DataChannel.FLUSH_CASCADE_SYNC && syncType != DataChannel.FLUSH_NOCASCADE_SYNC) { + return filterChain.onSync(originatingContext, beforeDiff, syncType); + } + + // don't collect changes if there are no listeners + if (listeners.isEmpty()) { + return filterChain.onSync(originatingContext, beforeDiff, syncType); + } + + MutableChangeMap changes = new MutableChangeMap(); + + // passing DataDomain, not ObjectContext to speed things up + // and avoid capturing changed state when fetching snapshots + DataChannel channel = originatingContext.getChannel(); + + beforeCommit(changes, channel, beforeDiff); + GraphDiff afterDiff = filterChain.onSync(originatingContext, beforeDiff, syncType); + afterCommit(changes, channel, beforeDiff, afterDiff); + notifyListeners(originatingContext, changes); + + return afterDiff; + } + + private void beforeCommit(MutableChangeMap changes, DataChannel channel, GraphDiff contextDiff) { + + // capture snapshots of deleted objects before they are purged from + // cache + + GraphChangeHandler handler = new DiffFilter(entityFactory, + new DeletedDiffProcessor(changes, channel, entityFactory)); + contextDiff.apply(handler); + } + + private void afterCommit(MutableChangeMap changes, DataChannel channel, GraphDiff contextDiff, GraphDiff dbDiff) { + + GraphChangeHandler handler = new DiffFilter(entityFactory, + new DiffProcessor(changes, channel.getEntityResolver())); + contextDiff.apply(handler); + dbDiff.apply(handler); + } + + private void notifyListeners(ObjectContext originatingContext, ChangeMap changes) { + for (PostCommitListener l : listeners) { + l.onPostCommit(originatingContext, changes); + } + } + +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitListener.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitListener.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitListener.java new file mode 100644 index 0000000..fc9412b --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitListener.java @@ -0,0 +1,32 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.postcommit; + +import org.apache.cayenne.ObjectContext; +import org.apache.cayenne.lifecycle.changemap.ChangeMap; + +/** + * An interface of a listener of post-commit events. + * + * @since 4.0 + */ +public interface PostCommitListener { + + void onPostCommit(ObjectContext originatingContext, ChangeMap changes); +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModuleBuilder.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModuleBuilder.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModuleBuilder.java new file mode 100644 index 0000000..99b3181 --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/PostCommitModuleBuilder.java @@ -0,0 +1,121 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.postcommit; + +import java.util.Collection; +import java.util.HashSet; + +import org.apache.cayenne.configuration.Constants; +import org.apache.cayenne.di.Binder; +import org.apache.cayenne.di.ListBuilder; +import org.apache.cayenne.di.Module; +import org.apache.cayenne.lifecycle.audit.Auditable; +import org.apache.cayenne.lifecycle.postcommit.meta.AuditablePostCommitEntityFactory; +import org.apache.cayenne.lifecycle.postcommit.meta.IncludeAllPostCommitEntityFactory; +import org.apache.cayenne.lifecycle.postcommit.meta.PostCommitEntity; +import org.apache.cayenne.lifecycle.postcommit.meta.PostCommitEntityFactory; + +/** + * A builder of a module that integrates {@link PostCommitFilter} and + * {@link PostCommitListener} in Cayenne. + * + * @since 4.0 + */ +public class PostCommitModuleBuilder { + + public static PostCommitModuleBuilder builder() { + return new PostCommitModuleBuilder(); + } + + private Class<? extends PostCommitEntityFactory> entityFactoryType; + private Collection<Class<? extends PostCommitListener>> listenerTypes; + private Collection<PostCommitListener> listenerInstances; + + PostCommitModuleBuilder() { + this.entityFactoryType = IncludeAllPostCommitEntityFactory.class; + this.listenerTypes = new HashSet<>(); + this.listenerInstances = new HashSet<>(); + } + + public PostCommitModuleBuilder listener(Class<? extends PostCommitListener> type) { + this.listenerTypes.add(type); + return this; + } + + public PostCommitModuleBuilder listener(PostCommitListener instance) { + this.listenerInstances.add(instance); + return this; + } + + /** + * Installs entity filter that would only include entities annotated with + * {@link Auditable} on the callbacks. Also {@link Auditable#confidential()} + * properties will be obfuscated and {@link Auditable#ignoredProperties()} - + * excluded from the change collection. + */ + public PostCommitModuleBuilder auditableEntitiesOnly() { + this.entityFactoryType = AuditablePostCommitEntityFactory.class; + return this; + } + + /** + * Installs a custom factory for {@link PostCommitEntity} objects that + * allows implementors to use their own annotations, etc. + */ + public PostCommitModuleBuilder entityFactory(Class<? extends PostCommitEntityFactory> entityFactoryType) { + this.entityFactoryType = entityFactoryType; + return this; + } + + /** + * Creates a DI module that would install {@link PostCommitFilter} and its + * listeners in Cayenne. + */ + public Module build() { + return new Module() { + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public void configure(Binder binder) { + + ListBuilder<PostCommitListener> listeners = binder + .<PostCommitListener> bindList(PostCommitFilter.POST_COMMIT_LISTENERS_LIST) + .addAll(listenerInstances); + + // types have to be added one-by-one + for (Class type : listenerTypes) { + + // TODO: temp hack - need to bind each type before adding to + // collection... + binder.bind(type).to(type); + + listeners.add(type); + } + + binder.bind(PostCommitFilter.class).to(PostCommitFilter.class); + + // TODO: should be ordering the filter to go inside transaction + // once the corresponding Jiras are available in Cayenne + binder.bindList(Constants.SERVER_DOMAIN_FILTERS_LIST).add(PostCommitFilter.class); + + binder.bind(PostCommitEntityFactory.class).to(entityFactoryType); + } + }; + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/AuditablePostCommitEntityFactory.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/AuditablePostCommitEntityFactory.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/AuditablePostCommitEntityFactory.java new file mode 100644 index 0000000..f7ce2fd --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/AuditablePostCommitEntityFactory.java @@ -0,0 +1,101 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.postcommit.meta; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.cayenne.DataChannel; +import org.apache.cayenne.ObjectId; +import org.apache.cayenne.di.Inject; +import org.apache.cayenne.di.Provider; +import org.apache.cayenne.lifecycle.audit.Auditable; +import org.apache.cayenne.map.EntityResolver; +import org.apache.cayenne.map.ObjEntity; +import org.apache.cayenne.reflect.ClassDescriptor; + +/** + * Compiles {@link PostCommitEntity}'s based on {@link Auditable} annotation. + * + * @since 4.0 + */ +public class AuditablePostCommitEntityFactory implements PostCommitEntityFactory { + + private static final PostCommitEntity BLOCKED_ENTITY = new PostCommitEntity() { + + @Override + public boolean isIncluded(String property) { + return false; + } + + @Override + public boolean isConfidential(String property) { + return false; + } + + @Override + public boolean isIncluded() { + return false; + } + }; + + private Provider<DataChannel> channelProvider; + private ConcurrentMap<String, PostCommitEntity> entities; + + public AuditablePostCommitEntityFactory(@Inject Provider<DataChannel> channelProvider) { + this.entities = new ConcurrentHashMap<>(); + + // injecting provider instead of DataChannel, as otherwise we end up + // with circular dependency. + this.channelProvider = channelProvider; + } + + @Override + public PostCommitEntity getEntity(ObjectId id) { + String entityName = id.getEntityName(); + + PostCommitEntity descriptor = entities.get(entityName); + if (descriptor == null) { + PostCommitEntity newDescriptor = createDescriptor(entityName); + PostCommitEntity existingDescriptor = entities.putIfAbsent(entityName, newDescriptor); + descriptor = (existingDescriptor != null) ? existingDescriptor : newDescriptor; + } + + return descriptor; + + } + + private EntityResolver getEntityResolver() { + return channelProvider.get().getEntityResolver(); + } + + private PostCommitEntity createDescriptor(String entityName) { + EntityResolver entityResolver = getEntityResolver(); + ClassDescriptor classDescriptor = entityResolver.getClassDescriptor(entityName); + + Auditable annotation = classDescriptor.getObjectClass().getAnnotation(Auditable.class); + if (annotation == null) { + return BLOCKED_ENTITY; + } + + ObjEntity entity = entityResolver.getObjEntity(entityName); + return new DefaultPostCommitEntity(entity, annotation.ignoredProperties(), annotation.confidential()); + } + +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/DefaultPostCommitEntity.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/DefaultPostCommitEntity.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/DefaultPostCommitEntity.java new file mode 100644 index 0000000..7d6ad8c --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/DefaultPostCommitEntity.java @@ -0,0 +1,78 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.postcommit.meta; + +import java.util.Collection; +import java.util.HashSet; + +import org.apache.cayenne.map.ObjEntity; +import org.apache.cayenne.map.ObjRelationship; + +/** + * @since 4.0 + */ +public class DefaultPostCommitEntity implements PostCommitEntity { + + private Collection<String> ignoredProperties; + private Collection<String> confidentialProperties; + + public DefaultPostCommitEntity(ObjEntity entity, String[] ignoredProperties, String[] confidentialProperties) { + + this.ignoredProperties = new HashSet<>(); + this.confidentialProperties = new HashSet<>(); + + // ignoring to-many (presumably traced via changes to target entities) + // TODO: M:N relationships will not be tracked as a result... + + for (ObjRelationship relationship : entity.getRelationships()) { + if (relationship.isToMany()) { + this.ignoredProperties.add(relationship.getName()); + } + } + + // ignore explicitly specified properties + if (ignoredProperties != null) { + for (String property : ignoredProperties) { + this.ignoredProperties.add(property); + } + } + + if (confidentialProperties != null) { + for (String property : confidentialProperties) { + this.confidentialProperties.add(property); + } + } + } + + @Override + public boolean isIncluded(String property) { + return !ignoredProperties.contains(property); + } + + @Override + public boolean isIncluded() { + return true; + } + + @Override + public boolean isConfidential(String property) { + return confidentialProperties.contains(property); + } + +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/IncludeAllPostCommitEntityFactory.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/IncludeAllPostCommitEntityFactory.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/IncludeAllPostCommitEntityFactory.java new file mode 100644 index 0000000..2064885 --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/IncludeAllPostCommitEntityFactory.java @@ -0,0 +1,51 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.postcommit.meta; + +import org.apache.cayenne.ObjectId; + +/** + * @since 4.0 + */ +public class IncludeAllPostCommitEntityFactory implements PostCommitEntityFactory { + + private static final PostCommitEntity ALLOWED_ENTITY = new PostCommitEntity() { + + @Override + public boolean isIncluded(String property) { + return true; + } + + @Override + public boolean isConfidential(String property) { + return false; + } + + @Override + public boolean isIncluded() { + return true; + } + }; + + @Override + public PostCommitEntity getEntity(ObjectId id) { + return ALLOWED_ENTITY; + + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/PostCommitEntity.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/PostCommitEntity.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/PostCommitEntity.java new file mode 100644 index 0000000..1f3d8ca --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/PostCommitEntity.java @@ -0,0 +1,34 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.postcommit.meta; + +/** + * Describes post-commit behavior for a given Cayenne entity. + * + * @since 4.0 + */ +public interface PostCommitEntity { + + boolean isIncluded(); + + boolean isConfidential(String property); + + boolean isIncluded(String property); + +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/PostCommitEntityFactory.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/PostCommitEntityFactory.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/PostCommitEntityFactory.java new file mode 100644 index 0000000..f8f7e0b --- /dev/null +++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/postcommit/meta/PostCommitEntityFactory.java @@ -0,0 +1,29 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.postcommit.meta; + +import org.apache.cayenne.ObjectId; + +/** + * @since 4.0 + */ +public interface PostCommitEntityFactory { + + PostCommitEntity getEntity(ObjectId id); +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/58c7c3b3/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/audit/AuditableFilterIT.java ---------------------------------------------------------------------- diff --git a/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/audit/AuditableFilterIT.java b/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/audit/AuditableFilterIT.java new file mode 100644 index 0000000..717bbca --- /dev/null +++ b/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/audit/AuditableFilterIT.java @@ -0,0 +1,249 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.lifecycle.audit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumMap; +import java.util.Map; + +import org.apache.cayenne.Cayenne; +import org.apache.cayenne.ObjectContext; +import org.apache.cayenne.Persistent; +import org.apache.cayenne.access.DataDomain; +import org.apache.cayenne.lifecycle.changeset.ChangeSetFilter; +import org.apache.cayenne.lifecycle.db.Auditable1; +import org.apache.cayenne.lifecycle.db.Auditable2; +import org.apache.cayenne.lifecycle.db.AuditableChild1; +import org.apache.cayenne.lifecycle.db.AuditableChild2; +import org.apache.cayenne.lifecycle.db.AuditableChild3; +import org.apache.cayenne.lifecycle.db.AuditableChildUuid; +import org.apache.cayenne.lifecycle.id.IdCoder; +import org.apache.cayenne.lifecycle.relationship.ObjectIdRelationshipHandler; +import org.apache.cayenne.lifecycle.unit.LifecycleServerCase; +import org.junit.Test; + +public class AuditableFilterIT extends LifecycleServerCase { + + @Test + public void testAudit_IgnoreRuntimeRelationships() throws Exception { + + auditable1.insert(1, "xx"); + auditable1.insert(2, "yy"); + auditable1.insert(3, "aa"); + auditableChild2.insert(1, 1, "zz"); + + DataDomain domain = runtime.getDataDomain(); + + Processor processor = new Processor(); + + AuditableFilter filter = new AuditableFilter(domain.getEntityResolver(), processor); + domain.addFilter(filter); + + // prerequisite for BaseAuditableProcessor use + ChangeSetFilter changeSetFilter = new ChangeSetFilter(); + domain.addFilter(changeSetFilter); + + ObjectContext context = runtime.newContext(); + + Auditable1 a2 = Cayenne.objectForPK(context, Auditable1.class, 2); + AuditableChild2 a21 = Cayenne.objectForPK(context, AuditableChild2.class, 1); + + a21.setParent(a2); + a21.setCharProperty1("XYZA"); + context.commitChanges(); + + assertEquals(0, processor.size); + + processor.reset(); + + Auditable1 a3 = Cayenne.objectForPK(context, Auditable1.class, 3); + a21.setParent(a3); + a3.setCharProperty1("12"); + + context.commitChanges(); + assertEquals(1, processor.size); + assertTrue(processor.audited.get(AuditableOperation.UPDATE).contains(a3)); + } + + @Test + public void testAudit_IncludeToManyRelationships() throws Exception { + + auditable1.insert(1, "xx"); + auditable1.insert(2, "yy"); + auditableChild1.insert(1, 1, "zz"); + + DataDomain domain = runtime.getDataDomain(); + + Processor processor = new Processor(); + + AuditableFilter filter = new AuditableFilter(domain.getEntityResolver(), processor); + domain.addFilter(filter); + + // prerequisite for BaseAuditableProcessor use + ChangeSetFilter changeSetFilter = new ChangeSetFilter(); + domain.addFilter(changeSetFilter); + + ObjectContext context = runtime.newContext(); + + Auditable1 a2 = Cayenne.objectForPK(context, Auditable1.class, 2); + AuditableChild1 a21 = Cayenne.objectForPK(context, AuditableChild1.class, 1); + + a21.setParent(a2); + context.commitChanges(); + + assertEquals(2, processor.size); + + assertTrue(processor.audited.get(AuditableOperation.UPDATE).contains(a2)); + assertTrue(processor.audited.get(AuditableOperation.UPDATE) + .contains(Cayenne.objectForPK(context, Auditable1.class, 1))); + } + + @Test + public void testAudit_IgnoreProperties() throws Exception { + + auditable2.insert(1, "P1_1", "P2_1"); + auditable2.insert(2, "P1_2", "P2_2"); + auditable2.insert(3, "P1_3", "P2_3"); + + DataDomain domain = runtime.getDataDomain(); + + Processor processor = new Processor(); + + AuditableFilter filter = new AuditableFilter(domain.getEntityResolver(), processor); + domain.addFilter(filter); + + // prerequisite for BaseAuditableProcessor use + ChangeSetFilter changeSetFilter = new ChangeSetFilter(); + domain.addFilter(changeSetFilter); + + ObjectContext context = runtime.newContext(); + + Auditable2 a1 = Cayenne.objectForPK(context, Auditable2.class, 1); + Auditable2 a2 = Cayenne.objectForPK(context, Auditable2.class, 2); + Auditable2 a3 = Cayenne.objectForPK(context, Auditable2.class, 3); + + a1.setCharProperty1("__"); + a2.setCharProperty2("__"); + a3.setCharProperty1("__"); + a3.setCharProperty2("__"); + + context.commitChanges(); + + assertEquals(2, processor.size); + assertTrue(processor.audited.get(AuditableOperation.UPDATE).contains(a2)); + assertTrue(processor.audited.get(AuditableOperation.UPDATE).contains(a3)); + } + + @Test + public void testAuditableChild_IgnoreProperties() throws Exception { + + auditable2.insert(1, "P1_1", "P2_1"); + auditable2.insert(2, "P1_2", "P2_2"); + auditableChild3.insert(1, 1, "C", "D"); + + DataDomain domain = runtime.getDataDomain(); + + Processor processor = new Processor(); + + AuditableFilter filter = new AuditableFilter(domain.getEntityResolver(), processor); + domain.addFilter(filter); + + // prerequisite for BaseAuditableProcessor use + ChangeSetFilter changeSetFilter = new ChangeSetFilter(); + domain.addFilter(changeSetFilter); + + ObjectContext context = runtime.newContext(); + + AuditableChild3 ac1 = Cayenne.objectForPK(context, AuditableChild3.class, 1); + + // a change to ignored property should not cause an audit event + ac1.setCharProperty1("X_X"); + + context.commitChanges(); + assertEquals(0, processor.size); + + processor.reset(); + ac1.setCharProperty2("XXXXX"); + context.commitChanges(); + assertEquals(1, processor.size); + } + + @Test + public void testAuditableChild_objectIdRelationship() throws Exception { + auditable1.insert(1, "xx"); + auditableChildUuid.insert(1, "Auditable1:1", "xxx", "yyy"); + + DataDomain domain = runtime.getDataDomain(); + Processor processor = new Processor(); + + AuditableFilter filter = new AuditableFilter(domain.getEntityResolver(), processor); + domain.addFilter(filter); + + // prerequisite for BaseAuditableProcessor use + ChangeSetFilter changeSetFilter = new ChangeSetFilter(); + domain.addFilter(changeSetFilter); + + ObjectContext context = runtime.newContext(); + AuditableChildUuid ac = Cayenne.objectForPK(context, AuditableChildUuid.class, 1); + Auditable1 a1 = Cayenne.objectForPK(context, Auditable1.class, 1); + IdCoder refHandler = new IdCoder(domain.getEntityResolver()); + ObjectIdRelationshipHandler handler = new ObjectIdRelationshipHandler(refHandler); + handler.relate(ac, a1); + + ac.setCharProperty1("xxxx"); + context.commitChanges(); + assertEquals(1, processor.size); + Collection<Object> auditables = processor.audited.get(AuditableOperation.UPDATE); + assertSame(a1, auditables.toArray()[0]); + + ac.setCharProperty2("yyyy"); + context.commitChanges(); + assertEquals(2, processor.size); + assertSame(a1, auditables.toArray()[1]); + } + + private final class Processor implements AuditableProcessor { + + Map<AuditableOperation, Collection<Object>> audited; + int size; + + Processor() { + reset(); + } + + void reset() { + + audited = new EnumMap<AuditableOperation, Collection<Object>>(AuditableOperation.class); + + for (AuditableOperation op : AuditableOperation.values()) { + audited.put(op, new ArrayList<Object>()); + } + } + + public void audit(Persistent object, AuditableOperation operation) { + audited.get(operation).add(object); + size++; + } + } +}