/*
* 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.memento;
import java.io.Serializable;
import java.util.List;
import com.google.common.collect.Lists;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.isis.core.commons.ensure.Assert;
import org.apache.isis.core.commons.exceptions.IsisException;
import org.apache.isis.core.commons.exceptions.UnknownTypeException;
import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
import org.apache.isis.core.metamodel.adapter.oid.Oid;
import org.apache.isis.core.metamodel.adapter.oid.OidMarshaller;
import org.apache.isis.core.metamodel.adapter.oid.RootOid;
import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy;
import org.apache.isis.core.metamodel.facets.collections.modify.CollectionFacet;
import org.apache.isis.core.metamodel.facets.collections.modify.CollectionFacetUtils;
import org.apache.isis.core.metamodel.facets.object.encodeable.EncodableFacet;
import org.apache.isis.core.metamodel.facets.propcoll.accessor.PropertyOrCollectionAccessorFacet;
import org.apache.isis.core.metamodel.facets.properties.update.modify.PropertySetterFacet;
import org.apache.isis.core.metamodel.spec.ObjectSpecification;
import org.apache.isis.core.metamodel.specloader.SpecificationLoader;
import org.apache.isis.core.metamodel.spec.feature.Contributed;
import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation;
import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
import org.apache.isis.core.runtime.system.context.IsisContext;
import org.apache.isis.core.runtime.system.persistence.PersistenceSession;
import org.apache.isis.core.runtime.system.session.IsisSessionFactory;
/**
* Holds the state for the specified object in serializable form.
*
* <p>
* This object is {@link Serializable} and can be passed over the network
* easily. Also for a persistent objects only the reference's {@link Oid}s are
* held, avoiding the need for serializing the whole object graph.
*/
public class Memento implements Serializable {
private final static Logger LOG = LoggerFactory.getLogger(Memento.class);
private final static long serialVersionUID = 1L;
private final static OidMarshaller OID_MARSHALLER = OidMarshaller.INSTANCE;
private final List<Oid> transientObjects = Lists.newArrayList();
private Data data;
////////////////////////////////////////////////
// constructor, Encodeable
////////////////////////////////////////////////
public Memento(final ObjectAdapter adapter) {
data = adapter == null ? null : createData(adapter);
if (LOG.isDebugEnabled()) {
LOG.debug("created memento for " + this);
}
}
////////////////////////////////////////////////
// createData
////////////////////////////////////////////////
private Data createData(final ObjectAdapter adapter) {
if (adapter.getSpecification().isParentedOrFreeCollection() && !adapter.getSpecification().isEncodeable()) {
return createCollectionData(adapter);
} else {
return createObjectData(adapter);
}
}
private Data createCollectionData(final ObjectAdapter adapter) {
final CollectionFacet facet = CollectionFacetUtils.getCollectionFacetFromSpec(adapter);
final Data[] collData = new Data[facet.size(adapter)];
int i = 0;
for (final ObjectAdapter ref : facet.iterable(adapter)) {
collData[i++] = createReferenceData(ref);
}
final String elementTypeSpecName = adapter.getSpecification().getFullIdentifier();
return new CollectionData(clone(adapter.getOid()), elementTypeSpecName, collData);
}
private ObjectData createObjectData(final ObjectAdapter adapter) {
final Oid adapterOid = clone(adapter.getOid());
transientObjects.add(adapterOid);
final ObjectSpecification cls = adapter.getSpecification();
final List<ObjectAssociation> associations = cls.getAssociations(Contributed.EXCLUDED);
final ObjectData data = new ObjectData(adapterOid, cls.getFullIdentifier());
for (int i = 0; i < associations.size(); i++) {
if (associations.get(i).isNotPersisted()) {
if (associations.get(i).isOneToManyAssociation()) {
continue;
}
if (associations.get(i).containsFacet(PropertyOrCollectionAccessorFacet.class) && !associations.get(i).containsFacet(PropertySetterFacet.class)) {
LOG.debug("ignoring not-settable field " + associations.get(i).getName());
continue;
}
}
createAssociationData(adapter, data, associations.get(i));
}
return data;
}
private void createAssociationData(final ObjectAdapter adapter, final ObjectData data, final ObjectAssociation objectAssoc) {
Object assocData;
if (objectAssoc.isOneToManyAssociation()) {
final ObjectAdapter collAdapter = objectAssoc.get(adapter, InteractionInitiatedBy.FRAMEWORK);
assocData = createCollectionData(collAdapter);
} else if (objectAssoc.getSpecification().isEncodeable()) {
final EncodableFacet facet = objectAssoc.getSpecification().getFacet(EncodableFacet.class);
final ObjectAdapter value = objectAssoc.get(adapter, InteractionInitiatedBy.FRAMEWORK);
assocData = facet.toEncodedString(value);
} else if (objectAssoc.isOneToOneAssociation()) {
final ObjectAdapter referencedAdapter = objectAssoc.get(adapter, InteractionInitiatedBy.FRAMEWORK);
assocData = createReferenceData(referencedAdapter);
} else {
throw new UnknownTypeException(objectAssoc);
}
data.addField(objectAssoc.getId(), assocData);
}
private Data createReferenceData(final ObjectAdapter referencedAdapter) {
if (referencedAdapter == null) {
return null;
}
Oid refOid = clone(referencedAdapter.getOid());
if (refOid == null) {
return createStandaloneData(referencedAdapter);
}
if ( (referencedAdapter.getSpecification().isParented() || refOid.isTransient()) &&
!transientObjects.contains(refOid)) {
transientObjects.add(refOid);
return createObjectData(referencedAdapter);
}
final String specification = referencedAdapter.getSpecification().getFullIdentifier();
return new Data(refOid, specification);
}
private static <T extends Oid> T clone(final T oid) {
if(oid == null) { return null; }
final String oidStr = oid.enString();
return (T) OID_MARSHALLER.unmarshal(oidStr, oid.getClass());
}
private Data createStandaloneData(final ObjectAdapter adapter) {
return new StandaloneData(adapter);
}
////////////////////////////////////////////////
// properties
////////////////////////////////////////////////
public Oid getOid() {
return data.getOid();
}
protected Data getData() {
return data;
}
////////////////////////////////////////////////
// recreateObject
////////////////////////////////////////////////
public ObjectAdapter recreateObject() {
if (data == null) {
return null;
}
final ObjectSpecification spec =
getSpecificationLoader().loadSpecification(data.getClassName());
ObjectAdapter adapter;
final Oid oid = getOid();
if (spec.isParentedOrFreeCollection()) {
final Object recreatedPojo = getPersistenceSession().instantiateAndInjectServices(spec);
adapter = getPersistenceSession().mapRecreatedPojo(oid, recreatedPojo);
populateCollection(adapter, (CollectionData) data);
} else {
Assert.assertTrue("oid must be a RootOid representing an object because spec is not a collection and cannot be a value", oid instanceof RootOid);
RootOid typedOid = (RootOid) oid;
// remove adapter if already in the adapter manager maps, because
// otherwise would (as a side-effect) update the version to that of the current.
adapter = getPersistenceSession().getAdapterFor(typedOid);
if(adapter != null) {
getPersistenceSession().removeAdapter(adapter);
}
// recreate an adapter for the original OID (with correct version)
adapter = getPersistenceSession().adapterFor(typedOid);
updateObject(adapter, data);
}
if (LOG.isDebugEnabled()) {
LOG.debug("recreated object " + adapter.getOid());
}
return adapter;
}
private void populateCollection(final ObjectAdapter collectionAdapter, final CollectionData state) {
final ObjectAdapter[] initData = new ObjectAdapter[state.elements.length];
int i = 0;
for (final Data elementData : state.elements) {
initData[i++] = recreateReference(elementData);
}
final CollectionFacet facet = collectionAdapter.getSpecification().getFacet(CollectionFacet.class);
facet.init(collectionAdapter, initData);
}
private ObjectAdapter recreateReference(final Data data) {
// handle values
if (data instanceof StandaloneData) {
final StandaloneData standaloneData = (StandaloneData) data;
return standaloneData.getAdapter();
}
// reference to entity
Oid oid = data.getOid();
Assert.assertTrue("can only create a reference to an entity", oid instanceof RootOid);
final RootOid rootOid = (RootOid) oid;
final ObjectAdapter referencedAdapter = getPersistenceSession().adapterFor(rootOid);
if (data instanceof ObjectData) {
if (rootOid.isTransient()) {
updateObject(referencedAdapter, data);
}
}
return referencedAdapter;
}
////////////////////////////////////////////////
// helpers
////////////////////////////////////////////////
private void updateObject(final ObjectAdapter adapter, final Data data) {
final Object oid = adapter.getOid();
if (oid != null && !oid.equals(data.getOid())) {
throw new IllegalArgumentException("This memento can only be used to update the ObjectAdapter with the Oid " + data.getOid() + " but is " + oid);
}
if (!(data instanceof ObjectData)) {
throw new IsisException("Expected an ObjectData but got " + data.getClass());
}
updateFieldsAndResolveState(adapter, data);
if (LOG.isDebugEnabled()) {
LOG.debug("object updated " + adapter.getOid());
}
}
private void updateFieldsAndResolveState(final ObjectAdapter objectAdapter, final Data data) {
boolean dataIsTransient = data.getOid().isTransient();
if (!dataIsTransient) {
updateFields(objectAdapter, data);
objectAdapter.getOid().setVersion(data.getOid().getVersion());
} else if (objectAdapter.isTransient() && dataIsTransient) {
updateFields(objectAdapter, data);
} else if (objectAdapter.isParentedCollection()) {
// this branch is kind-a wierd, I think it's to handle aggregated adapters.
updateFields(objectAdapter, data);
} else {
final ObjectData od = (ObjectData) data;
if (od.containsField()) {
throw new IsisException("Resolve state (for " + objectAdapter + ") inconsistent with fact that data exists for fields");
}
}
}
private void updateFields(final ObjectAdapter object, final Data state) {
final ObjectData od = (ObjectData) state;
final List<ObjectAssociation> fields = object.getSpecification().getAssociations(Contributed.EXCLUDED);
for (final ObjectAssociation field : fields) {
if (field.isNotPersisted()) {
if (field.isOneToManyAssociation()) {
continue;
}
if (field.containsFacet(PropertyOrCollectionAccessorFacet.class) && !field.containsFacet(PropertySetterFacet.class)) {
LOG.debug("ignoring not-settable field " + field.getName());
continue;
}
}
updateField(object, od, field);
}
}
private void updateField(final ObjectAdapter objectAdapter, final ObjectData objectData, final ObjectAssociation objectAssoc) {
final Object fieldData = objectData.getEntry(objectAssoc.getId());
if (objectAssoc.isOneToManyAssociation()) {
updateOneToManyAssociation(objectAdapter, (OneToManyAssociation) objectAssoc, (CollectionData) fieldData);
} else if (objectAssoc.getSpecification().containsFacet(EncodableFacet.class)) {
final EncodableFacet facet = objectAssoc.getSpecification().getFacet(EncodableFacet.class);
final ObjectAdapter value = facet.fromEncodedString((String) fieldData);
((OneToOneAssociation) objectAssoc).initAssociation(objectAdapter, value);
} else if (objectAssoc.isOneToOneAssociation()) {
updateOneToOneAssociation(objectAdapter, (OneToOneAssociation) objectAssoc, (Data) fieldData);
}
}
private void updateOneToManyAssociation(final ObjectAdapter objectAdapter, final OneToManyAssociation otma, final CollectionData collectionData) {
final ObjectAdapter collection = otma.get(objectAdapter, InteractionInitiatedBy.FRAMEWORK);
final CollectionFacet facet = CollectionFacetUtils.getCollectionFacetFromSpec(collection);
final List<ObjectAdapter> original = Lists.newArrayList();
for (final ObjectAdapter adapter : facet.iterable(collection)) {
original.add(adapter);
}
final Data[] elements = collectionData.elements;
for (final Data data : elements) {
final ObjectAdapter elementAdapter = recreateReference(data);
if (!facet.contains(collection, elementAdapter)) {
if (LOG.isDebugEnabled()) {
LOG.debug(" association " + otma + " changed, added " + elementAdapter.getOid());
}
otma.addElement(objectAdapter, elementAdapter, InteractionInitiatedBy.FRAMEWORK);
} else {
otma.removeElement(objectAdapter, elementAdapter, InteractionInitiatedBy.FRAMEWORK);
}
}
for (final ObjectAdapter element : original) {
if (LOG.isDebugEnabled()) {
LOG.debug(" association " + otma + " changed, removed " + element.getOid());
}
otma.removeElement(objectAdapter, element, InteractionInitiatedBy.FRAMEWORK);
}
}
private void updateOneToOneAssociation(final ObjectAdapter objectAdapter, final OneToOneAssociation otoa, final Data assocData) {
if (assocData == null) {
otoa.initAssociation(objectAdapter, null);
} else {
final ObjectAdapter ref = recreateReference(assocData);
if (otoa.get(objectAdapter, InteractionInitiatedBy.FRAMEWORK) != ref) {
if (LOG.isDebugEnabled()) {
LOG.debug(" association " + otoa + " changed to " + ref.getOid());
}
otoa.initAssociation(objectAdapter, ref);
}
}
}
// ///////////////////////////////////////////////////////////////
// toString, debug
// ///////////////////////////////////////////////////////////////
@Override
public String toString() {
return "[" + (data == null ? null : data.getClassName() + "/" + data.getOid() + data) + "]";
}
// ///////////////////////////////////////////////////////////////
// Dependencies (from context)
// ///////////////////////////////////////////////////////////////
protected SpecificationLoader getSpecificationLoader() {
return getIsisSessionFactory().getSpecificationLoader();
}
protected PersistenceSession getPersistenceSession() {
return getIsisSessionFactory().getCurrentSession().getPersistenceSession();
}
IsisSessionFactory getIsisSessionFactory() {
return IsisContext.getSessionFactory();
}
}