View Javadoc

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