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