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