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