/* * 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.system.persistence; import java.lang.reflect.Array; import java.lang.reflect.Modifier; import java.sql.Timestamp; import java.text.MessageFormat; import java.util.List; import java.util.Map; import java.util.UUID; import javax.jdo.FetchGroup; import javax.jdo.FetchPlan; import javax.jdo.PersistenceManager; import javax.jdo.PersistenceManagerFactory; import javax.jdo.listener.InstanceLifecycleListener; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.datanucleus.enhancement.Persistable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.isis.applib.annotation.Bulk; import org.apache.isis.applib.query.Query; import org.apache.isis.applib.services.bookmark.Bookmark; import org.apache.isis.applib.services.bookmark.BookmarkService2; import org.apache.isis.applib.services.clock.ClockService; import org.apache.isis.applib.services.command.Command; import org.apache.isis.applib.services.command.Command2; import org.apache.isis.applib.services.command.Command3; import org.apache.isis.applib.services.command.CommandContext; import org.apache.isis.applib.services.command.spi.CommandService; import org.apache.isis.applib.services.eventbus.AbstractLifecycleEvent; import org.apache.isis.applib.services.eventbus.EventBusService; import org.apache.isis.applib.services.exceprecog.ExceptionRecognizer; import org.apache.isis.applib.services.exceprecog.ExceptionRecognizer2; import org.apache.isis.applib.services.factory.FactoryService; import org.apache.isis.applib.services.iactn.Interaction; import org.apache.isis.applib.services.iactn.InteractionContext; import org.apache.isis.applib.services.metrics.MetricsService; import org.apache.isis.applib.services.user.UserService; import org.apache.isis.core.commons.authentication.AuthenticationSession; import org.apache.isis.core.commons.components.SessionScopedComponent; import org.apache.isis.core.commons.config.IsisConfiguration; import org.apache.isis.core.commons.ensure.Assert; import org.apache.isis.core.commons.ensure.IsisAssertException; import org.apache.isis.core.commons.exceptions.IsisException; import org.apache.isis.core.commons.factory.InstanceUtil; import org.apache.isis.core.commons.util.ToString; import org.apache.isis.core.metamodel.adapter.ObjectAdapter; import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager; import org.apache.isis.core.metamodel.adapter.oid.Oid; import org.apache.isis.core.metamodel.adapter.oid.ParentedCollectionOid; import org.apache.isis.core.metamodel.adapter.oid.RootOid; import org.apache.isis.core.metamodel.adapter.version.ConcurrencyException; import org.apache.isis.core.metamodel.adapter.version.Version; import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy; import org.apache.isis.core.metamodel.facets.actcoll.typeof.ElementSpecificationProviderFromTypeOfFacet; import org.apache.isis.core.metamodel.facets.actcoll.typeof.TypeOfFacet; import org.apache.isis.core.metamodel.facets.collections.modify.CollectionFacetUtils; import org.apache.isis.core.metamodel.facets.object.callbacks.CallbackFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.CreatedCallbackFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.CreatedLifecycleEventFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.LifecycleEventFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.LoadedCallbackFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.LoadedLifecycleEventFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.PersistedCallbackFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.PersistedLifecycleEventFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.PersistingCallbackFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.PersistingLifecycleEventFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.RemovingCallbackFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.RemovingLifecycleEventFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.UpdatedCallbackFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.UpdatedLifecycleEventFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.UpdatingCallbackFacet; import org.apache.isis.core.metamodel.facets.object.callbacks.UpdatingLifecycleEventFacet; import org.apache.isis.core.metamodel.facets.object.value.ValueFacet; import org.apache.isis.core.metamodel.facets.object.viewmodel.ViewModelFacet; import org.apache.isis.core.metamodel.facets.propcoll.accessor.PropertyOrCollectionAccessorFacet; import org.apache.isis.core.metamodel.services.ServicesInjector; import org.apache.isis.core.metamodel.services.container.query.QueryCardinality; import org.apache.isis.core.metamodel.spec.FreeStandingList; import org.apache.isis.core.metamodel.spec.ObjectSpecId; 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.metamodel.spec.feature.OneToManyAssociation; import org.apache.isis.core.metamodel.specloader.SpecificationLoader; import org.apache.isis.core.runtime.persistence.FixturesInstalledFlag; import org.apache.isis.core.runtime.persistence.NotPersistableException; import org.apache.isis.core.runtime.persistence.ObjectNotFoundException; import org.apache.isis.core.runtime.persistence.PojoRecreationException; import org.apache.isis.core.runtime.persistence.PojoRefreshException; import org.apache.isis.core.runtime.persistence.UnsupportedFindException; import org.apache.isis.core.runtime.persistence.adapter.PojoAdapter; import org.apache.isis.core.runtime.persistence.objectstore.transaction.CreateObjectCommand; import org.apache.isis.core.runtime.persistence.objectstore.transaction.DestroyObjectCommand; import org.apache.isis.core.runtime.persistence.objectstore.transaction.PersistenceCommand; import org.apache.isis.core.runtime.persistence.objectstore.transaction.TransactionalResource; import org.apache.isis.core.runtime.persistence.query.PersistenceQueryFindAllInstances; import org.apache.isis.core.runtime.persistence.query.PersistenceQueryFindUsingApplibQueryDefault; import org.apache.isis.core.runtime.runner.opts.OptionHandlerFixtureAbstract; import org.apache.isis.core.runtime.services.RequestScopedService; import org.apache.isis.core.runtime.services.changes.ChangedObjectsServiceInternal; import org.apache.isis.core.runtime.system.persistence.adaptermanager.OidAdapterHashMap; import org.apache.isis.core.runtime.system.persistence.adaptermanager.PojoAdapterHashMap; import org.apache.isis.core.runtime.system.persistence.adaptermanager.RootAndCollectionAdapters; import org.apache.isis.core.runtime.system.transaction.IsisTransaction; import org.apache.isis.core.runtime.system.transaction.IsisTransactionManager; import org.apache.isis.core.runtime.system.transaction.TransactionalClosure; import org.apache.isis.core.runtime.system.transaction.TransactionalClosureWithReturn; import org.apache.isis.objectstore.jdo.datanucleus.persistence.commands.DataNucleusCreateObjectCommand; import org.apache.isis.objectstore.jdo.datanucleus.persistence.commands.DataNucleusDeleteObjectCommand; import org.apache.isis.objectstore.jdo.datanucleus.persistence.queries.PersistenceQueryFindAllInstancesProcessor; import org.apache.isis.objectstore.jdo.datanucleus.persistence.queries.PersistenceQueryFindUsingApplibQueryProcessor; import org.apache.isis.objectstore.jdo.datanucleus.persistence.queries.PersistenceQueryProcessor; import org.apache.isis.objectstore.jdo.datanucleus.persistence.spi.JdoObjectIdSerializer; import static org.apache.isis.core.commons.ensure.Ensure.ensureThatArg; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; /** * A wrapper around the JDO {@link PersistenceManager}, which also manages concurrency * and maintains an identity map of {@link ObjectAdapter adapter}s and {@link Oid * identities} for each and every POJO that is being used by the framework. */ public class PersistenceSession implements TransactionalResource, SessionScopedComponent, AdapterManager, IsisLifecycleListener2.PersistenceSessionLifecycleManagement { //region > constants private static final Logger LOG = LoggerFactory.getLogger(PersistenceSession.class); /** * @see #isFixturesInstalled() */ public static final String INSTALL_FIXTURES_KEY = OptionHandlerFixtureAbstract.DATANUCLEUS_INSTALL_FIXTURES_KEY; public static final boolean INSTALL_FIXTURES_DEFAULT = false; private static final String ROOT_KEY = OptionHandlerFixtureAbstract.DATANUCLEUS_ROOT_KEY; /** * Append regular <a href="http://www.datanucleus.org/products/accessplatform/persistence_properties.html">datanucleus properties</a> to this key */ public static final String DATANUCLEUS_PROPERTIES_ROOT = ROOT_KEY + "impl."; //endregion //region > constructor, fields, finalize() private final FixturesInstalledFlag fixturesInstalledFlag; private final PersistenceQueryFactory persistenceQueryFactory; private final IsisConfiguration configuration; private final SpecificationLoader specificationLoader; private final AuthenticationSession authenticationSession; private final ServicesInjector servicesInjector; private final CommandContext commandContext; private final CommandService commandService; private final InteractionContext interactionContext; private final EventBusService eventBusService ; private final ChangedObjectsServiceInternal changedObjectsServiceInternal; private final FactoryService factoryService; private final MetricsService metricsService; private final ClockService clockService; private final UserService userService; private final Bulk.InteractionContext bulkInteractionContext; /** * Used to create the {@link #persistenceManager} when {@link #open()}ed. */ private final PersistenceManagerFactory jdoPersistenceManagerFactory; // not final only for testing purposes private IsisTransactionManager transactionManager; /** * populated only when {@link #open()}ed. */ private PersistenceManager persistenceManager; /** * populated only when {@link #open()}ed. */ private final Map<Class<?>, PersistenceQueryProcessor<?>> persistenceQueryProcessorByClass = Maps.newHashMap(); private final boolean concurrencyCheckingGloballyEnabled; /** * Initialize the object store so that calls to this object store access * persisted objects and persist changes to the object that are saved. */ public PersistenceSession( final ServicesInjector servicesInjector, final AuthenticationSession authenticationSession, final PersistenceManagerFactory jdoPersistenceManagerFactory, final FixturesInstalledFlag fixturesInstalledFlag) { if (LOG.isDebugEnabled()) { LOG.debug("creating " + this); } this.servicesInjector = servicesInjector; this.jdoPersistenceManagerFactory = jdoPersistenceManagerFactory; this.fixturesInstalledFlag = fixturesInstalledFlag; // injected this.configuration = servicesInjector.getConfigurationServiceInternal(); this.specificationLoader = servicesInjector.getSpecificationLoader(); this.authenticationSession = authenticationSession; this.commandContext = lookupService(CommandContext.class); this.commandService = lookupService(CommandService.class); this.interactionContext = lookupService(InteractionContext.class); this.eventBusService = lookupService(EventBusService.class); this.changedObjectsServiceInternal = lookupService(ChangedObjectsServiceInternal.class); this.metricsService = lookupService(MetricsService.class); this.factoryService = lookupService(FactoryService.class); this.clockService = lookupService(ClockService.class); this.userService = lookupService(UserService.class); this.bulkInteractionContext = lookupService(Bulk.InteractionContext.class); // sub-components final AdapterManager adapterManager = this; this.persistenceQueryFactory = new PersistenceQueryFactory(adapterManager, this.specificationLoader); this.transactionManager = new IsisTransactionManager(this, authenticationSession, servicesInjector); this.state = State.NOT_INITIALIZED; final boolean concurrencyCheckingGloballyDisabled = this.configuration.getBoolean("isis.persistor.disableConcurrencyChecking", false); this.concurrencyCheckingGloballyEnabled = !concurrencyCheckingGloballyDisabled; } @Override protected void finalize() throws Throwable { super.finalize(); LOG.debug("finalizing persistence session"); } //endregion //region > open /** * Only populated once {@link #open()}'d */ public PersistenceManager getPersistenceManager() { return persistenceManager; } /** * Injects components, calls open on subcomponents, and then creates service * adapters. */ public void open() { ensureNotOpened(); if (LOG.isDebugEnabled()) { LOG.debug("opening " + this); } oidAdapterMap.open(); pojoAdapterMap.open(); persistenceManager = jdoPersistenceManagerFactory.getPersistenceManager(); final IsisLifecycleListener2.PersistenceSessionLifecycleManagement psLifecycleMgmt = this; final IsisLifecycleListener2 isisLifecycleListener = new IsisLifecycleListener2(psLifecycleMgmt); persistenceManager.addInstanceLifecycleListener(isisLifecycleListener, (Class[]) null); persistenceQueryProcessorByClass.put( PersistenceQueryFindAllInstances.class, new PersistenceQueryFindAllInstancesProcessor(this)); persistenceQueryProcessorByClass.put( PersistenceQueryFindUsingApplibQueryDefault.class, new PersistenceQueryFindUsingApplibQueryProcessor(this)); initServices(); // tell the proxy of all request-scoped services to instantiate the underlying // services, store onto the thread-local and inject into them... startRequestOnRequestScopedServices(); // ... and invoke all @PostConstruct postConstructOnRequestScopedServices(); if(metricsService instanceof InstanceLifecycleListener) { final InstanceLifecycleListener metricsService = (InstanceLifecycleListener) this.metricsService; persistenceManager.addInstanceLifecycleListener(metricsService, (Class[]) null); } final Command command = createCommand(); final UUID transactionId = UUID.randomUUID(); final Interaction interaction = factoryService.instantiate(Interaction.class); final Timestamp timestamp = clockService.nowAsJavaSqlTimestamp(); final String userName = userService.getUser().getName(); command.setTimestamp(timestamp); command.setUser(userName); command.setTransactionId(transactionId); interaction.setTransactionId(transactionId); commandContext.setCommand(command); interactionContext.setInteraction(interaction); Bulk.InteractionContext.current.set(bulkInteractionContext); this.state = State.OPEN; } private void postConstructOnRequestScopedServices() { for (final Object service : servicesInjector.getRegisteredServices()) { if(service instanceof RequestScopedService) { ((RequestScopedService)service).__isis_postConstruct(); } } } private void startRequestOnRequestScopedServices() { for (final Object service : servicesInjector.getRegisteredServices()) { if(service instanceof RequestScopedService) { ((RequestScopedService)service).__isis_startRequest(servicesInjector); } } } /** * Creates {@link ObjectAdapter adapters} for the service list, ensuring that these are mapped correctly, * and have the same OIDs as in any previous sessions. */ private void initServices() { final List<Object> registeredServices = servicesInjector.getRegisteredServices(); for (final Object service : registeredServices) { final ObjectAdapter serviceAdapter = adapterFor(service); remapAsPersistentIfRequired(serviceAdapter); } } private void remapAsPersistentIfRequired(final ObjectAdapter serviceAdapter) { if (serviceAdapter.getOid().isTransient()) { remapAsPersistent(serviceAdapter, null); } } private Command createCommand() { final Command command = commandService.create(); servicesInjector.injectServicesInto(command); return command; } //endregion //region > close /** * Closes the subcomponents. * * <p> * Automatically {@link IsisTransactionManager#endTransaction() ends * (commits)} the current (Isis) {@link IsisTransaction}. This in turn commits the underlying * JDO transaction. * * <p> * The corresponding DataNucleus entity is then closed. */ public void close() { if (state == State.CLOSED) { // nothing to do return; } completeCommandFromInteractionAndClearDomainEvents(); transactionManager.flushTransaction(); try { final IsisTransaction currentTransaction = transactionManager.getCurrentTransaction(); if (currentTransaction != null && !currentTransaction.getState().isComplete()) { if(currentTransaction.getState().canCommit()) { transactionManager.endTransaction(); } else if(currentTransaction.getState().canAbort()) { transactionManager.abortTransaction(); } } } catch(final Throwable ex) { // ignore LOG.error("close: failed to end transaction; continuing to avoid memory leakage"); } Bulk.InteractionContext.current.set(null); // tell the proxy of all request-scoped services to invoke @PreDestroy // (if any) on all underlying services stored on their thread-locals... preDestroyOnRequestScopedServices(); // ... and then remove those underlying services from the thread-local endRequestOnRequestScopeServices(); try { persistenceManager.close(); } catch(final Throwable ex) { // ignore LOG.error( "close: failed to close JDO persistenceManager; continuing to avoid memory leakage"); } try { oidAdapterMap.close(); } catch(final Throwable ex) { // ignore LOG.error("close: oidAdapterMap#close() failed; continuing to avoid memory leakage"); } try { pojoAdapterMap.close(); } catch(final Throwable ex) { // ignore LOG.error("close: pojoAdapterMap#close() failed; continuing to avoid memory leakage"); } this.state = State.CLOSED; } private void endRequestOnRequestScopeServices() { for (final Object service : servicesInjector.getRegisteredServices()) { if(service instanceof RequestScopedService) { ((RequestScopedService)service).__isis_endRequest(); } } } private void preDestroyOnRequestScopedServices() { for (final Object service : servicesInjector.getRegisteredServices()) { if(service instanceof RequestScopedService) { ((RequestScopedService)service).__isis_preDestroy(); } } } private void completeCommandFromInteractionAndClearDomainEvents() { final Command command = commandContext.getCommand(); final Interaction interaction = interactionContext.getInteraction(); if(command.getStartedAt() != null && command.getCompletedAt() == null) { // the guard is in case we're here as the result of a redirect following a previous exception;just ignore. final Timestamp completedAt; final Interaction.Execution priorExecution = interaction.getPriorExecution(); if (priorExecution != null) { // copy over from the most recent (which will be the top-level) interaction completedAt = priorExecution.getCompletedAt(); } else { // this could arise as the result of calling SessionManagementService#nextSession within an action // the best we can do is to use the current time // REVIEW: as for the interaction object, it is left somewhat high-n-dry. completedAt = clockService.nowAsJavaSqlTimestamp(); } command.setCompletedAt(completedAt); } // ensureCommandsPersistedIfDirtyXactn // ensure that any changed objects means that the command should be persisted if(command.getMemberIdentifier() != null) { if(metricsService.numberObjectsDirtied() > 0) { command.setPersistHint(true); } } commandService.complete(command); if(command instanceof Command3) { final Command3 command3 = (Command3) command; command3.flushActionDomainEvents(); } else if(command instanceof Command2) { final Command2 command2 = (Command2) command; command2.flushActionInteractionEvents(); } interaction.clear(); } //endregion //region > QuerySubmitter impl, findInstancesInTransaction public <T> List<ObjectAdapter> allMatchingQuery(final Query<T> query) { final ObjectAdapter instances = findInstancesInTransaction(query, QueryCardinality.MULTIPLE); return CollectionFacetUtils.convertToAdapterList(instances); } public <T> ObjectAdapter firstMatchingQuery(final Query<T> query) { final ObjectAdapter instances = findInstancesInTransaction(query, QueryCardinality.SINGLE); final List<ObjectAdapter> list = CollectionFacetUtils.convertToAdapterList(instances); return list.size() > 0 ? list.get(0) : null; } /** * Finds and returns instances that match the specified query. * * <p> * The {@link QueryCardinality} determines whether all instances or just the * first matching instance is returned. * * @throws org.apache.isis.core.runtime.persistence.UnsupportedFindException * if the criteria is not support by this persistor */ private <T> ObjectAdapter findInstancesInTransaction(final Query<T> query, final QueryCardinality cardinality) { if (LOG.isDebugEnabled()) { LOG.debug("findInstances using (applib) Query: " + query); } // TODO: unify PersistenceQuery and PersistenceQueryProcessor final PersistenceQuery persistenceQuery = createPersistenceQueryFor(query, cardinality); if (LOG.isDebugEnabled()) { LOG.debug("maps to (core runtime) PersistenceQuery: " + persistenceQuery); } final PersistenceQueryProcessor<? extends PersistenceQuery> processor = lookupProcessorFor(persistenceQuery); final List<ObjectAdapter> instances = transactionManager.executeWithinTransaction( new TransactionalClosureWithReturn<List<ObjectAdapter>>() { @Override public List<ObjectAdapter> execute() { return processPersistenceQuery(processor, persistenceQuery); } }); final ObjectSpecification specification = persistenceQuery.getSpecification(); final FreeStandingList results = new FreeStandingList(specification, instances); return adapterFor(results); } /** * Converts the {@link Query applib representation of a query} into the * {@link PersistenceQuery NOF-internal representation}. */ private final PersistenceQuery createPersistenceQueryFor( final Query<?> query, final QueryCardinality cardinality) { final PersistenceQuery persistenceQuery = persistenceQueryFactory.createPersistenceQueryFor(query, cardinality); if (persistenceQuery == null) { throw new IllegalArgumentException("Unknown Query type: " + query.getDescription()); } return persistenceQuery; } private PersistenceQueryProcessor<? extends PersistenceQuery> lookupProcessorFor(final PersistenceQuery persistenceQuery) { final Class<? extends PersistenceQuery> persistenceQueryClass = persistenceQuery.getClass(); final PersistenceQueryProcessor<? extends PersistenceQuery> processor = persistenceQueryProcessorByClass.get(persistenceQueryClass); if (processor == null) { throw new UnsupportedFindException(MessageFormat.format( "Unsupported PersistenceQuery class: {0}", persistenceQueryClass.getName())); } return processor; } @SuppressWarnings("unchecked") private <Q extends PersistenceQuery> List<ObjectAdapter> processPersistenceQuery( final PersistenceQueryProcessor<Q> persistenceQueryProcessor, final PersistenceQuery persistenceQuery) { return persistenceQueryProcessor.process((Q) persistenceQuery); } public IsisConfiguration getConfiguration() { return configuration; } //endregion //region > State private enum State { NOT_INITIALIZED, OPEN, CLOSED } private State state; protected void ensureNotOpened() { if (state != State.NOT_INITIALIZED) { throw new IllegalStateException("Persistence session has already been initialized"); } } public void ensureOpened() { ensureStateIs(State.OPEN); } private void ensureStateIs(final State stateRequired) { if (state == stateRequired) { return; } throw new IllegalStateException("State is: " + state + "; should be: " + stateRequired); } //endregion //region > createTransientInstance, createViewModelInstance /** * Create a root or standalone {@link ObjectAdapter adapter}. * * <p> * Creates a new instance of the specified type and returns it in an adapter. * * <p> * The returned object will be initialised (had the relevant callback * lifecycle methods invoked). * * <p> * While creating the object it will be initialised with default values and * its created lifecycle method (its logical constructor) will be invoked. * * <p> * This method is ultimately delegated to by the * {@link org.apache.isis.applib.DomainObjectContainer}. */ public ObjectAdapter createTransientInstance(final ObjectSpecification objectSpec) { return createInstance(objectSpec, Variant.TRANSIENT, null); } public ObjectAdapter createViewModelInstance(final ObjectSpecification objectSpec, final String memento) { return createInstance(objectSpec, Variant.VIEW_MODEL, memento); } private enum Variant { TRANSIENT, VIEW_MODEL } private ObjectAdapter createInstance( final ObjectSpecification spec, final Variant variant, final String memento) { if (LOG.isDebugEnabled()) { LOG.debug("creating " + variant + " instance of " + spec); } final Object pojo; if(variant == Variant.VIEW_MODEL) { pojo = recreateViewModel(spec, memento); } else { pojo = instantiateAndInjectServices(spec); } final ObjectAdapter adapter = adapterFor(pojo); return initializePropertiesAndDoCallback(adapter); } private Object recreateViewModel(final ObjectSpecification spec, final String memento) { final ViewModelFacet facet = spec.getFacet(ViewModelFacet.class); if(facet == null) { throw new IllegalArgumentException("spec does not have ViewModelFacet; spec is " + spec.getFullIdentifier()); } final Object viewModelPojo; if(facet.getRecreationMechanism().isInitializes()) { viewModelPojo = instantiateAndInjectServices(spec); facet.initialize(viewModelPojo, memento); } else { viewModelPojo = facet.instantiate(spec.getCorrespondingClass(), memento); } return viewModelPojo; } public Object instantiateAndInjectServices(final ObjectSpecification objectSpec) { final Class<?> correspondingClass = objectSpec.getCorrespondingClass(); if (correspondingClass.isArray()) { return Array.newInstance(correspondingClass.getComponentType(), 0); } final Class<?> cls = correspondingClass; if (Modifier.isAbstract(cls.getModifiers())) { throw new IsisException("Cannot create an instance of an abstract class: " + cls); } final Object newInstance; if (Modifier.isAbstract(cls.getModifiers())) { throw new IsisException("Cannot create an instance of an abstract class: " + cls); } try { newInstance = cls.newInstance(); } catch (final IllegalAccessException | InstantiationException e) { throw new IsisException("Failed to create instance of type " + objectSpec.getFullIdentifier(), e); } servicesInjector.injectServicesInto(newInstance); return newInstance; } private ObjectAdapter initializePropertiesAndDoCallback(final ObjectAdapter adapter) { // initialize new object final List<ObjectAssociation> fields = adapter.getSpecification().getAssociations(Contributed.EXCLUDED); for (ObjectAssociation field : fields) { field.toDefault(adapter); } final Object pojo = adapter.getObject(); servicesInjector.injectServicesInto(pojo); CallbackFacet.Util.callCallback(adapter, CreatedCallbackFacet.class); if (Command.class.isAssignableFrom(pojo.getClass())) { // special case... the command object is created while the transaction is being started and before // the event bus service is initialized (nb: we initialize services *within* a transaction). To resolve // this catch-22 situation, we simply suppress the posting of this event for this domain class. // this seems the least unpleasant of the various options available: // * we could have put a check in the EventBusService to ignore the post if not yet initialized; // however this might hide other genuine errors // * we could have used the thread-local in JdoStateManagerForIsis and the "skip(...)" hook in EventBusServiceJdo // to have this event be skipped; but that seems like co-opting some other design // * we could have the transaction initialize the EventBusService as a "special case" before creating the Command; // but then do we worry about it being re-init'd later by the ServicesInitializer? // so, doing it this way is this simplest, least obscure. if(LOG.isDebugEnabled()) { LOG.debug("Skipping postEvent for creation of Command pojo"); } } else { postLifecycleEventIfRequired(adapter, CreatedLifecycleEventFacet.class); } return adapter; } //endregion //region > getServices, getService public List<ObjectAdapter> getServices() { final List<Object> services = servicesInjector.getRegisteredServices(); final List<ObjectAdapter> serviceAdapters = Lists.newArrayList(); for (final Object servicePojo : services) { ObjectAdapter serviceAdapter = getAdapterFor(servicePojo); if(serviceAdapter == null) { throw new IllegalStateException("ObjectAdapter for service " + servicePojo + " does not exist?!?"); } serviceAdapters.add(serviceAdapter); } return serviceAdapters; } //endregion //region > helper: postEvent void postLifecycleEventIfRequired( final ObjectAdapter adapter, final Class<? extends LifecycleEventFacet> lifecycleEventFacetClass) { final LifecycleEventFacet facet = adapter.getSpecification().getFacet(lifecycleEventFacetClass); if(facet != null) { final Class<? extends AbstractLifecycleEvent<?>> eventType = facet.getEventType(); final Object instance = InstanceUtil.createInstance(eventType); final Object pojo = adapter.getObject(); postEvent((AbstractLifecycleEvent) instance, pojo); } } void postEvent(final AbstractLifecycleEvent<Object> event, final Object pojo) { event.setSource(pojo); eventBusService.post(event); } //endregion //region > fixture installation /** * Determine if the object store has been initialized with its set of start * up objects. * * <p> * This method is called only once after the init has been called. If this flag * returns <code>false</code> the framework will run the fixtures to * initialise the persistor. * * <p> * Returns the cached value of {@link #isFixturesInstalled() * whether fixtures are installed} from the * {@link PersistenceSessionFactory}. * <p> * This caching is important because if we've determined, for a given run, * that fixtures are not installed, then we don't want to change our mind by * asking the object store again in another session. * * @see FixturesInstalledFlag */ public boolean isFixturesInstalled() { if (fixturesInstalledFlag.isFixturesInstalled() == null) { fixturesInstalledFlag.setFixturesInstalled(objectStoreIsFixturesInstalled()); } return fixturesInstalledFlag.isFixturesInstalled(); } /** * Determine if the object store has been initialized with its set of start * up objects. * * <p> * This method is called only once after the session is opened called. If it returns <code>false</code> then the * framework will run the fixtures to initialise the object store. * * <p> * Implementation looks for the {@link #INSTALL_FIXTURES_KEY} in the injected {@link #configuration configuration}. * * <p> * By default this is not expected to be there, but utilities can add in on * the fly during bootstrapping if required. */ public boolean objectStoreIsFixturesInstalled() { final boolean installFixtures = configuration.getBoolean(INSTALL_FIXTURES_KEY, INSTALL_FIXTURES_DEFAULT); LOG.info("isFixturesInstalled: {} = {}", INSTALL_FIXTURES_KEY, installFixtures); return !installFixtures; } //endregion //region > loadObject /** * Loads the object identified by the specified {@link RootOid}. * * <p> * That is, it retrieves the object identified by the specified {@link RootOid} from the object * store, {@link AdapterManager#mapRecreatedPojo(org.apache.isis.core.metamodel.adapter.oid.Oid, Object) mapped by * the adapter manager}. * * <p>The cache should be checked first and, if the object is cached, * the cached version should be returned. It is important that if this * method is called again, while the originally returned object is in * working memory, then this method must return that same Java object. * * <p> * Assuming that the object is not cached then the data for the object * should be retrieved from the persistence mechanism and the object * recreated (as describe previously). The specified OID should then be * assigned to the recreated object by calling its <method>setOID </method>. * Before returning the object its resolved flag should also be set by * calling its <method>setResolved </method> method as well. * * <p> * If the persistence mechanism does not known of an object with the * specified {@link RootOid} then a {@link org.apache.isis.core.runtime.persistence.ObjectNotFoundException} should be * thrown. * * <p> * Note that the OID could be for an internal collection, and is * therefore related to the parent object (using a {@link ParentedCollectionOid}). * The elements for an internal collection are commonly stored as * part of the parent object, so to get element the parent object needs to * be retrieved first, and the internal collection can be got from that. * * <p> * Returns the stored {@link ObjectAdapter} object. * * * @return the requested {@link ObjectAdapter} that has the specified * {@link RootOid}. * * @throws org.apache.isis.core.runtime.persistence.ObjectNotFoundException * when no object corresponding to the oid can be found */ public ObjectAdapter loadObjectInTransaction(final RootOid oid) { // can be either a view model or a persistent entity. ensureThatArg(oid, is(notNullValue())); final ObjectAdapter adapter = getAdapterFor(oid); if (adapter != null) { return adapter; } return transactionManager.executeWithinTransaction( new TransactionalClosureWithReturn<ObjectAdapter>() { @Override public ObjectAdapter execute() { if (LOG.isDebugEnabled()) { LOG.debug("getObject; oid=" + oid); } final Object pojo = loadPojo(oid); return mapRecreatedPojo(oid, pojo); } }); } //endregion //region > loadPojo public Object loadPojo(final RootOid rootOid) { Object result; try { final Class<?> cls = clsOf(rootOid); final Object jdoObjectId = JdoObjectIdSerializer.toJdoObjectId(rootOid); FetchPlan fetchPlan = persistenceManager.getFetchPlan(); fetchPlan.addGroup(FetchGroup.DEFAULT); result = persistenceManager.getObjectById(cls, jdoObjectId); } catch (final RuntimeException e) { Class<ExceptionRecognizer> serviceClass = ExceptionRecognizer.class; final List<ExceptionRecognizer> exceptionRecognizers = lookupServices(serviceClass); for (ExceptionRecognizer exceptionRecognizer : exceptionRecognizers) { if(exceptionRecognizer instanceof ExceptionRecognizer2) { final ExceptionRecognizer2 recognizer = (ExceptionRecognizer2) exceptionRecognizer; final ExceptionRecognizer2.Recognition recognition = recognizer.recognize2(e); if(recognition != null) { if(recognition.getCategory() == ExceptionRecognizer2.Category.NOT_FOUND) { throw new ObjectNotFoundException(rootOid, e); } } } } throw e; } if (result == null) { throw new ObjectNotFoundException(rootOid); } return result; } private Class<?> clsOf(final RootOid oid) { final ObjectSpecification objectSpec = getSpecificationLoader().lookupBySpecId(oid.getObjectSpecId()); return objectSpec.getCorrespondingClass(); } //endregion //region > lazilyLoaded public ObjectAdapter mapPersistent(final Persistable pojo) { if (persistenceManager.getObjectId(pojo) == null) { return null; } final RootOid oid = createPersistentOrViewModelOid(pojo); final ObjectAdapter adapter = mapRecreatedPojo(oid, pojo); return adapter; } //endregion //region > refreshRootInTransaction, refreshRoot, resolve /** * Re-initialises the fields of an object. If the object is unresolved then * the object's missing data should be retrieved from the persistence * mechanism and be used to set up the value objects and associations. */ public void refreshRootInTransaction(final ObjectAdapter adapter) { Assert.assertTrue("only resolve object that is persistent", adapter, adapter.representsPersistent()); getTransactionManager().executeWithinTransaction(new TransactionalClosure() { @Override public void execute() { if (LOG.isDebugEnabled()) { LOG.debug("resolveImmediately; oid=" + adapter.getOid().enString()); } if (!adapter.representsPersistent()) { if (LOG.isDebugEnabled()) { LOG.debug("; not persistent - ignoring"); } return; } refreshRoot(adapter); } }); } /** * Forces a reload (refresh in JDO terminology) of the domain object wrapped in the {@link ObjectAdapter}. */ public void refreshRoot(final ObjectAdapter adapter) { final Object domainObject = adapter.getObject(); if (domainObject == null) { // REVIEW: is this possible? throw new PojoRefreshException(adapter.getOid()); } try { persistenceManager.refresh(domainObject); } catch (final RuntimeException e) { throw new PojoRefreshException(adapter.getOid(), e); } // possibly redundant because also called in the post-load event // listener, but (with JPA impl) found it was required if we were ever to // get an eager left-outer-join as the result of a refresh (sounds possible). initializeMapAndCheckConcurrency((Persistable) domainObject); } public void resolve(final Object parent) { final ObjectAdapter adapter = adapterFor(parent); refreshRootInTransaction(adapter); } //endregion //region > makePersistent /** * Makes an {@link ObjectAdapter} persistent. The specified object should be * stored away via this object store's persistence mechanism, and have a * new and unique OID assigned to it. The object, should also be added to * the {@link PersistenceSession} as the object is implicitly 'in use'. * * <p> * If the object has any associations then each of these, where they aren't * already persistent, should also be made persistent by recursively calling * this method. * * <p> * If the object to be persisted is a collection, then each element of that * collection, that is not already persistent, should be made persistent by * recursively calling this method. */ public void makePersistentInTransaction(final ObjectAdapter adapter) { if (adapter.representsPersistent()) { throw new NotPersistableException("Object already persistent: " + adapter); } if (!adapter.getSpecification().persistability().isPersistable()) { throw new NotPersistableException("Object is not persistable: " + adapter); } final ObjectSpecification specification = adapter.getSpecification(); if (specification.isService()) { throw new NotPersistableException("Cannot persist services: " + adapter); } getTransactionManager().executeWithinTransaction(new TransactionalClosure() { @Override public void execute() { makePersistentTransactionAssumed(adapter); // clear out the map of transient -> persistent PersistenceSession.this.persistentByTransient.clear(); } }); } private void makePersistentTransactionAssumed(final ObjectAdapter adapter) { if (alreadyPersistedOrNotPersistable(adapter)) { return; } if (LOG.isDebugEnabled()) { LOG.debug("persist " + adapter); } // previously we called the PersistingCallback here. // this is now done in the JDO framework synchronizer. // // the guard below used to be because (apparently) // the callback might have caused the adapter to become persistent. // leaving it in as think it does no harm... if (alreadyPersistedOrNotPersistable(adapter)) { return; } addCreateObjectCommand(adapter); } /** * {@link #newCreateObjectCommand(ObjectAdapter) Create}s a {@link CreateObjectCommand}, and adds to the * {@link IsisTransactionManager}. */ private void addCreateObjectCommand(final ObjectAdapter object) { final CreateObjectCommand createObjectCommand = newCreateObjectCommand(object); transactionManager.addCommand(createObjectCommand); } private static boolean alreadyPersistedOrNotPersistable(final ObjectAdapter adapter) { return adapter.representsPersistent() || objectSpecNotPersistable(adapter); } private static boolean objectSpecNotPersistable(final ObjectAdapter adapter) { return !adapter.getSpecification().persistability().isPersistable() || adapter.isParentedCollection(); } //endregion //region > ObjectPersistor impl public void makePersistent(final ObjectAdapter adapter) { makePersistentInTransaction(adapter); } public void remove(final ObjectAdapter adapter) { destroyObjectInTransaction(adapter); } //endregion //region > destroyObjectInTransaction /** * Removes the specified object from the system. The specified object's data * should be removed from the persistence mechanism. */ public void destroyObjectInTransaction(final ObjectAdapter adapter) { final ObjectSpecification spec = adapter.getSpecification(); if (spec.isParented()) { return; } if (LOG.isDebugEnabled()) { LOG.debug("destroyObject " + adapter); } transactionManager.executeWithinTransaction(new TransactionalClosure() { @Override public void execute() { final DestroyObjectCommand command = newDestroyObjectCommand(adapter); transactionManager.addCommand(command); } }); } //endregion //region > newXxxCommand /** * Makes an {@link ObjectAdapter} persistent. The specified object should be * stored away via this object store's persistence mechanism, and have an * new and unique OID assigned to it (by calling the object's * <code>setOid</code> method). The object, should also be added to the * cache as the object is implicitly 'in use'. * * <p> * If the object has any associations then each of these, where they aren't * already persistent, should also be made persistent by recursively calling * this method. * </p> * * <p> * If the object to be persisted is a collection, then each element of that * collection, that is not already persistent, should be made persistent by * recursively calling this method. * </p> * */ private CreateObjectCommand newCreateObjectCommand(final ObjectAdapter adapter) { ensureOpened(); if (LOG.isDebugEnabled()) { LOG.debug("create object - creating command for: " + adapter); } if (adapter.representsPersistent()) { throw new IllegalArgumentException("Adapter is persistent; adapter: " + adapter); } return new DataNucleusCreateObjectCommand(adapter, persistenceManager); } private DestroyObjectCommand newDestroyObjectCommand(final ObjectAdapter adapter) { ensureOpened(); if (LOG.isDebugEnabled()) { LOG.debug("destroy object - creating command for: " + adapter); } if (!adapter.representsPersistent()) { throw new IllegalArgumentException("Adapter is not persistent; adapter: " + adapter); } return new DataNucleusDeleteObjectCommand(adapter, persistenceManager); } //endregion //region > execute public void execute(final List<PersistenceCommand> commands) { // previously we used to check that there were some commands, and skip processing otherwise. // we no longer do that; it could be (is quite likely) that DataNucleus has some dirty objects anyway that // don't have commands wrapped around them... executeCommands(commands); } private void executeCommands(final List<PersistenceCommand> commands) { for (final PersistenceCommand command : commands) { command.execute(null); } persistenceManager.flush(); } //endregion //region > getAggregateRoot, remappedFrom private Map<Oid, Oid> persistentByTransient = Maps.newHashMap(); public ObjectAdapter getAggregateRoot(final ParentedCollectionOid collectionOid) { final Oid rootOid = collectionOid.getRootOid(); ObjectAdapter rootadapter = getAdapterFor(rootOid); if(rootadapter == null) { final Oid parentOidNowPersisted = remappedFrom(rootOid); rootadapter = getAdapterFor(parentOidNowPersisted); } return rootadapter; } /** * To support ISIS-234; keep track, for the duration of the transaction only, * of the old transient {@link Oid}s and their corresponding persistent {@link Oid}s. */ private Oid remappedFrom(final Oid transientOid) { return persistentByTransient.get(transientOid); } //endregion //region > transactions public void startTransaction() { final javax.jdo.Transaction transaction = persistenceManager.currentTransaction(); if (transaction.isActive()) { throw new IllegalStateException("Transaction already active"); } transaction.begin(); } public void endTransaction() { final javax.jdo.Transaction transaction = persistenceManager.currentTransaction(); if (transaction.isActive()) { transaction.commit(); } } public void abortTransaction() { final javax.jdo.Transaction transaction = persistenceManager.currentTransaction(); if (transaction.isActive()) { transaction.rollback(); } } //endregion //region > dependencies (from constructor) protected SpecificationLoader getSpecificationLoader() { return specificationLoader; } protected AuthenticationSession getAuthenticationSession() { return authenticationSession; } /** * The configured {@link ServicesInjector}. */ public ServicesInjector getServicesInjector() { return servicesInjector; } //endregion //region > transactionManager /** * The configured {@link IsisTransactionManager}. */ public IsisTransactionManager getTransactionManager() { return transactionManager; } // // for testing only // void setTransactionManager(final IsisTransactionManager transactionManager) { // this.transactionManager = transactionManager; // } //endregion //region > jdoPersistenceManager delegate methods public javax.jdo.Query newJdoQuery(Class<?> cls) { return persistenceManager.newQuery(cls); } public javax.jdo.Query newJdoNamedQuery (Class<?> cls, String queryName) { return persistenceManager.newNamedQuery(cls, queryName); } public javax.jdo.Query newJdoQuery (Class<?> cls, String filter) { return persistenceManager.newQuery(cls, filter); } // endregion //region > AdapterManager implementation private final PojoAdapterHashMap pojoAdapterMap = new PojoAdapterHashMap(); private final OidAdapterHashMap oidAdapterMap = new OidAdapterHashMap(); @Override public ObjectAdapter getAdapterFor(final Object pojo) { ensureThatArg(pojo, is(notNullValue())); return pojoAdapterMap.getAdapter(pojo); } @Override public ObjectAdapter getAdapterFor(final Oid oid) { ensureThatArg(oid, is(notNullValue())); ensureMapsConsistent(oid); return oidAdapterMap.getAdapter(oid); } private ObjectAdapter existingOrValueAdapter(Object pojo) { // attempt to locate adapter for the pojo ObjectAdapter adapter = getAdapterFor(pojo); if (adapter != null) { return adapter; } // pojo may have been lazily loaded by object store, but we haven't yet seen it if (pojo instanceof Persistable) { adapter = mapPersistent((Persistable) pojo); // TODO: could return null if the pojo passed in !dnIsPersistent() || !dnIsDetached() // in which case, we would ought to map as a transient object, rather than fall through and treat as a value? } else { adapter = null; } if(adapter != null) { return adapter; } // need to create (and possibly map) the adapter. final ObjectSpecification objSpec = specificationLoader.loadSpecification(pojo.getClass()); // we create value facets as standalone (so not added to maps) if (objSpec.containsFacet(ValueFacet.class)) { adapter = createStandaloneAdapter(pojo); return adapter; } return null; } /** * Fail early if any problems. */ private void ensureMapsConsistent(final ObjectAdapter adapter) { if (adapter.isValue()) { return; } if (adapter.isParentedCollection()) { return; } ensurePojoAdapterMapConsistent(adapter); ensureOidAdapterMapConsistent(adapter); } /** * Fail early if any problems. */ private void ensureMapsConsistent(final Oid oid) { ensureThatArg(oid, is(notNullValue())); final ObjectAdapter adapter = oidAdapterMap.getAdapter(oid); if (adapter == null) { return; } ensureOidAdapterMapConsistent(adapter); ensurePojoAdapterMapConsistent(adapter); } private void ensurePojoAdapterMapConsistent(final ObjectAdapter adapter) { final Object adapterPojo = adapter.getObject(); final ObjectAdapter adapterAccordingToMap = pojoAdapterMap.getAdapter(adapterPojo); if(adapterPojo == null) { // nothing to check return; } ensureMapConsistent(adapter, adapterAccordingToMap, "PojoAdapterMap"); } private void ensureOidAdapterMapConsistent(final ObjectAdapter adapter) { final Oid adapterOid = adapter.getOid(); final ObjectAdapter adapterAccordingToMap = oidAdapterMap.getAdapter(adapterOid); if(adapterOid == null) { // nothing to check return; } ensureMapConsistent(adapter, adapterAccordingToMap, "OidAdapterMap"); } private void ensureMapConsistent( final ObjectAdapter adapter, final ObjectAdapter adapterAccordingToMap, final String mapName) { final Oid adapterOid = adapter.getOid(); // take care not to touch the pojo, since it might have been deleted. if(adapterAccordingToMap == null) { throw new IllegalStateException("mismatch in " + mapName + ": provided adapter's OID: " + adapterOid + "; but no adapter found in map"); } ensureThatArg( adapter, is(adapterAccordingToMap), "mismatch in " + mapName + ": provided adapter's OID: " + adapterOid + ", \n" + "but map's adapter's OID was: " + adapterAccordingToMap.getOid()); } public ObjectAdapter adapterForAny(RootOid rootOid) { final ObjectSpecId specId = rootOid.getObjectSpecId(); final ObjectSpecification spec = getSpecificationLoader().lookupBySpecId(specId); if(spec == null) { // eg "NONEXISTENT:123" return null; } if(spec.containsFacet(ViewModelFacet.class)) { // this is a hack; the RO viewer when rendering the URL for the view model loses the "view model" indicator // ("*") from the specId, meaning that the marshalling logic above in RootOidDefault.deString() creates an // oid in the wrong state. The code below checks for this and recreates the oid with the current state of 'view model' if(!rootOid.isViewModel()) { rootOid = new RootOid(rootOid.getObjectSpecId(), rootOid.getIdentifier(), Oid.State.VIEWMODEL); } try { return adapterFor(rootOid); } catch(final ObjectNotFoundException ex) { return null; } catch(final PojoRecreationException ex) { return null; } } else { try { ObjectAdapter objectAdapter = loadObjectInTransaction(rootOid); return objectAdapter.isTransient() ? null : objectAdapter; } catch(final ObjectNotFoundException ex) { return null; } } } /** * As per {@link #adapterFor(RootOid, ConcurrencyChecking)}, with * {@link ConcurrencyChecking#NO_CHECK no checking}. * * <p> * This method will <i>always</i> return an object, possibly indicating it is persistent; so make sure that you * know that the oid does indeed represent an object you know exists. * </p> */ public ObjectAdapter adapterFor(final RootOid rootOid) { return adapterFor(rootOid, ConcurrencyChecking.NO_CHECK); } /** * Either returns an existing {@link ObjectAdapter adapter} (as per * {@link #getAdapterFor(Oid)}), otherwise re-creates an adapter with the * specified (persistent) {@link Oid}. * * <p> * Typically called when the {@link Oid} is already known, that is, when * resolving an already-persisted object. Is also available for * <tt>Memento</tt> support however, so {@link Oid} could also represent a * {@link Oid#isTransient() transient} object. * * <p> * The pojo itself is recreated by delegating to a {@link AdapterManager}. * * <p> * The {@link ConcurrencyChecking} parameter determines whether concurrency checking is performed. * If it is requested, then a check is made to ensure that the {@link Oid#getVersion() version} * of the {@link RootOid oid} of the recreated adapter is the same as that of the provided {@link RootOid oid}. * If the version differs, then a {@link ConcurrencyException} is thrown. * * <p> * ALSO, even if a {@link ConcurrencyException}, then the provided {@link RootOid oid}'s {@link Version version} * will be {@link RootOid#setVersion(Version) set} to the current * value. This allows the client to retry if they wish. * * @throws {@link org.apache.isis.core.runtime.persistence.ObjectNotFoundException} if the object does not exist. */ public ObjectAdapter adapterFor( final RootOid rootOid, final ConcurrencyChecking concurrencyChecking) { // attempt to locate adapter for the Oid ObjectAdapter adapter = getAdapterFor(rootOid); if (adapter == null) { // else recreate try { final Object pojo = recreatePojo(rootOid); adapter = mapRecreatedPojo(rootOid, pojo); } catch(ObjectNotFoundException ex) { throw ex; // just rethrow } catch(RuntimeException ex) { throw new PojoRecreationException(rootOid, ex); } } // sync versions of original, with concurrency checking if required Oid adapterOid = adapter.getOid(); if(adapterOid instanceof RootOid) { final RootOid recreatedOid = (RootOid) adapterOid; final RootOid originalOid = rootOid; try { if(concurrencyChecking.isChecking()) { // check for exception, but don't throw if suppressed through thread-local final Version otherVersion = originalOid.getVersion(); final Version thisVersion = recreatedOid.getVersion(); if( thisVersion != null && otherVersion != null && thisVersion.different(otherVersion)) { if(concurrencyCheckingGloballyEnabled && ConcurrencyChecking.isCurrentlyEnabled()) { LOG.info("concurrency conflict detected on " + recreatedOid + " (" + otherVersion + ")"); final String currentUser = authenticationSession.getUserName(); throw new ConcurrencyException(currentUser, recreatedOid, thisVersion, otherVersion); } else { LOG.info("concurrency conflict detected but suppressed, on " + recreatedOid + " (" + otherVersion + ")"); } } } } finally { final Version originalVersion = originalOid.getVersion(); final Version recreatedVersion = recreatedOid.getVersion(); if(recreatedVersion != null && ( originalVersion == null || recreatedVersion.different(originalVersion)) ) { if(LOG.isDebugEnabled()) { LOG.debug("updating version in oid, on " + originalOid + " (" + originalVersion + ") to (" + recreatedVersion +")"); } originalOid.setVersion(recreatedVersion); } } } return adapter; } private Object recreatePojo(RootOid oid) { if(oid.isTransient() || oid.isViewModel()) { return recreatePojoDefault(oid); } else { return loadPojo(oid); } } private Object recreatePojoDefault(final RootOid rootOid) { final ObjectSpecification spec = specificationLoader.lookupBySpecId(rootOid.getObjectSpecId()); final Object pojo; if(rootOid.isViewModel()) { final String memento = rootOid.getIdentifier(); pojo = recreateViewModel(spec, memento); } else { pojo = instantiateAndInjectServices(spec); } return pojo; } /** * {@inheritDoc} */ @Override public ObjectAdapter adapterFor(final Object pojo) { if(pojo == null) { return null; } final ObjectAdapter existingOrValueAdapter = existingOrValueAdapter(pojo); if(existingOrValueAdapter != null) { return existingOrValueAdapter; } final ObjectAdapter newAdapter = createTransientOrViewModelRootAdapter(pojo); return mapAndInjectServices(newAdapter); } /** * {@inheritDoc} */ @Override public ObjectAdapter adapterFor(final Object pojo, final ObjectAdapter parentAdapter, final OneToManyAssociation collection) { assert parentAdapter != null; assert collection != null; final ObjectAdapter existingOrValueAdapter = existingOrValueAdapter(pojo); if(existingOrValueAdapter != null) { return existingOrValueAdapter; } // the List, Set etc. instance gets wrapped in its own adapter final ObjectAdapter newAdapter = createCollectionAdapter(pojo, parentAdapter, collection); return mapAndInjectServices(newAdapter); } /** * Creates an {@link ObjectAdapter adapter} to represent a collection * of the parent. * * <p> * The returned adapter will have a {@link ParentedCollectionOid}; its version * and its persistence are the same as its owning parent. * * <p> * Should only be called if the pojo is known not to be * {@link #getAdapterFor(Object) mapped}. */ private ObjectAdapter createCollectionAdapter( final Object pojo, final ObjectAdapter parentAdapter, final OneToManyAssociation otma) { ensureMapsConsistent(parentAdapter); Assert.assertNotNull(pojo); final Oid parentOid = parentAdapter.getOid(); // persistence of collection follows the parent final ParentedCollectionOid collectionOid = new ParentedCollectionOid((RootOid) parentOid, otma); final ObjectAdapter collectionAdapter = createCollectionAdapter(pojo, collectionOid); // we copy over the type onto the adapter itself // [not sure why this is really needed, surely we have enough info in // the adapter // to look this up on the fly?] final TypeOfFacet facet = otma.getFacet(TypeOfFacet.class); collectionAdapter.setElementSpecificationProvider(ElementSpecificationProviderFromTypeOfFacet.createFrom(facet)); return collectionAdapter; } /** * {@inheritDoc} * * <p> * Note that there is no management of {@link Version}s here. That is * because the {@link PersistenceSession} is expected to manage this. * * @param hintRootOid - allow a different persistent root oid to be provided. */ public void remapAsPersistent(final ObjectAdapter adapter, RootOid hintRootOid) { final ObjectAdapter rootAdapter = adapter.getAggregateRoot(); // TODO: REVIEW: think this is redundant; would seem this method is only ever called for roots anyway. final RootOid transientRootOid = (RootOid) rootAdapter.getOid(); // no longer true, because isTransient now looks directly at the underlying pojo's state (for entities) // and doesn't apply for services. // Ensure.ensureThatArg(rootAdapter.isTransient(), is(true), "root adapter should be transient; oid:" + transientRootOid); // Ensure.ensureThatArg(transientRootOid.isTransient(), is(true), "root adapter's OID should be transient; oid:" + transientRootOid); final RootAndCollectionAdapters rootAndCollectionAdapters = new RootAndCollectionAdapters(adapter, this); if (LOG.isDebugEnabled()) { LOG.debug("remapAsPersistent: " + transientRootOid); } if (LOG.isDebugEnabled()) { LOG.debug("removing root adapter from oid map"); } boolean removed = oidAdapterMap.remove(transientRootOid); if (!removed) { LOG.warn("could not remove oid: " + transientRootOid); // should we fail here with a more serious error? } if (LOG.isDebugEnabled()) { LOG.debug("removing collection adapter(s) from oid map"); } for (final ObjectAdapter collectionAdapter : rootAndCollectionAdapters) { final Oid collectionOid = collectionAdapter.getOid(); removed = oidAdapterMap.remove(collectionOid); if (!removed) { LOG.warn("could not remove collectionOid: " + collectionOid); // should we fail here with a more serious error? } } if (LOG.isDebugEnabled()) { LOG.debug("updating the Oid"); } // intended for testing (bit nasty) final RootOid persistedRootOid; if(hintRootOid != null) { if(hintRootOid.isTransient()) { throw new IsisAssertException("hintRootOid must be persistent"); } final ObjectSpecId hintRootOidObjectSpecId = hintRootOid.getObjectSpecId(); final ObjectSpecId adapterObjectSpecId = adapter.getSpecification().getSpecId(); if(!hintRootOidObjectSpecId.equals(adapterObjectSpecId)) { throw new IsisAssertException("hintRootOid's objectType must be same as that of adapter " + "(was: '" + hintRootOidObjectSpecId + "'; adapter's is " + adapterObjectSpecId + "'"); } // ok persistedRootOid = hintRootOid; } else { // normal flow - delegate to OidGenerator to obtain a persistent root oid persistedRootOid = createPersistentOrViewModelOid(adapter.getObject()); } // associate root adapter with the new Oid, and remap if (LOG.isDebugEnabled()) { LOG.debug("replacing Oid for root adapter and re-adding into maps; oid is now: " + persistedRootOid.enString( ) + " (was: " + transientRootOid.enString() + ")"); } adapter.replaceOid(persistedRootOid); oidAdapterMap.add(persistedRootOid, adapter); // associate the collection adapters with new Oids, and re-map if (LOG.isDebugEnabled()) { LOG.debug("replacing Oids for collection adapter(s) and re-adding into maps"); } for (final ObjectAdapter collectionAdapter : rootAndCollectionAdapters) { final ParentedCollectionOid previousCollectionOid = (ParentedCollectionOid) collectionAdapter.getOid(); final ParentedCollectionOid persistedCollectionOid = previousCollectionOid.asPersistent(persistedRootOid); oidAdapterMap.add(persistedCollectionOid, collectionAdapter); } // some object store implementations may replace collection instances (eg ORM may replace with a cglib-enhanced // proxy equivalent. So, ensure that the collection adapters still wrap the correct pojos. if (LOG.isDebugEnabled()) { LOG.debug("synchronizing collection pojos, remapping in pojo map if required"); } for (final OneToManyAssociation otma : rootAndCollectionAdapters.getCollections()) { final ObjectAdapter collectionAdapter = rootAndCollectionAdapters.getCollectionAdapter(otma); final Object collectionPojoWrappedByAdapter = collectionAdapter.getObject(); final Object collectionPojoActuallyOnPojo = getCollectionPojo(otma, adapter); if (collectionPojoActuallyOnPojo != collectionPojoWrappedByAdapter) { pojoAdapterMap.remove(collectionAdapter); collectionAdapter.replacePojo(collectionPojoActuallyOnPojo); pojoAdapterMap.add(collectionPojoActuallyOnPojo, collectionAdapter); } } if (LOG.isDebugEnabled()) { LOG.debug("made persistent " + adapter + "; was " + transientRootOid); } } private static Object getCollectionPojo(final OneToManyAssociation association, final ObjectAdapter ownerAdapter) { final PropertyOrCollectionAccessorFacet accessor = association.getFacet(PropertyOrCollectionAccessorFacet.class); return accessor.getProperty(ownerAdapter, InteractionInitiatedBy.FRAMEWORK); } /** * Either returns an existing {@link ObjectAdapter adapter} (as per * {@link #getAdapterFor(Object)} or {@link #getAdapterFor(Oid)}), otherwise * re-creates an adapter with the specified (persistent) {@link Oid}. * * <p> * Typically called when the {@link Oid} is already known, that is, when * resolving an already-persisted object. Is also available for * <tt>Memento</tt> support however, so {@link Oid} could also represent a * {@link Oid#isTransient() transient} object. * * @param oid * @param recreatedPojo - already known to the object store impl, or a service */ @Override public ObjectAdapter mapRecreatedPojo(final Oid oid, final Object recreatedPojo) { // attempt to locate adapter for the pojo // REVIEW: this check is possibly redundant because the pojo will most likely // have just been instantiated, so won't yet be in any maps final ObjectAdapter adapterLookedUpByPojo = getAdapterFor(recreatedPojo); if (adapterLookedUpByPojo != null) { return adapterLookedUpByPojo; } // attempt to locate adapter for the Oid final ObjectAdapter adapterLookedUpByOid = getAdapterFor(oid); if (adapterLookedUpByOid != null) { return adapterLookedUpByOid; } final ObjectAdapter createdAdapter = createRootOrAggregatedAdapter(oid, recreatedPojo); return mapAndInjectServices(createdAdapter); } /** * Removes the specified object from both the identity-adapter map, and the * pojo-adapter map. * * <p> * This indicates that the object is no longer in use, and therefore that no * objects exists within the system. * * <p> * If an {@link ObjectAdapter adapter} is removed while its pojo still is * referenced then a subsequent interaction of that pojo will create a * different {@link ObjectAdapter adapter}. * * <p> * TODO: should do a cascade remove of any aggregated objects. */ @Override public void removeAdapter(final ObjectAdapter adapter) { ensureMapsConsistent(adapter); if (LOG.isDebugEnabled()) { LOG.debug("removing adapter: " + adapter); } unmap(adapter); } private void unmap(final ObjectAdapter adapter) { ensureMapsConsistent(adapter); final Oid oid = adapter.getOid(); if (oid != null) { oidAdapterMap.remove(oid); } pojoAdapterMap.remove(adapter); } public void remapRecreatedPojo(ObjectAdapter adapter, final Object pojo) { removeAdapter(adapter); adapter.replacePojo(pojo); mapAndInjectServices(adapter); } private ObjectAdapter createRootOrAggregatedAdapter(final Oid oid, final Object pojo) { final ObjectAdapter createdAdapter; if(oid instanceof RootOid) { final RootOid rootOid = (RootOid) oid; createdAdapter = createRootAdapter(pojo, rootOid); } else /*if (oid instanceof CollectionOid)*/ { final ParentedCollectionOid collectionOid = (ParentedCollectionOid) oid; createdAdapter = createCollectionAdapter(pojo, collectionOid); } return createdAdapter; } /** * Creates a new transient root {@link ObjectAdapter adapter} for the supplied domain * object. */ private ObjectAdapter createTransientOrViewModelRootAdapter(final Object pojo) { final RootOid rootOid = createTransientOrViewModelOid(pojo); return createRootAdapter(pojo, rootOid); } /** * Creates a {@link ObjectAdapter adapter} with no {@link Oid}. * * <p> * Standalone adapters are never {@link #mapAndInjectServices(ObjectAdapter) mapped} * (they have no {@link Oid}, after all). * * <p> * Should only be called if the pojo is known not to be * {@link #getAdapterFor(Object) mapped}, and for immutable value types * referenced. */ private ObjectAdapter createStandaloneAdapter(final Object pojo) { return createAdapter(pojo, null); } /** * Creates (but does not {@link #mapAndInjectServices(ObjectAdapter) map}) a new * root {@link ObjectAdapter adapter} for the supplied domain object. * * @see #createStandaloneAdapter(Object) * @see #createCollectionAdapter(Object, ParentedCollectionOid) */ private ObjectAdapter createRootAdapter(final Object pojo, RootOid rootOid) { assert rootOid != null; return createAdapter(pojo, rootOid); } private ObjectAdapter createCollectionAdapter( final Object pojo, ParentedCollectionOid collectionOid) { assert collectionOid != null; return createAdapter(pojo, collectionOid); } private PojoAdapter createAdapter( final Object pojo, final Oid oid) { return new PojoAdapter( pojo, oid, authenticationSession, specificationLoader, this); } private ObjectAdapter mapAndInjectServices(final ObjectAdapter adapter) { // since the whole point of this method is to map an adapter that's just been created. // so we *don't* call ensureMapsConsistent(adapter); Assert.assertNotNull(adapter); final Object pojo = adapter.getObject(); Assert.assertFalse("POJO Map already contains object", pojo, pojoAdapterMap.containsPojo(pojo)); if (LOG.isDebugEnabled()) { // don't interact with the underlying object because may be a ghost // and would trigger a resolve // don't call toString() on adapter because calls hashCode on // underlying object, may also trigger a resolve. LOG.debug("adding identity for adapter with oid=" + adapter.getOid()); } // value adapters are not mapped (but all others - root and aggregated adapters - are) if (adapter.isValue()) { if (LOG.isDebugEnabled()) { LOG.debug("not mapping value adapter"); } servicesInjector.injectServicesInto(pojo); return adapter; } // add all aggregated collections final ObjectSpecification objSpec = adapter.getSpecification(); if (!adapter.isParentedCollection() || adapter.isParentedCollection() && !objSpec.isImmutable()) { pojoAdapterMap.add(pojo, adapter); } // order is important - add to pojo map first, then identity map oidAdapterMap.add(adapter.getOid(), adapter); // must inject after mapping, otherwise infinite loop servicesInjector.injectServicesInto(pojo); return adapter; } //endregion //region > TransactionManager delegate methods protected IsisTransaction getCurrentTransaction() { return transactionManager.getCurrentTransaction(); } //endregion //region > FrameworkSynchronizer delegate methods public void enlistDeletingAndInvokeIsisRemovingCallbackFacet(final Persistable pojo) { ObjectAdapter adapter = adapterFor(pojo); changedObjectsServiceInternal.enlistDeleting(adapter); CallbackFacet.Util.callCallback(adapter, RemovingCallbackFacet.class); postLifecycleEventIfRequired(adapter, RemovingLifecycleEventFacet.class); } public void initializeMapAndCheckConcurrency(final Persistable pojo) { final Persistable pc = pojo; // need to do eagerly, because (if a viewModel then) a // viewModel's #viewModelMemento might need to use services servicesInjector.injectInto(pojo); final Version datastoreVersion = getVersionIfAny(pc); final RootOid originalOid; ObjectAdapter adapter = getAdapterFor(pojo); if (adapter != null) { ensureRootObject(pojo); originalOid = (RootOid) adapter.getOid(); final Version originalVersion = adapter.getVersion(); // sync the pojo held by the adapter with that just loaded remapRecreatedPojo(adapter, pojo); // since there was already an adapter, do concurrency check // (but don't set abort cause if checking is suppressed through thread-local) final RootOid thisOid = originalOid; final Version thisVersion = originalVersion; final Version otherVersion = datastoreVersion; if ( thisVersion != null && otherVersion != null && thisVersion.different(otherVersion)) { if (ConcurrencyChecking.isCurrentlyEnabled()) { LOG.info("concurrency conflict detected on " + thisOid + " (" + otherVersion + ")"); final String currentUser = authenticationSession.getUserName(); final ConcurrencyException abortCause = new ConcurrencyException(currentUser, thisOid, thisVersion, otherVersion); getCurrentTransaction().setAbortCause(abortCause); } else { LOG.info("concurrency conflict detected but suppressed, on " + thisOid + " (" + otherVersion + ")"); } } } else { originalOid = createPersistentOrViewModelOid(pojo); // it appears to be possible that there is already an adapter for this Oid, // ie from ObjectStore#resolveImmediately() adapter = getAdapterFor(originalOid); if (adapter != null) { remapRecreatedPojo(adapter, pojo); } else { adapter = mapRecreatedPojo(originalOid, pojo); CallbackFacet.Util.callCallback(adapter, LoadedCallbackFacet.class); postLifecycleEventIfRequired(adapter, LoadedLifecycleEventFacet.class); } } adapter.setVersion(datastoreVersion); } //region > create...Oid (main API) /** * Create a new {@link Oid#isTransient() transient} {@link Oid} for the * supplied pojo, uniquely distinguishable from any other {@link Oid}. */ public final RootOid createTransientOrViewModelOid(final Object pojo) { return newIdentifier(pojo, Type.TRANSIENT); } /** * Return an equivalent {@link RootOid}, but being persistent. * * <p> * It is the responsibility of the implementation to determine the new unique identifier. * For example, the generator may simply assign a new value from a sequence, or a GUID; * or, the generator may use the oid to look up the object and inspect the object in order * to obtain an application-defined value. * * @param pojo - being persisted */ public final RootOid createPersistentOrViewModelOid(Object pojo) { return newIdentifier(pojo, Type.PERSISTENT); } enum Type { TRANSIENT, PERSISTENT } private RootOid newIdentifier(final Object pojo, final Type type) { final ObjectSpecification spec = objectSpecFor(pojo); if(spec.isService()) { return newRootId(spec, "1", type); } final ViewModelFacet recreatableObjectFacet = spec.getFacet(ViewModelFacet.class); final String identifier = recreatableObjectFacet != null ? recreatableObjectFacet.memento(pojo) : newIdentifierFor(pojo, type); return newRootId(spec, identifier, type); } private String newIdentifierFor(final Object pojo, final Type type) { return type == Type.TRANSIENT ? UUID.randomUUID().toString() : JdoObjectIdSerializer.toOidIdentifier(getPersistenceManager().getObjectId(pojo)); } private RootOid newRootId(final ObjectSpecification spec, final String identifier, final Type type) { final Oid.State state = spec.containsDoOpFacet(ViewModelFacet.class) ? Oid.State.VIEWMODEL : type == Type.TRANSIENT ? Oid.State.TRANSIENT : Oid.State.PERSISTENT; final ObjectSpecId objectSpecId = spec.getSpecId(); return new RootOid(objectSpecId, identifier, state); } private ObjectSpecification objectSpecFor(final Object pojo) { final Class<?> pojoClass = pojo.getClass(); return getSpecificationLoader().loadSpecification(pojoClass); } //endregion /** * Called either when an entity is initially persisted, or when an entity is updated; fires the appropriate * lifecycle callback. * * <p> * The implementation therefore uses Isis' {@link org.apache.isis.core.metamodel.adapter.oid.Oid#isTransient() oid} * to determine which callback to fire. */ public void invokeIsisPersistingCallback(final Persistable pojo) { final ObjectAdapter adapter = getAdapterFor(pojo); if (adapter == null) { // not expected. return; } final RootOid isisOid = (RootOid) adapter.getOid(); if (isisOid.isTransient()) { // persisting // previously this was performed in the DataNucleusSimplePersistAlgorithm. CallbackFacet.Util.callCallback(adapter, PersistingCallbackFacet.class); postLifecycleEventIfRequired(adapter, PersistingLifecycleEventFacet.class); } else { // updating // don't call here, already called in preDirty. // CallbackFacet.Util.callCallback(adapter, UpdatingCallbackFacet.class); } } /** * Called either when an entity is initially persisted, or when an entity is updated; * fires the appropriate lifecycle callback * * <p> * The implementation therefore uses Isis' {@link org.apache.isis.core.metamodel.adapter.oid.Oid#isTransient() oid} * to determine which callback to fire. */ public void enlistCreatedAndRemapIfRequiredThenInvokeIsisInvokePersistingOrUpdatedCallback(final Persistable pojo) { final ObjectAdapter adapter = adapterFor(pojo); final RootOid rootOid = (RootOid) adapter.getOid(); // ok since this is for a Persistable if (rootOid.isTransient()) { // persisting final RootOid persistentOid = createPersistentOrViewModelOid(pojo); remapAsPersistent(adapter, persistentOid); CallbackFacet.Util.callCallback(adapter, PersistedCallbackFacet.class); postLifecycleEventIfRequired(adapter, PersistedLifecycleEventFacet.class); changedObjectsServiceInternal.enlistCreated(adapter); } else { // updating; // the callback and transaction.enlist are done in the preDirty callback // (can't be done here, as the enlist requires to capture the 'before' values) CallbackFacet.Util.callCallback(adapter, UpdatedCallbackFacet.class); postLifecycleEventIfRequired(adapter, UpdatedLifecycleEventFacet.class); } Version versionIfAny = getVersionIfAny(pojo); adapter.setVersion(versionIfAny); } public void enlistUpdatingAndInvokeIsisUpdatingCallback(final Persistable pojo) { ObjectAdapter adapter = getAdapterFor(pojo); if (adapter == null) { // seen this happen in the case when a parent entity (LeaseItem) has a collection of children // objects (LeaseTerm) for which we haven't had a loaded callback fired and so are not yet // mapped. // it seems reasonable in this case to simply map into Isis here ("just-in-time"); presumably // DN would not be calling this callback if the pojo was not persistent. adapter = mapPersistent(pojo); if (adapter == null) { throw new RuntimeException( "DN could not find objectId for pojo (unexpected) and so could not map into Isis; pojo=[" + pojo + "]"); } } if (adapter.isTransient()) { // seen this happen in the case when there's a 1<->m bidirectional collection, and we're // attaching the child object, which is being persisted by DN as a result of persistence-by-reachability, // and it "helpfully" sets up the parent attribute on the child, causing this callback to fire. // // however, at the same time, Isis has only queued up a CreateObjectCommand for the transient object, but it // hasn't yet executed, so thinks that the adapter is still transient. return; } final boolean wasAlreadyEnlisted = changedObjectsServiceInternal.isEnlisted(adapter); // we call this come what may; // additional properties may now have been changed, and the changeKind for publishing might also be modified changedObjectsServiceInternal.enlistUpdating(adapter); if(!wasAlreadyEnlisted) { // prevent an infinite loop... don't call the 'updating()' callback on this object if we have already done so CallbackFacet.Util.callCallback(adapter, UpdatingCallbackFacet.class); postLifecycleEventIfRequired(adapter, UpdatingLifecycleEventFacet.class); } ensureRootObject(pojo); } /** * makes sure the entity is known to Isis and is a root * @param pojo */ public void ensureRootObject(final Persistable pojo) { final Oid oid = adapterFor(pojo).getOid(); if (!(oid instanceof RootOid)) { throw new IsisException(MessageFormat.format("Not a RootOid: oid={0}, for {1}", oid, pojo)); } } private Version getVersionIfAny(final Persistable pojo) { return Utils.getVersionIfAny(pojo, authenticationSession); } //endregion //region > DomainObjectServices impl public Object lookup( final Bookmark bookmark, final BookmarkService2.FieldResetPolicy fieldResetPolicy) { RootOid oid = RootOid.create(bookmark); final ObjectAdapter adapter = adapterFor(oid); if(adapter == null) { return null; } if(fieldResetPolicy == BookmarkService2.FieldResetPolicy.RESET && !adapter.getSpecification().isViewModel()) { refreshRootInTransaction(adapter); } else { loadObjectInTransaction(oid); } return adapter.getObject(); } public boolean flush() { return getTransactionManager().flushTransaction(); } //endregion //region > helpers: lookupService, lookupServices private <T> T lookupService(Class<T> serviceType) { T service = lookupServiceIfAny(serviceType); if(service == null) { throw new IllegalStateException("Could not locate service of type '" + serviceType + "'"); } return service; } private <T> T lookupServiceIfAny(final Class<T> serviceType) { return servicesInjector.lookupService(serviceType); } private <T> List<T> lookupServices(final Class<T> serviceClass) { return servicesInjector.lookupServices(serviceClass); } //endregion //region > toString @Override public String toString() { return new ToString(this).toString(); } //endregion }