View Javadoc
1   /**
2    * This file Copyright (c) 2017-2018 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.AppDescriptor;
43  import info.magnolia.ui.api.app.AppEventBus;
44  import info.magnolia.ui.api.app.SubAppContext;
45  import info.magnolia.ui.api.app.SubAppDescriptor;
46  import info.magnolia.ui.api.app.SubAppEventBus;
47  import info.magnolia.ui.api.context.UiContext;
48  import info.magnolia.ui.api.event.AdmincentralEventBus;
49  import info.magnolia.ui.api.event.ChooseDialogEventBus;
50  import info.magnolia.ui.api.ioc.AdmincentralScoped;
51  import info.magnolia.ui.api.ioc.AdmincentralScopedEager;
52  import info.magnolia.ui.api.ioc.AppScoped;
53  import info.magnolia.ui.api.ioc.SubAppScoped;
54  import info.magnolia.ui.api.ioc.UiContextScoped;
55  import info.magnolia.ui.api.shell.Shell;
56  import info.magnolia.ui.datasource.WithDatasource;
57  import info.magnolia.ui.datasource.DatasourceDefinition;
58  
59  import java.lang.annotation.Annotation;
60  import java.util.HashMap;
61  import java.util.Map;
62  import java.util.Objects;
63  import java.util.Optional;
64  import java.util.function.Consumer;
65  import java.util.function.Function;
66  import java.util.stream.Stream;
67  
68  import javax.inject.Named;
69  
70  import com.google.inject.AbstractModule;
71  import com.google.inject.Key;
72  import com.google.inject.Provider;
73  import com.google.inject.Provides;
74  import com.google.inject.Scope;
75  import com.google.inject.binder.LinkedBindingBuilder;
76  import com.google.inject.name.Names;
77  import com.vaadin.server.Page;
78  import com.vaadin.server.VaadinServlet;
79  import com.vaadin.server.VaadinSession;
80  import com.vaadin.ui.UI;
81  
82  /**
83   * Guice module which pre-configures some essential Magnolia UI components like
84   * event-buses, UI contexts, Vaadin core components.
85   */
86  public class UiBaseModule extends AbstractModule {
87  
88      private final Map<String, EventBusProxyScope> eventBusScopes = new HashMap<>();
89  
90      private final EventBus systemEventBus;
91  
92      private final UiScopes uiScopes;
93  
94      UiBaseModule(EventBus systemEventBus, UiScopes uiScopes) {
95          this.systemEventBus = systemEventBus;
96          this.uiScopes = uiScopes;
97  
98          eventBusScopes.put(AdmincentralEventBus.NAME, new EventBusProxyScope(this.uiScopes.eagerAdmincentralScope(), systemEventBus, this.uiScopes));
99          eventBusScopes.put(AppEventBus.NAME, new EventBusProxyScope(this.uiScopes.eagerAppScope(), systemEventBus, this.uiScopes));
100         eventBusScopes.put(SubAppEventBus.NAME, new EventBusProxyScope(this.uiScopes.eagerSubAppScope(), systemEventBus, this.uiScopes));
101         eventBusScopes.put(ChooseDialogEventBus.NAME, new EventBusProxyScope(this.uiScopes.eagerViewScope(), systemEventBus, this.uiScopes));
102         // There is a named constant for the media-editor event-bus but it is not accessible from ui-framework module (which seems to be
103         // wrong). In order to make the media editor classes accessible to this Guice module, all the IoC stuff would have to be moved
104         // all the way up to ui-admincentral, which makes very little sense.
105         eventBusScopes.put("mediaeditor", new EventBusProxyScope(this.uiScopes.eagerViewScope(), systemEventBus, this.uiScopes));
106     }
107 
108     public UiBaseModule(EventBus systemEventBus) {
109         this(systemEventBus, new UiScopes(systemEventBus));
110     }
111 
112     /**
113      * Makes the {@link SubAppContext} injectable. An instance of the
114      * context is expected to be present in the corresponding sub-app's
115      * bean storage.
116      */
117     @Provides
118     @SubAppScoped
119     SubAppContext getSubAppCtx(CurrentUiContextReference currentUiContextReference) {
120         Optional<UiContextReference> currentSubAppReference = currentUiContextReference.getSubAppReference();
121         return currentSubAppReference
122                 .map(reference -> SessionStore.access().getBeanStore(reference).get(SubAppContext.class))
123                 .orElseThrow(IllegalStateException::new);
124     }
125 
126     /**
127      * Makes the {@link AppContext} injectable. An instance of the
128      * context is expected to be present in the corresponding app's
129      * bean storage.
130      */
131     @Provides
132     @AppScoped
133     AppContext getAppCtx(CurrentUiContextReference currentUiContextReference) {
134         Optional<UiContextReference> currentAppKey = currentUiContextReference.getAppReference();
135         return currentAppKey
136                 .map(key -> SessionStore.access().getBeanStore(key).get(AppContext.class))
137                 .orElseThrow(IllegalStateException::new);
138     }
139 
140     @Provides
141     @AppScoped
142     AppDescriptor getAppDescriptor(AppContext appContext) {
143         return appContext.getAppDescriptor();
144     }
145 
146     @Provides
147     @SubAppScoped
148     SubAppDescriptor getSubAppDescriptor(SubAppContext subAppContext) {
149         return subAppContext.getSubAppDescriptor();
150     }
151 
152     @Provides
153     @AdmincentralScopedEager
154     CurrentUiContextReference getCurrentUiContextReference() {
155         return SessionStore.access().getBeanStore(UiContextReference.ofCurrentUi()).get(CurrentUiContextReference.class);
156     }
157 
158     @Provides
159     @UiContextScoped
160     DatasourceDefinition getDatasourceDefinition(UiContextReference uiContextReference) {
161         Stream<UiContextReference> reachableUiContexts = Stream.concat(Stream.of(uiContextReference), uiContextReference.getParentReferences().stream());
162         Function<UiContextReference, DatasourceDefinition> locateDatasourceDefinition = ref -> Optional.ofNullable(SessionStore.access().getBeanStore(ref))
163                 .flatMap(beanStore -> {
164                     if (beanStore.hasInstanceOf(DatasourceDefinition.class)) {
165                         return beanStore.getInstance(DatasourceDefinition.class);
166                     } else  {
167                         return beanStore.getInstance(WithDatasource.class).map(WithDatasource::getDatasource);
168                     }
169                 }).orElse(null);
170 
171         return reachableUiContexts
172                 .map(locateDatasourceDefinition)
173                 .filter(Objects::nonNull)
174                 .findFirst()
175                 .orElse(null);
176     }
177 
178     @Provides
179     @AdmincentralScoped
180     SessionStore getSessionStore() {
181         return new SessionStore(VaadinSession.getCurrent(), this.systemEventBus);
182     }
183 
184     /**
185      * Allows the only UI's Guice component provider to be injected.
186      */
187     @Provides
188     GuiceComponentProvider getUiGuiceComponentProvider() {
189         return (GuiceComponentProvider) VaadinServlet.getCurrent().getServletContext().getAttribute("componentProvider");
190     }
191 
192     /**
193      * Provides access to the Guice component provider from 'main'. Should not be needed
194      * in any normal case, but might be useful when the more complex UI bindings
195      * overlap with the bindings from main.
196      */
197     @Provides
198     @Named("main") GuiceComponentProvider getMainGuiceComponentProvider(GuiceComponentProvider guiceComponentProvider) {
199         return guiceComponentProvider.getParent();
200     }
201 
202     @Provides
203     @UiContextScoped
204     ComponentProvider getComponentProvider(GuiceComponentProvider parentComponentProvider, CurrentUiContextReference currentUiContextReference) {
205         return new UiContextBoundComponentProvider(currentUiContextReference.getUiContextReference(), parentComponentProvider, currentUiContextReference);
206     }
207 
208     @Provides
209     @UiContextScoped
210     UiComponentProvider getComponentProvider(ComponentProvider componentProvider) {
211         return new UiComponentProvider/../../../info/magnolia/ui/framework/ioc/UiComponentProvider.html#UiComponentProvider">UiComponentProvider(((UiComponentProvider) componentProvider).getUiContextReference());
212     }
213 
214     @Provides
215     @UiContextScoped
216     UiContextReference getCurrentContextReference(CurrentUiContextReference currentUiContextReference) {
217         return currentUiContextReference.getUiContextReference();
218     }
219 
220     @Override
221     protected void configure() {
222         // Bind scope annotations to Magnolia UI scopes
223         uiScopes.bind(binder());
224 
225         // Bind event buses
226         bindEventBuses();
227 
228         // Bind UiContext to UI scopes
229         bindUiContext();
230 
231         // Make Vaadin UI and VaadinSession instances injectable
232         bind(UI.class).toProvider(UI::getCurrent);
233         bind(VaadinSession.class).toProvider(VaadinSession::getCurrent);
234         bind(Page.class).toProvider(Page::getCurrent);
235 
236         bind(UiScopes.class).toInstance(this.uiScopes);
237     }
238 
239     private void bindUiContext() {
240         final Function<Annotation, LinkedBindingBuilder<UiContext>> uiContextKey = annotation -> bind(UiContext.class).annotatedWith(annotation);
241 
242         uiContextKey.apply(UiAnnotations.forAdmincentral()).to(Shell.class);
243         uiContextKey.apply(UiAnnotations.forM5Admincentral()).to(Shell.class);
244         uiContextKey.apply(UiAnnotations.forApps()).to(AppContext.class);
245         uiContextKey.apply(UiAnnotations.forSubApps()).to(SubAppContext.class);
246         uiContextKey.apply(UiAnnotations.forViews()).toProvider(() ->
247                 CurrentUiContextReference.get().getViewReference()
248                 .map(viewContextKey -> SessionStore.access().getBeanStore(viewContextKey).get(Key.get(UiContext.class, viewContextKey.getAnnotation())))
249                 .orElseThrow(() -> new IllegalStateException("Not in view context, cannot resolve view UiContext")));
250 
251         bind(UiContext.class).toProvider(new UiContextApplyingProvider<>(UiContext.class));
252     }
253 
254     private void bindEventBuses() {
255         // Make event buses injectable. Use EventBusProxyScope to automatically wrap the event buses injected across
256         // scopes with ResettableEventBus
257         final Provider<EventBus> eventBusProvider = () -> new ResettableEventBus(new SimpleEventBus());
258         final Function<String, Key<EventBus>> createEventBusKey = (name) -> Key.get(EventBus.class, Names.named(name));
259 
260         final Consumer<String> bindEventBus = name ->
261                 bind(createEventBusKey.apply(name))
262                 .toProvider(eventBusProvider)
263                 .in(eventBusScopes.get(name));
264 
265         bindEventBus.accept(AdmincentralEventBus.NAME);
266         bindEventBus.accept(AppEventBus.NAME);
267         bindEventBus.accept(SubAppEventBus.NAME);
268         bindEventBus.accept(ChooseDialogEventBus.NAME);
269         bindEventBus.accept("mediaeditor");
270     }
271 
272     /**
273      * Proxies {@link EventBus} injection between UI scopes for the sake of
274      * preventing event handler leakages. This class does what {@link info.magnolia.event.EventBusProtector}
275      * is essentially doing, but without the need to explicitly configuring it and triggering the clean-up.
276      *
277      * <p>
278      * Actual instance of an {@link EventBus} ends up residing in the original
279      * {@link Scope scope} which is specified in c-tor.
280      * </p>
281      * <p>
282      * Whenever there is an injection request corresponding to a different scope
283      * (i.e. the state of the {@link CurrentUiContextReference} points to a scope other than
284      * the 'original' one) the {@link EventBus} instance from the original scope
285      * is wrapped into a {@link ResettableEventBus}.
286      * </p>
287      * <p>
288      * This way, when the current scope will be finalised - it will possible
289      * to remove the event handlers bound to event buses other scopes
290      * all at once (by resetting the {@link ResettableEventBus}).
291      * </p>
292      */
293     @SuppressWarnings("unchecked")
294     private static class EventBusProxyScope extends ProxyScope {
295 
296         EventBusProxyScope(SessionStoreScope originalScope, EventBus systemEventBus, UiScopes allUiScopes) {
297             super(originalScope, allUiScopes);
298             systemEventBus.addHandler(BeanStoreLifecycleEvent.BeforeDestroy.class, event -> this.releaseEventHandlers(event.getRelatedContextKey(), event.getStore()));
299         }
300 
301         // When a bean store related to this scope is destroyed - reset all the resettable
302         // event buses from that store
303         private void releaseEventHandlers(UiContextReference relatedContextReference, BeanStore store) {
304             // First - resolve the scope corresponding to the bean store that is being destroyed
305             final Scope destroyedScope = getAllUiScopes().getScope(relatedContextReference.getAnnotation().getRelatedScopeAnnotation(true));
306             // If that scope is the one that is wrapped - take all the resettable event buses stored in that bean store and reset them,
307             // which will effectively remove the handlers related to the UI context that is currently being destroyed
308             // (see class JavaDoc which explains how the event buses get wrapped).
309             if (Objects.equals(getOriginalScope(), destroyedScope)) {
310                 store.forEach((key, value) -> {
311                     if (value instanceof ResettableEventBus) {
312                         ((ResettableEventBus) value).reset();
313                     }
314                 });
315             }
316         }
317 
318         @Override
319         protected <T> Provider<T> proxyScope(Key<T> key, Provider<T> scopedProvider, SessionStoreScope currentScope) {
320             if (key.getTypeLiteral().getRawType().isAssignableFrom(EventBus.class)) {
321                 return () -> {
322                     final BeanStore relatedStore = currentScope.getStore();
323                     EventBus protectedEventBus = relatedStore.get((Key<EventBus>) key);
324                     if (protectedEventBus == null) {
325                         protectedEventBus = new ResettableEventBus((EventBus) scopedProvider.get());
326                         relatedStore.put(key, protectedEventBus);
327                     }
328                     return (T) protectedEventBus;
329                     
330                 };
331             }
332 
333             throw new IllegalArgumentException("Can only proxy EventBus objects, do not know what to do with [" + key + "]");
334         }
335     }
336 }