View Javadoc
1   /**
2    * This file Copyright (c) 2012-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.app;
35  
36  import info.magnolia.context.Context;
37  import info.magnolia.context.MgnlContext;
38  import info.magnolia.event.EventBus;
39  import info.magnolia.i18nsystem.I18nizer;
40  import info.magnolia.i18nsystem.SimpleTranslator;
41  import info.magnolia.init.MagnoliaConfigurationProperties;
42  import info.magnolia.monitoring.SystemMonitor;
43  import info.magnolia.objectfactory.ComponentProvider;
44  import info.magnolia.objectfactory.Components;
45  import info.magnolia.ui.api.app.App;
46  import info.magnolia.ui.api.app.AppContext;
47  import info.magnolia.ui.api.app.AppController;
48  import info.magnolia.ui.api.app.AppDescriptor;
49  import info.magnolia.ui.api.app.AppInstanceController;
50  import info.magnolia.ui.api.app.AppView;
51  import info.magnolia.ui.api.app.SubApp;
52  import info.magnolia.ui.api.app.SubAppContext;
53  import info.magnolia.ui.api.app.SubAppDescriptor;
54  import info.magnolia.ui.api.app.SubAppEventBus;
55  import info.magnolia.ui.api.app.SubAppLifecycleEvent;
56  import info.magnolia.ui.api.location.DefaultLocation;
57  import info.magnolia.ui.api.location.Location;
58  import info.magnolia.ui.api.location.LocationController;
59  import info.magnolia.ui.api.message.Message;
60  import info.magnolia.ui.api.overlay.OverlayLayer;
61  import info.magnolia.ui.api.shell.Shell;
62  import info.magnolia.ui.api.view.View;
63  import info.magnolia.ui.framework.app.stub.FailedAppStub;
64  import info.magnolia.ui.framework.app.stub.FailedSubAppStub;
65  import info.magnolia.ui.framework.ioc.BeanStore;
66  import info.magnolia.ui.framework.ioc.SessionStore;
67  import info.magnolia.ui.framework.ioc.UiContextBoundComponentProvider;
68  import info.magnolia.ui.framework.ioc.UiContextReference;
69  import info.magnolia.ui.framework.message.MessagesManager;
70  import info.magnolia.ui.vaadin.overlay.MessageStyleTypeEnum;
71  import info.magnolia.ui.vaadin.tabsheet.MagnoliaTab;
72  
73  import java.util.Arrays;
74  import java.util.Collection;
75  import java.util.HashMap;
76  import java.util.Map;
77  import java.util.Objects;
78  import java.util.Optional;
79  import java.util.concurrent.ConcurrentHashMap;
80  import java.util.stream.Collectors;
81  import java.util.stream.Stream;
82  
83  import javax.inject.Inject;
84  import javax.inject.Provider;
85  
86  import org.apache.commons.lang3.StringUtils;
87  import org.slf4j.Logger;
88  import org.slf4j.LoggerFactory;
89  
90  import com.google.inject.Key;
91  import com.google.inject.name.Names;
92  import com.vaadin.ui.Component;
93  import com.vaadin.ui.TabSheet;
94  import com.vaadin.ui.UI;
95  
96  /**
97   * Implements both - the controlling of an app instance as well as the housekeeping of the context for an app.
98   */
99  public class AppInstanceControllerImpl implements AppContext, AppInstanceController {
100 
101     private static final Logger log = LoggerFactory.getLogger(AppInstanceControllerImpl.class);
102 
103     private static class SubAppDetails {
104         private SubAppContext context;
105         private ComponentProvider componentProvider;
106     }
107 
108     private Map<String, SubAppDetails> subApps = new ConcurrentHashMap<>();
109 
110     private AppController appController;
111 
112     private LocationController locationController;
113 
114     private Shell shell;
115 
116     private MessagesManager messagesManager;
117 
118     private ComponentProvider componentProvider;
119 
120     private App app;
121 
122     private AppDescriptor appDescriptor;
123 
124     private SubAppContext currentSubAppContext;
125 
126     private final SystemMonitor systemMonitor;
127 
128     private final SimpleTranslator i18n;
129 
130     private final Provider<Context> contextProvider;
131 
132     @Inject
133     public AppInstanceControllerImpl(AppController appController, LocationController locationController,
134                               Shell shell, MessagesManager messagesManager, AppDescriptor appDescriptor,
135                               SystemMonitor systemMonitor, I18nizer i18nizer, SimpleTranslator i18n,
136                               Provider<Context> contextProvider
137     ) {
138         this.appController = appController;
139         this.locationController = locationController;
140         this.shell = shell;
141         this.messagesManager = messagesManager;
142         this.contextProvider = contextProvider;
143         this.appDescriptor = i18nizer.decorate(appDescriptor);
144         this.systemMonitor = systemMonitor;
145         this.i18n = i18n;
146     }
147 
148     /**
149      * @deprecated since 6.2 - use {@link #AppInstanceControllerImpl(info.magnolia.ui.api.app.AppController, info.magnolia.ui.api.location.LocationController, info.magnolia.ui.api.shell.Shell, info.magnolia.ui.framework.message.MessagesManager, info.magnolia.ui.api.app.AppDescriptor, info.magnolia.monitoring.SystemMonitor, info.magnolia.i18nsystem.I18nizer, info.magnolia.i18nsystem.SimpleTranslator, javax.inject.Provider)} instead.
150      */
151     @Deprecated
152     public AppInstanceControllerImpl(AppController appController, LocationController locationController,
153                                      Shell shell, MessagesManager messagesManager, AppDescriptor appDescriptor,
154                                      SystemMonitor systemMonitor, I18nizer i18nizer, SimpleTranslator i18n) {
155         this(appController, locationController, shell, messagesManager, appDescriptor, systemMonitor, i18nizer, i18n, MgnlContext::getInstance);
156     }
157 
158     @Override
159     public void setAppComponentProvider(ComponentProvider componentProvider) {
160         this.componentProvider = componentProvider;
161     }
162 
163     @Override
164     public void setApp(App app) {
165         this.app = app;
166     }
167 
168     @Override
169     public App getApp() {
170         return app;
171     }
172 
173     @Override
174     public String getName() {
175         return appDescriptor.getName();
176     }
177 
178     @Override
179     public String getLabel() {
180         return appDescriptor.getLabel();
181     }
182 
183     @Override
184     public AppDescriptor getAppDescriptor() {
185         return appDescriptor;
186     }
187 
188     @Override
189     public SubAppDescriptor getDefaultSubAppDescriptor() {
190         Collection<SubAppDescriptor> subAppDescriptors = getAppDescriptor().getSubApps().values();
191         return subAppDescriptors.isEmpty() ? null : subAppDescriptors.iterator().next();
192     }
193 
194     private SubAppDescriptor getSubAppDescriptorById(String subAppId) {
195         Map<String, SubAppDescriptor> subAppDescriptors = getAppDescriptor().getSubApps();
196         return subAppDescriptors.get(subAppId);
197     }
198 
199     @Override
200     public AppView getView() {
201         return app.getView();
202     }
203 
204     /**
205      * Called when the app is launched from the app launcher OR a location change event triggers
206      * it to start.
207      */
208     @Override
209     public void start(Location location) {
210         if (systemMonitor.isMemoryLimitReached()) {
211             String memoryMessageCloseApps = i18n.translate("ui-framework.appInstanceController.memoryLimitWarningMessage.closeApps");
212             shell.openNotification(MessageStyleTypeEnum.WARNING, false, i18n.translate("ui-framework.memoryLimitWarningMessage.template", memoryMessageCloseApps));
213         }
214 
215         try {
216             app = componentProvider.newInstance(appDescriptor.getAppClass());
217             if (getAppDescriptor().isHtmlCaption()) {
218                 app.getView().setTabCaptionsAsHtml();
219             }
220             app.start(location);
221             if ((getDefaultLocation() == null && location != null) || (location != null && !getDefaultLocation().getSubAppId().equals(location.getSubAppId()))) {
222                 // focus sub-app preselected by location
223                 onLocationUpdate(location);
224             }
225             if (StringUtils.isNotBlank(appDescriptor.getTheme())) {
226                 app.getView().setTheme(appDescriptor.getTheme());
227             }
228         } catch (final Exception e) {
229             log.error("App {} failed to start: {}", appDescriptor.getName(), e.getMessage(), e);
230             app = componentProvider.newInstance(FailedAppStub.class, this, e);
231             app.start(location);
232         }
233 
234         // Set app icon
235         if (StringUtils.isNotBlank(appDescriptor.getIcon())) {
236             app.getView().setAppLogo(appDescriptor.getIcon());
237         }
238     }
239 
240     /**
241      * Called when a location change occurs and the app is already running.
242      */
243     @Override
244     public void onLocationUpdate(Location location) {
245         app.locationChanged(location);
246         if (getActiveSubAppContext() != null) {
247             String instanceId = getActiveSubAppContext().getInstanceId();
248             // FIXME old tab icon handling does not belong here
249             Component activeTab = app.getView().getSubAppViewContainer(instanceId).asVaadinComponent();
250             if (activeTab instanceof MagnoliaTab) {
251                 ((MagnoliaTab) activeTab).setIcon(getActiveSubAppContext().getSubApp().getIcon(location));
252             }
253         }
254     }
255 
256     @Override
257     public void onFocus(String instanceId) {
258         if (subApps.containsKey(instanceId)) {
259             /**
260              * If some other sub-app is in focus at the moment - fire a blur event for it.
261              */
262             if (currentSubAppContext != null) {
263                 sendSubAppLifecycleEvent(currentSubAppContext, SubAppLifecycleEvent.Type.BLURRED);
264             }
265             SubAppContext subAppContext = subApps.get(instanceId).context;
266             locationController.goTo(subAppContext.getLocation());
267             sendSubAppLifecycleEvent(subAppContext, SubAppLifecycleEvent.Type.FOCUSED);
268         }
269     }
270 
271     private void sendSubAppLifecycleEvent(SubAppContext ctx, SubAppLifecycleEvent.Type type) {
272         Optional.ofNullable(getSubAppEventBus(ctx)).ifPresent(seb -> seb.fireEvent(new SubAppLifecycleEvent(ctx.getSubAppDescriptor(), type)));
273     }
274 
275     private EventBus getSubAppEventBus(SubAppContext subAppContext) {
276         final BeanStore beanStore = SessionStore.access().getBeanStore(UiContextReference.ofSubApp(subAppContext));
277         return beanStore == null ? null : beanStore.get(Key.get(EventBus.class, Names.named(SubAppEventBus.NAME)));
278     }
279 
280     @Override
281     public void onClose(String instanceId) {
282         stopSubAppInstance(instanceId);
283         onFocus(app.getView().getActiveSubAppView());
284     }
285 
286     @Override
287     public String mayStop() {
288         return null;
289     }
290 
291     @Override
292     public void stop() {
293         for (String instanceId : subApps.keySet()) {
294             stopSubAppInstance(instanceId);
295         }
296 
297         currentSubAppContext = null;
298         if (app != null) {
299             app.stop();
300         } else {
301             log.warn("Cannot call stop on app that's already set to null. Name: {}, Thread: {}", getName(), Thread.currentThread().getName());
302         }
303 
304         SessionStore.access().releaseBeanStore(UiContextReference.ofApp(this));
305     }
306 
307     private void stopSubAppInstance(String instanceId) {
308         SubAppDetails subAppDetails = subApps.get(instanceId);
309 
310         /**
311          * Notify listeners of sub-app stopping before breaking it down.
312          */
313         sendSubAppLifecycleEvent(subAppDetails.context, SubAppLifecycleEvent.Type.STOPPED);
314 
315         subAppDetails.context.getSubApp().stop();
316         SessionStore.access().releaseBeanStore(UiContextReference.ofSubApp(subAppDetails.context));
317 
318         subApps.remove(instanceId);
319     }
320 
321     @Override
322     public Location getCurrentLocation() {
323         SubAppContext subAppContext = getActiveSubAppContext();
324         if (subAppContext != null) {
325             return subAppContext.getLocation();
326         }
327         return new DefaultLocation(Location.LOCATION_TYPE_APP, appDescriptor.getName());
328     }
329 
330     @Override
331     public Location getDefaultLocation() {
332         SubAppDescriptor subAppDescriptor = getDefaultSubAppDescriptor();
333         if (subAppDescriptor != null) {
334             return new DefaultLocation(Location.LOCATION_TYPE_APP, appDescriptor.getName(), subAppDescriptor.getName());
335         } else {
336             return null;
337         }
338     }
339 
340     @Override
341     public void openSubApp(Location location) {
342         // main sub app has always to be there - open it if not yet running
343         final Location defaultLocation = getDefaultLocation();
344         boolean isDefaultSubApp = false;
345 
346         if (defaultLocation != null) {
347             isDefaultSubApp = defaultLocation.getSubAppId().equals(location.getSubAppId());
348             if (!isDefaultSubApp) {
349                 SubAppContext subAppContext = getSupportingSubAppContext(defaultLocation);
350                 if (subAppContext == null) {
351                     startSubApp(defaultLocation, false);
352                 }
353             }
354         }
355 
356         // If the location targets an existing sub app then activate it and update its location
357         // launch running subapp
358         SubAppContext subAppContext = getSupportingSubAppContext(location);
359         if (subAppContext != null) {
360             subAppContext.getSubApp().locationChanged(location);
361             subAppContext.setLocation(location);
362             // update the caption
363             getView().updateCaption(subAppContext.getInstanceId(), subAppContext.getSubApp().getCaption());
364             // set active subApp view if it isn't already active
365             if (!subAppContext.getInstanceId().equals(app.getView().getActiveSubAppView())) {
366                 app.getView().setActiveSubAppView(subAppContext.getInstanceId());
367             }
368         } else {
369             subAppContext = startSubApp(location, !isDefaultSubApp);
370         }
371         currentSubAppContext = subAppContext;
372     }
373 
374     /**
375      * Used to close a running subApp from server side. Delegates to {@link AppView#closeSubAppView(String)}.
376      * The actual closing and cleaning up, will be handled by the callback {@link AppView.Listener#onClose(String)} implemented in {@link #onClose(String)}.
377      */
378     @Override
379     public void closeSubApp(String instanceId) {
380         getView().closeSubAppView(instanceId);
381         onClose(instanceId);
382     }
383 
384     @Override
385     public Collection<SubAppContext> getSubAppContexts() {
386         return subApps.values().stream().map(subAppDetails -> subAppDetails.context).collect(Collectors.toList());
387     }
388 
389     private SubAppContext startSubApp(Location location, boolean allowClose) {
390 
391         SubAppDescriptor subAppDescriptor = getSubAppDescriptorById(location.getSubAppId());
392 
393         if (subAppDescriptor == null) {
394             subAppDescriptor = getDefaultSubAppDescriptor();
395             if (subAppDescriptor == null) {
396                 log.warn("No subapp could be found for the '{}' app, please check configuration.", appDescriptor.getName());
397                 return null;
398             }
399         }
400         SubAppContext subAppContext = componentProvider.newInstance(SubAppContext.class, subAppDescriptor, shell);
401 
402         subAppContext.setAppContext(this);
403         subAppContext.setLocation(location);
404         subAppContext.setAuthoringLocale(contextProvider.get().getLocale());
405 
406         SubAppDetails subAppDetails = createSubAppComponentProvider(subAppContext);
407         subAppDetails.context = subAppContext;
408 
409         if (this.currentSubAppContext == null) {
410             this.currentSubAppContext = subAppContext;
411         }
412 
413         Class<? extends SubApp> subAppClass = subAppDescriptor.getSubAppClass();
414         if (subAppClass == null) {
415             log.warn("Sub App {} doesn't define its sub app class or class doesn't exist or can't be instantiated.", subAppDescriptor.getName());
416         } else {
417             SubApp subApp;
418             View subAppView;
419             boolean closable = true;
420             try {
421                 subApp = subAppDetails.componentProvider.newInstance(subAppClass);
422                 subAppContext.setSubApp(subApp);
423                 subAppView = subApp.start(location);
424                 closable = allowClose && subApp.isCloseable();
425             } catch (Exception e) {
426                 String instanceId = subAppDescriptor.getName();
427                 log.error("Sub-app [{}] of app [{}] failed to start", instanceId, appDescriptor.getName(), e);
428                 getView().closeSubAppView(instanceId);
429                 subApps.remove(instanceId);
430 
431                 subApp = subAppDetails.componentProvider.newInstance(FailedSubAppStub.class, e);
432                 subAppView = subApp.start(location);
433                 subAppContext.setSubApp(subApp);
434             }
435 
436             String instanceId = app.getView().addSubAppView(subAppView, subApp.getCaption(), subApp.getIcon(location), closable);
437 
438             subAppContext.setInstanceId(instanceId);
439 
440             subApps.put(instanceId, subAppDetails);
441 
442             sendSubAppLifecycleEvent(subAppContext, SubAppLifecycleEvent.Type.STARTED);
443         }
444         return subAppContext;
445     }
446 
447     /**
448      * Used to update the framework about changes to locations inside the app and circumventing the {@link info.magnolia.ui.api.location.LocationController} mechanism.
449      * Example Usages:
450      *
451      * <pre>
452      *     <ul>
453      *         <li>Inside ContentApp framework to update {@link info.magnolia.ui.api.app.SubAppContext#getLocation()} and the {@link Shell} fragment</li>
454      *         <li>In the Pages App when navigating pages inside the PageEditor</li>
455      *     </ul>
456      * </pre>
457      *
458      * When ever possible use the {@link info.magnolia.ui.api.location.LocationController} to not have to do this.
459      *
460      * @param subAppContext The subAppContext to be updated.
461      * @param location The new {@link Location}.
462      */
463     @Override
464     public void updateSubAppLocation(SubAppContext subAppContext, Location location) {
465         subAppContext.setLocation(location);
466 
467         // the restoreBrowser() method in the BrowserSubApp is not initialized at this point
468         if (subAppContext.getInstanceId() != null) {
469             getView().updateCaption(subAppContext.getInstanceId(), subAppContext.getSubApp().getCaption());
470         }
471 
472         if (appController.getCurrentApp() == getApp() && getActiveSubAppContext() == subAppContext) {
473             shell.setFragment(location.toString());
474         }
475     }
476 
477     @Override
478     public void sendUserMessage(final String user, final Message message) {
479         messagesManager.sendMessage(user, message);
480     }
481 
482     @Override
483     public void sendGroupMessage(final String group, final Message message) {
484         messagesManager.sendGroupMessage(group, message);
485     }
486 
487     @Override
488     public void sendLocalMessage(Message message) {
489         messagesManager.sendLocalMessage(message);
490     }
491 
492     @Override
493     public void broadcastMessage(Message message) {
494         messagesManager.broadcastMessage(message);
495     }
496 
497     @Override
498     public void showConfirmationMessage(String message) {
499         log.info("If confirmation message was already implemented you'd get a {} now...", message);
500     }
501 
502     @Override
503     public SubAppContext getActiveSubAppContext() {
504         return currentSubAppContext;
505     }
506 
507     /**
508      * Will return a running subAppContext which will handle the current location.
509      * Only subApps with matching subAppId will be asked whether they support the location.
510      */
511     private SubAppContext getSupportingSubAppContext(Location location) {
512         SubAppContext supportingContext = null;
513 
514         // If the location has no subAppId defined, get default
515         if (getDefaultSubAppDescriptor() != null) {
516             String subAppId = (location.getSubAppId().isEmpty()) ? getDefaultSubAppDescriptor().getName() : location.getSubAppId();
517 
518             for (SubAppDetails subAppDetails : subApps.values()) {
519                 SubAppContext context = subAppDetails.context;
520                 if (!subAppId.equals(context.getSubAppId())) {
521                     continue;
522                 }
523                 if (context.getSubApp().supportsLocation(location)) {
524                     supportingContext = context;
525                     break;
526                 }
527             }
528         }
529 
530         return supportingContext;
531     }
532 
533     private SubAppDetails createSubAppComponentProvider(SubAppContext subAppContext) {
534         final SubAppDetails subAppDetails = new SubAppDetails();
535         final UiContextReference uiContextReference = UiContextReference.ofSubApp(subAppContext);
536 
537         final Map<Key, Object> instances = new HashMap<>();
538         instances.put(Key.get(SubAppContext.class), subAppContext);
539         instances.put(Key.get(SubAppDescriptor.class), subAppContext.getSubAppDescriptor());
540         final BeanStore beanStore = SessionStore.access().createBeanStore(uiContextReference, instances);
541         subAppDetails.componentProvider = new UiContextBoundComponentProvider(uiContextReference);
542         beanStore.put(ComponentProvider.class, subAppDetails.componentProvider);
543 
544         return subAppDetails;
545     }
546 
547     @Override
548     public boolean isHeaderApp() {
549         return Arrays.stream(HEADER_APPS)
550                 .anyMatch(headerAppName -> headerAppName.equalsIgnoreCase(getName()));
551     }
552 
553     @Override
554     public OverlayLayer getOverlayDelegate() {
555         return shell;
556     }
557 }