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