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