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