/***************************************************************** * 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.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.apache.cayenne.DataChannel; import org.apache.cayenne.DataChannelFilter; import org.apache.cayenne.DataChannelFilterChain; import org.apache.cayenne.DataObject; import org.apache.cayenne.ObjectContext; import org.apache.cayenne.Persistent; import org.apache.cayenne.QueryResponse; import org.apache.cayenne.annotation.PostPersist; import org.apache.cayenne.annotation.PostRemove; import org.apache.cayenne.annotation.PostUpdate; import org.apache.cayenne.graph.GraphDiff; import org.apache.cayenne.lifecycle.changeset.ChangeSetFilter; import org.apache.cayenne.map.EntityResolver; import org.apache.cayenne.map.ObjEntity; import org.apache.cayenne.query.Query; /** * A {@link DataChannelFilter} that enables audit of entities annotated with * {@link Auditable} and {@link AuditableChild}. Note that this filter relies on * {@link ChangeSetFilter} presence in the DataDomain filter chain to be able to * analyze ignored properties. * * @since 3.1 * @deprecated since 4.0, use {@link org.apache.cayenne.lifecycle.postcommit.PostCommitFilter} */ @Deprecated public class AuditableFilter implements DataChannelFilter { private ThreadLocal<AuditableAggregator> threadAggregator; private ConcurrentMap<String, AuditableEntityDescriptor> entityDescriptors; protected AuditableProcessor processor; protected EntityResolver entityResolver; /** * @since 4.0 */ public AuditableFilter(AuditableProcessor processor) { this.processor = processor; this.entityDescriptors = new ConcurrentHashMap<>(); this.threadAggregator = new ThreadLocal<>(); } /** * @deprecated since 3.1 use {@link #AuditableFilter(AuditableProcessor)} * constructor - EntityResolver will be initialized in 'init'. */ @Deprecated public AuditableFilter(EntityResolver entityResolver, AuditableProcessor processor) { this(processor); } public void init(DataChannel channel) { this.entityResolver = channel.getEntityResolver(); } public QueryResponse onQuery(ObjectContext originatingContext, Query query, DataChannelFilterChain filterChain) { return filterChain.onQuery(originatingContext, query); } public GraphDiff onSync(ObjectContext originatingContext, GraphDiff changes, int syncType, DataChannelFilterChain filterChain) { GraphDiff response; try { response = filterChain.onSync(originatingContext, changes, syncType); if (syncType == DataChannel.FLUSH_CASCADE_SYNC || syncType == DataChannel.FLUSH_NOCASCADE_SYNC) { postSync(); } } finally { cleanupPostSync(); } return response; } /** * A method called at the end of every * {@link #onSync(ObjectContext, GraphDiff, int, DataChannelFilterChain)} * invocation. This implementation uses it for cleaning up thread-local * state of the filter. Subclasses may override it to do their own cleanup, * and are expected to call super. */ protected void cleanupPostSync() { threadAggregator.set(null); } void postSync() { AuditableAggregator aggregator = threadAggregator.get(); if (aggregator != null) { // must reset thread aggregator before processing the audit // operations // to avoid an endless processing loop if audit processor commits // something threadAggregator.set(null); aggregator.postSync(); } } private AuditableAggregator getAggregator() { AuditableAggregator aggregator = threadAggregator.get(); if (aggregator == null) { aggregator = new AuditableAggregator(processor); threadAggregator.set(aggregator); } return aggregator; } @PostPersist(entityAnnotations = Auditable.class) void insertAudit(Persistent object) { getAggregator().audit(object, AuditableOperation.INSERT); } @PostRemove(entityAnnotations = Auditable.class) void deleteAudit(Persistent object) { getAggregator().audit(object, AuditableOperation.DELETE); } @PostUpdate(entityAnnotations = Auditable.class) void updateAudit(Persistent object) { if (isAuditableUpdate(object, false)) { getAggregator().audit(object, AuditableOperation.UPDATE); } } // only catching child updates... child insert/delete presumably causes an // event on // the owner object @PostUpdate(entityAnnotations = AuditableChild.class) void updateAuditChild(Persistent object) { if (isAuditableUpdate(object, true)) { Persistent parent = getParent(object); if (parent != null) { // not calling 'updateAudit' to skip checking // 'isAuditableUpdate' on // parent getAggregator().audit(parent, AuditableOperation.UPDATE); } else { // TODO: maybe log this fact... shouldn't normally happen, but I // can // imagine certain combinations of object graphs, disconnected // relationships, delete rules, etc. may cause this } } } protected Persistent getParent(Persistent object) { if (object == null) { throw new NullPointerException("Null object"); } if (!(object instanceof DataObject)) { throw new IllegalArgumentException("Object is not a DataObject: " + object.getClass().getName()); } DataObject dataObject = (DataObject) object; AuditableChild annotation = dataObject.getClass().getAnnotation(AuditableChild.class); if (annotation == null) { throw new IllegalArgumentException("No 'AuditableChild' annotation found"); } String propertyPath = annotation.value(); if (propertyPath.equals("")) { propertyPath = objectIdRelationshipName(annotation.objectIdRelationship()); } if (propertyPath == null || propertyPath.equals("")) { throw new IllegalStateException("Either 'value' or 'objectIdRelationship' of @AuditableChild must be set"); } return (Persistent) dataObject.readNestedProperty(propertyPath); } // TODO: It's a temporary clone method of {@link // org.apache.cayenne.lifecycle.relationship.ObjectIdRelationshipHandler#objectIdRelationshipName(String)}. // Needs to be encapsulated to some separate class to avoid a code // duplication private String objectIdRelationshipName(String uuidPropertyName) { return "cay:related:" + uuidPropertyName; } protected boolean isAuditableUpdate(Persistent object, boolean child) { AuditableEntityDescriptor descriptor = getEntityDescriptor(object, child); return descriptor.auditableChange(object); } private AuditableEntityDescriptor getEntityDescriptor(Persistent object, boolean child) { ObjEntity entity = entityResolver.getObjEntity(object); AuditableEntityDescriptor descriptor = entityDescriptors.get(entity.getName()); if (descriptor == null) { String[] ignoredProperties; if (child) { AuditableChild annotation = object.getClass().getAnnotation(AuditableChild.class); ignoredProperties = annotation != null ? annotation.ignoredProperties() : null; } else { Auditable annotation = object.getClass().getAnnotation(Auditable.class); ignoredProperties = annotation != null ? annotation.ignoredProperties() : null; } descriptor = new AuditableEntityDescriptor(entity, ignoredProperties); AuditableEntityDescriptor existingDescriptor = entityDescriptors.putIfAbsent(entity.getName(), descriptor); if (existingDescriptor != null) { descriptor = existingDescriptor; } } return descriptor; } }