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