View Javadoc
1   /**
2    * This file Copyright (c) 2012-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.ui.framework.app;
35  
36  import info.magnolia.config.registry.DefinitionProvider;
37  import info.magnolia.config.registry.Registry;
38  import info.magnolia.event.EventBus;
39  import info.magnolia.i18nsystem.SimpleTranslator;
40  import info.magnolia.module.ModuleRegistry;
41  import info.magnolia.objectfactory.ComponentProvider;
42  import info.magnolia.ui.api.app.App;
43  import info.magnolia.ui.api.app.AppContext;
44  import info.magnolia.ui.api.app.AppController;
45  import info.magnolia.ui.api.app.AppDescriptor;
46  import info.magnolia.ui.api.app.AppInstanceController;
47  import info.magnolia.ui.api.app.AppLifecycleEvent;
48  import info.magnolia.ui.api.app.AppLifecycleEventType;
49  import info.magnolia.ui.api.app.ChooseDialogCallback;
50  import info.magnolia.ui.api.app.registry.AppDescriptorRegistry;
51  import info.magnolia.ui.api.context.UiContext;
52  import info.magnolia.ui.api.event.AdmincentralEventBus;
53  import info.magnolia.ui.api.ioc.AdmincentralScoped;
54  import info.magnolia.ui.api.location.DefaultLocation;
55  import info.magnolia.ui.api.location.Location;
56  import info.magnolia.ui.api.location.LocationChangeRequestedEvent;
57  import info.magnolia.ui.api.location.LocationChangedEvent;
58  import info.magnolia.ui.api.location.LocationController;
59  import info.magnolia.ui.api.message.Message;
60  import info.magnolia.ui.api.message.MessageType;
61  import info.magnolia.ui.api.view.Viewport;
62  import info.magnolia.ui.framework.app.stub.FailedAppStub;
63  import info.magnolia.ui.framework.ioc.SessionStore;
64  import info.magnolia.ui.framework.ioc.UiContextBoundComponentProvider;
65  import info.magnolia.ui.framework.ioc.UiContextReference;
66  import info.magnolia.ui.framework.message.MessagesManager;
67  
68  import java.util.HashMap;
69  import java.util.LinkedList;
70  import java.util.Map;
71  
72  import javax.inject.Inject;
73  import javax.inject.Named;
74  
75  import org.apache.commons.lang3.StringUtils;
76  import org.slf4j.Logger;
77  import org.slf4j.LoggerFactory;
78  
79  import com.google.inject.Key;
80  
81  /**
82   * Implementation of the {@link info.magnolia.ui.api.app.AppController}.
83   *
84   * The App controller that manages the lifecycle of running apps and raises callbacks to the app.
85   * It provides methods to start, stop and focus already running {@link info.magnolia.ui.api.app.App}s.
86   * Registers handlers to the following location change events triggered by the {@link LocationController}:
87   * <ul>
88   * <li>{@link LocationChangedEvent}</li>
89   * <li>{@link LocationChangeRequestedEvent}</li>
90   * </ul>
91   *
92   * @see LocationController
93   * @see info.magnolia.ui.api.app.AppContext
94   * @see info.magnolia.ui.api.app.App
95   */
96  @AdmincentralScoped
97  public class AppControllerImpl implements AppController, LocationChangedEvent.Handler, LocationChangeRequestedEvent.Handler {
98  
99      private static final Logger log = LoggerFactory.getLogger(AppControllerImpl.class);
100 
101     private final ComponentProvider componentProvider;
102     private final AppDescriptorRegistry appDescriptorRegistry;
103     private final LocationController locationController;
104     private final EventBus eventBus;
105     private final Map<String, AppInstanceController> runningApps = new HashMap<>();
106     private final LinkedList<AppInstanceController> appHistory = new LinkedList<>();
107     private final MessagesManager messagesManager;
108     private final SimpleTranslator i18n;
109 
110     private Map<String, AppInstanceController> appInstances = new HashMap<>();
111     private Viewport viewport;
112     private AppInstanceController currentAppInstanceController;
113 
114     /**
115      * @deprecated since 5.5.8 - use {@link #AppControllerImpl(ComponentProvider, AppDescriptorRegistry, LocationController, EventBus, MessagesManager, SimpleTranslator)} instead.
116      */
117     @Deprecated
118     public AppControllerImpl(ModuleRegistry moduleRegistry, ComponentProvider componentProvider, AppDescriptorRegistry appDescriptorRegistry, LocationController locationController, EventBus admincentralEventBus, MessagesManager messagesManager, SimpleTranslator i18n) {
119         this(componentProvider, appDescriptorRegistry, locationController, admincentralEventBus, messagesManager, i18n);
120     }
121 
122     @Inject
123     public AppControllerImpl(ComponentProvider componentProvider, AppDescriptorRegistry appDescriptorRegistry, LocationController locationController, @Named(AdmincentralEventBus.NAME) EventBus admincentralEventBus, MessagesManager messagesManager, SimpleTranslator i18n) {
124         this.componentProvider = componentProvider;
125         this.appDescriptorRegistry = appDescriptorRegistry;
126         this.locationController = locationController;
127         this.eventBus = admincentralEventBus;
128         this.messagesManager = messagesManager;
129         this.i18n = i18n;
130 
131         admincentralEventBus.addHandler(LocationChangedEvent.class, this);
132         admincentralEventBus.addHandler(LocationChangeRequestedEvent.class, this);
133     }
134 
135     @Override
136     public void setViewport(Viewport viewport) {
137         this.viewport = viewport;
138     }
139 
140     /**
141      * This method is called to create an instance of an app independent from the {@link LocationController} and the {@link AppController} handling.
142      * It will not open in the {@link info.magnolia.ui.api.view.Viewport} and will not register itself to the running apps.
143      * This is e.g. used to pass the {@link info.magnolia.ui.api.app.App} into a dialog and obtain app-specific information from outside the app.
144      *
145      * @param appName of the {@link info.magnolia.ui.api.app.App} to instantiate.
146      */
147     private App getAppWithoutStarting(String appName) {
148         AppInstanceController appInstanceController = createNewAppInstance(appName);
149         ComponentProvider appComponentProvider = createAppComponentProvider(appInstanceController);
150         App app = appComponentProvider.newInstance(appInstanceController.getAppDescriptor().getAppClass());
151 
152         appInstanceController.setApp(app);
153         return app;
154     }
155 
156     /**
157      * This method can be called to launch an {@link App} and then delegate it to the {@link LocationController}.
158      * It should have the same effect as calling the {@link LocationController} directly.
159      *
160      * @param appName of the {@link App} to start.
161      * @param location holds information about the subApp to use and the parameters.
162      */
163     public App startIfNotAlreadyRunningThenFocus(String appName, Location location) {
164         AppInstanceController appInstanceController = getAppInstance(appName);
165         appInstanceController = doStartIfNotAlreadyRunning(appInstanceController, location);
166         doFocus(appInstanceController);
167         return appInstanceController.getApp();
168     }
169 
170     /**
171      * This method is called to launch an app independent from the {@link LocationController}.
172      * It will not open in the {@link info.magnolia.ui.api.view.Viewport}.
173      * This is e.g. used to pass the {@link App} into a dialog and obtain app-specific information from outside the app.
174      *
175      * See MGNLUI-379.
176      *
177      * @param appName of the {@link App} to start.
178      * @param location holds information about the subApp to use and the parameters.
179      */
180     @Deprecated
181     public App startIfNotAlreadyRunning(String appName, Location location) {
182         AppInstanceController appInstanceController = getAppInstance(appName);
183 
184         return doStartIfNotAlreadyRunning(appInstanceController, location).getApp();
185     }
186 
187     @Override
188     public void stopApp(String appName) {
189         final AppInstanceController appInstanceController = runningApps.get(appName);
190         if (appInstanceController != null) {
191             doStop(appInstanceController);
192         }
193     }
194 
195     @Override
196     public void stopCurrentApp() {
197         final AppInstanceController appInstanceController = appHistory.peekFirst();
198         if (appInstanceController != null) {
199             doStop(appInstanceController);
200         }
201     }
202 
203     @Override
204     public boolean isAppStarted(String appName) {
205         return runningApps.containsKey(appName);
206     }
207 
208     @Override
209     public void focusCurrentApp() {
210         if (currentAppInstanceController != null) {
211             doFocus(currentAppInstanceController);
212         }
213     }
214 
215     @Override
216     public App getCurrentApp() {
217         return currentAppInstanceController == null ? null : currentAppInstanceController.getApp();
218     }
219 
220     /**
221      * Returns the current location of the focused app. This can differ from the actual location of the admin central, e.g. when a shell app is open.
222      *
223      * @see info.magnolia.ui.api.location.LocationController#getWhere()
224      */
225     @Override
226     public Location getCurrentAppLocation() {
227         return currentAppInstanceController == null ? null : currentAppInstanceController.getCurrentLocation();
228     }
229 
230     /**
231      * Returns the current location of a running app instance or null, if it is not running. The App does not have to be focused.
232      */
233     @Override
234     public Location getAppLocation(String appName) {
235         AppInstanceController appInstanceController = runningApps.get(appName);
236         return appInstanceController == null ? null : appInstanceController.getCurrentLocation();
237     }
238 
239     /**
240      * Delegates the starting of an {@link App} to the {@link info.magnolia.ui.api.app.AppContext}. In
241      * case the app is already started, it will update its location.
242      */
243     private AppInstanceController doStartIfNotAlreadyRunning(AppInstanceController appInstanceController, Location location) {
244         if (isAppStarted(appInstanceController.getAppDescriptor().getName())) {
245             appInstanceController.onLocationUpdate(location);
246             return appInstanceController;
247         }
248 
249         runningApps.put(appInstanceController.getAppDescriptor().getName(), appInstanceController);
250         appInstanceController.start(location);
251         sendEvent(AppLifecycleEventType.STARTED, appInstanceController.getAppDescriptor());
252         return appInstanceController;
253     }
254 
255     /**
256      * Focuses an already running {@link App} by passing it to the
257      * {@link LocationController}.
258      */
259     private void doFocus(AppInstanceController appInstanceController) {
260         locationController.goTo(appInstanceController.getCurrentLocation());
261         appHistory.addFirst(appInstanceController);
262         sendEvent(AppLifecycleEventType.FOCUSED, appInstanceController.getAppDescriptor());
263     }
264 
265     private void doStop(AppInstanceController appInstanceController) {
266         final Location currentLocation = locationController.getWhere();
267 
268         sendEvent(AppLifecycleEventType.STOPPED, appInstanceController.getAppDescriptor());
269         appInstanceController.stop();
270         while (appHistory.remove(appInstanceController)) {
271             ;
272         }
273 
274         final String appName = appInstanceController.getAppDescriptor().getName();
275         runningApps.remove(appName);
276         appInstances.remove(appName);
277 
278         if (currentAppInstanceController == appInstanceController) {
279             currentAppInstanceController = null;
280             viewport.setView(null);
281         }
282 
283         if (!appHistory.isEmpty()) {
284             doFocus(appHistory.peekFirst());
285         }
286 
287         // Only 'fallback' to app launcher we are closing the currently visible app
288         if (StringUtils.equalsIgnoreCase(appName, currentLocation.getAppName())) {
289             locationController.goTo(new DefaultLocation(Location.LOCATION_TYPE_SHELL_APP, "applauncher"));
290         }
291     }
292 
293     private void sendEvent(AppLifecycleEventType appEventType, AppDescriptor appDescriptor) {
294         eventBus.fireEvent(new AppLifecycleEvent(appDescriptor, appEventType));
295     }
296 
297     /**
298      * Takes care of {@link LocationChangedEvent}s by:
299      * <ul>
300      * <li>Obtaining the {@link AppDescriptor} associated with the {@link Location}.</li>
301      * <li>Creating a new {@link info.magnolia.ui.api.app.AppContext} if not running, otherwise obtain it from the running apps.</li>
302      * <li>Updating the {@Link Location} and redirecting in case of missing subAppId.</li>
303      * <li>Starting the App.</li>
304      * <li>Adding the {@link info.magnolia.ui.api.app.AppContext} to the appHistory.</li>
305      * <li>Setting the viewport and updating the current running app.</li>
306      * </ul>
307      */
308     @Override
309     public void onLocationChanged(LocationChangedEvent event) {
310         Location newLocation = event.getNewLocation();
311 
312         if (!newLocation.getAppType().equals(Location.LOCATION_TYPE_APP)) {
313             return;
314         }
315 
316         if (newLocation.equals(getCurrentAppLocation())) {
317             return;
318         }
319 
320         AppDescriptor nextApp = getAppForLocation(newLocation);
321         if (nextApp == null) {
322             return;
323         }
324 
325         AppInstanceController nextAppContext = getAppInstance(nextApp.getName());
326 
327         // update location
328         Location updateLocation = updateLocation(nextAppContext, newLocation);
329         if (!updateLocation.equals(newLocation)) {
330             locationController.goTo(updateLocation);
331             return;
332         }
333 
334         if (currentAppInstanceController != nextAppContext) {
335             appHistory.addFirst(nextAppContext);
336             currentAppInstanceController = nextAppContext;
337         }
338 
339         nextAppContext = doStartIfNotAlreadyRunning(nextAppContext, newLocation);
340 
341         try {
342             viewport.setView(nextAppContext.getApp().getView());
343         } catch (Exception e) {
344             log.error("App {} failed to start: {}", nextApp.getName(), e.getMessage(), e);
345             App failedApp = componentProvider.newInstance(FailedAppStub.class, nextAppContext, e);
346             failedApp.start(newLocation);
347             viewport.setView(failedApp.getView());
348         }
349     }
350 
351     /**
352      * Updates the {@link Location} in case of missing subAppId:
353      * <ul>
354      * <li>If the app is running, it will fetch the current Location associated with the App.</li>
355      * <li>Will fetch the configured default subAppId otherwise.</li>
356      * </ul>
357      */
358     private Location updateLocation(AppInstanceController appInstanceController, Location location) {
359         String appType = location.getAppType();
360         String appName = location.getAppName();
361         String subAppId = location.getSubAppId();
362         String params = location.getParameter();
363 
364         if (StringUtils.isBlank(subAppId)) {
365 
366             if (isAppStarted(appName)) {
367                 AppInstanceController runningAppContext = runningApps.get(appName);
368                 subAppId = runningAppContext.getCurrentLocation().getSubAppId();
369             } else if (StringUtils.isBlank(subAppId)) {
370                 Location defaultLocation = appInstanceController.getDefaultLocation();
371                 if (defaultLocation != null) {
372                     subAppId = defaultLocation.getSubAppId();
373                 } else {
374                     log.warn("No default location could be found for the '{}' app, please check subapp configuration.", appName);
375                 }
376 
377             }
378         }
379 
380         return new DefaultLocation(appType, appName, subAppId, params);
381     }
382 
383     private AppInstanceController getAppInstance(String appName) {
384         return appInstances.computeIfAbsent(appName, this::createNewAppInstance);
385     }
386 
387     private AppInstanceController createNewAppInstance(String appName) {
388         AppDescriptor descriptor = getAppDescriptor(appName);
389         if (descriptor == null) {
390             return null;
391         }
392 
393         AppInstanceController appInstanceController = componentProvider.newInstance(AppInstanceController.class, descriptor);
394         createAppComponentProvider(appInstanceController);
395 
396         return appInstanceController;
397     }
398 
399     @Override
400     public void onLocationChangeRequested(LocationChangeRequestedEvent event) {
401         if (currentAppInstanceController != null) {
402             final String message = currentAppInstanceController.mayStop();
403             if (message != null) {
404                 event.setWarning(message);
405             }
406         }
407     }
408 
409     @Override
410     public void openChooseDialog(String appName, UiContext uiContext, String selectedId, ChooseDialogCallback callback) {
411         openChooseDialog(appName, uiContext, null, selectedId, callback);
412     }
413 
414     @Override
415     public void openChooseDialog(String appName, UiContext uiContext, String targetTreeRootPath, String selectedId, ChooseDialogCallback callback) {
416         App targetApp = getAppWithoutStarting(appName);
417         if (targetApp != null) {
418             final ChooseDialogCallback composedCallback = ChooseDialogCallback.composeWith(targetApp::stop, callback);
419             if (StringUtils.isNotBlank(targetTreeRootPath)) {
420                 targetApp.openChooseDialog(uiContext, targetTreeRootPath, selectedId, composedCallback);
421             } else {
422                 targetApp.openChooseDialog(uiContext, selectedId, callback);
423             }
424         }
425     }
426 
427     private AppDescriptor getAppForLocation(Location location) {
428         return getAppDescriptor(location.getAppName());
429     }
430 
431     private AppDescriptor getAppDescriptor(String name) throws RuntimeException {
432         final DefinitionProvider<AppDescriptor> definitionProvider;
433         try {
434             definitionProvider = appDescriptorRegistry.getProvider(name);
435         } catch (Registry.NoSuchDefinitionException | IllegalStateException e) {
436             Message errorMessage = new Message();
437             errorMessage.setType(MessageType.ERROR);
438             errorMessage.setSubject(i18n.translate("ui-framework.app.appdescriptorReadError.subject"));
439             errorMessage.setMessage(String.format(i18n.translate("ui-framework.app.appdescriptorReadError.message"), name));
440             messagesManager.sendLocalMessage(errorMessage);
441             throw new RuntimeException(e);
442         }
443         return definitionProvider.get();
444     }
445 
446     private ComponentProvider createAppComponentProvider(AppInstanceController appInstanceController) {
447         final UiContextReference uiContextReference = UiContextReference.ofApp(appInstanceController);
448         final UiContextBoundComponentProvider appComponentProvider = new UiContextBoundComponentProvider(uiContextReference);
449         appInstanceController.setAppComponentProvider(appComponentProvider);
450 
451         final Map<Key, Object> initialScopedInstances = new HashMap<>();
452         initialScopedInstances.put(Key.get(AppContext.class), appInstanceController);
453 
454         SessionStore.access().createBeanStore(uiContextReference, initialScopedInstances);
455 
456         return appComponentProvider;
457     }
458 
459 }