From: Mathieu Baudier Date: Thu, 24 Sep 2020 05:30:44 +0000 (+0200) Subject: Start refactoring CMS Web X-Git-Tag: argeo-commons-2.1.89~90 X-Git-Url: https://git.argeo.org/?p=lgpl%2Fargeo-commons.git;a=commitdiff_plain;h=c6bb0874829b5e5fbaf3f1437939ef00bcbd94b0 Start refactoring CMS Web --- diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/AbstractCmsEntryPoint.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/AbstractCmsEntryPoint.java deleted file mode 100644 index 6b61ae396..000000000 --- a/org.argeo.cms.ui/src/org/argeo/cms/ui/AbstractCmsEntryPoint.java +++ /dev/null @@ -1,389 +0,0 @@ -package org.argeo.cms.ui; - -import static org.argeo.naming.SharedSecret.X_SHARED_SECRET; - -import java.io.IOException; -import java.security.PrivilegedAction; -import java.util.HashMap; -import java.util.Map; - -import javax.jcr.Node; -import javax.jcr.PathNotFoundException; -import javax.jcr.Property; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.nodetype.NodeType; -import javax.security.auth.Subject; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.UnsupportedCallbackException; -import javax.security.auth.login.LoginContext; -import javax.security.auth.login.LoginException; -import javax.servlet.http.HttpServletRequest; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.argeo.api.NodeConstants; -import org.argeo.cms.CmsException; -import org.argeo.cms.auth.CurrentUser; -import org.argeo.cms.auth.HttpRequestCallback; -import org.argeo.cms.auth.HttpRequestCallbackHandler; -import org.argeo.eclipse.ui.specific.UiContext; -import org.argeo.jcr.JcrUtils; -import org.argeo.naming.AuthPassword; -import org.argeo.naming.SharedSecret; -import org.eclipse.rap.rwt.RWT; -import org.eclipse.rap.rwt.application.AbstractEntryPoint; -import org.eclipse.rap.rwt.client.WebClient; -import org.eclipse.rap.rwt.client.service.BrowserNavigation; -import org.eclipse.rap.rwt.client.service.BrowserNavigationEvent; -import org.eclipse.rap.rwt.client.service.BrowserNavigationListener; -import org.eclipse.rap.rwt.client.service.JavaScriptExecutor; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Display; -import org.eclipse.swt.widgets.Shell; - -/** Manages history and navigation */ -public abstract class AbstractCmsEntryPoint extends AbstractEntryPoint implements CmsView { - private static final long serialVersionUID = 906558779562569784L; - - private final Log log = LogFactory.getLog(AbstractCmsEntryPoint.class); - - // private final Subject subject; - private LoginContext loginContext; - - private final Repository repository; - private final String workspace; - private final String defaultPath; - private final Map factoryProperties; - - // Current state - private Session session; - private Node node; - private String nodePath;// useful when changing auth - private String state; - private Throwable exception; - - // Client services - private final JavaScriptExecutor jsExecutor; - private final BrowserNavigation browserNavigation; - - public AbstractCmsEntryPoint(Repository repository, String workspace, String defaultPath, - Map factoryProperties) { - this.repository = repository; - this.workspace = workspace; - this.defaultPath = defaultPath; - this.factoryProperties = new HashMap(factoryProperties); - // subject = new Subject(); - - // Initial login - LoginContext lc; - try { - lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, - new HttpRequestCallbackHandler(UiContext.getHttpRequest(), UiContext.getHttpResponse())); - lc.login(); - } catch (LoginException e) { - try { - lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_ANONYMOUS); - lc.login(); - } catch (LoginException e1) { - throw new CmsException("Cannot log in as anonymous", e1); - } - } - authChange(lc); - - jsExecutor = RWT.getClient().getService(JavaScriptExecutor.class); - browserNavigation = RWT.getClient().getService(BrowserNavigation.class); - if (browserNavigation != null) - browserNavigation.addBrowserNavigationListener(new CmsNavigationListener()); - } - - @Override - protected Shell createShell(Display display) { - Shell shell = super.createShell(display); - shell.setData(RWT.CUSTOM_VARIANT, CmsStyles.CMS_SHELL); - display.disposeExec(new Runnable() { - - @Override - public void run() { - if (log.isTraceEnabled()) - log.trace("Logging out " + session); - JcrUtils.logoutQuietly(session); - } - }); - return shell; - } - - @Override - protected final void createContents(final Composite parent) { - UiContext.setData(CmsView.KEY, this); - Subject.doAs(getSubject(), new PrivilegedAction() { - @Override - public Void run() { - try { - initUi(parent); - } catch (Exception e) { - throw new CmsException("Cannot create entrypoint contents", e); - } - return null; - } - }); - } - - /** Create UI */ - protected abstract void initUi(Composite parent); - - /** Recreate UI after navigation or auth change */ - protected abstract void refresh(); - - /** - * The node to return when no node was found (for authenticated users and - * anonymous) - */ -// private Node getDefaultNode(Session session) throws RepositoryException { -// if (!session.hasPermission(defaultPath, "read")) { -// String userId = session.getUserID(); -// if (userId.equals(NodeConstants.ROLE_ANONYMOUS)) -// // TODO throw a special exception -// throw new CmsException("Login required"); -// else -// throw new CmsException("Unauthorized"); -// } -// return session.getNode(defaultPath); -// } - - protected String getBaseTitle() { - return factoryProperties.get(WebClient.PAGE_TITLE); - } - - public void navigateTo(String state) { - exception = null; - String title = setState(state); - doRefresh(); - if (browserNavigation != null) - browserNavigation.pushState(state, title); - } - - // @Override - // public synchronized Subject getSubject() { - // return subject; - // } - - // @Override - // public LoginContext getLoginContext() { - // return loginContext; - // } - protected Subject getSubject() { - return loginContext.getSubject(); - } - - @Override - public boolean isAnonymous() { - return CurrentUser.isAnonymous(getSubject()); - } - - @Override - public synchronized void logout() { - if (loginContext == null) - throw new CmsException("Login context should not be null"); - try { - CurrentUser.logoutCmsSession(loginContext.getSubject()); - loginContext.logout(); - LoginContext anonymousLc = new LoginContext(NodeConstants.LOGIN_CONTEXT_ANONYMOUS); - anonymousLc.login(); - authChange(anonymousLc); - } catch (LoginException e) { - log.error("Cannot logout", e); - } - } - - @Override - public synchronized void authChange(LoginContext lc) { - if (lc == null) - throw new CmsException("Login context cannot be null"); - // logout previous login context - if (this.loginContext != null) - try { - this.loginContext.logout(); - } catch (LoginException e1) { - log.warn("Could not log out: " + e1); - } - this.loginContext = lc; - Subject.doAs(getSubject(), new PrivilegedAction() { - - @Override - public Void run() { - try { - JcrUtils.logoutQuietly(session); - session = repository.login(workspace); - if (nodePath != null) - try { - node = session.getNode(nodePath); - } catch (PathNotFoundException e) { - navigateTo("~"); - } - - // refresh UI - doRefresh(); - } catch (RepositoryException e) { - throw new CmsException("Cannot perform auth change", e); - } - return null; - } - - }); - } - - @Override - public void exception(final Throwable e) { - AbstractCmsEntryPoint.this.exception = e; - log.error("Unexpected exception in CMS", e); - doRefresh(); - } - - protected synchronized void doRefresh() { - Subject.doAs(getSubject(), new PrivilegedAction() { - @Override - public Void run() { - refresh(); - return null; - } - }); - } - - /** Sets the state of the entry point and retrieve the related JCR node. */ - protected synchronized String setState(String newState) { - String previousState = this.state; - - String newNodePath = null; - String prefix = null; - this.state = newState; - if (newState.equals("~")) - this.state = ""; - - try { - int firstSlash = state.indexOf('/'); - if (firstSlash == 0) { - newNodePath = state; - prefix = ""; - } else if (firstSlash > 0) { - prefix = state.substring(0, firstSlash); - newNodePath = state.substring(firstSlash); - } else { - newNodePath = defaultPath; - prefix = state; - - } - - // auth - int colonIndex = prefix.indexOf('$'); - if (colonIndex > 0) { - SharedSecret token = new SharedSecret(new AuthPassword(X_SHARED_SECRET + '$' + prefix)) { - - @Override - public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { - super.handle(callbacks); - // handle HTTP context - for (Callback callback : callbacks) { - if (callback instanceof HttpRequestCallback) { - ((HttpRequestCallback) callback).setRequest(UiContext.getHttpRequest()); - ((HttpRequestCallback) callback).setResponse(UiContext.getHttpResponse()); - } - } - } - }; - LoginContext lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, token); - lc.login(); - authChange(lc);// sets the node as well - // } else { - // // TODO check consistency - // } - } else { - Node newNode = null; - if (session.nodeExists(newNodePath)) - newNode = session.getNode(newNodePath); - else { -// throw new CmsException("Data " + newNodePath + " does not exist"); - newNode = null; - } - setNode(newNode); - } - String title = publishMetaData(getNode()); - - if (log.isTraceEnabled()) - log.trace("node=" + newNodePath + ", state=" + state + " (prefix=" + prefix + ")"); - - return title; - } catch (Exception e) { - log.error("Cannot set state '" + state + "'", e); - if (state.equals("") || newState.equals("~") || newState.equals(previousState)) - return "Unrecoverable exception : " + e.getClass().getSimpleName(); - if (previousState.equals("")) - previousState = "~"; - navigateTo(previousState); - throw new CmsException("Unexpected issue when accessing #" + newState, e); - } - } - - private String publishMetaData(Node node) throws RepositoryException { - // Title - String title; - if (node != null && node.isNodeType(NodeType.MIX_TITLE) && node.hasProperty(Property.JCR_TITLE)) - title = node.getProperty(Property.JCR_TITLE).getString() + " - " + getBaseTitle(); - else - title = getBaseTitle(); - - HttpServletRequest request = UiContext.getHttpRequest(); - if (request == null) - return null; - - StringBuilder js = new StringBuilder(); - if (title == null) - title = ""; - title = title.replace("'", "\\'");// sanitize - js.append("document.title = '" + title + "';"); - jsExecutor.execute(js.toString()); - return title; - } - - // Simply remove some illegal character - // private String clean(String stringToClean) { - // return stringToClean.replaceAll("'", "").replaceAll("\\n", "") - // .replaceAll("\\t", ""); - // } - - protected synchronized Node getNode() { - return node; - } - - private synchronized void setNode(Node node) throws RepositoryException { - this.node = node; - this.nodePath = node == null ? null : node.getPath(); - } - - protected String getState() { - return state; - } - - protected Throwable getException() { - return exception; - } - - protected void resetException() { - exception = null; - } - - protected Session getSession() { - return session; - } - - private class CmsNavigationListener implements BrowserNavigationListener { - private static final long serialVersionUID = -3591018803430389270L; - - @Override - public void navigated(BrowserNavigationEvent event) { - setState(event.getState()); - doRefresh(); - } - } -} \ No newline at end of file diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/script/AppUi.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/script/AppUi.java index 229378674..ad7cadc13 100644 --- a/org.argeo.cms.ui/src/org/argeo/cms/ui/script/AppUi.java +++ b/org.argeo.cms.ui/src/org/argeo/cms/ui/script/AppUi.java @@ -12,7 +12,7 @@ import javax.script.ScriptException; import org.argeo.cms.ui.CmsUiProvider; import org.argeo.cms.ui.util.CmsPane; import org.argeo.cms.ui.util.CmsUiUtils; -import org.argeo.cms.ui.util.SimpleErgonomics; +import org.argeo.cms.web.SimpleErgonomics; import org.argeo.eclipse.ui.Selected; import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.application.Application; diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/script/CmsScriptApp.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/script/CmsScriptApp.java index c3e1a72e6..d3267004e 100644 --- a/org.argeo.cms.ui/src/org/argeo/cms/ui/script/CmsScriptApp.java +++ b/org.argeo.cms.ui/src/org/argeo/cms/ui/script/CmsScriptApp.java @@ -27,7 +27,7 @@ import org.argeo.cms.ui.CmsConstants; import org.argeo.cms.ui.CmsUiProvider; import org.argeo.cms.ui.util.BundleResourceLoader; import org.argeo.cms.ui.util.CmsUiUtils; -import org.argeo.cms.ui.util.SimpleErgonomics; +import org.argeo.cms.web.SimpleErgonomics; import org.eclipse.rap.rwt.application.Application; import org.eclipse.rap.rwt.application.Application.OperationMode; import org.eclipse.rap.rwt.application.ApplicationConfiguration; diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/util/SimpleApp.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/util/SimpleApp.java index 0f5b079c8..c5d86bc5f 100644 --- a/org.argeo.cms.ui/src/org/argeo/cms/ui/util/SimpleApp.java +++ b/org.argeo.cms.ui/src/org/argeo/cms/ui/util/SimpleApp.java @@ -26,6 +26,7 @@ import org.argeo.cms.CmsException; import org.argeo.cms.ui.CmsConstants; import org.argeo.cms.ui.CmsUiProvider; import org.argeo.cms.ui.LifeCycleUiProvider; +import org.argeo.cms.web.SimpleErgonomics; import org.argeo.jcr.JcrUtils; import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.application.Application; diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/util/SimpleErgonomics.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/util/SimpleErgonomics.java deleted file mode 100644 index 92133f572..000000000 --- a/org.argeo.cms.ui/src/org/argeo/cms/ui/util/SimpleErgonomics.java +++ /dev/null @@ -1,229 +0,0 @@ -package org.argeo.cms.ui.util; - -import java.util.Map; - -import javax.jcr.Node; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.argeo.cms.CmsException; -import org.argeo.cms.ui.AbstractCmsEntryPoint; -import org.argeo.cms.ui.CmsImageManager; -import org.argeo.cms.ui.CmsStyles; -import org.argeo.cms.ui.CmsUiProvider; -import org.argeo.cms.ui.UxContext; -import org.eclipse.rap.rwt.RWT; -import org.eclipse.swt.SWT; -import org.eclipse.swt.layout.FillLayout; -import org.eclipse.swt.layout.GridData; -import org.eclipse.swt.layout.GridLayout; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Control; - -/** Simple header/body ergonomics. */ -public class SimpleErgonomics extends AbstractCmsEntryPoint { - private static final long serialVersionUID = 8743413921359548523L; - - private final static Log log = LogFactory.getLog(SimpleErgonomics.class); - - private boolean uiInitialized = false; - private Composite headerArea; - private Composite leftArea; - private Composite rightArea; - private Composite footerArea; - private Composite bodyArea; - private final CmsUiProvider uiProvider; - - private CmsUiProvider header; - private Integer headerHeight = 0; - private Integer footerHeight = 0; - private CmsUiProvider lead; - private CmsUiProvider end; - private CmsUiProvider footer; - - private CmsImageManager imageManager = new DefaultImageManager(); - private UxContext uxContext = null; - - public SimpleErgonomics(Repository repository, String workspace, String defaultPath, CmsUiProvider uiProvider, - Map factoryProperties) { - super(repository, workspace, defaultPath, factoryProperties); - this.uiProvider = uiProvider; - } - - @Override - protected void initUi(Composite parent) { - parent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); - parent.setLayout(CmsUiUtils.noSpaceGridLayout(new GridLayout(3, false))); - - uxContext = new SimpleUxContext(); - if (!getUxContext().isMasterData()) - createAdminArea(parent); - headerArea = new Composite(parent, SWT.NONE); - headerArea.setLayout(new FillLayout()); - GridData headerData = new GridData(SWT.FILL, SWT.FILL, false, false, 3, 1); - headerData.heightHint = headerHeight; - headerArea.setLayoutData(headerData); - - // TODO: bi-directional - leftArea = new Composite(parent, SWT.NONE); - leftArea.setLayoutData(new GridData(SWT.LEAD, SWT.TOP, false, false)); - leftArea.setLayout(CmsUiUtils.noSpaceGridLayout()); - - bodyArea = new Composite(parent, SWT.NONE); - bodyArea.setData(RWT.CUSTOM_VARIANT, CmsStyles.CMS_BODY); - bodyArea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); - bodyArea.setLayout(CmsUiUtils.noSpaceGridLayout()); - - // TODO: bi-directional - rightArea = new Composite(parent, SWT.NONE); - rightArea.setLayoutData(new GridData(SWT.END, SWT.TOP, false, false)); - rightArea.setLayout(CmsUiUtils.noSpaceGridLayout()); - - footerArea = new Composite(parent, SWT.NONE); - // footerArea.setLayout(new FillLayout()); - GridData footerData = new GridData(SWT.FILL, SWT.FILL, false, false, 3, 1); - footerData.heightHint = footerHeight; - footerArea.setLayoutData(footerData); - - uiInitialized = true; - refresh(); - } - - @Override - protected void refresh() { - if (!uiInitialized) - return; - if (getState() == null) - setState(""); - refreshSides(); - refreshBody(); - if (log.isTraceEnabled()) - log.trace("UI refreshed " + getNode()); - } - - protected void createAdminArea(Composite parent) { - } - - @Deprecated - protected void refreshHeader() { - if (header == null) - return; - - for (Control child : headerArea.getChildren()) - child.dispose(); - try { - header.createUi(headerArea, getNode()); - } catch (RepositoryException e) { - throw new CmsException("Cannot refresh header", e); - } - headerArea.layout(true, true); - } - - protected void refreshSides() { - refresh(headerArea, header, CmsStyles.CMS_HEADER); - refresh(leftArea, lead, CmsStyles.CMS_LEAD); - refresh(rightArea, end, CmsStyles.CMS_END); - refresh(footerArea, footer, CmsStyles.CMS_FOOTER); - } - - private void refresh(Composite area, CmsUiProvider uiProvider, String style) { - if (uiProvider == null) - return; - - for (Control child : area.getChildren()) - child.dispose(); - CmsUiUtils.style(area, style); - try { - uiProvider.createUi(area, getNode()); - } catch (RepositoryException e) { - throw new CmsException("Cannot refresh header", e); - } - area.layout(true, true); - } - - protected void refreshBody() { - // Exception - Throwable exception = getException(); - if (exception != null) { - SystemNotifications systemNotifications = new SystemNotifications(bodyArea); - systemNotifications.notifyException(exception); - resetException(); - return; - // TODO report - } - - // clear - for (Control child : bodyArea.getChildren()) - child.dispose(); - bodyArea.setLayout(CmsUiUtils.noSpaceGridLayout()); - - try { - Node node = getNode(); -// if (node == null) -// log.error("Context cannot be null"); -// else - uiProvider.createUi(bodyArea, node); - } catch (RepositoryException e) { - throw new CmsException("Cannot refresh body", e); - } - - bodyArea.layout(true, true); - } - - @Override - public UxContext getUxContext() { - return uxContext; - } - - @Override - public CmsImageManager getImageManager() { - return imageManager; - } - - public void setHeader(CmsUiProvider header) { - this.header = header; - } - - public void setHeaderHeight(Integer headerHeight) { - this.headerHeight = headerHeight; - } - - public void setImageManager(CmsImageManager imageManager) { - this.imageManager = imageManager; - } - - public CmsUiProvider getLead() { - return lead; - } - - public void setLead(CmsUiProvider lead) { - this.lead = lead; - } - - public CmsUiProvider getEnd() { - return end; - } - - public void setEnd(CmsUiProvider end) { - this.end = end; - } - - public CmsUiProvider getFooter() { - return footer; - } - - public void setFooter(CmsUiProvider footer) { - this.footer = footer; - } - - public CmsUiProvider getHeader() { - return header; - } - - public void setFooterHeight(Integer footerHeight) { - this.footerHeight = footerHeight; - } - -} diff --git a/org.argeo.cms.ui/src/org/argeo/cms/web/AbstractCmsEntryPoint.java b/org.argeo.cms.ui/src/org/argeo/cms/web/AbstractCmsEntryPoint.java new file mode 100644 index 000000000..1c5d60bc4 --- /dev/null +++ b/org.argeo.cms.ui/src/org/argeo/cms/web/AbstractCmsEntryPoint.java @@ -0,0 +1,391 @@ +package org.argeo.cms.web; + +import static org.argeo.naming.SharedSecret.X_SHARED_SECRET; + +import java.io.IOException; +import java.security.PrivilegedAction; +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.api.NodeConstants; +import org.argeo.cms.CmsException; +import org.argeo.cms.auth.CurrentUser; +import org.argeo.cms.auth.HttpRequestCallback; +import org.argeo.cms.auth.HttpRequestCallbackHandler; +import org.argeo.cms.ui.CmsStyles; +import org.argeo.cms.ui.CmsView; +import org.argeo.eclipse.ui.specific.UiContext; +import org.argeo.jcr.JcrUtils; +import org.argeo.naming.AuthPassword; +import org.argeo.naming.SharedSecret; +import org.eclipse.rap.rwt.RWT; +import org.eclipse.rap.rwt.application.AbstractEntryPoint; +import org.eclipse.rap.rwt.client.WebClient; +import org.eclipse.rap.rwt.client.service.BrowserNavigation; +import org.eclipse.rap.rwt.client.service.BrowserNavigationEvent; +import org.eclipse.rap.rwt.client.service.BrowserNavigationListener; +import org.eclipse.rap.rwt.client.service.JavaScriptExecutor; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +/** Manages history and navigation */ +public abstract class AbstractCmsEntryPoint extends AbstractEntryPoint implements CmsView { + private static final long serialVersionUID = 906558779562569784L; + + private final Log log = LogFactory.getLog(AbstractCmsEntryPoint.class); + + // private final Subject subject; + private LoginContext loginContext; + + private final Repository repository; + private final String workspace; + private final String defaultPath; + private final Map factoryProperties; + + // Current state + private Session session; + private Node node; + private String nodePath;// useful when changing auth + private String state; + private Throwable exception; + + // Client services + private final JavaScriptExecutor jsExecutor; + private final BrowserNavigation browserNavigation; + + public AbstractCmsEntryPoint(Repository repository, String workspace, String defaultPath, + Map factoryProperties) { + this.repository = repository; + this.workspace = workspace; + this.defaultPath = defaultPath; + this.factoryProperties = new HashMap(factoryProperties); + // subject = new Subject(); + + // Initial login + LoginContext lc; + try { + lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, + new HttpRequestCallbackHandler(UiContext.getHttpRequest(), UiContext.getHttpResponse())); + lc.login(); + } catch (LoginException e) { + try { + lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_ANONYMOUS); + lc.login(); + } catch (LoginException e1) { + throw new CmsException("Cannot log in as anonymous", e1); + } + } + authChange(lc); + + jsExecutor = RWT.getClient().getService(JavaScriptExecutor.class); + browserNavigation = RWT.getClient().getService(BrowserNavigation.class); + if (browserNavigation != null) + browserNavigation.addBrowserNavigationListener(new CmsNavigationListener()); + } + + @Override + protected Shell createShell(Display display) { + Shell shell = super.createShell(display); + shell.setData(RWT.CUSTOM_VARIANT, CmsStyles.CMS_SHELL); + display.disposeExec(new Runnable() { + + @Override + public void run() { + if (log.isTraceEnabled()) + log.trace("Logging out " + session); + JcrUtils.logoutQuietly(session); + } + }); + return shell; + } + + @Override + protected final void createContents(final Composite parent) { + UiContext.setData(CmsView.KEY, this); + Subject.doAs(getSubject(), new PrivilegedAction() { + @Override + public Void run() { + try { + initUi(parent); + } catch (Exception e) { + throw new CmsException("Cannot create entrypoint contents", e); + } + return null; + } + }); + } + + /** Create UI */ + protected abstract void initUi(Composite parent); + + /** Recreate UI after navigation or auth change */ + protected abstract void refresh(); + + /** + * The node to return when no node was found (for authenticated users and + * anonymous) + */ +// private Node getDefaultNode(Session session) throws RepositoryException { +// if (!session.hasPermission(defaultPath, "read")) { +// String userId = session.getUserID(); +// if (userId.equals(NodeConstants.ROLE_ANONYMOUS)) +// // TODO throw a special exception +// throw new CmsException("Login required"); +// else +// throw new CmsException("Unauthorized"); +// } +// return session.getNode(defaultPath); +// } + + protected String getBaseTitle() { + return factoryProperties.get(WebClient.PAGE_TITLE); + } + + public void navigateTo(String state) { + exception = null; + String title = setState(state); + doRefresh(); + if (browserNavigation != null) + browserNavigation.pushState(state, title); + } + + // @Override + // public synchronized Subject getSubject() { + // return subject; + // } + + // @Override + // public LoginContext getLoginContext() { + // return loginContext; + // } + protected Subject getSubject() { + return loginContext.getSubject(); + } + + @Override + public boolean isAnonymous() { + return CurrentUser.isAnonymous(getSubject()); + } + + @Override + public synchronized void logout() { + if (loginContext == null) + throw new CmsException("Login context should not be null"); + try { + CurrentUser.logoutCmsSession(loginContext.getSubject()); + loginContext.logout(); + LoginContext anonymousLc = new LoginContext(NodeConstants.LOGIN_CONTEXT_ANONYMOUS); + anonymousLc.login(); + authChange(anonymousLc); + } catch (LoginException e) { + log.error("Cannot logout", e); + } + } + + @Override + public synchronized void authChange(LoginContext lc) { + if (lc == null) + throw new CmsException("Login context cannot be null"); + // logout previous login context + if (this.loginContext != null) + try { + this.loginContext.logout(); + } catch (LoginException e1) { + log.warn("Could not log out: " + e1); + } + this.loginContext = lc; + Subject.doAs(getSubject(), new PrivilegedAction() { + + @Override + public Void run() { + try { + JcrUtils.logoutQuietly(session); + session = repository.login(workspace); + if (nodePath != null) + try { + node = session.getNode(nodePath); + } catch (PathNotFoundException e) { + navigateTo("~"); + } + + // refresh UI + doRefresh(); + } catch (RepositoryException e) { + throw new CmsException("Cannot perform auth change", e); + } + return null; + } + + }); + } + + @Override + public void exception(final Throwable e) { + AbstractCmsEntryPoint.this.exception = e; + log.error("Unexpected exception in CMS", e); + doRefresh(); + } + + protected synchronized void doRefresh() { + Subject.doAs(getSubject(), new PrivilegedAction() { + @Override + public Void run() { + refresh(); + return null; + } + }); + } + + /** Sets the state of the entry point and retrieve the related JCR node. */ + protected synchronized String setState(String newState) { + String previousState = this.state; + + String newNodePath = null; + String prefix = null; + this.state = newState; + if (newState.equals("~")) + this.state = ""; + + try { + int firstSlash = state.indexOf('/'); + if (firstSlash == 0) { + newNodePath = state; + prefix = ""; + } else if (firstSlash > 0) { + prefix = state.substring(0, firstSlash); + newNodePath = state.substring(firstSlash); + } else { + newNodePath = defaultPath; + prefix = state; + + } + + // auth + int colonIndex = prefix.indexOf('$'); + if (colonIndex > 0) { + SharedSecret token = new SharedSecret(new AuthPassword(X_SHARED_SECRET + '$' + prefix)) { + + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + super.handle(callbacks); + // handle HTTP context + for (Callback callback : callbacks) { + if (callback instanceof HttpRequestCallback) { + ((HttpRequestCallback) callback).setRequest(UiContext.getHttpRequest()); + ((HttpRequestCallback) callback).setResponse(UiContext.getHttpResponse()); + } + } + } + }; + LoginContext lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, token); + lc.login(); + authChange(lc);// sets the node as well + // } else { + // // TODO check consistency + // } + } else { + Node newNode = null; + if (session.nodeExists(newNodePath)) + newNode = session.getNode(newNodePath); + else { +// throw new CmsException("Data " + newNodePath + " does not exist"); + newNode = null; + } + setNode(newNode); + } + String title = publishMetaData(getNode()); + + if (log.isTraceEnabled()) + log.trace("node=" + newNodePath + ", state=" + state + " (prefix=" + prefix + ")"); + + return title; + } catch (Exception e) { + log.error("Cannot set state '" + state + "'", e); + if (state.equals("") || newState.equals("~") || newState.equals(previousState)) + return "Unrecoverable exception : " + e.getClass().getSimpleName(); + if (previousState.equals("")) + previousState = "~"; + navigateTo(previousState); + throw new CmsException("Unexpected issue when accessing #" + newState, e); + } + } + + private String publishMetaData(Node node) throws RepositoryException { + // Title + String title; + if (node != null && node.isNodeType(NodeType.MIX_TITLE) && node.hasProperty(Property.JCR_TITLE)) + title = node.getProperty(Property.JCR_TITLE).getString() + " - " + getBaseTitle(); + else + title = getBaseTitle(); + + HttpServletRequest request = UiContext.getHttpRequest(); + if (request == null) + return null; + + StringBuilder js = new StringBuilder(); + if (title == null) + title = ""; + title = title.replace("'", "\\'");// sanitize + js.append("document.title = '" + title + "';"); + jsExecutor.execute(js.toString()); + return title; + } + + // Simply remove some illegal character + // private String clean(String stringToClean) { + // return stringToClean.replaceAll("'", "").replaceAll("\\n", "") + // .replaceAll("\\t", ""); + // } + + protected synchronized Node getNode() { + return node; + } + + private synchronized void setNode(Node node) throws RepositoryException { + this.node = node; + this.nodePath = node == null ? null : node.getPath(); + } + + protected String getState() { + return state; + } + + protected Throwable getException() { + return exception; + } + + protected void resetException() { + exception = null; + } + + protected Session getSession() { + return session; + } + + private class CmsNavigationListener implements BrowserNavigationListener { + private static final long serialVersionUID = -3591018803430389270L; + + @Override + public void navigated(BrowserNavigationEvent event) { + setState(event.getState()); + doRefresh(); + } + } +} \ No newline at end of file diff --git a/org.argeo.cms.ui/src/org/argeo/cms/web/MinimalWebApp.java b/org.argeo.cms.ui/src/org/argeo/cms/web/MinimalWebApp.java new file mode 100644 index 000000000..b4af7595d --- /dev/null +++ b/org.argeo.cms.ui/src/org/argeo/cms/web/MinimalWebApp.java @@ -0,0 +1,53 @@ +package org.argeo.cms.web; + +import java.util.HashMap; +import java.util.Map; + +import org.argeo.cms.ui.util.CmsTheme; +import org.eclipse.rap.rwt.RWT; +import org.eclipse.rap.rwt.application.AbstractEntryPoint; +import org.eclipse.rap.rwt.application.Application; +import org.eclipse.rap.rwt.application.ApplicationConfiguration; +import org.eclipse.rap.rwt.client.WebClient; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.osgi.framework.BundleContext; + +/** Lightweight web app using only RWT and not the whole Eclipse platform. */ +public class MinimalWebApp implements ApplicationConfiguration { + + private CmsTheme theme; + + public void init(BundleContext bundleContext) { + theme = new CmsTheme(bundleContext); + } + + public void destroy() { + + } + + @Override + public void configure(Application application) { + theme.apply(application); + + Map properties = new HashMap<>(); + properties.put(WebClient.THEME_ID, RWT.DEFAULT_THEME_ID); + properties.put(WebClient.HEAD_HTML, theme.getAdditionalHeaders()); + application.addEntryPoint("/test", TextEntryPoint.class, properties); + + } + + static class TextEntryPoint extends AbstractEntryPoint { + private static final long serialVersionUID = 2245808564950897823L; + + @Override + protected void createContents(Composite parent) { + parent.setLayout(new GridLayout()); + new Label(parent, SWT.NONE).setText("Hello World!"); + } + + } + +} diff --git a/org.argeo.cms.ui/src/org/argeo/cms/web/SimpleErgonomics.java b/org.argeo.cms.ui/src/org/argeo/cms/web/SimpleErgonomics.java new file mode 100644 index 000000000..0ca37e2fd --- /dev/null +++ b/org.argeo.cms.ui/src/org/argeo/cms/web/SimpleErgonomics.java @@ -0,0 +1,232 @@ +package org.argeo.cms.web; + +import java.util.Map; + +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.cms.CmsException; +import org.argeo.cms.ui.CmsImageManager; +import org.argeo.cms.ui.CmsStyles; +import org.argeo.cms.ui.CmsUiProvider; +import org.argeo.cms.ui.UxContext; +import org.argeo.cms.ui.util.CmsUiUtils; +import org.argeo.cms.ui.util.DefaultImageManager; +import org.argeo.cms.ui.util.SimpleUxContext; +import org.argeo.cms.ui.util.SystemNotifications; +import org.eclipse.rap.rwt.RWT; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + +/** Simple header/body ergonomics. */ +public class SimpleErgonomics extends AbstractCmsEntryPoint { + private static final long serialVersionUID = 8743413921359548523L; + + private final static Log log = LogFactory.getLog(SimpleErgonomics.class); + + private boolean uiInitialized = false; + private Composite headerArea; + private Composite leftArea; + private Composite rightArea; + private Composite footerArea; + private Composite bodyArea; + private final CmsUiProvider uiProvider; + + private CmsUiProvider header; + private Integer headerHeight = 0; + private Integer footerHeight = 0; + private CmsUiProvider lead; + private CmsUiProvider end; + private CmsUiProvider footer; + + private CmsImageManager imageManager = new DefaultImageManager(); + private UxContext uxContext = null; + + public SimpleErgonomics(Repository repository, String workspace, String defaultPath, CmsUiProvider uiProvider, + Map factoryProperties) { + super(repository, workspace, defaultPath, factoryProperties); + this.uiProvider = uiProvider; + } + + @Override + protected void initUi(Composite parent) { + parent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + parent.setLayout(CmsUiUtils.noSpaceGridLayout(new GridLayout(3, false))); + + uxContext = new SimpleUxContext(); + if (!getUxContext().isMasterData()) + createAdminArea(parent); + headerArea = new Composite(parent, SWT.NONE); + headerArea.setLayout(new FillLayout()); + GridData headerData = new GridData(SWT.FILL, SWT.FILL, false, false, 3, 1); + headerData.heightHint = headerHeight; + headerArea.setLayoutData(headerData); + + // TODO: bi-directional + leftArea = new Composite(parent, SWT.NONE); + leftArea.setLayoutData(new GridData(SWT.LEAD, SWT.TOP, false, false)); + leftArea.setLayout(CmsUiUtils.noSpaceGridLayout()); + + bodyArea = new Composite(parent, SWT.NONE); + bodyArea.setData(RWT.CUSTOM_VARIANT, CmsStyles.CMS_BODY); + bodyArea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + bodyArea.setLayout(CmsUiUtils.noSpaceGridLayout()); + + // TODO: bi-directional + rightArea = new Composite(parent, SWT.NONE); + rightArea.setLayoutData(new GridData(SWT.END, SWT.TOP, false, false)); + rightArea.setLayout(CmsUiUtils.noSpaceGridLayout()); + + footerArea = new Composite(parent, SWT.NONE); + // footerArea.setLayout(new FillLayout()); + GridData footerData = new GridData(SWT.FILL, SWT.FILL, false, false, 3, 1); + footerData.heightHint = footerHeight; + footerArea.setLayoutData(footerData); + + uiInitialized = true; + refresh(); + } + + @Override + protected void refresh() { + if (!uiInitialized) + return; + if (getState() == null) + setState(""); + refreshSides(); + refreshBody(); + if (log.isTraceEnabled()) + log.trace("UI refreshed " + getNode()); + } + + protected void createAdminArea(Composite parent) { + } + + @Deprecated + protected void refreshHeader() { + if (header == null) + return; + + for (Control child : headerArea.getChildren()) + child.dispose(); + try { + header.createUi(headerArea, getNode()); + } catch (RepositoryException e) { + throw new CmsException("Cannot refresh header", e); + } + headerArea.layout(true, true); + } + + protected void refreshSides() { + refresh(headerArea, header, CmsStyles.CMS_HEADER); + refresh(leftArea, lead, CmsStyles.CMS_LEAD); + refresh(rightArea, end, CmsStyles.CMS_END); + refresh(footerArea, footer, CmsStyles.CMS_FOOTER); + } + + private void refresh(Composite area, CmsUiProvider uiProvider, String style) { + if (uiProvider == null) + return; + + for (Control child : area.getChildren()) + child.dispose(); + CmsUiUtils.style(area, style); + try { + uiProvider.createUi(area, getNode()); + } catch (RepositoryException e) { + throw new CmsException("Cannot refresh header", e); + } + area.layout(true, true); + } + + protected void refreshBody() { + // Exception + Throwable exception = getException(); + if (exception != null) { + SystemNotifications systemNotifications = new SystemNotifications(bodyArea); + systemNotifications.notifyException(exception); + resetException(); + return; + // TODO report + } + + // clear + for (Control child : bodyArea.getChildren()) + child.dispose(); + bodyArea.setLayout(CmsUiUtils.noSpaceGridLayout()); + + try { + Node node = getNode(); +// if (node == null) +// log.error("Context cannot be null"); +// else + uiProvider.createUi(bodyArea, node); + } catch (RepositoryException e) { + throw new CmsException("Cannot refresh body", e); + } + + bodyArea.layout(true, true); + } + + @Override + public UxContext getUxContext() { + return uxContext; + } + + @Override + public CmsImageManager getImageManager() { + return imageManager; + } + + public void setHeader(CmsUiProvider header) { + this.header = header; + } + + public void setHeaderHeight(Integer headerHeight) { + this.headerHeight = headerHeight; + } + + public void setImageManager(CmsImageManager imageManager) { + this.imageManager = imageManager; + } + + public CmsUiProvider getLead() { + return lead; + } + + public void setLead(CmsUiProvider lead) { + this.lead = lead; + } + + public CmsUiProvider getEnd() { + return end; + } + + public void setEnd(CmsUiProvider end) { + this.end = end; + } + + public CmsUiProvider getFooter() { + return footer; + } + + public void setFooter(CmsUiProvider footer) { + this.footer = footer; + } + + public CmsUiProvider getHeader() { + return header; + } + + public void setFooterHeight(Integer footerHeight) { + this.footerHeight = footerHeight; + } + +}