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