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