1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package info.magnolia.ui.framework.app;
35
36 import info.magnolia.context.Context;
37 import info.magnolia.context.MgnlContext;
38 import info.magnolia.event.EventBus;
39 import info.magnolia.i18nsystem.I18nizer;
40 import info.magnolia.i18nsystem.SimpleTranslator;
41 import info.magnolia.init.MagnoliaConfigurationProperties;
42 import info.magnolia.monitoring.SystemMonitor;
43 import info.magnolia.objectfactory.ComponentProvider;
44 import info.magnolia.objectfactory.Components;
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.location.DefaultLocation;
57 import info.magnolia.ui.api.location.Location;
58 import info.magnolia.ui.api.location.LocationController;
59 import info.magnolia.ui.api.message.Message;
60 import info.magnolia.ui.api.overlay.OverlayLayer;
61 import info.magnolia.ui.api.shell.Shell;
62 import info.magnolia.ui.api.view.View;
63 import info.magnolia.ui.framework.app.stub.FailedAppStub;
64 import info.magnolia.ui.framework.app.stub.FailedSubAppStub;
65 import info.magnolia.ui.framework.ioc.BeanStore;
66 import info.magnolia.ui.framework.ioc.SessionStore;
67 import info.magnolia.ui.framework.ioc.UiContextBoundComponentProvider;
68 import info.magnolia.ui.framework.ioc.UiContextReference;
69 import info.magnolia.ui.framework.message.MessagesManager;
70 import info.magnolia.ui.vaadin.overlay.MessageStyleTypeEnum;
71 import info.magnolia.ui.vaadin.tabsheet.MagnoliaTab;
72
73 import java.util.Arrays;
74 import java.util.Collection;
75 import java.util.HashMap;
76 import java.util.Map;
77 import java.util.Objects;
78 import java.util.Optional;
79 import java.util.concurrent.ConcurrentHashMap;
80 import java.util.stream.Collectors;
81 import java.util.stream.Stream;
82
83 import javax.inject.Inject;
84 import javax.inject.Provider;
85
86 import org.apache.commons.lang3.StringUtils;
87 import org.slf4j.Logger;
88 import org.slf4j.LoggerFactory;
89
90 import com.google.inject.Key;
91 import com.google.inject.name.Names;
92 import com.vaadin.ui.Component;
93 import com.vaadin.ui.TabSheet;
94 import com.vaadin.ui.UI;
95
96
97
98
99 public class AppInstanceControllerImpl implements AppContext, AppInstanceController {
100
101 private static final Logger log = LoggerFactory.getLogger(AppInstanceControllerImpl.class);
102
103 private static class SubAppDetails {
104 private SubAppContext context;
105 private ComponentProvider componentProvider;
106 }
107
108 private Map<String, SubAppDetails> subApps = new ConcurrentHashMap<>();
109
110 private AppController appController;
111
112 private LocationController locationController;
113
114 private Shell shell;
115
116 private MessagesManager messagesManager;
117
118 private ComponentProvider componentProvider;
119
120 private App app;
121
122 private AppDescriptor appDescriptor;
123
124 private SubAppContext currentSubAppContext;
125
126 private final SystemMonitor systemMonitor;
127
128 private final SimpleTranslator i18n;
129
130 private final Provider<Context> contextProvider;
131
132 @Inject
133 public AppInstanceControllerImpl(AppController appController, LocationController locationController,
134 Shell shell, MessagesManager messagesManager, AppDescriptor appDescriptor,
135 SystemMonitor systemMonitor, I18nizer i18nizer, SimpleTranslator i18n,
136 Provider<Context> contextProvider
137 ) {
138 this.appController = appController;
139 this.locationController = locationController;
140 this.shell = shell;
141 this.messagesManager = messagesManager;
142 this.contextProvider = contextProvider;
143 this.appDescriptor = i18nizer.decorate(appDescriptor);
144 this.systemMonitor = systemMonitor;
145 this.i18n = i18n;
146 }
147
148
149
150
151 @Deprecated
152 public AppInstanceControllerImpl(AppController appController, LocationController locationController,
153 Shell shell, MessagesManager messagesManager, AppDescriptor appDescriptor,
154 SystemMonitor systemMonitor, I18nizer i18nizer, SimpleTranslator i18n) {
155 this(appController, locationController, shell, messagesManager, appDescriptor, systemMonitor, i18nizer, i18n, MgnlContext::getInstance);
156 }
157
158 @Override
159 public void setAppComponentProvider(ComponentProvider componentProvider) {
160 this.componentProvider = componentProvider;
161 }
162
163 @Override
164 public void setApp(App app) {
165 this.app = app;
166 }
167
168 @Override
169 public App getApp() {
170 return app;
171 }
172
173 @Override
174 public String getName() {
175 return appDescriptor.getName();
176 }
177
178 @Override
179 public String getLabel() {
180 return appDescriptor.getLabel();
181 }
182
183 @Override
184 public AppDescriptor getAppDescriptor() {
185 return appDescriptor;
186 }
187
188 @Override
189 public SubAppDescriptor getDefaultSubAppDescriptor() {
190 Collection<SubAppDescriptor> subAppDescriptors = getAppDescriptor().getSubApps().values();
191 return subAppDescriptors.isEmpty() ? null : subAppDescriptors.iterator().next();
192 }
193
194 private SubAppDescriptor getSubAppDescriptorById(String subAppId) {
195 Map<String, SubAppDescriptor> subAppDescriptors = getAppDescriptor().getSubApps();
196 return subAppDescriptors.get(subAppId);
197 }
198
199 @Override
200 public AppView getView() {
201 return app.getView();
202 }
203
204
205
206
207
208 @Override
209 public void start(Location location) {
210 if (systemMonitor.isMemoryLimitReached()) {
211 String memoryMessageCloseApps = i18n.translate("ui-framework.appInstanceController.memoryLimitWarningMessage.closeApps");
212 shell.openNotification(MessageStyleTypeEnum.WARNING, false, i18n.translate("ui-framework.memoryLimitWarningMessage.template", memoryMessageCloseApps));
213 }
214
215 try {
216 app = componentProvider.newInstance(appDescriptor.getAppClass());
217 if (getAppDescriptor().isHtmlCaption()) {
218 app.getView().setTabCaptionsAsHtml();
219 }
220 app.start(location);
221 if ((getDefaultLocation() == null && location != null) || (location != null && !getDefaultLocation().getSubAppId().equals(location.getSubAppId()))) {
222
223 onLocationUpdate(location);
224 }
225 if (StringUtils.isNotBlank(appDescriptor.getTheme())) {
226 app.getView().setTheme(appDescriptor.getTheme());
227 }
228 } catch (final Exception e) {
229 log.error("App {} failed to start: {}", appDescriptor.getName(), e.getMessage(), e);
230 app = componentProvider.newInstance(FailedAppStub.class, this, e);
231 app.start(location);
232 }
233
234
235 if (StringUtils.isNotBlank(appDescriptor.getIcon())) {
236 app.getView().setAppLogo(appDescriptor.getIcon());
237 }
238 }
239
240
241
242
243 @Override
244 public void onLocationUpdate(Location location) {
245 app.locationChanged(location);
246 if (getActiveSubAppContext() != null) {
247 String instanceId = getActiveSubAppContext().getInstanceId();
248
249 Component activeTab = app.getView().getSubAppViewContainer(instanceId).asVaadinComponent();
250 if (activeTab instanceof MagnoliaTab) {
251 ((MagnoliaTab) activeTab).setIcon(getActiveSubAppContext().getSubApp().getIcon(location));
252 }
253 }
254 }
255
256 @Override
257 public void onFocus(String instanceId) {
258 if (subApps.containsKey(instanceId)) {
259
260
261
262 if (currentSubAppContext != null) {
263 sendSubAppLifecycleEvent(currentSubAppContext, SubAppLifecycleEvent.Type.BLURRED);
264 }
265 SubAppContext subAppContext = subApps.get(instanceId).context;
266 locationController.goTo(subAppContext.getLocation());
267 sendSubAppLifecycleEvent(subAppContext, SubAppLifecycleEvent.Type.FOCUSED);
268 }
269 }
270
271 private void sendSubAppLifecycleEvent(SubAppContext ctx, SubAppLifecycleEvent.Type type) {
272 Optional.ofNullable(getSubAppEventBus(ctx)).ifPresent(seb -> seb.fireEvent(new SubAppLifecycleEvent(ctx.getSubAppDescriptor(), type)));
273 }
274
275 private EventBus getSubAppEventBus(SubAppContext subAppContext) {
276 final BeanStore beanStore = SessionStore.access().getBeanStore(UiContextReference.ofSubApp(subAppContext));
277 return beanStore == null ? null : beanStore.get(Key.get(EventBus.class, Names.named(SubAppEventBus.NAME)));
278 }
279
280 @Override
281 public void onClose(String instanceId) {
282 stopSubAppInstance(instanceId);
283 onFocus(app.getView().getActiveSubAppView());
284 }
285
286 @Override
287 public String mayStop() {
288 return null;
289 }
290
291 @Override
292 public void stop() {
293 for (String instanceId : subApps.keySet()) {
294 stopSubAppInstance(instanceId);
295 }
296
297 currentSubAppContext = null;
298 if (app != null) {
299 app.stop();
300 } else {
301 log.warn("Cannot call stop on app that's already set to null. Name: {}, Thread: {}", getName(), Thread.currentThread().getName());
302 }
303
304 SessionStore.access().releaseBeanStore(UiContextReference.ofApp(this));
305 }
306
307 private void stopSubAppInstance(String instanceId) {
308 SubAppDetails subAppDetails = subApps.get(instanceId);
309
310
311
312
313 sendSubAppLifecycleEvent(subAppDetails.context, SubAppLifecycleEvent.Type.STOPPED);
314
315 subAppDetails.context.getSubApp().stop();
316 SessionStore.access().releaseBeanStore(UiContextReference.ofSubApp(subAppDetails.context));
317
318 subApps.remove(instanceId);
319 }
320
321 @Override
322 public Location getCurrentLocation() {
323 SubAppContext subAppContext = getActiveSubAppContext();
324 if (subAppContext != null) {
325 return subAppContext.getLocation();
326 }
327 return new DefaultLocation(Location.LOCATION_TYPE_APP, appDescriptor.getName());
328 }
329
330 @Override
331 public Location getDefaultLocation() {
332 SubAppDescriptor subAppDescriptor = getDefaultSubAppDescriptor();
333 if (subAppDescriptor != null) {
334 return new DefaultLocation(Location.LOCATION_TYPE_APP, appDescriptor.getName(), subAppDescriptor.getName());
335 } else {
336 return null;
337 }
338 }
339
340 @Override
341 public void openSubApp(Location location) {
342
343 final Location defaultLocation = getDefaultLocation();
344 boolean isDefaultSubApp = false;
345
346 if (defaultLocation != null) {
347 isDefaultSubApp = defaultLocation.getSubAppId().equals(location.getSubAppId());
348 if (!isDefaultSubApp) {
349 SubAppContext subAppContext = getSupportingSubAppContext(defaultLocation);
350 if (subAppContext == null) {
351 startSubApp(defaultLocation, false);
352 }
353 }
354 }
355
356
357
358 SubAppContext subAppContext = getSupportingSubAppContext(location);
359 if (subAppContext != null) {
360 subAppContext.getSubApp().locationChanged(location);
361 subAppContext.setLocation(location);
362
363 getView().updateCaption(subAppContext.getInstanceId(), subAppContext.getSubApp().getCaption());
364
365 if (!subAppContext.getInstanceId().equals(app.getView().getActiveSubAppView())) {
366 app.getView().setActiveSubAppView(subAppContext.getInstanceId());
367 }
368 } else {
369 subAppContext = startSubApp(location, !isDefaultSubApp);
370 }
371 currentSubAppContext = subAppContext;
372 }
373
374
375
376
377
378 @Override
379 public void closeSubApp(String instanceId) {
380 getView().closeSubAppView(instanceId);
381 onClose(instanceId);
382 }
383
384 @Override
385 public Collection<SubAppContext> getSubAppContexts() {
386 return subApps.values().stream().map(subAppDetails -> subAppDetails.context).collect(Collectors.toList());
387 }
388
389 private SubAppContext startSubApp(Location location, boolean allowClose) {
390
391 SubAppDescriptor subAppDescriptor = getSubAppDescriptorById(location.getSubAppId());
392
393 if (subAppDescriptor == null) {
394 subAppDescriptor = getDefaultSubAppDescriptor();
395 if (subAppDescriptor == null) {
396 log.warn("No subapp could be found for the '{}' app, please check configuration.", appDescriptor.getName());
397 return null;
398 }
399 }
400 SubAppContext subAppContext = componentProvider.newInstance(SubAppContext.class, subAppDescriptor, shell);
401
402 subAppContext.setAppContext(this);
403 subAppContext.setLocation(location);
404 subAppContext.setAuthoringLocale(contextProvider.get().getLocale());
405
406 SubAppDetails subAppDetails = createSubAppComponentProvider(subAppContext);
407 subAppDetails.context = subAppContext;
408
409 if (this.currentSubAppContext == null) {
410 this.currentSubAppContext = subAppContext;
411 }
412
413 Class<? extends SubApp> subAppClass = subAppDescriptor.getSubAppClass();
414 if (subAppClass == null) {
415 log.warn("Sub App {} doesn't define its sub app class or class doesn't exist or can't be instantiated.", subAppDescriptor.getName());
416 } else {
417 SubApp subApp;
418 View subAppView;
419 boolean closable = true;
420 try {
421 subApp = subAppDetails.componentProvider.newInstance(subAppClass);
422 subAppContext.setSubApp(subApp);
423 subAppView = subApp.start(location);
424 closable = allowClose && subApp.isCloseable();
425 } catch (Exception e) {
426 String instanceId = subAppDescriptor.getName();
427 log.error("Sub-app [{}] of app [{}] failed to start", instanceId, appDescriptor.getName(), e);
428 getView().closeSubAppView(instanceId);
429 subApps.remove(instanceId);
430
431 subApp = subAppDetails.componentProvider.newInstance(FailedSubAppStub.class, e);
432 subAppView = subApp.start(location);
433 subAppContext.setSubApp(subApp);
434 }
435
436 String instanceId = app.getView().addSubAppView(subAppView, subApp.getCaption(), subApp.getIcon(location), closable);
437
438 subAppContext.setInstanceId(instanceId);
439
440 subApps.put(instanceId, subAppDetails);
441
442 sendSubAppLifecycleEvent(subAppContext, SubAppLifecycleEvent.Type.STARTED);
443 }
444 return subAppContext;
445 }
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463 @Override
464 public void updateSubAppLocation(SubAppContext subAppContext, Location location) {
465 subAppContext.setLocation(location);
466
467
468 if (subAppContext.getInstanceId() != null) {
469 getView().updateCaption(subAppContext.getInstanceId(), subAppContext.getSubApp().getCaption());
470 }
471
472 if (appController.getCurrentApp() == getApp() && getActiveSubAppContext() == subAppContext) {
473 shell.setFragment(location.toString());
474 }
475 }
476
477 @Override
478 public void sendUserMessage(final String user, final Message message) {
479 messagesManager.sendMessage(user, message);
480 }
481
482 @Override
483 public void sendGroupMessage(final String group, final Message message) {
484 messagesManager.sendGroupMessage(group, message);
485 }
486
487 @Override
488 public void sendLocalMessage(Message message) {
489 messagesManager.sendLocalMessage(message);
490 }
491
492 @Override
493 public void broadcastMessage(Message message) {
494 messagesManager.broadcastMessage(message);
495 }
496
497 @Override
498 public void showConfirmationMessage(String message) {
499 log.info("If confirmation message was already implemented you'd get a {} now...", message);
500 }
501
502 @Override
503 public SubAppContext getActiveSubAppContext() {
504 return currentSubAppContext;
505 }
506
507
508
509
510
511 private SubAppContext getSupportingSubAppContext(Location location) {
512 SubAppContext supportingContext = null;
513
514
515 if (getDefaultSubAppDescriptor() != null) {
516 String subAppId = (location.getSubAppId().isEmpty()) ? getDefaultSubAppDescriptor().getName() : location.getSubAppId();
517
518 for (SubAppDetails subAppDetails : subApps.values()) {
519 SubAppContext context = subAppDetails.context;
520 if (!subAppId.equals(context.getSubAppId())) {
521 continue;
522 }
523 if (context.getSubApp().supportsLocation(location)) {
524 supportingContext = context;
525 break;
526 }
527 }
528 }
529
530 return supportingContext;
531 }
532
533 private SubAppDetails createSubAppComponentProvider(SubAppContext subAppContext) {
534 final SubAppDetails subAppDetails = new SubAppDetails();
535 final UiContextReference uiContextReference = UiContextReference.ofSubApp(subAppContext);
536
537 final Map<Key, Object> instances = new HashMap<>();
538 instances.put(Key.get(SubAppContext.class), subAppContext);
539 instances.put(Key.get(SubAppDescriptor.class), subAppContext.getSubAppDescriptor());
540 final BeanStore beanStore = SessionStore.access().createBeanStore(uiContextReference, instances);
541 subAppDetails.componentProvider = new UiContextBoundComponentProvider(uiContextReference);
542 beanStore.put(ComponentProvider.class, subAppDetails.componentProvider);
543
544 return subAppDetails;
545 }
546
547 @Override
548 public boolean isHeaderApp() {
549 return Arrays.stream(HEADER_APPS)
550 .anyMatch(headerAppName -> headerAppName.equalsIgnoreCase(getName()));
551 }
552
553 @Override
554 public OverlayLayer getOverlayDelegate() {
555 return shell;
556 }
557 }