Repository: isis Updated Branches: refs/heads/master 8a9378b1f -> 0bca9b01b
ISIS-948, ISIS-947: refactoring event bus, removing special case handling of event bus in Isis core runtime; adding in ability to inject into request-scoped services and for request-scoped services to be event bus subscribers. Some of the new reference counting logic has moved from EventBusDefault into EventBusService (abstract class in applib). Also (for ISIS-947) includes enhancements to ServiceInstantiator so that can call toString(), equals() and hashCode() even if there is no session. In addition: - ToDoItemSubscriptions (for todoapp) moved to menu item, rather than contributed (better intent traded against slightly worse workflow during demos). Project: http://git-wip-us.apache.org/repos/asf/isis/repo Commit: http://git-wip-us.apache.org/repos/asf/isis/commit/f90a62ec Tree: http://git-wip-us.apache.org/repos/asf/isis/tree/f90a62ec Diff: http://git-wip-us.apache.org/repos/asf/isis/diff/f90a62ec Branch: refs/heads/master Commit: f90a62ec16986937ca12b21f39d1e66d7c344c1f Parents: 8a9378b Author: Dan Haywood <[email protected]> Authored: Thu Nov 13 00:40:34 2014 +0000 Committer: Dan Haywood <[email protected]> Committed: Thu Nov 13 00:40:34 2014 +0000 ---------------------------------------------------------------------- .../services/eventbus/EventBusService.java | 75 +++++--- .../services/ServicesInjectorDefault.java | 1 + .../runtime/services/RequestScopedService.java | 67 +++++-- .../runtime/services/ServiceInstantiator.java | 43 ++++- .../eventbus/EventBusServiceDefault.java | 52 ++---- .../eventbus/RequestScopedEventBus.java | 186 +++++++++++++++++++ .../runtime/system/session/IsisSession.java | 18 -- .../system/session/IsisSessionDefault.java | 40 ---- .../system/transaction/IsisTransaction.java | 5 - .../transaction/IsisTransactionManager.java | 38 ++-- .../services/ServiceInstantiatorTest.java | 22 ++- .../java/dom/todo/ToDoItemSubscriptions.java | 65 +++++-- .../integration/tests/ToDoItemIntegTest.java | 24 +-- 13 files changed, 448 insertions(+), 188 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/isis/blob/f90a62ec/core/applib/src/main/java/org/apache/isis/applib/services/eventbus/EventBusService.java ---------------------------------------------------------------------- diff --git a/core/applib/src/main/java/org/apache/isis/applib/services/eventbus/EventBusService.java b/core/applib/src/main/java/org/apache/isis/applib/services/eventbus/EventBusService.java index 059b91d..8325e8a 100644 --- a/core/applib/src/main/java/org/apache/isis/applib/services/eventbus/EventBusService.java +++ b/core/applib/src/main/java/org/apache/isis/applib/services/eventbus/EventBusService.java @@ -16,8 +16,13 @@ */ package org.apache.isis.applib.services.eventbus; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; +import com.google.common.collect.MapMaker; +import com.google.common.collect.Sets; import com.google.common.eventbus.EventBus; - import org.apache.isis.applib.annotation.Hidden; import org.apache.isis.applib.annotation.Programmatic; @@ -26,10 +31,15 @@ import org.apache.isis.applib.annotation.Programmatic; * subscribed to. * * <p> - * It is highly advisable that only domain services - not domain entities - are registered as subscribers. - * Domain services are guaranteed to be instantiated and resident in memory, whereas the same is not true - * of domain entities. The typical implementation of a domain service subscriber is to identify the impacted entities, - * load them using a repository, and then to delegate to the event to them. + * Only domain services (not domain entities or view models) should be registered; only they are guaranteed to be + * instantiated and resident in memory. + * </p> + * <p> + * It <i>is</i> possible to register request-scoped services, however they should register their proxy + * rather than themselves. This ensures that the actual subscribers are all singletons. This implementation uses + * reference counting to keep track of whether there are any concurrent instances of a request-scoped service + * (keeps things nice and symmetrical). + * </p> */ @Hidden public abstract class EventBusService { @@ -56,34 +66,47 @@ public abstract class EventBusService { /** * @return an {@link EventBus} scoped to the current session. */ + @Programmatic protected abstract EventBus getEventBus(); - - /** - * Register the domain object with the service. - * - * <p> - * This must be called manually, but a good technique is for the domain object to call - * this method when the service is injected into it. - * - * <p> - * For example: - * <pre> - * private EventBusService eventBusService; - * public void injectEventBusService(final EventBusService eventBusService) { - * this.eventBusService = eventBusService; - * eventBusService.register(this); - * } - * </pre> - */ + + //region > register, unregister + @Programmatic public void register(Object domainObject) { - getEventBus().register(domainObject); + referenceCountBySubscriber.putIfAbsent(domainObject, new AtomicInteger(0)); + referenceCountBySubscriber.get(domainObject).incrementAndGet(); } - + @Programmatic public void unregister(Object domainObject) { - getEventBus().unregister(domainObject); + final AtomicInteger atomicInteger = referenceCountBySubscriber.get(domainObject); + atomicInteger.decrementAndGet(); + } + + //endregion + + //region > subscribers + + private final ConcurrentMap<Object, AtomicInteger> referenceCountBySubscriber = new MapMaker().weakKeys().makeMap(); + + /** + * Not API + */ + @Programmatic + public Set<Object> getSubscribers() { + + + + // only those subscribers that are "active" + Set<Object> subscribers = Sets.newLinkedHashSet(); + for (Map.Entry<Object, AtomicInteger> subscriberReferenceCount : this.referenceCountBySubscriber.entrySet()) { + if(subscriberReferenceCount.getValue().get()>0) { + subscribers.add(subscriberReferenceCount.getKey()); + } + } + return subscribers; } + //endregion /** * Post an event. http://git-wip-us.apache.org/repos/asf/isis/blob/f90a62ec/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/ServicesInjectorDefault.java ---------------------------------------------------------------------- diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/ServicesInjectorDefault.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/ServicesInjectorDefault.java index 5a2b3de..dd34686 100644 --- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/ServicesInjectorDefault.java +++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/ServicesInjectorDefault.java @@ -169,6 +169,7 @@ public class ServicesInjectorDefault implements ServicesInjectorSpi, Specificati //region > helpers private void injectServices(final Object object, final List<Object> services) { + final Class<?> cls = object.getClass(); autowireViaFields(object, services, cls); http://git-wip-us.apache.org/repos/asf/isis/blob/f90a62ec/core/runtime/src/main/java/org/apache/isis/core/runtime/services/RequestScopedService.java ---------------------------------------------------------------------- diff --git a/core/runtime/src/main/java/org/apache/isis/core/runtime/services/RequestScopedService.java b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/RequestScopedService.java index b85eb6f..c01b459 100644 --- a/core/runtime/src/main/java/org/apache/isis/core/runtime/services/RequestScopedService.java +++ b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/RequestScopedService.java @@ -19,25 +19,62 @@ package org.apache.isis.core.runtime.services; -import com.google.common.base.Predicate; +import org.apache.isis.applib.annotation.Programmatic; +import org.apache.isis.core.metamodel.runtimecontext.ServicesInjector; +/** + * Interface in support of request-scoped domain services (is implemented by the Javassist proxy). + * + * <p> + * Do NOT implement directly (will cause the javassist generation to fail). Instead the request-scoped service + * can provide a (conventional) <code>@PostConstruct</code> and <code>@PreDestroy</code> method. + * </p> + */ public interface RequestScopedService { - public static final class Predicates { - private Predicates(){} - public static Predicate<Object> instanceOf() { - return new Predicate<Object>(){ - @Override - public boolean apply(Object input) { - return input instanceof RequestScopedService; - } - }; - } - }; - - public void __isis_startRequest(); - + /** + * Indicates to the proxy that a new request is starting, so should instantiate a new instance of the underlying + * service and bind to the thread, and inject into that service using the provided {@link org.apache.isis.core.metamodel.runtimecontext.ServicesInjector}. + * + * <p> + * This is done before the <code>@PostConstruct</code>, see {@link #__isis_postConstruct()}. + * </p> + */ + @Programmatic + public void __isis_startRequest(ServicesInjector injector); + + /** + * Indicates to the proxy that <code>@PostConstruct</code> should be called on + * underlying instance for current thread. + * + * <p> + * This is done after the request has started, see {@link #__isis_startRequest(org.apache.isis.core.metamodel.runtimecontext.ServicesInjector)}. + * </p> + */ + @Programmatic + public void __isis_postConstruct(); + + /** + * Indicates to the proxy that <code>@PreDestroy</code> should be called on + * underlying instance for current thread. + * + * <p> + * This is done prior to the request ending, see {@link #__isis_endRequest()}. + * </p> + */ + @Programmatic + public void __isis_preDestroy(); + + /** + * Indicates to the proxy that request is ending, so should clean up and remove the + * underlying instance for current thread. + * + * <p> + * This is done after the <code>@PreDestroy</code>, see {@link #__isis_preDestroy()}. + * </p> + */ + @Programmatic public void __isis_endRequest(); } http://git-wip-us.apache.org/repos/asf/isis/blob/f90a62ec/core/runtime/src/main/java/org/apache/isis/core/runtime/services/ServiceInstantiator.java ---------------------------------------------------------------------- diff --git a/core/runtime/src/main/java/org/apache/isis/core/runtime/services/ServiceInstantiator.java b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/ServiceInstantiator.java index 31705db..36d5145 100644 --- a/core/runtime/src/main/java/org/apache/isis/core/runtime/services/ServiceInstantiator.java +++ b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/ServiceInstantiator.java @@ -41,6 +41,7 @@ import org.apache.isis.core.commons.factory.InstanceCreationClassException; import org.apache.isis.core.commons.factory.InstanceCreationException; import org.apache.isis.core.commons.lang.ArrayExtensions; import org.apache.isis.core.commons.lang.MethodExtensions; +import org.apache.isis.core.metamodel.runtimecontext.ServicesInjector; import org.apache.isis.core.metamodel.specloader.classsubstitutor.JavassistEnhanced; /** @@ -131,7 +132,7 @@ public final class ServiceInstantiator { final T newInstance = proxySubclass.newInstance(); final ProxyObject proxyObject = (ProxyObject) newInstance; proxyObject.setHandler(new MethodHandler() { - private ThreadLocal<T> serviceByThread = new ThreadLocal<T>(); + private ThreadLocal<T> serviceByThread = new ThreadLocal<>(); @Override public Object invoke(final Object proxied, final Method proxyMethod, final Method proxiedMethod, final Object[] args) throws Throwable { @@ -139,24 +140,58 @@ public final class ServiceInstantiator { cacheMethodsIfNecessary(cls); if(proxyMethod.getName().equals("__isis_startRequest")) { + T service = instantiate(cls); + serviceByThread.set(service); + + ServicesInjector servicesInjector = (ServicesInjector) args[0]; + servicesInjector.injectServicesInto(service); + + return null; + + } else if(proxyMethod.getName().equals("__isis_postConstruct")) { + + final T service = serviceByThread.get(); callPostConstructIfPresent(service); - serviceByThread.set(service); return null; - } else if(proxyMethod.getName().equals("__isis_endRequest")) { + + } else if(proxyMethod.getName().equals("__isis_preDestroy")) { final T service = serviceByThread.get(); callPreDestroyIfPresent(service); + return null; + + } else if(proxyMethod.getName().equals("__isis_endRequest")) { + serviceByThread.set(null); return null; + + } else if(proxyMethod.getName().equals("hashCode") && proxyMethod.getParameterTypes().length == 0) { + + final T service = serviceByThread.get(); + return service != null? service.hashCode(): this.hashCode(); + + } else if(proxyMethod.getName().equals("equals") && proxyMethod.getParameterTypes().length == 1 && proxyMethod.getParameterTypes()[0] == Object.class) { + + final T service = serviceByThread.get(); + return service != null? service.equals(args[0]): this.equals(args[0]); + + } else if(proxyMethod.getName().equals("toString") && proxyMethod.getParameterTypes().length == 0) { + + final T service = serviceByThread.get(); + return service != null? service.toString(): this.toString(); + } else { T service = serviceByThread.get(); if(service == null) { - throw new IllegalStateException("No service found for thread; make sure ((RequestScopedService)service).__isis_startRequest() is called first"); + // ignore; this could potentially happen during an application-level init (PersistenceSessionFactory#init) + // it has also happened in past when enabling debugging, though the + // new hashCode(), equals() and toString() checks above should mitigate. + return null; } final Object proxiedReturn = proxyMethod.invoke(service, args); return proxiedReturn; http://git-wip-us.apache.org/repos/asf/isis/blob/f90a62ec/core/runtime/src/main/java/org/apache/isis/core/runtime/services/eventbus/EventBusServiceDefault.java ---------------------------------------------------------------------- diff --git a/core/runtime/src/main/java/org/apache/isis/core/runtime/services/eventbus/EventBusServiceDefault.java b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/eventbus/EventBusServiceDefault.java index e79243b..3e5e5f9 100644 --- a/core/runtime/src/main/java/org/apache/isis/core/runtime/services/eventbus/EventBusServiceDefault.java +++ b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/eventbus/EventBusServiceDefault.java @@ -16,57 +16,33 @@ */ package org.apache.isis.core.runtime.services.eventbus; -import java.util.Set; - -import com.google.common.collect.Sets; +import javax.inject.Inject; import com.google.common.eventbus.EventBus; - +import org.apache.isis.applib.annotation.Programmatic; import org.apache.isis.applib.services.eventbus.EventBusService; -import org.apache.isis.core.runtime.system.context.IsisContext; /** * @deprecated - because <tt>EventBusServiceJdo</tt> is annotated as the default implementation. */ @Deprecated public class EventBusServiceDefault extends EventBusService { - - private final Set<Object> objectsToRegister = Sets.newHashSet(); - + + //region > API (getEventBus) + + @Programmatic @Override protected EventBus getEventBus() { - return IsisContext.getSession().getEventBus(); - } - - - @Override - public void register(Object domainObject) { - // lazily registered - // (a) there may be no session initially - // (b) so can be unregistered at when closed - objectsToRegister.add(domainObject); - } - - @Override - public void unregister(Object domainObject) { - if(IsisContext.inSession()) { - getEventBus().unregister(domainObject); - } - objectsToRegister.remove(domainObject); + return requestScopedEventBus.getEventBus(); } + //endregion - public void open() { - final Set<Object> objectsToRegister = this.objectsToRegister; - for (final Object object : objectsToRegister) { - getEventBus().register(object); - } - } - public void close() { - final Set<Object> objectsToRegister = this.objectsToRegister; - for (final Object object : objectsToRegister) { - getEventBus().unregister(object); - } - } + //region > injected services + + @Inject + private RequestScopedEventBus requestScopedEventBus; + + //endregion } http://git-wip-us.apache.org/repos/asf/isis/blob/f90a62ec/core/runtime/src/main/java/org/apache/isis/core/runtime/services/eventbus/RequestScopedEventBus.java ---------------------------------------------------------------------- diff --git a/core/runtime/src/main/java/org/apache/isis/core/runtime/services/eventbus/RequestScopedEventBus.java b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/eventbus/RequestScopedEventBus.java new file mode 100644 index 0000000..39b12ba --- /dev/null +++ b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/eventbus/RequestScopedEventBus.java @@ -0,0 +1,186 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.isis.core.runtime.services.eventbus; + +import java.util.List; +import java.util.Set; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.enterprise.context.RequestScoped; +import com.google.common.base.Throwables; +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.SubscriberExceptionContext; +import com.google.common.eventbus.SubscriberExceptionHandler; +import org.apache.isis.applib.NonRecoverableException; +import org.apache.isis.applib.RecoverableException; +import org.apache.isis.applib.annotation.DomainService; +import org.apache.isis.applib.annotation.Programmatic; +import org.apache.isis.core.commons.exceptions.IsisApplicationException; +import org.apache.isis.core.runtime.system.context.IsisContext; +import org.apache.isis.core.runtime.system.transaction.IsisTransactionManager; + + +/** + * Manages a guava {@link com.google.common.eventbus.EventBus}, scoped and private to a given request. + * + * <p> + * This involves obtaining the set of subscribers from the (owning, singleton) {@link org.apache.isis.applib.services.eventbus.EventBusService event bus service} + * and registering/unregistering them with the guava event bus at the appropriate time. + * </p> + */ +@DomainService +@RequestScoped +public class RequestScopedEventBus { + + //region > startRequest, endRequest + + /** + * Cannot do the setup of the event bus here (so this is asymmetric with <code>@PreDestroy</code>) because there is + * no guarantee of the order in which <code>@PostConstruct</code> is called on any request-scoped services. We + * therefore let the request-scoped services register themselves with (the owning, singleton) + * {@link org.apache.isis.applib.services.eventbus.EventBusService} in their + * <code>@PostConstruct</code> and do the actual instantiation of the guava {@link com.google.common.eventbus.EventBus} + * and registering of subscribers lazily, in {@link #getEventBus()}. This lifecycle method ({@link #startRequest()}) + * is therefore a no-op. + * + * <p> + * The guava {@link com.google.common.eventbus.EventBus} can however (and is) be torndown in the + * <code>@PreDestroy</code> {@link #endRequest()} lifecycle method. + * </p> + */ + @Programmatic + @PostConstruct + public void startRequest() { + // no-op + } + + @Programmatic + @PreDestroy + public void endRequest() { + teardownEventBus(); + } + + //endregion + + //region > (guava) event bus + + /** + * Lazily populated in {@link #getEventBus()}. + */ + private EventBus eventBus; + + /** + * Lazily populates the event bus and captures the set of subscribers from + * {@link EventBusServiceDefault#getSubscribers()}}. + * + * <p> + * These are torn down when the {@link #endRequest() request ends}. + * </p> + */ + @Programmatic + public EventBus getEventBus() { + setupEventBus(); + return eventBus; + } + + /** + * Set of subscribers registered with the event bus. + * + * <p> + * Lazily populated in {@link #setupEventBus()}. + * </p> + */ + private Set<Object> subscribers; + + /** + * Populates {@link #eventBus} and {@link #subscribers}. + * + * <p> + * Guava event bus will throw an exception if attempt to unsubscribe any subscribers that were not subscribed. + * It is therefore the responsibility of this (wrapper) service to remember which services were registered + * at the start of the request, and to unregister precisely this same set of services at the end. + * </p> + */ + protected void setupEventBus() { + if(eventBus != null) { + return; + } + this.eventBus = newEventBus(); + + // "pulls" subscribers from the (singleton) event bus service + subscribers = eventBusService.getSubscribers(); + + for (Object subscriber : this.subscribers) { + eventBus.register(subscriber); + } + } + + protected void teardownEventBus() { + if(subscribers != null) { + for (Object subscriber : this.subscribers) { + eventBus.unregister(subscriber); + } + } + + this.eventBus = null; + } + + protected EventBus newEventBus() { + return new EventBus(newEventBusSubscriberExceptionHandler()); + } + + protected SubscriberExceptionHandler newEventBusSubscriberExceptionHandler() { + return new SubscriberExceptionHandler(){ + @Override + public void handleException(Throwable exception, SubscriberExceptionContext context) { + final List<Throwable> causalChain = Throwables.getCausalChain(exception); + for (Throwable cause : causalChain) { + if(cause instanceof RecoverableException || cause instanceof NonRecoverableException) { + getTransactionManager().getTransaction().setAbortCause(new IsisApplicationException(exception)); + return; + } + } + // otherwise simply ignore + } + }; + } + + protected IsisTransactionManager getTransactionManager() { + return IsisContext.getTransactionManager(); + } + + //endregion + + //region > injected services + private EventBusServiceDefault eventBusService; + /** + * Singleton holding the list of subscribers. + * + * <p> + * Must use an <code>injectXxx</code> method because Isis does not (currently) support field injection into + * request-scoped services (the javassist proxy does not delegate on). + * </p> + */ + @Programmatic + public void injectEventBusService(EventBusServiceDefault eventBusService) { + this.eventBusService = eventBusService; + } + + //endregion + +} + http://git-wip-us.apache.org/repos/asf/isis/blob/f90a62ec/core/runtime/src/main/java/org/apache/isis/core/runtime/system/session/IsisSession.java ---------------------------------------------------------------------- diff --git a/core/runtime/src/main/java/org/apache/isis/core/runtime/system/session/IsisSession.java b/core/runtime/src/main/java/org/apache/isis/core/runtime/system/session/IsisSession.java index a06be35..db5b7d2 100644 --- a/core/runtime/src/main/java/org/apache/isis/core/runtime/system/session/IsisSession.java +++ b/core/runtime/src/main/java/org/apache/isis/core/runtime/system/session/IsisSession.java @@ -39,15 +39,6 @@ import org.apache.isis.core.runtime.system.transaction.IsisTransaction; public interface IsisSession extends SessionScopedComponent { // ////////////////////////////////////////////////////// - // ExecutionContextFactory - // ////////////////////////////////////////////////////// - - /** - * The {@link IsisSessionFactory factory} that created this session. - */ - public IsisSessionFactory getSessionFactory(); - - // ////////////////////////////////////////////////////// // closeAll // ////////////////////////////////////////////////////// @@ -95,15 +86,6 @@ public interface IsisSession extends SessionScopedComponent { // ////////////////////////////////////////////////////// - // EventBus - // ////////////////////////////////////////////////////// - - /** - * Guava {@link EventBus}, scoped to this session. - */ - public EventBus getEventBus(); - - // ////////////////////////////////////////////////////// // Transaction (if in progress) // ////////////////////////////////////////////////////// http://git-wip-us.apache.org/repos/asf/isis/blob/f90a62ec/core/runtime/src/main/java/org/apache/isis/core/runtime/system/session/IsisSessionDefault.java ---------------------------------------------------------------------- diff --git a/core/runtime/src/main/java/org/apache/isis/core/runtime/system/session/IsisSessionDefault.java b/core/runtime/src/main/java/org/apache/isis/core/runtime/system/session/IsisSessionDefault.java index 0f20891..c74a4b8 100644 --- a/core/runtime/src/main/java/org/apache/isis/core/runtime/system/session/IsisSessionDefault.java +++ b/core/runtime/src/main/java/org/apache/isis/core/runtime/system/session/IsisSessionDefault.java @@ -21,20 +21,14 @@ package org.apache.isis.core.runtime.system.session; import java.text.SimpleDateFormat; import java.util.Date; -import com.google.common.eventbus.EventBus; -import com.google.common.eventbus.SubscriberExceptionContext; -import com.google.common.eventbus.SubscriberExceptionHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.isis.applib.NonRecoverableException; -import org.apache.isis.applib.RecoverableException; 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.debug.DebugBuilder; import org.apache.isis.core.commons.debug.DebugString; import org.apache.isis.core.commons.debug.DebuggableWithTitle; -import org.apache.isis.core.commons.exceptions.IsisApplicationException; import org.apache.isis.core.commons.util.ToString; import org.apache.isis.core.metamodel.spec.SpecificationLoaderSpi; import org.apache.isis.core.runtime.system.DeploymentType; @@ -71,8 +65,6 @@ public class IsisSessionDefault implements IsisSession { private long accessTime; private String debugSnapshot; - private EventBus eventBus; - public IsisSessionDefault( final IsisSessionFactory sessionFactory, final AuthenticationSession authenticationSession, @@ -102,29 +94,9 @@ public class IsisSessionDefault implements IsisSession { @Override public void open() { - this.eventBus = newEventBus(); - persistenceSession.open(); } - protected EventBus newEventBus() { - return new EventBus(newEventBusSubscriberExceptionHandler()); - } - - protected SubscriberExceptionHandler newEventBusSubscriberExceptionHandler() { - return new SubscriberExceptionHandler(){ - @Override - public void handleException(Throwable exception, SubscriberExceptionContext context) { - if(exception instanceof RecoverableException || - exception instanceof NonRecoverableException) { - getTransactionManager().getTransaction().setAbortCause(new IsisApplicationException(exception)); - } else { - // simply ignore - } - } - }; - } - /** * Closes session. */ @@ -132,8 +104,6 @@ public class IsisSessionDefault implements IsisSession { public void close() { takeSnapshot(); getPersistenceSession().close(); - - eventBus = null; } // ////////////////////////////////////////////////////// @@ -161,11 +131,6 @@ public class IsisSessionDefault implements IsisSession { // ExecutionContextFactory // ////////////////////////////////////////////////////// - @Override - public IsisSessionFactory getSessionFactory() { - return isisSessionFactory; - } - /** * Convenience method. */ @@ -251,11 +216,6 @@ public class IsisSessionDefault implements IsisSession { return getTransactionManager().getTransaction(); } - @Override - public EventBus getEventBus() { - return eventBus; - } - // ////////////////////////////////////////////////////// // testSetObjectPersistor // ////////////////////////////////////////////////////// http://git-wip-us.apache.org/repos/asf/isis/blob/f90a62ec/core/runtime/src/main/java/org/apache/isis/core/runtime/system/transaction/IsisTransaction.java ---------------------------------------------------------------------- diff --git a/core/runtime/src/main/java/org/apache/isis/core/runtime/system/transaction/IsisTransaction.java b/core/runtime/src/main/java/org/apache/isis/core/runtime/system/transaction/IsisTransaction.java index c5171e9..aeea2db 100644 --- a/core/runtime/src/main/java/org/apache/isis/core/runtime/system/transaction/IsisTransaction.java +++ b/core/runtime/src/main/java/org/apache/isis/core/runtime/system/transaction/IsisTransaction.java @@ -75,7 +75,6 @@ import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation; import org.apache.isis.core.runtime.persistence.ObjectPersistenceException; import org.apache.isis.core.runtime.persistence.PersistenceConstants; import org.apache.isis.core.runtime.persistence.objectstore.transaction.*; -import org.apache.isis.core.runtime.services.eventbus.EventBusServiceDefault; import org.apache.isis.core.runtime.system.context.IsisContext; import static org.apache.isis.core.commons.ensure.Ensure.ensureThatArg; @@ -872,10 +871,6 @@ public class IsisTransaction implements TransactionScopedComponent { if(bic != null) { Bulk.InteractionContext.current.set(null); } - EventBusServiceDefault ebs = getServiceOrNull(EventBusServiceDefault.class); - if(ebs != null) { - ebs.close(); - } } private void completeCommandIfConfigured() { http://git-wip-us.apache.org/repos/asf/isis/blob/f90a62ec/core/runtime/src/main/java/org/apache/isis/core/runtime/system/transaction/IsisTransactionManager.java ---------------------------------------------------------------------- diff --git a/core/runtime/src/main/java/org/apache/isis/core/runtime/system/transaction/IsisTransactionManager.java b/core/runtime/src/main/java/org/apache/isis/core/runtime/system/transaction/IsisTransactionManager.java index 2d89384..0fd9308 100644 --- a/core/runtime/src/main/java/org/apache/isis/core/runtime/system/transaction/IsisTransactionManager.java +++ b/core/runtime/src/main/java/org/apache/isis/core/runtime/system/transaction/IsisTransactionManager.java @@ -40,7 +40,6 @@ import org.apache.isis.core.metamodel.runtimecontext.ServicesInjector; 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.services.RequestScopedService; -import org.apache.isis.core.runtime.services.eventbus.EventBusServiceDefault; 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.IsisSession; @@ -251,11 +250,10 @@ public class IsisTransactionManager implements SessionScopedComponent { if (getTransaction() == null || getTransaction().getState().isComplete()) { noneInProgress = true; - final List<Object> registeredServices = servicesInjector.getRegisteredServices(); - startRequestOnRequestScopedServices(registeredServices); + startRequestOnRequestScopedServices(); createCommandIfConfigured(); - initOtherApplibServicesIfConfigured(registeredServices); + initOtherApplibServicesIfConfigured(); IsisTransaction isisTransaction = createTransaction(); transactionLevel = 0; @@ -273,29 +271,43 @@ public class IsisTransactionManager implements SessionScopedComponent { } - private void initOtherApplibServicesIfConfigured(final List<Object> registeredServices) { - - final EventBusServiceDefault ebsd = getServiceOrNull(EventBusServiceDefault.class); - if(ebsd != null) { - ebsd.open(); - } + private void initOtherApplibServicesIfConfigured() { final Bulk.InteractionContext bic = getServiceOrNull(Bulk.InteractionContext.class); if(bic != null) { Bulk.InteractionContext.current.set(bic); } - } - private void startRequestOnRequestScopedServices(final List<Object> registeredServices) { + private void startRequestOnRequestScopedServices() { + + final List<Object> registeredServices = servicesInjector.getRegisteredServices(); + + // tell the proxy of all request-scoped services to instantiate the underlying + // services, store onto the thread-local and inject into them... + for (final Object service : registeredServices) { + if(service instanceof RequestScopedService) { + ((RequestScopedService)service).__isis_startRequest(servicesInjector); + } + } + // ... and invoke all @PostConstruct for (final Object service : registeredServices) { if(service instanceof RequestScopedService) { - ((RequestScopedService)service).__isis_startRequest(); + ((RequestScopedService)service).__isis_postConstruct(); } } } private void endRequestOnRequestScopeServices() { + // tell the proxy of all request-scoped services to invoke @PreDestroy + // (if any) on all underlying services stored on their thread-locals... + for (final Object service : servicesInjector.getRegisteredServices()) { + if(service instanceof RequestScopedService) { + ((RequestScopedService)service).__isis_preDestroy(); + } + } + + // ... and then remove those underlying services from the thread-local for (final Object service : servicesInjector.getRegisteredServices()) { if(service instanceof RequestScopedService) { ((RequestScopedService)service).__isis_endRequest(); http://git-wip-us.apache.org/repos/asf/isis/blob/f90a62ec/core/runtime/src/test/java/org/apache/isis/core/runtime/services/ServiceInstantiatorTest.java ---------------------------------------------------------------------- diff --git a/core/runtime/src/test/java/org/apache/isis/core/runtime/services/ServiceInstantiatorTest.java b/core/runtime/src/test/java/org/apache/isis/core/runtime/services/ServiceInstantiatorTest.java index 049d2d3..ff16b30 100644 --- a/core/runtime/src/test/java/org/apache/isis/core/runtime/services/ServiceInstantiatorTest.java +++ b/core/runtime/src/test/java/org/apache/isis/core/runtime/services/ServiceInstantiatorTest.java @@ -19,20 +19,32 @@ package org.apache.isis.core.runtime.services; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; import javax.enterprise.context.RequestScoped; +import org.jmock.auto.Mock; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.apache.isis.core.commons.config.IsisConfigurationDefault; +import org.apache.isis.core.metamodel.runtimecontext.ServicesInjector; +import org.apache.isis.core.unittestsupport.jmocking.JUnitRuleMockery2; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; public class ServiceInstantiatorTest { + @Rule + public JUnitRuleMockery2 context = JUnitRuleMockery2.createFor(JUnitRuleMockery2.Mode.INTERFACES_AND_CLASSES); + private ServiceInstantiator serviceInstantiator; - + + @JUnitRuleMockery2.Ignoring + @Mock + private ServicesInjector mockServiceInjector; + @Before public void setUp() throws Exception { + serviceInstantiator = new ServiceInstantiator(); serviceInstantiator.setConfiguration(new IsisConfigurationDefault()); } @@ -53,7 +65,7 @@ public class ServiceInstantiatorTest { public void requestScoped_justOneThread() { AccumulatingCalculator calculator = serviceInstantiator.createInstance(AccumulatingCalculator.class); try { - ((RequestScopedService)calculator).__isis_startRequest(); + ((RequestScopedService)calculator).__isis_startRequest(mockServiceInjector); assertThat(calculator.add(3), is(3)); assertThat(calculator.add(4), is(7)); assertThat(calculator.getTotal(), is(7)); @@ -88,16 +100,14 @@ public class ServiceInstantiatorTest { new Thread() { public void run() { try { - ((RequestScopedService)calculator).__isis_startRequest(); + ((RequestScopedService)calculator).__isis_startRequest(mockServiceInjector); // keep incrementing, til no more steps while(steps[0]>0) { try { calculator.add((j+1)); totals[j] = calculator.getTotal(); barrier.await(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (BrokenBarrierException e) { + } catch (InterruptedException | BrokenBarrierException e) { throw new RuntimeException(e); } } http://git-wip-us.apache.org/repos/asf/isis/blob/f90a62ec/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItemSubscriptions.java ---------------------------------------------------------------------- diff --git a/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItemSubscriptions.java b/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItemSubscriptions.java index 9e3aa4c..d2fce51 100644 --- a/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItemSubscriptions.java +++ b/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItemSubscriptions.java @@ -20,6 +20,8 @@ package dom.todo; import java.util.EventObject; import java.util.List; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.Lists; @@ -37,6 +39,16 @@ import static com.google.common.collect.Iterables.filter; import static com.google.common.collect.Iterables.transform; import static com.google.common.collect.Lists.newArrayList; +/** + * Subscribes to changes made to the {@link dom.todo.ToDoItem} entity. + * + * <p> + * (For demo purposes) the behaviour can be influenced using {@link #subscriberBehaviour(dom.todo.ToDoItemSubscriptions.Behaviour)}. + * In particular, the subscriber can be used to hide/disable/validate actions, or just to perform pre- or post-execute + * tasks. This also includes being set to throw an exception during the execution of the action (also in effect + * vetoing the change). + * </p> + */ @DomainService public class ToDoItemSubscriptions { @@ -44,6 +56,38 @@ public class ToDoItemSubscriptions { private final static org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(ToDoItemSubscriptions.class); //endregion + //region > postConstruct, preDestroy + + /** + * Registers this service with the {@link org.apache.isis.applib.services.eventbus.EventBusService}. + * + * <p> + * Because this service is a singleton, this is called during initial bootstrap. + * </p> + */ + @Programmatic + @PostConstruct + public void postConstruct() { + LOG.info("postConstruct: registering to event bus"); + eventBusService.register(this); + } + + /** + * Unregisters this service from the {@link org.apache.isis.applib.services.eventbus.EventBusService}. + * + * <p> + * Because this service is a singleton, this is only done when the system is shutdown. + * </p> + */ + @Programmatic + @PreDestroy + public void preDestroy() { + LOG.info("preDestroy: unregistering from event bus"); + eventBusService.unregister(this); + } + //endregion + + //region > on(Event)... public static enum Behaviour { @@ -65,6 +109,10 @@ public class ToDoItemSubscriptions { DependenciesCollectionInvalidateRemove, SimilarToCollectionHide } + + /** + * The desired behaviour of this service. + */ private Behaviour behaviour = Behaviour.AnyExecuteAccept; /** @@ -73,14 +121,13 @@ public class ToDoItemSubscriptions { @Prototype @MemberOrder(name = "Prototyping", sequence = "80") @Named("Set subscriber behaviour") - @NotInServiceMenu + @NotContributed @ActionSemantics(ActionSemantics.Of.IDEMPOTENT) - public ToDoItem subscriberBehaviour(ToDoItem toDoItem, @Named("Behaviour") Behaviour behaviour) { + public void subscriberBehaviour(@Named("Behaviour") Behaviour behaviour) { this.behaviour = behaviour; container.informUser("Subscriber behaviour set to: " + behaviour); - return toDoItem; } - public Behaviour default1SubscriberBehaviour() { + public Behaviour default0SubscriberBehaviour() { return this.behaviour; } @@ -93,7 +140,7 @@ public class ToDoItemSubscriptions { throw new RecoverableException("Rejecting event (recoverable exception thrown)"); } if(behaviour == Behaviour.AnyExecuteVetoWithNonRecoverableException) { - throw new NonRecoverableException("Rejecting event (recoverable exception thrown)"); + throw new NonRecoverableException("Rejecting event (non-recoverable exception thrown)"); } if(behaviour == Behaviour.AnyExecuteVetoWithOtherException) { throw new RuntimeException("Throwing some other exception"); @@ -302,7 +349,7 @@ public class ToDoItemSubscriptions { @Programmatic public void reset() { receivedEvents.clear(); - subscriberBehaviour(null, ToDoItemSubscriptions.Behaviour.AnyExecuteAccept); + subscriberBehaviour(ToDoItemSubscriptions.Behaviour.AnyExecuteAccept); } //endregion @@ -310,12 +357,8 @@ public class ToDoItemSubscriptions { @javax.inject.Inject private DomainObjectContainer container; - @SuppressWarnings("unused") + @javax.inject.Inject private EventBusService eventBusService; - @Programmatic - public final void injectEventBusService(EventBusService eventBusService) { - eventBusService.register(this); - } //endregion http://git-wip-us.apache.org/repos/asf/isis/blob/f90a62ec/example/application/todoapp/integtests/src/test/java/integration/tests/ToDoItemIntegTest.java ---------------------------------------------------------------------- diff --git a/example/application/todoapp/integtests/src/test/java/integration/tests/ToDoItemIntegTest.java b/example/application/todoapp/integtests/src/test/java/integration/tests/ToDoItemIntegTest.java index ff63e8f..d95c2ee 100644 --- a/example/application/todoapp/integtests/src/test/java/integration/tests/ToDoItemIntegTest.java +++ b/example/application/todoapp/integtests/src/test/java/integration/tests/ToDoItemIntegTest.java @@ -233,7 +233,7 @@ public class ToDoItemIntegTest extends AbstractToDoIntegTest { public void subscriberVetoesEventWithRecoverableException() throws Exception { // given - toDoItemSubscriptions.subscriberBehaviour(null, ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithRecoverableException); + toDoItemSubscriptions.subscriberBehaviour(ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithRecoverableException); // then expectedExceptions.expect(RecoverableException.class); @@ -246,7 +246,7 @@ public class ToDoItemIntegTest extends AbstractToDoIntegTest { public void subscriberVetoesEventWithNonRecoverableException() throws Exception { // given - toDoItemSubscriptions.subscriberBehaviour(null, ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithNonRecoverableException); + toDoItemSubscriptions.subscriberBehaviour(ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithNonRecoverableException); // then expectedExceptions.expect(NonRecoverableException.class); @@ -259,7 +259,7 @@ public class ToDoItemIntegTest extends AbstractToDoIntegTest { public void subscriberThrowingOtherExceptionIsIgnored() throws Exception { // given - toDoItemSubscriptions.subscriberBehaviour(null, ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithOtherException); + toDoItemSubscriptions.subscriberBehaviour(ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithOtherException); // when toDoItem.completed(); @@ -536,7 +536,7 @@ public class ToDoItemIntegTest extends AbstractToDoIntegTest { public void subscriberVetoesEventWithRecoverableException() throws Exception { // given - toDoItemSubscriptions.subscriberBehaviour(null, ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithRecoverableException); + toDoItemSubscriptions.subscriberBehaviour(ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithRecoverableException); // then expectedExceptions.expect(RecoverableException.class); @@ -549,7 +549,7 @@ public class ToDoItemIntegTest extends AbstractToDoIntegTest { public void subscriberVetoesEventWithNonRecoverableException() throws Exception { // given - toDoItemSubscriptions.subscriberBehaviour(null, ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithNonRecoverableException); + toDoItemSubscriptions.subscriberBehaviour(ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithNonRecoverableException); // then expectedExceptions.expect(NonRecoverableException.class); @@ -562,7 +562,7 @@ public class ToDoItemIntegTest extends AbstractToDoIntegTest { public void subscriberThrowingOtherExceptionIsIgnored() throws Exception { // given - toDoItemSubscriptions.subscriberBehaviour(null, ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithOtherException); + toDoItemSubscriptions.subscriberBehaviour(ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithOtherException); // when toDoItem.add(otherToDoItem); @@ -634,7 +634,7 @@ public class ToDoItemIntegTest extends AbstractToDoIntegTest { public void subscriberVetoesEventWithRecoverableException() throws Exception { // given - toDoItemSubscriptions.subscriberBehaviour(null, ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithRecoverableException); + toDoItemSubscriptions.subscriberBehaviour(ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithRecoverableException); // then expectedExceptions.expect(RecoverableException.class); @@ -647,7 +647,7 @@ public class ToDoItemIntegTest extends AbstractToDoIntegTest { public void subscriberVetoesEventWithNonRecoverableException() throws Exception { // given - toDoItemSubscriptions.subscriberBehaviour(null, ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithNonRecoverableException); + toDoItemSubscriptions.subscriberBehaviour(ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithNonRecoverableException); // then expectedExceptions.expect(NonRecoverableException.class); @@ -660,7 +660,7 @@ public class ToDoItemIntegTest extends AbstractToDoIntegTest { public void subscriberThrowingOtherExceptionIsIgnored() throws Exception { // given - toDoItemSubscriptions.subscriberBehaviour(null, ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithOtherException); + toDoItemSubscriptions.subscriberBehaviour(ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithOtherException); // when toDoItem.remove(otherToDoItem); @@ -873,7 +873,7 @@ public class ToDoItemIntegTest extends AbstractToDoIntegTest { public void subscriberVetoesEventWithRecoverableException() throws Exception { // given - toDoItemSubscriptions.subscriberBehaviour(null, ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithRecoverableException); + toDoItemSubscriptions.subscriberBehaviour(ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithRecoverableException); // then expectedExceptions.expect(RecoverableException.class); @@ -887,7 +887,7 @@ public class ToDoItemIntegTest extends AbstractToDoIntegTest { public void subscriberVetoesEventWithNonRecoverableException() throws Exception { // given - toDoItemSubscriptions.subscriberBehaviour(null, ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithNonRecoverableException); + toDoItemSubscriptions.subscriberBehaviour(ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithNonRecoverableException); // then expectedExceptions.expect(NonRecoverableException.class); @@ -901,7 +901,7 @@ public class ToDoItemIntegTest extends AbstractToDoIntegTest { public void subscriberThrowingOtherExceptionIsIgnored() throws Exception { // given - toDoItemSubscriptions.subscriberBehaviour(null, ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithOtherException); + toDoItemSubscriptions.subscriberBehaviour(ToDoItemSubscriptions.Behaviour.AnyExecuteVetoWithOtherException); // when toDoItem.setDescription("Buy bread and butter");
