View Javadoc
1   /**
2    * This file Copyright (c) 2017 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.ui.framework.ioc;
35  
36  import info.magnolia.event.EventBus;
37  import info.magnolia.event.ResettableEventBus;
38  import info.magnolia.event.SimpleEventBus;
39  import info.magnolia.objectfactory.ComponentProvider;
40  import info.magnolia.objectfactory.guice.GuiceComponentProvider;
41  import info.magnolia.ui.api.app.AppContext;
42  import info.magnolia.ui.api.app.AppEventBus;
43  import info.magnolia.ui.api.app.SubAppContext;
44  import info.magnolia.ui.api.app.SubAppEventBus;
45  import info.magnolia.ui.api.context.UiContext;
46  import info.magnolia.ui.api.event.AdmincentralEventBus;
47  import info.magnolia.ui.api.event.ChooseDialogEventBus;
48  import info.magnolia.ui.api.ioc.AdmincentralScoped;
49  import info.magnolia.ui.api.ioc.AdmincentralScopedEager;
50  import info.magnolia.ui.api.ioc.AppScoped;
51  import info.magnolia.ui.api.ioc.SubAppScoped;
52  import info.magnolia.ui.api.ioc.UiContextScoped;
53  import info.magnolia.ui.api.shell.Shell;
54  
55  import java.lang.annotation.Annotation;
56  import java.util.HashMap;
57  import java.util.Map;
58  import java.util.Objects;
59  import java.util.Optional;
60  import java.util.function.Consumer;
61  import java.util.function.Function;
62  
63  import javax.inject.Named;
64  
65  import com.google.inject.AbstractModule;
66  import com.google.inject.Key;
67  import com.google.inject.Provider;
68  import com.google.inject.Provides;
69  import com.google.inject.Scope;
70  import com.google.inject.binder.LinkedBindingBuilder;
71  import com.google.inject.name.Names;
72  import com.vaadin.server.Page;
73  import com.vaadin.server.VaadinServlet;
74  import com.vaadin.server.VaadinSession;
75  import com.vaadin.ui.UI;
76  
77  /**
78   * Guice module which pre-configures some essential Magnolia UI components like
79   * event-buses, UI contexts, Vaadin core components.
80   */
81  public class UiBaseModule extends AbstractModule {
82  
83      private final Map<String, EventBusProxyScope> eventBusScopes = new HashMap<>();
84  
85      private final EventBus systemEventBus;
86  
87      private final UiScopes uiScopes;
88  
89      UiBaseModule(EventBus systemEventBus, UiScopes uiScopes) {
90          this.systemEventBus = systemEventBus;
91          this.uiScopes = uiScopes;
92  
93          eventBusScopes.put(AdmincentralEventBus.NAME, new EventBusProxyScope(this.uiScopes.eagerAdmincentralScope(), systemEventBus, this.uiScopes));
94          eventBusScopes.put(AppEventBus.NAME, new EventBusProxyScope(this.uiScopes.eagerAppScope(), systemEventBus, this.uiScopes));
95          eventBusScopes.put(SubAppEventBus.NAME, new EventBusProxyScope(this.uiScopes.eagerSubAppScope(), systemEventBus, this.uiScopes));
96          eventBusScopes.put(ChooseDialogEventBus.NAME, new EventBusProxyScope(this.uiScopes.eagerViewScope(), systemEventBus, this.uiScopes));
97          // There is a named constant for the media-editor event-bus but it is not accessible from ui-framework module (which seems to be
98          // wrong). In order to make the media editor classes accessible to this Guice module, all the IoC stuff would have to be moved
99          // all the way up to ui-admincentral, which makes very little sense.
100         eventBusScopes.put("mediaeditor", new EventBusProxyScope(this.uiScopes.eagerViewScope(), systemEventBus, this.uiScopes));
101     }
102 
103     public UiBaseModule(EventBus systemEventBus) {
104         this(systemEventBus, new UiScopes(systemEventBus));
105     }
106 
107     /**
108      * Makes the {@link SubAppContext} injectable. An instance of the
109      * context is expected to be present in the corresponding sub-app's
110      * bean storage.
111      */
112     @Provides
113     @SubAppScoped
114     SubAppContext getSubAppCtx(CurrentUiContextReference currentUiContextReference) {
115         Optional<UiContextReference> currentSubAppReference = currentUiContextReference.getSubAppReference();
116         return currentSubAppReference
117                 .map(reference -> SessionStore.access().getBeanStore(reference).get(SubAppContext.class))
118                 .orElseThrow(IllegalStateException::new);
119     }
120 
121     /**
122      * Makes the {@link AppContext} injectable. An instance of the
123      * context is expected to be present in the corresponding app's
124      * bean storage.
125      */
126     @Provides
127     @AppScoped
128     AppContext getAppCtx(CurrentUiContextReference currentUiContextReference) {
129         Optional<UiContextReference> currentAppKey = currentUiContextReference.getAppReference();
130         return currentAppKey
131                 .map(key -> SessionStore.access().getBeanStore(key).get(AppContext.class))
132                 .orElseThrow(IllegalStateException::new);
133     }
134 
135     @Provides
136     @AdmincentralScopedEager
137     CurrentUiContextReference getCurrentUiContextReference() {
138         return SessionStore.access().getBeanStore(UiContextReference.ofCurrentUi()).get(CurrentUiContextReference.class);
139     }
140 
141     @Provides
142     @AdmincentralScoped
143     SessionStore getSessionStore() {
144         return new SessionStore(VaadinSession.getCurrent(), this.systemEventBus);
145     }
146 
147     /**
148      * Allows the only UI's Guice component provider to be injected.
149      */
150     @Provides
151     GuiceComponentProvider getUiGuiceComponentProvider() {
152         return (GuiceComponentProvider) VaadinServlet.getCurrent().getServletContext().getAttribute("componentProvider");
153     }
154 
155     /**
156      * Provides access to the Guice component provider from 'main'. Should not be needed
157      * in any normal case, but might be useful when the more complex UI bindings
158      * overlap with the bindings from main.
159      */
160     @Provides
161     @Named("main") GuiceComponentProvider getMainGuiceComponentProvider(GuiceComponentProvider guiceComponentProvider) {
162         return guiceComponentProvider.getParent();
163     }
164 
165     @Provides
166     @UiContextScoped
167     ComponentProvider getComponentProvider(GuiceComponentProvider parentComponentProvider, CurrentUiContextReference currentUiContextReference) {
168         return new UiContextBoundComponentProvider(currentUiContextReference.getUiContextReference(), parentComponentProvider, currentUiContextReference);
169     }
170 
171     @Override
172     protected void configure() {
173         // Bind scope annotations to Magnolia UI scopes
174         uiScopes.bind(binder());
175 
176         // Bind event buses
177         bindEventBuses();
178 
179         // Bind UiContext to UI scopes
180         bindUiContext();
181 
182         // Make Vaadin UI and VaadinSession instances injectable
183         bind(UI.class).toProvider(UI::getCurrent);
184         bind(VaadinSession.class).toProvider(VaadinSession::getCurrent);
185         bind(Page.class).toProvider(Page::getCurrent);
186 
187         bind(UiScopes.class).toInstance(this.uiScopes);
188     }
189 
190     private void bindUiContext() {
191         final Function<Annotation, LinkedBindingBuilder<UiContext>> uiContextKey = annotation -> bind(UiContext.class).annotatedWith(annotation);
192 
193         uiContextKey.apply(UiAnnotations.forAdmincentral()).to(Shell.class);
194         uiContextKey.apply(UiAnnotations.forApps()).to(AppContext.class);
195         uiContextKey.apply(UiAnnotations.forSubApps()).to(SubAppContext.class);
196         uiContextKey.apply(UiAnnotations.forViews()).toProvider(() ->
197                 CurrentUiContextReference.get().getViewReference()
198                 .map(viewContextKey -> SessionStore.access().getBeanStore(viewContextKey).get(Key.get(UiContext.class, viewContextKey.getAnnotation())))
199                 .orElseThrow(() -> new IllegalStateException("Not in view context, cannot resolve view UiContext")));
200 
201         bind(UiContext.class).toProvider(new UiContextApplyingProvider<>(UiContext.class));
202     }
203 
204     private void bindEventBuses() {
205         // Make event buses injectable. Use EventBusProxyScope to automatically wrap the event buses injected across
206         // scopes with ResettableEventBus
207         final Provider<EventBus> eventBusProvider = () -> new ResettableEventBus(new SimpleEventBus());
208         final Function<String, Key<EventBus>> createEventBusKey = (name) -> Key.get(EventBus.class, Names.named(name));
209 
210         final Consumer<String> bindEventBus = name ->
211                 bind(createEventBusKey.apply(name))
212                 .toProvider(eventBusProvider)
213                 .in(eventBusScopes.get(name));
214 
215         bindEventBus.accept(AdmincentralEventBus.NAME);
216         bindEventBus.accept(AppEventBus.NAME);
217         bindEventBus.accept(SubAppEventBus.NAME);
218         bindEventBus.accept(ChooseDialogEventBus.NAME);
219         bindEventBus.accept("mediaeditor");
220     }
221 
222     /**
223      * Proxies {@link EventBus} injection between UI scopes for the sake of
224      * preventing event handler leakages. This class does what {@link info.magnolia.event.EventBusProtector}
225      * is essentially doing, but without the need to explicitly configuring it and triggering the clean-up.
226      *
227      * <p>
228      * Actual instance of an {@link EventBus} ends up residing in the original
229      * {@link Scope scope} which is specified in c-tor.
230      * </p>
231      * <p>
232      * Whenever there is an injection request corresponding to a different scope
233      * (i.e. the state of the {@link CurrentUiContextReference} points to a scope other than
234      * the 'original' one) the {@link EventBus} instance from the original scope
235      * is wrapped into a {@link ResettableEventBus}.
236      * </p>
237      * <p>
238      * This way, when the current scope will be finalised - it will possible
239      * to remove the event handlers bound to event buses other scopes
240      * all at once (by resetting the {@link ResettableEventBus}).
241      * </p>
242      */
243     @SuppressWarnings("unchecked")
244     private static class EventBusProxyScope extends ProxyScope {
245 
246         EventBusProxyScope(SessionStoreScope originalScope, EventBus systemEventBus, UiScopes allUiScopes) {
247             super(originalScope, allUiScopes);
248             systemEventBus.addHandler(BeanStoreLifecycleEvent.BeforeDestroy.class, event -> this.releaseEventHandlers(event.getRelatedContextKey(), event.getStore()));
249         }
250 
251         // When a bean store related to this scope is destroyed - reset all the resettable
252         // event buses from that store
253         private void releaseEventHandlers(UiContextReference relatedContextReference, BeanStore store) {
254             // First - resolve the scope corresponding to the bean store that is being destroyed
255             final Scope destroyedScope = getAllUiScopes().getScope(relatedContextReference.getAnnotation().getRelatedScopeAnnotation(true));
256             // If that scope is the one that is wrapped - take all the resettable event buses stored in that bean store and reset them,
257             // which will effectively remove the handlers related to the UI context that is currently being destroyed
258             // (see class JavaDoc which explains how the event buses get wrapped).
259             if (Objects.equals(getOriginalScope(), destroyedScope)) {
260                 store.forEach((key, value) -> {
261                     if (value instanceof ResettableEventBus) {
262                         ((ResettableEventBus) value).reset();
263                     }
264                 });
265             }
266         }
267 
268         @Override
269         protected <T> Provider<T> proxyScope(Key<T> key, Provider<T> scopedProvider, SessionStoreScope currentScope) {
270             if (key.getTypeLiteral().getRawType().isAssignableFrom(EventBus.class)) {
271                 return () -> {
272                     final BeanStore relatedStore = currentScope.getStore();
273                     EventBus protectedEventBus = relatedStore.get((Key<EventBus>) key);
274                     if (protectedEventBus == null) {
275                         protectedEventBus = new ResettableEventBus((EventBus) scopedProvider.get());
276                         relatedStore.put(key, protectedEventBus);
277                     }
278                     return (T) protectedEventBus;
279                     
280                 };
281             }
282 
283             throw new IllegalArgumentException("Can only proxy EventBus objects, do not know what to do with [" + key + "]");
284         }
285     }
286 }