ead041ca99f8032f5176fd071ad7ca82f084a4c8
[gpl/argeo-suite.git] / org.argeo.suite.ui / src / org / argeo / suite / ui / SuiteApp.java
1 package org.argeo.suite.ui;
2
3 import static org.argeo.cms.ui.CmsView.CMS_VIEW_UID_PROPERTY;
4
5 import java.util.Collections;
6 import java.util.HashMap;
7 import java.util.HashSet;
8 import java.util.List;
9 import java.util.Map;
10 import java.util.Set;
11 import java.util.TreeMap;
12 import java.util.TreeSet;
13
14 import javax.jcr.Node;
15 import javax.jcr.RepositoryException;
16 import javax.jcr.Session;
17 import javax.jcr.nodetype.NodeType;
18 import javax.naming.InvalidNameException;
19 import javax.naming.ldap.LdapName;
20
21 import org.apache.commons.logging.Log;
22 import org.apache.commons.logging.LogFactory;
23 import org.argeo.api.NodeUtils;
24 import org.argeo.cms.CmsUserManager;
25 import org.argeo.cms.auth.CmsSession;
26 import org.argeo.cms.ui.AbstractCmsApp;
27 import org.argeo.cms.ui.CmsTheme;
28 import org.argeo.cms.ui.CmsUiProvider;
29 import org.argeo.cms.ui.CmsView;
30 import org.argeo.cms.ui.dialogs.CmsFeedback;
31 import org.argeo.cms.ui.util.CmsEvent;
32 import org.argeo.cms.ui.util.CmsUiUtils;
33 import org.argeo.entity.EntityConstants;
34 import org.argeo.entity.EntityNames;
35 import org.argeo.entity.EntityType;
36 import org.argeo.jcr.Jcr;
37 import org.argeo.suite.RankedObject;
38 import org.argeo.suite.SuiteUtils;
39 import org.argeo.util.LangUtils;
40 import org.eclipse.swt.SWT;
41 import org.eclipse.swt.widgets.Composite;
42 import org.osgi.framework.Constants;
43 import org.osgi.service.event.Event;
44 import org.osgi.service.event.EventHandler;
45 import org.osgi.service.useradmin.User;
46
47 /** The Argeo Suite App. */
48 public class SuiteApp extends AbstractCmsApp implements EventHandler {
49         private final static Log log = LogFactory.getLog(SuiteApp.class);
50
51         public final static String PID_PREFIX = "argeo.suite.ui.";
52         public final static String HEADER_PID = PID_PREFIX + "header";
53         public final static String LEAD_PANE_PID = PID_PREFIX + "leadPane";
54         public final static String LOGIN_SCREEN_PID = PID_PREFIX + "loginScreen";
55         public final static String DASHBOARD_PID = PID_PREFIX + "dashboard";
56         public final static String RECENT_ITEMS_PID = PID_PREFIX + "recentItems";
57
58         private final static String DEFAULT_UI_NAME = "app";
59         private final static String DEFAULT_THEME_ID = "org.argeo.suite.theme.default";
60
61         private Map<String, RankedObject<CmsUiProvider>> uiProvidersByPid = Collections.synchronizedMap(new HashMap<>());
62         private Map<String, RankedObject<CmsUiProvider>> uiProvidersByType = Collections.synchronizedMap(new HashMap<>());
63         private Map<String, RankedObject<SuiteLayer>> layersByPid = Collections.synchronizedSortedMap(new TreeMap<>());
64         private Map<String, RankedObject<SuiteLayer>> layersByType = Collections.synchronizedSortedMap(new TreeMap<>());
65
66         private CmsUserManager cmsUserManager;
67
68         // TODO make more optimal or via CmsSession/CmsView
69         private Map<String, SuiteUi> managedUis = new HashMap<>();
70
71 //      private CmsUiProvider headerPart = null;
72
73         public void init(Map<String, String> properties) {
74                 if (log.isDebugEnabled())
75                         log.info("Argeo Suite App started");
76         }
77
78         public void destroy(Map<String, String> properties) {
79                 for (SuiteUi ui : managedUis.values())
80                         if (!ui.isDisposed())
81                                 ui.dispose();
82                 if (log.isDebugEnabled())
83                         log.info("Argeo Suite App stopped");
84
85         }
86
87         @Override
88         public Set<String> getUiNames() {
89                 HashSet<String> uiNames = new HashSet<>();
90                 uiNames.add(DEFAULT_UI_NAME);
91                 return uiNames;
92         }
93
94         @Override
95         public Composite initUi(Composite parent) {
96                 String uiName = parent.getData(UI_NAME_PROPERTY) != null ? parent.getData(UI_NAME_PROPERTY).toString() : null;
97                 CmsView cmsView = CmsView.getCmsView(parent);
98                 if (cmsView == null)
99                         throw new IllegalStateException("No CMS view is registered.");
100                 CmsTheme theme = getTheme(uiName);
101                 if (theme != null)
102                         CmsTheme.registerCmsTheme(parent.getShell(), theme);
103                 SuiteUi argeoSuiteUi = new SuiteUi(parent, SWT.NONE);
104                 String uid = cmsView.getUid();
105                 managedUis.put(uid, argeoSuiteUi);
106                 argeoSuiteUi.addDisposeListener((e) -> {
107                         managedUis.remove(uid);
108                         if (log.isDebugEnabled())
109                                 log.debug("Suite UI " + uid + " has been disposed.");
110                 });
111                 refreshUi(argeoSuiteUi, null);
112                 return argeoSuiteUi;
113         }
114
115         @Override
116         public String getThemeId(String uiName) {
117                 // TODO make it configurable
118                 return DEFAULT_THEME_ID;
119         }
120
121         @Override
122         public void refreshUi(Composite parent, String state) {
123                 try {
124                         Node context = null;
125                         SuiteUi ui = (SuiteUi) parent;
126                         refreshPart(findUiProvider(HEADER_PID), ui.getHeader(), context);
127                         CmsView cmsView = CmsView.getCmsView(parent);
128                         if (cmsView.isAnonymous()) {
129                                 ui.logout();
130                                 ui.refreshBelowHeader(false);
131                                 refreshPart(findUiProvider(LOGIN_SCREEN_PID), ui.getBelowHeader(), context);
132                                 ui.layout(true, true);
133                         } else {
134                                 if (ui.getUserDir() == null) {
135                                         CmsSession cmsSession = cmsView.getCmsSession();
136                                         Session adminSession = null;
137                                         try {
138                                                 adminSession = NodeUtils.openDataAdminSession(getRepository(), null);
139                                                 Node userDir = SuiteUtils.getOrCreateSessionDir(adminSession, cmsSession);
140                                                 ui.initSessions(getRepository(), userDir.getPath());
141                                         } finally {
142                                                 Jcr.logout(adminSession);
143                                         }
144                                 }
145                                 context = stateToNode(ui, state);
146                                 if (context == null)
147                                         context = ui.getUserDir();
148
149                                 ui.refreshBelowHeader(true);
150                                 for (String key : layersByPid.keySet()) {
151                                         SuiteLayer layer = layersByPid.get(key).get();
152                                         ui.addLayer(key, layer);
153                                 }
154                                 refreshPart(findUiProvider(LEAD_PANE_PID), ui.getLeadPane(), context);
155                                 ui.layout(true, true);
156                                 setState(parent, state);
157                         }
158                 } catch (Exception e) {
159                         CmsFeedback.show("Unexpected exception", e);
160                 }
161         }
162
163         private void refreshPart(CmsUiProvider uiProvider, Composite part, Node context) {
164                 CmsUiUtils.clear(part);
165                 uiProvider.createUiPart(part, context);
166         }
167
168         private CmsUiProvider findUiProvider(String pid) {
169                 if (!uiProvidersByPid.containsKey(pid))
170                         throw new IllegalArgumentException("No UI provider registered as " + pid);
171                 return uiProvidersByPid.get(pid).get();
172         }
173
174         private <T> T findByType(Map<String, RankedObject<T>> byType, Node context) {
175                 try {
176                         // mixins
177                         Set<String> types = new TreeSet<>();
178                         for (NodeType nodeType : context.getMixinNodeTypes()) {
179                                 String typeName = nodeType.getName();
180                                 if (byType.containsKey(typeName)) {
181                                         types.add(typeName);
182                                 }
183                         }
184                         // primary node type
185                         {
186                                 NodeType nodeType = context.getPrimaryNodeType();
187                                 String typeName = nodeType.getName();
188                                 if (byType.containsKey(typeName)) {
189                                         types.add(typeName);
190                                 }
191                                 for (NodeType mixin : nodeType.getDeclaredSupertypes()) {
192                                         if (byType.containsKey(mixin.getName())) {
193                                                 types.add(mixin.getName());
194                                         }
195                                 }
196                         }
197                         // entity type
198                         if (context.isNodeType(EntityType.entity.get())) {
199                                 if (context.hasProperty(EntityNames.ENTITY_TYPE)) {
200                                         String typeName = context.getProperty(EntityNames.ENTITY_TYPE).getString();
201                                         if (byType.containsKey(typeName)) {
202                                                 types.add(typeName);
203                                         }
204                                 }
205                         }
206
207 //                      if (context.getPath().equals("/")) {// root node
208 //                              types.add("nt:folder");
209 //                      }
210                         if (NodeUtils.isUserHome(context) && byType.containsKey("nt:folder")) {// home node
211                                 types.add("nt:folder");
212                         }
213
214                         if (types.size() == 0)
215                                 throw new IllegalArgumentException("No type found for " + context);
216                         String type = types.iterator().next();
217                         if (!byType.containsKey(type))
218                                 throw new IllegalArgumentException("No component found for " + context + " with type " + type);
219                         return byType.get(type).get();
220                 } catch (RepositoryException e) {
221                         throw new IllegalStateException(e);
222                 }
223         }
224
225         @Override
226         public void setState(Composite parent, String state) {
227                 if (state == null || !state.startsWith("/"))
228                         return;
229                 SuiteUi suiteUi = (SuiteUi) parent;
230                 Node node = stateToNode(suiteUi, state);
231                 if (node == null) {
232                         suiteUi.getCmsView().navigateTo("~");
233                 } else {
234                         suiteUi.getCmsView().sendEvent(SuiteEvent.switchLayer.topic(), SuiteEvent.eventProperties(node));
235                         suiteUi.getCmsView().sendEvent(SuiteEvent.refreshPart.topic(), SuiteEvent.eventProperties(node));
236                 }
237
238 //              CmsView cmsView = CmsView.getCmsView(parent);
239 //              if (cmsView.isAnonymous())
240 //                      return;
241 //              Session session = null;
242 //              try {
243 //                      if (state != null && state.startsWith("/")) {
244 //                              String path = state.substring(1);
245 //                              String workspace;
246 //                              if (path.equals("")) {
247 //                                      workspace = null;
248 //                                      path = "/";
249 //                              } else {
250 //                                      int index = path.indexOf('/');
251 //                                      if (index == 0) {
252 //                                              log.error("Cannot interpret " + state);
253 //                                              cmsView.navigateTo("~");
254 //                                              return;
255 //                                      } else if (index > 0) {
256 //                                              workspace = path.substring(0, index);
257 //                                              path = path.substring(index);
258 //                                      } else {// index<0, assuming root node
259 //                                              workspace = path;
260 //                                              path = "/";
261 //                                      }
262 //                              }
263 //                              session = cmsView.doAs(() -> Jcr.login(getRepository(), workspace));
264 //
265 //                              Node node = session.getNode(path);
266 //
267 //                              cmsView.sendEvent(SuiteEvent.switchLayer.topic(), SuiteEvent.eventProperties(node));
268 //                              cmsView.sendEvent(SuiteEvent.refreshPart.topic(), SuiteEvent.eventProperties(node));
269 //                      }
270 //              } catch (RepositoryException e) {
271 //                      log.error("Cannot load state " + state, e);
272 //                      cmsView.navigateTo("~");
273 //              } finally {
274 //                      JcrUtils.logoutQuietly(session);
275 //              }
276         }
277
278         private String nodeToState(Node node) {
279                 return '/' + Jcr.getWorkspaceName(node) + Jcr.getPath(node);
280         }
281
282         private Node stateToNode(SuiteUi suiteUi, String state) {
283                 if (suiteUi == null)
284                         return null;
285                 if (state == null)
286                         return null;
287
288                 String path = state.substring(1);
289                 String workspace;
290                 if (path.equals("")) {
291                         workspace = null;
292                         path = "/";
293                 } else {
294                         int index = path.indexOf('/');
295                         if (index == 0) {
296                                 log.error("Cannot interpret " + state);
297 //                              cmsView.navigateTo("~");
298                                 return null;
299                         } else if (index > 0) {
300                                 workspace = path.substring(0, index);
301                                 path = path.substring(index);
302                         } else {// index<0, assuming root node
303                                 workspace = path;
304                                 path = "/";
305                         }
306                 }
307 //              session = cmsView.doAs(() -> Jcr.login(getRepository(), workspace));
308
309                 Session session = suiteUi.getSession(workspace);
310                 if (session == null)
311                         return null;
312                 Node node = Jcr.getNode(session, path);
313                 return node;
314         }
315
316         /*
317          * Events management
318          */
319
320         @Override
321         public void handleEvent(Event event) {
322
323                 // Specific UI related events
324                 SuiteUi ui = getRelatedUi(event);
325                 try {
326 //                      String currentLayerId = ui.getCurrentLayerId();
327 //                      SuiteLayer currentLayer = currentLayerId != null ? layersByPid.get(currentLayerId).get() : null;
328                         if (isTopic(event, SuiteEvent.refreshPart)) {
329                                 Node node = getNode(ui, event);
330                                 CmsUiProvider uiProvider = findByType(uiProvidersByType, node);
331                                 SuiteLayer layer = findByType(layersByType, node);
332                                 ui.switchToLayer(layer, node);
333                                 layer.view(uiProvider, ui.getCurrentWorkArea(), node);
334                                 ui.getCmsView().stateChanged(nodeToState(node), Jcr.getTitle(node));
335                         } else if (isTopic(event, SuiteEvent.openNewPart)) {
336                                 Node node = getNode(ui, event);
337                                 CmsUiProvider uiProvider = findByType(uiProvidersByType, node);
338                                 SuiteLayer layer = findByType(layersByType, node);
339                                 ui.switchToLayer(layer, node);
340                                 layer.open(uiProvider, ui.getCurrentWorkArea(), node);
341                                 ui.getCmsView().stateChanged(nodeToState(node), Jcr.getTitle(node));
342                         } else if (isTopic(event, SuiteEvent.switchLayer)) {
343                                 String layerId = get(event, SuiteEvent.LAYER);
344                                 if (layerId != null) {
345                                         ui.switchToLayer(layerId, Jcr.getRootNode(ui.getSession(null)));
346                                 } else {
347                                         Node node = getNode(ui, event);
348                                         if (node != null) {
349                                                 SuiteLayer layer = findByType(layersByType, node);
350                                                 ui.switchToLayer(layer, node);
351                                         }
352                                 }
353                         }
354                 } catch (Exception e) {
355                         log.error("Cannot handle event " + event, e);
356 //                      CmsView.getCmsView(ui).exception(e);
357                 }
358
359         }
360
361         private Node getNode(SuiteUi ui, Event event) {
362                 String nodePath = get(event, SuiteEvent.NODE_PATH);
363                 String workspaceName = get(event, SuiteEvent.WORKSPACE);
364                 Session session = ui.getSession(workspaceName);
365                 Node node;
366                 if (nodePath == null) {
367                         // look for a user
368                         String username = get(event, SuiteEvent.USERNAME);
369                         if (username == null)
370                                 return null;
371                         User user = cmsUserManager.getUser(username);
372                         if (user == null)
373                                 return null;
374                         LdapName userDn;
375                         try {
376                                 userDn = new LdapName(user.getName());
377                         } catch (InvalidNameException e) {
378                                 throw new IllegalArgumentException("Badly formatted username", e);
379                         }
380                         String userNodePath = SuiteUtils.getUserNodePath(userDn);
381                         if (Jcr.itemExists(session, userNodePath))
382                                 node = Jcr.getNode(session, userNodePath);
383                         else {
384                                 Session adminSession = null;
385                                 try {
386                                         adminSession = NodeUtils.openDataAdminSession(getRepository(), workspaceName);
387                                         SuiteUtils.getOrCreateUserNode(adminSession, userDn);
388                                 } finally {
389                                         Jcr.logout(adminSession);
390                                 }
391                                 node = Jcr.getNode(session, userNodePath);
392                         }
393                 } else {
394                         node = Jcr.getNode(session, nodePath);
395                 }
396                 return node;
397         }
398
399         private SuiteUi getRelatedUi(Event event) {
400                 return managedUis.get(get(event, CMS_VIEW_UID_PROPERTY));
401         }
402
403         private static boolean isTopic(Event event, CmsEvent cmsEvent) {
404                 return event.getTopic().equals(cmsEvent.topic());
405         }
406
407         private static String get(Event event, String key) {
408                 Object value = event.getProperty(key);
409                 if (value == null)
410                         return null;
411 //                      throw new IllegalArgumentException("Property " + key + " must be set");
412                 return value.toString();
413
414         }
415
416         /*
417          * Dependency injection.
418          */
419
420         public void addUiProvider(CmsUiProvider uiProvider, Map<String, Object> properties) {
421                 if (properties.containsKey(Constants.SERVICE_PID)) {
422                         String pid = (String) properties.get(Constants.SERVICE_PID);
423                         RankedObject.putIfHigherRank(uiProvidersByPid, pid, uiProvider, properties);
424                 }
425                 if (properties.containsKey(EntityConstants.TYPE)) {
426                         List<String> types = LangUtils.toStringList(properties.get(EntityConstants.TYPE));
427                         for (String type : types)
428                                 RankedObject.putIfHigherRank(uiProvidersByType, type, uiProvider, properties);
429                 }
430         }
431
432         public void removeUiProvider(CmsUiProvider uiProvider, Map<String, Object> properties) {
433                 if (properties.containsKey(Constants.SERVICE_PID)) {
434                         String pid = (String) properties.get(Constants.SERVICE_PID);
435                         if (uiProvidersByPid.containsKey(pid)) {
436                                 if (uiProvidersByPid.get(pid).equals(new RankedObject<CmsUiProvider>(uiProvider, properties))) {
437                                         uiProvidersByPid.remove(pid);
438                                 }
439                         }
440                 }
441                 if (properties.containsKey(EntityConstants.TYPE)) {
442                         List<String> types = LangUtils.toStringList(properties.get(EntityConstants.TYPE));
443                         for (String type : types) {
444                                 if (uiProvidersByType.containsKey(type)) {
445                                         if (uiProvidersByType.get(type).equals(new RankedObject<CmsUiProvider>(uiProvider, properties))) {
446                                                 uiProvidersByType.remove(type);
447                                         }
448                                 }
449                         }
450                 }
451         }
452
453         public void addLayer(SuiteLayer layer, Map<String, Object> properties) {
454                 if (properties.containsKey(Constants.SERVICE_PID)) {
455                         String pid = (String) properties.get(Constants.SERVICE_PID);
456                         RankedObject.putIfHigherRank(layersByPid, pid, layer, properties);
457                 }
458                 if (properties.containsKey(EntityConstants.TYPE)) {
459                         List<String> types = LangUtils.toStringList(properties.get(EntityConstants.TYPE));
460                         for (String type : types)
461                                 RankedObject.putIfHigherRank(layersByType, type, layer, properties);
462                 }
463         }
464
465         public void removeLayer(SuiteLayer layer, Map<String, Object> properties) {
466                 if (properties.containsKey(Constants.SERVICE_PID)) {
467                         String pid = (String) properties.get(Constants.SERVICE_PID);
468                         if (layersByPid.containsKey(pid)) {
469                                 if (layersByPid.get(pid).equals(new RankedObject<SuiteLayer>(layer, properties))) {
470                                         layersByPid.remove(pid);
471                                 }
472                         }
473                 }
474                 if (properties.containsKey(EntityConstants.TYPE)) {
475                         List<String> types = LangUtils.toStringList(properties.get(EntityConstants.TYPE));
476                         for (String type : types) {
477                                 if (layersByType.containsKey(type)) {
478                                         if (layersByType.get(type).equals(new RankedObject<CmsUiProvider>(layer, properties))) {
479                                                 layersByType.remove(type);
480                                         }
481                                 }
482                         }
483                 }
484         }
485
486         public void setCmsUserManager(CmsUserManager cmsUserManager) {
487                 this.cmsUserManager = cmsUserManager;
488         }
489
490 }