/*
* 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;
}
}