/* * 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.isis.core.runtime.services.changes; import java.util.Collections; import java.util.Map; import java.util.Set; import javax.enterprise.context.RequestScoped; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.apache.isis.applib.annotation.DomainService; import org.apache.isis.applib.annotation.NatureOfService; import org.apache.isis.applib.annotation.Programmatic; import org.apache.isis.applib.annotation.PublishedObject; import org.apache.isis.applib.services.HasTransactionId; import org.apache.isis.applib.services.WithTransactionScope; import org.apache.isis.core.metamodel.adapter.ObjectAdapter; import org.apache.isis.core.metamodel.spec.ObjectSpecification; import org.apache.isis.core.metamodel.spec.feature.Contributed; import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation; import org.apache.isis.core.runtime.system.transaction.IsisTransaction; @DomainService( nature = NatureOfService.DOMAIN, menuOrder = "" + Integer.MAX_VALUE ) @RequestScoped public class ChangedObjectsServiceInternal implements WithTransactionScope { /** * Used for auditing: this contains the pre- values of every property of every object enlisted. * * <p> * When {@link #getChangedObjectProperties()} is called, then this is cleared out and {@link #changedObjectProperties} is non-null, containing * the actual differences. * </p> */ private final Map<AdapterAndProperty, PreAndPostValues> enlistedObjectProperties = Maps.newLinkedHashMap(); /** * Used for auditing; contains the pre- and post- values of every property of every object that actually changed. * * <p> * Will be null until {@link #getChangedObjectProperties()} is called, thereafter contains the actual changes. * </p> */ private Set<Map.Entry<AdapterAndProperty, PreAndPostValues>> changedObjectProperties; // used for publishing private final Map<ObjectAdapter,PublishedObject.ChangeKind> changeKindByEnlistedAdapter = Maps.newLinkedHashMap(); @Programmatic public boolean isEnlisted(ObjectAdapter adapter) { return changeKindByEnlistedAdapter.containsKey(adapter); } /** * Auditing and publishing support: for object stores to enlist an object that has just been created, * capturing a dummy value <tt>'[NEW]'</tt> for the pre-modification value. * * <p> * The post-modification values are captured when the transaction commits. * * <p> * Supported by the JDO object store; check documentation for support in other objectstores. */ @Programmatic public void enlistCreated(final ObjectAdapter adapter) { if(shouldIgnore(adapter)) { return; } enlistForPublishing(adapter, PublishedObject.ChangeKind.CREATE); for (ObjectAssociation property : adapter.getSpecification().getAssociations(Contributed.EXCLUDED, ObjectAssociation.Filters.PROPERTIES)) { final AdapterAndProperty aap = AdapterAndProperty.of(adapter, property); if(property.isNotPersisted()) { continue; } if(enlistedObjectProperties.containsKey(aap)) { // already enlisted, so ignore return; } PreAndPostValues papv = PreAndPostValues.pre(IsisTransaction.Placeholder.NEW); enlistedObjectProperties.put(aap, papv); } } /** * Auditing and publishing support: for object stores to enlist an object that is about to be updated, * capturing the pre-modification values of the properties of the {@link ObjectAdapter}. * * <p> * The post-modification values are captured when the transaction commits. * * <p> * Supported by the JDO object store; check documentation for support in other objectstores. */ @Programmatic public void enlistUpdating(final ObjectAdapter adapter) { if(shouldIgnore(adapter)) { return; } enlistForPublishing(adapter, PublishedObject.ChangeKind.UPDATE); for (ObjectAssociation property : adapter.getSpecification().getAssociations(Contributed.EXCLUDED, ObjectAssociation.Filters.PROPERTIES)) { final AdapterAndProperty aap = AdapterAndProperty.of(adapter, property); if(property.isNotPersisted()) { continue; } if(enlistedObjectProperties.containsKey(aap)) { // already enlisted, so ignore continue; } PreAndPostValues papv = PreAndPostValues.pre(aap.getPropertyValue()); enlistedObjectProperties.put(aap, papv); } } /** * Auditing and publishing support: for object stores to enlist an object that is about to be deleted, * capturing the pre-deletion value of the properties of the {@link ObjectAdapter}. * * <p> * The post-modification values are captured when the transaction commits. In the case of deleted objects, a * dummy value <tt>'[DELETED]'</tt> is used as the post-modification value. * * <p> * Supported by the JDO object store; check documentation for support in other objectstores. */ @Programmatic public void enlistDeleting(final ObjectAdapter adapter) { if(shouldIgnore(adapter)) { return; } final boolean enlisted = enlistForPublishing(adapter, PublishedObject.ChangeKind.DELETE); if(!enlisted) { return; } for (ObjectAssociation property : adapter.getSpecification().getAssociations(Contributed.EXCLUDED, ObjectAssociation.Filters.PROPERTIES)) { final AdapterAndProperty aap = AdapterAndProperty.of(adapter, property); if(property.isNotPersisted()) { continue; } if(enlistedObjectProperties.containsKey(aap)) { // already enlisted, so ignore return; } PreAndPostValues papv = PreAndPostValues.pre(aap.getPropertyValue()); enlistedObjectProperties.put(aap, papv); } } /** * @return <code>true</code> if successfully enlisted, <code>false</code> if was already enlisted */ private boolean enlistForPublishing(final ObjectAdapter adapter, final PublishedObject.ChangeKind current) { final PublishedObject.ChangeKind previous = changeKindByEnlistedAdapter.get(adapter); if(previous == null) { changeKindByEnlistedAdapter.put(adapter, current); return true; } switch (previous) { case CREATE: switch (current) { case DELETE: changeKindByEnlistedAdapter.remove(adapter); case CREATE: case UPDATE: return false; } break; case UPDATE: switch (current) { case DELETE: changeKindByEnlistedAdapter.put(adapter, current); return true; case CREATE: case UPDATE: return false; } break; case DELETE: return false; } return previous == null; } /** * Intended to be called at the end of the transaction. Use {@link #resetForNextTransaction()} once fully read. */ @Programmatic public Set<Map.Entry<AdapterAndProperty, PreAndPostValues>> getChangedObjectProperties() { return changedObjectProperties != null ? changedObjectProperties : (changedObjectProperties = capturePostValuesAndDrain(enlistedObjectProperties)); } private Set<Map.Entry<AdapterAndProperty, PreAndPostValues>> capturePostValuesAndDrain(final Map<AdapterAndProperty, PreAndPostValues> changedObjectProperties) { final Map<AdapterAndProperty, PreAndPostValues> processedObjectProperties1 = Maps.newLinkedHashMap(); while(!changedObjectProperties.isEmpty()) { final Set<AdapterAndProperty> keys = Sets.newLinkedHashSet(changedObjectProperties.keySet()); for (final AdapterAndProperty aap : keys) { final PreAndPostValues papv = changedObjectProperties.remove(aap); final ObjectAdapter adapter = aap.getAdapter(); if(adapter.isDestroyed()) { // don't touch the object!!! // JDO, for example, will complain otherwise... papv.setPost(IsisTransaction.Placeholder.DELETED); } else { papv.setPost(aap.getPropertyValue()); } // if we encounter the same objectProperty again, this will simply overwrite it processedObjectProperties1.put(aap, papv); } } return Collections.unmodifiableSet( Sets.filter(processedObjectProperties1.entrySet(), PreAndPostValues.Predicates.CHANGED)); } protected boolean shouldIgnore(final ObjectAdapter adapter) { final ObjectSpecification adapterSpec = adapter.getSpecification(); final Class<?> adapterClass = adapterSpec.getCorrespondingClass(); return HasTransactionId.class.isAssignableFrom(adapterClass); } @Programmatic public Map<ObjectAdapter, PublishedObject.ChangeKind> getChangeKindByEnlistedAdapter() { return changeKindByEnlistedAdapter; } @Programmatic public int numberObjectsDirtied() { return changeKindByEnlistedAdapter.size(); } @Programmatic public int numberObjectPropertiesModified() { if(changedObjectProperties == null) { // normally done during auditing, but in case none of the objects in this xactn are audited... getChangedObjectProperties(); } return changedObjectProperties.size(); } /** * Intended to be called at the end of a transaction. (This service really ought to be considered * a transaction-scoped service; since that isn't yet supported by the framework, we have to manually reset). */ @Override @Programmatic public void resetForNextTransaction() { enlistedObjectProperties.clear(); changedObjectProperties = null; } static String asString(Object object) { return object != null? object.toString(): null; } }