X-Git-Url: https://git.argeo.org/?p=gpl%2Fargeo-suite.git;a=blobdiff_plain;f=org.argeo.app.ui%2Fsrc%2Forg%2Fargeo%2Fapp%2Fui%2FSuiteApp.java;fp=org.argeo.app.ui%2Fsrc%2Forg%2Fargeo%2Fapp%2Fui%2FSuiteApp.java;h=e4d40cfa8f77c32a0a8e6f4372c679d368bb870e;hp=0000000000000000000000000000000000000000;hb=6e56ffa34cb02ab04d028423aea342e3dfed4358;hpb=c285180bece610b2c2921d44fe14b6dde2123efa diff --git a/org.argeo.app.ui/src/org/argeo/app/ui/SuiteApp.java b/org.argeo.app.ui/src/org/argeo/app/ui/SuiteApp.java new file mode 100644 index 0000000..e4d40cf --- /dev/null +++ b/org.argeo.app.ui/src/org/argeo/app/ui/SuiteApp.java @@ -0,0 +1,642 @@ +package org.argeo.app.ui; + +import static org.argeo.api.cms.CmsView.CMS_VIEW_UID_PROPERTY; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; + +import org.argeo.api.cms.CmsSession; +import org.argeo.api.cms.CmsTheme; +import org.argeo.api.cms.CmsUi; +import org.argeo.api.cms.CmsView; +import org.argeo.app.api.EntityConstants; +import org.argeo.app.api.EntityNames; +import org.argeo.app.api.EntityType; +import org.argeo.app.api.RankedObject; +import org.argeo.app.core.SuiteUtils; +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.AbstractCmsApp; +import org.argeo.cms.CmsUserManager; +import org.argeo.cms.LocaleUtils; +import org.argeo.cms.Localized; +import org.argeo.cms.jcr.CmsJcrUtils; +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.cms.swt.dialogs.CmsFeedback; +import org.argeo.cms.ui.CmsUiProvider; +import org.argeo.eclipse.ui.specific.UiContext; +import org.argeo.jcr.Jcr; +import org.argeo.jcr.JcrException; +import org.argeo.util.LangUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.osgi.framework.Constants; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventHandler; +import org.osgi.service.useradmin.User; + +/** The Argeo Suite App. */ +public class SuiteApp extends AbstractCmsApp implements EventHandler { + private final static CmsLog log = CmsLog.getLog(SuiteApp.class); + + public final static String PUBLIC_BASE_PATH_PROPERTY = "publicBasePath"; + public final static String DEFAULT_UI_NAME_PROPERTY = "defaultUiName"; + public final static String DEFAULT_THEME_ID_PROPERTY = "defaultThemeId"; + public final static String DEFAULT_LAYER_PROPERTY = "defaultLayer"; + private final static String LOGIN = "login"; + private final static String HOME_STATE = "~"; + + private String publicBasePath = null; + + private String pidPrefix; + private String headerPid; + private String footerPid; + private String leadPanePid; + private String adminLeadPanePid; + private String loginScreenPid; + + private String defaultUiName = "app"; + private String adminUiName = "admin"; + + // FIXME such default names make refactoring more dangerous + @Deprecated + private String defaultLayerPid = "argeo.suite.ui.dashboardLayer"; + @Deprecated + private String defaultThemeId = "org.argeo.app.theme.default"; + + private Map> uiProvidersByPid = Collections.synchronizedMap(new HashMap<>()); + private Map> uiProvidersByType = Collections.synchronizedMap(new HashMap<>()); + private Map> layersByPid = Collections.synchronizedSortedMap(new TreeMap<>()); + private Map> layersByType = Collections.synchronizedSortedMap(new TreeMap<>()); + + private CmsUserManager cmsUserManager; + + // TODO make more optimal or via CmsSession/CmsView + private Map managedUis = new HashMap<>(); + + private Repository repository; + + public void init(Map properties) { + if (log.isDebugEnabled()) + log.info("Argeo Suite App started"); + + if (properties.containsKey(DEFAULT_UI_NAME_PROPERTY)) + defaultUiName = LangUtils.get(properties, DEFAULT_UI_NAME_PROPERTY); + if (properties.containsKey(DEFAULT_THEME_ID_PROPERTY)) + defaultThemeId = LangUtils.get(properties, DEFAULT_THEME_ID_PROPERTY); + if (properties.containsKey(DEFAULT_LAYER_PROPERTY)) + defaultLayerPid = LangUtils.get(properties, DEFAULT_LAYER_PROPERTY); + publicBasePath = LangUtils.get(properties, PUBLIC_BASE_PATH_PROPERTY); + + if (properties.containsKey(Constants.SERVICE_PID)) { + String servicePid = properties.get(Constants.SERVICE_PID).toString(); + if (servicePid.endsWith(".app")) { + pidPrefix = servicePid.substring(0, servicePid.length() - "app".length()); + } + } + + if (pidPrefix == null) + throw new IllegalArgumentException("PID prefix must be set."); + + headerPid = pidPrefix + "header"; + footerPid = pidPrefix + "footer"; + leadPanePid = pidPrefix + "leadPane"; + adminLeadPanePid = pidPrefix + "adminLeadPane"; + loginScreenPid = pidPrefix + "loginScreen"; + } + + public void destroy(Map properties) { + for (SuiteUi ui : managedUis.values()) + if (!ui.isDisposed()) + ui.dispose(); + if (log.isDebugEnabled()) + log.info("Argeo Suite App stopped"); + + } + + @Override + public Set getUiNames() { + HashSet uiNames = new HashSet<>(); + uiNames.add(defaultUiName); + uiNames.add(adminUiName); + return uiNames; + } + + @Override + public CmsUi initUi(Object parent) { + Composite uiParent = (Composite) parent; + String uiName = uiParent.getData(UI_NAME_PROPERTY) != null ? uiParent.getData(UI_NAME_PROPERTY).toString() + : null; + CmsView cmsView = CmsSwtUtils.getCmsView(uiParent); + if (cmsView == null) + throw new IllegalStateException("No CMS view is registered."); + CmsTheme theme = getTheme(uiName); + if (theme != null) + CmsSwtUtils.registerCmsTheme(uiParent.getShell(), theme); + SuiteUi argeoSuiteUi = new SuiteUi(uiParent, SWT.INHERIT_DEFAULT); + String uid = cmsView.getUid(); + managedUis.put(uid, argeoSuiteUi); + argeoSuiteUi.addDisposeListener((e) -> { + managedUis.remove(uid); + if (log.isDebugEnabled()) + log.debug("Suite UI " + uid + " has been disposed."); + }); + return argeoSuiteUi; + } + + @Override + public String getThemeId(String uiName) { + String themeId = System.getProperty("org.argeo.app.theme.default"); + if (themeId != null) + return themeId; + return defaultThemeId; + } + + @Override + public void refreshUi(CmsUi cmsUi, String state) { + try { + Node context = null; + SuiteUi ui = (SuiteUi) cmsUi; + + String uiName = Objects.toString(ui.getParent().getData(UI_NAME_PROPERTY), null); + if (uiName == null) + throw new IllegalStateException("UI name should not be null"); + CmsView cmsView = CmsSwtUtils.getCmsView(ui); + CmsUiProvider headerUiProvider = findUiProvider(headerPid); + CmsUiProvider footerUiProvider = findUiProvider(footerPid); + CmsUiProvider leadPaneUiProvider; + if (adminUiName.equals(uiName)) { + leadPaneUiProvider = findUiProvider(adminLeadPanePid); + } else { + leadPaneUiProvider = findUiProvider(leadPanePid); + } + + Localized appTitle = null; + if (headerUiProvider instanceof DefaultHeader) { + appTitle = ((DefaultHeader) headerUiProvider).getTitle(); + } + ui.setTitle(appTitle); + + if (cmsView.isAnonymous() && publicBasePath == null) {// internal app, must login + ui.logout(); + if (headerUiProvider != null) + refreshPart(headerUiProvider, ui.getHeader(), context); + ui.refreshBelowHeader(false); + refreshPart(findUiProvider(loginScreenPid), ui.getBelowHeader(), context); + if (footerUiProvider != null) + refreshPart(footerUiProvider, ui.getFooter(), context); + ui.layout(true, true); + setState(ui, LOGIN); + } else { + if (LOGIN.equals(state)) + state = null; + CmsSession cmsSession = cmsView.getCmsSession(); + if (ui.getUserDir() == null) { + // FIXME NPE on CMSSession when logging in from anonymous + if (cmsSession == null || cmsView.isAnonymous()) { + assert publicBasePath != null; + ui.initSessions(getRepository(), publicBasePath); + } else { + Session adminSession = null; + try { + adminSession = CmsJcrUtils.openDataAdminSession(getRepository(), null); + Node userDir = SuiteUtils.getOrCreateCmsSessionNode(adminSession, cmsSession); + ui.initSessions(getRepository(), userDir.getPath()); + } finally { + Jcr.logout(adminSession); + } + } + } + initLocale(cmsSession); + context = stateToNode(ui, state); + if (context == null) + context = ui.getUserDir(); + + if (headerUiProvider != null) + refreshPart(headerUiProvider, ui.getHeader(), context); + ui.refreshBelowHeader(true); + for (String key : layersByPid.keySet()) { + SuiteLayer layer = layersByPid.get(key).get(); + ui.addLayer(key, layer); + } + + if (leadPaneUiProvider != null) + refreshPart(leadPaneUiProvider, ui.getLeadPane(), context); + if (footerUiProvider != null) + refreshPart(footerUiProvider, ui.getFooter(), context); + ui.layout(true, true); + setState(ui, state != null ? state : defaultLayerPid); + } + } catch (Exception e) { + CmsFeedback.show("Unexpected exception", e); + } + } + + private void initLocale(CmsSession cmsSession) { + if (cmsSession == null) + return; + Locale locale = cmsSession.getLocale(); + UiContext.setLocale(locale); + LocaleUtils.setThreadLocale(locale); + + } + + private void refreshPart(CmsUiProvider uiProvider, Composite part, Node context) { + CmsSwtUtils.clear(part); + uiProvider.createUiPart(part, context); + } + + private CmsUiProvider findUiProvider(String pid) { + if (!uiProvidersByPid.containsKey(pid)) + return null; + return uiProvidersByPid.get(pid).get(); + } + + private SuiteLayer findLayer(String pid) { + if (!layersByPid.containsKey(pid)) + return null; + return layersByPid.get(pid).get(); + } + + private T findByType(Map> byType, Node context) { + if (context == null) + throw new IllegalArgumentException("A node should be provided"); + try { + // mixins + Set types = new TreeSet<>(); + for (NodeType mixinType : context.getMixinNodeTypes()) { + String mixinTypeName = mixinType.getName(); + if (byType.containsKey(mixinTypeName)) { + types.add(mixinTypeName); + } + for (NodeType superType : mixinType.getDeclaredSupertypes()) { + if (byType.containsKey(superType.getName())) { + types.add(superType.getName()); + } + } + } + // primary node type + NodeType primaryType = context.getPrimaryNodeType(); + String primaryTypeName = primaryType.getName(); + if (byType.containsKey(primaryTypeName)) { + types.add(primaryTypeName); + } + for (NodeType superType : primaryType.getDeclaredSupertypes()) { + if (byType.containsKey(superType.getName())) { + types.add(superType.getName()); + } + } + // entity type + if (context.isNodeType(EntityType.entity.get())) { + if (context.hasProperty(EntityNames.ENTITY_TYPE)) { + String entityTypeName = context.getProperty(EntityNames.ENTITY_TYPE).getString(); + if (byType.containsKey(entityTypeName)) { + types.add(entityTypeName); + } + } + } + +// if (context.getPath().equals("/")) {// root node +// types.add("nt:folder"); +// } + if (CmsJcrUtils.isUserHome(context) && byType.containsKey("nt:folder")) {// home node + types.add("nt:folder"); + } + + if (types.size() == 0) + throw new IllegalArgumentException("No type found for " + context + " (" + listTypes(context) + ")"); + String type = types.iterator().next(); + if (!byType.containsKey(type)) + throw new IllegalArgumentException("No component found for " + context + " with type " + type); + return byType.get(type).get(); + } catch (RepositoryException e) { + throw new IllegalStateException(e); + } + } + + private static String listTypes(Node context) { + try { + StringBuilder sb = new StringBuilder(); + sb.append(context.getPrimaryNodeType().getName()); + for (NodeType superType : context.getPrimaryNodeType().getDeclaredSupertypes()) { + sb.append(' '); + sb.append(superType.getName()); + } + + for (NodeType nodeType : context.getMixinNodeTypes()) { + sb.append(' '); + sb.append(nodeType.getName()); + if (nodeType.getName().equals(EntityType.local.get())) + sb.append('/').append(context.getProperty(EntityNames.ENTITY_TYPE).getString()); + for (NodeType superType : nodeType.getDeclaredSupertypes()) { + sb.append(' '); + sb.append(superType.getName()); + } + } + return sb.toString(); + } catch (RepositoryException e) { + throw new JcrException(e); + } + } + + @Override + public void setState(CmsUi cmsUi, String state) { + if (state == null) + return; + if (!state.startsWith("/")) { + if (cmsUi instanceof SuiteUi) { + SuiteUi ui = (SuiteUi) cmsUi; + if (LOGIN.equals(state)) { + String appTitle = ""; + if (ui.getTitle() != null) + appTitle = ui.getTitle().lead(); + ui.getCmsView().stateChanged(state, appTitle); + return; + } + Map properties = new HashMap<>(); + String layerId = HOME_STATE.equals(state) ? defaultLayerPid : state; + properties.put(SuiteEvent.LAYER, layerId); + properties.put(SuiteEvent.NODE_PATH, HOME_STATE); + ui.getCmsView().sendEvent(SuiteEvent.switchLayer.topic(), properties); + } + return; + } + SuiteUi suiteUi = (SuiteUi) cmsUi; + Node node = stateToNode(suiteUi, state); + if (node == null) { + suiteUi.getCmsView().navigateTo(HOME_STATE); + } else { + suiteUi.getCmsView().sendEvent(SuiteEvent.switchLayer.topic(), SuiteEvent.eventProperties(node)); + suiteUi.getCmsView().sendEvent(SuiteEvent.refreshPart.topic(), SuiteEvent.eventProperties(node)); + } + } + + // TODO move it to an internal package? + static String nodeToState(Node node) { + return '/' + Jcr.getWorkspaceName(node) + Jcr.getPath(node); + } + + private Node stateToNode(SuiteUi suiteUi, String state) { + if (suiteUi == null) + return null; + if (state == null || !state.startsWith("/")) + return null; + + String path = state.substring(1); + String workspace; + if (path.equals("")) { + workspace = null; + path = "/"; + } else { + int index = path.indexOf('/'); + if (index == 0) { + log.error("Cannot interpret " + state); +// cmsView.navigateTo("~"); + return null; + } else if (index > 0) { + workspace = path.substring(0, index); + path = path.substring(index); + } else {// index<0, assuming root node + workspace = path; + path = "/"; + } + } + Session session = suiteUi.getSession(workspace); + if (session == null) + return null; + Node node = Jcr.getNode(session, path); + return node; + } + + /* + * Events management + */ + + @Override + public void handleEvent(Event event) { + + // Specific UI related events + SuiteUi ui = getRelatedUi(event); + if (ui == null) + return; + try { + String appTitle = ""; + if (ui.getTitle() != null) + appTitle = ui.getTitle().lead() + " - "; + +// String currentLayerId = ui.getCurrentLayerId(); +// SuiteLayer currentLayer = currentLayerId != null ? layersByPid.get(currentLayerId).get() : null; + if (SuiteUiUtils.isTopic(event, SuiteEvent.refreshPart)) { + Node node = getNode(ui, event); + if (node == null) + return; + CmsUiProvider uiProvider = findByType(uiProvidersByType, node); + SuiteLayer layer = findByType(layersByType, node); + ui.switchToLayer(layer, node); + ui.getCmsView().runAs(() -> layer.view(uiProvider, ui.getCurrentWorkArea(), node)); + ui.getCmsView().stateChanged(nodeToState(node), appTitle + Jcr.getTitle(node)); + } else if (SuiteUiUtils.isTopic(event, SuiteEvent.openNewPart)) { + Node node = getNode(ui, event); + if (node == null) + return; + CmsUiProvider uiProvider = findByType(uiProvidersByType, node); + SuiteLayer layer = findByType(layersByType, node); + ui.switchToLayer(layer, node); + ui.getCmsView().runAs(() -> layer.open(uiProvider, ui.getCurrentWorkArea(), node)); + ui.getCmsView().stateChanged(nodeToState(node), appTitle + Jcr.getTitle(node)); + } else if (SuiteUiUtils.isTopic(event, SuiteEvent.switchLayer)) { + String layerId = get(event, SuiteEvent.LAYER); + if (layerId != null) { +// ui.switchToLayer(layerId, ui.getUserDir()); + SuiteLayer suiteLayer = findLayer(layerId); + if (suiteLayer == null) + throw new IllegalArgumentException("No layer '" + layerId + "' available."); + Localized layerTitle = suiteLayer.getTitle(); + // FIXME make sure we don't rebuild the work area twice + Composite workArea = ui.getCmsView().doAs(() -> ui.switchToLayer(layerId, ui.getUserDir())); + String title = null; + if (layerTitle != null) + title = layerTitle.lead(); + Node nodeFromState = getNode(ui, event); + if (nodeFromState != null && nodeFromState.getPath().equals(ui.getUserDir().getPath())) { + // default layer view is forced + String state = defaultLayerPid.equals(layerId) ? "~" : layerId; + ui.getCmsView().stateChanged(state, appTitle + title); + suiteLayer.view(null, workArea, nodeFromState); + } else { + Node layerCurrentContext = suiteLayer.getCurrentContext(workArea); + if (layerCurrentContext != null) { + // layer was already showing a context so we set the state to it + ui.getCmsView().stateChanged(nodeToState(layerCurrentContext), + appTitle + Jcr.getTitle(layerCurrentContext)); + } else { + // no context was shown + ui.getCmsView().stateChanged(layerId, appTitle + title); + } + } + } else { + Node node = getNode(ui, event); + if (node != null) { + SuiteLayer layer = findByType(layersByType, node); + ui.getCmsView().runAs(() -> ui.switchToLayer(layer, node)); + } + } + } + } catch (Exception e) { + log.error("Cannot handle event " + event, e); +// CmsView.getCmsView(ui).exception(e); + } + + } + + private Node getNode(SuiteUi ui, Event event) { + String nodePath = get(event, SuiteEvent.NODE_PATH); + if (nodePath != null && nodePath.equals(HOME_STATE)) + return ui.getUserDir(); + String workspaceName = get(event, SuiteEvent.WORKSPACE); + Session session = ui.getSession(workspaceName); + Node node; + if (nodePath == null) { + // look for a user + String username = get(event, SuiteEvent.USERNAME); + if (username == null) + return null; + User user = cmsUserManager.getUser(username); + if (user == null) + return null; + LdapName userDn; + try { + userDn = new LdapName(user.getName()); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Badly formatted username", e); + } + String userNodePath = SuiteUtils.getUserNodePath(userDn); + if (Jcr.itemExists(session, userNodePath)) + node = Jcr.getNode(session, userNodePath); + else { + Session adminSession = null; + try { + adminSession = CmsJcrUtils.openDataAdminSession(getRepository(), workspaceName); + SuiteUtils.getOrCreateUserNode(adminSession, userDn); + } finally { + Jcr.logout(adminSession); + } + node = Jcr.getNode(session, userNodePath); + } + } else { + node = Jcr.getNode(session, nodePath); + } + return node; + } + + private SuiteUi getRelatedUi(Event event) { + return managedUis.get(get(event, CMS_VIEW_UID_PROPERTY)); + } + + public static String get(Event event, String key) { + Object value = event.getProperty(key); + if (value == null) + return null; +// throw new IllegalArgumentException("Property " + key + " must be set"); + return value.toString(); + + } + + /* + * Dependency injection. + */ + + public void addUiProvider(CmsUiProvider uiProvider, Map properties) { + if (properties.containsKey(Constants.SERVICE_PID)) { + String pid = (String) properties.get(Constants.SERVICE_PID); + RankedObject.putIfHigherRank(uiProvidersByPid, pid, uiProvider, properties); + } + if (properties.containsKey(EntityConstants.TYPE)) { + List types = LangUtils.toStringList(properties.get(EntityConstants.TYPE)); + for (String type : types) + RankedObject.putIfHigherRank(uiProvidersByType, type, uiProvider, properties); + } + } + + public void removeUiProvider(CmsUiProvider uiProvider, Map properties) { + if (properties.containsKey(Constants.SERVICE_PID)) { + String pid = (String) properties.get(Constants.SERVICE_PID); + if (uiProvidersByPid.containsKey(pid)) { + if (uiProvidersByPid.get(pid).equals(new RankedObject(uiProvider, properties))) { + uiProvidersByPid.remove(pid); + } + } + } + if (properties.containsKey(EntityConstants.TYPE)) { + List types = LangUtils.toStringList(properties.get(EntityConstants.TYPE)); + for (String type : types) { + if (uiProvidersByType.containsKey(type)) { + if (uiProvidersByType.get(type).equals(new RankedObject(uiProvider, properties))) { + uiProvidersByType.remove(type); + } + } + } + } + } + + public void addLayer(SuiteLayer layer, Map properties) { + if (properties.containsKey(Constants.SERVICE_PID)) { + String pid = (String) properties.get(Constants.SERVICE_PID); + RankedObject.putIfHigherRank(layersByPid, pid, layer, properties); + } + if (properties.containsKey(EntityConstants.TYPE)) { + List types = LangUtils.toStringList(properties.get(EntityConstants.TYPE)); + for (String type : types) + RankedObject.putIfHigherRank(layersByType, type, layer, properties); + } + } + + public void removeLayer(SuiteLayer layer, Map properties) { + if (properties.containsKey(Constants.SERVICE_PID)) { + String pid = (String) properties.get(Constants.SERVICE_PID); + if (layersByPid.containsKey(pid)) { + if (layersByPid.get(pid).equals(new RankedObject(layer, properties))) { + layersByPid.remove(pid); + } + } + } + if (properties.containsKey(EntityConstants.TYPE)) { + List types = LangUtils.toStringList(properties.get(EntityConstants.TYPE)); + for (String type : types) { + if (layersByType.containsKey(type)) { + if (layersByType.get(type).equals(new RankedObject(layer, properties))) { + layersByType.remove(type); + } + } + } + } + } + + public void setCmsUserManager(CmsUserManager cmsUserManager) { + this.cmsUserManager = cmsUserManager; + } + + public Repository getRepository() { + return repository; + } + + public void setRepository(Repository repository) { + this.repository = repository; + } + +}