/** * 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.applib.services.eventbus; import java.util.Collections; import java.util.Map; import java.util.Set; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import com.google.common.collect.Sets; import org.apache.isis.applib.annotation.Programmatic; /** * A service implementing an Event Bus, allowing arbitrary events to be posted and * subscribed to. * * <p> * 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> */ public abstract class EventBusService { /** * A no-op implementation to use as a default for domain objects that are being * instantiated and for which the event bus service has not yet been injected. */ public static class Noop extends EventBusService { @Override public void register(Object domainService) {} @Override public void unregister(Object domainService) {} @Override public void post(Object event) {} @Override protected EventBusImplementation getEventBusImplementation() { return null; } @Override protected EventBusImplementation newEventBus() { return null; } } public static final EventBusService NOOP = new Noop(); //region > init, shutdown /** * 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 allow all services (singleton or request-scoped) to {@link #register(Object) register} themselves * with this service 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 #getEventBusImplementation()}. * This lifecycle method ({@link #init(Map)}) 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 #shutdown()} lifecycle method. * </p> */ @Programmatic @PostConstruct public void init(final Map<String, String> properties) { // no-op } @Programmatic @PreDestroy public void shutdown() { teardownEventBus(); } //endregion //region > register, unregister /** * Both singleton and request-scoped domain services can register on the event bus; this should be done in their * <code>@PostConstruct</code> callback method. * * <p> * <b>Important:</b> Request-scoped services should register their proxy, not themselves. This is because it is * the responsibility of the proxy to ensure that the correct underlying (thread-local) instance of the service * is delegated to. If the actual instance were to be registered, this would cause a memory leak and all sorts * of other unexpected issues. * </p> * * <p> * Also, request-scoped services should <i>NOT</i> unregister themselves. This is because the * <code>@PreDestroy</code> lifecycle method is called at the end of each transaction. The proxy needs to * remain registered on behalf for any subsequent transactions. * </p> * * <p>For example:</p> * <pre> * @RequestScoped @DomainService * public class SomeSubscribingService { * * @javax.inject.Inject private EventBusService ebs; * @javax.inject.Inject private SomeSubscribingService proxy; * * @PostConstruct * public void startRequest() { * // register with bus * ebs.register(proxy); * } * @PreDestroy * public void endRequest() { * //no-op * } * } * </pre> * * <p> * The <code>@PostConstruct</code> callback is the correct place to register for both singleton and * request-scoped services. For singleton domain services, this is done during the initial bootstrapping of * the system. For request-scoped services, this is done for the first transaction. In fact, because * singleton domain services are initialized <i>within a current transaction</i>, the request-scoped services * will actually be registered <i>before</i> the singleton services. Each subsequent transaction will have the * request-scoped service re-register with the event bus, however the event bus stores its subscribers in a * set and so these re-registrations are basically a no-op. * </p> * * @param domainService */ @Programmatic public void register(final Object domainService) { doRegister(domainService); } /** * Extracted out only to make it easier for subclasses to override {@link #register(Object)} if there were ever a * need to. */ protected void doRegister(Object domainService) { if(eventBusImplementation == null) { subscribers.add(domainService); } else { eventBusImplementation.register(domainService); } } /** * Notionally allows subscribers to unregister from the event bus; however this is a no-op. * * <p> * It is safe for singleton services to unregister from the bus, however this is only ever called when the * app is being shutdown so there is no real effect. For request-scoped services meanwhile that (as * explained in {@link #register(Object)}'s documentation) actually register their proxy, it would be an error * to unregister the proxy; subsequent transactions (for this thread or others) must be routed through that * proxy. * </p> */ @Programmatic public void unregister(final Object domainService) { // intentionally no-op } //endregion //region > subscribers private final Set<Object> subscribers = Sets.newConcurrentHashSet(); /** * Returns an immutable snapshot of the current subscribers. */ @Programmatic public Set<Object> getSubscribers() { return Collections.unmodifiableSet(Sets.newLinkedHashSet(subscribers)); } //endregion //region > post /** * Post an event. */ @Programmatic public void post(Object event) { if(skip(event)) { return; } getEventBusImplementation().post(event); } protected boolean hasPosted() { return this.eventBusImplementation != null; } //endregion //region > getEventBus /** * Lazily populated in {@link #getEventBusImplementation()} as result of the first {@link #post(Object)}. */ protected EventBusImplementation eventBusImplementation; /** * Lazily populates the event bus for the current {@link #getSubscribers() subscribers}. */ @Programmatic protected EventBusImplementation getEventBusImplementation() { setupEventBus(); return eventBusImplementation; } /** * Set of subscribers registered with the event bus. * * <p> * Lazily populated in {@link #setupEventBus()}. * </p> */ private Set<Object> registeredSubscribers; /** * Populates {@link #eventBusImplementation} with the {@link #registeredSubscribers currently registered 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 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> * * <p> * That said, the Guava event bus is only ever instantiated once (it is in essence an application-scoped singleton), * and so once created it is not possible for any subscribers to be registered. For this reason, the * {@link #register(Object)} will throw an exception if any attempt is made to register once the event bus * has been instantiated. * </p> */ protected void setupEventBus() { if(eventBusImplementation != null) { return; } this.eventBusImplementation = newEventBus(); registeredSubscribers = getSubscribers(); for (Object subscriber : this.registeredSubscribers) { eventBusImplementation.register(subscriber); } } protected void teardownEventBus() { if(registeredSubscribers != null) { for (Object subscriber : this.registeredSubscribers) { eventBusImplementation.unregister(subscriber); } } this.eventBusImplementation = null; } //endregion //region > hook methods (newEventBus, skip) /** * Mandatory hook method for subclass to instantiate an appropriately configured Guava event bus. */ protected abstract EventBusImplementation newEventBus(); /** * A hook to allow subclass implementations to skip the publication of certain events. * * <p> * For example, the <tt>EventBusServiceJdo</tt> does not publish events if the method * is called by JDO/DataNucleus infrastructure, eg during hydration or commits. */ protected boolean skip(Object event) { return false; } //endregion }