From c811008ac91053f068c26f48427617de9bb7b79c Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Sat, 26 Aug 2017 15:52:57 +0200 Subject: [PATCH] Start making theming separate --- .../argeo/cms/ui/AbstractCmsEntryPoint.java | 69 ++++++++++++++----- .../src/org/argeo/cms/ui/UxContext.java | 11 ++- .../argeo/cms/util/BundleResourceLoader.java | 32 ++------- .../src/org/argeo/cms/util/SimpleApp.java | 63 +++++++++++++++-- .../org/argeo/cms/util/SimpleErgonomics.java | 5 +- .../org/argeo/cms/util/SimpleUxContext.java | 5 ++ .../cms/util/StyleSheetResourceLoader.java | 22 +++--- .../internal/backup/SimpleBackupPurge.java | 28 ++++---- .../eclipse/ui/jcr/AsyncUiEventListener.java | 52 +++++++------- 9 files changed, 183 insertions(+), 104 deletions(-) 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 index 2d83f8ded..43906fae7 100644 --- a/org.argeo.cms.ui/src/org/argeo/cms/ui/AbstractCmsEntryPoint.java +++ b/org.argeo.cms.ui/src/org/argeo/cms/ui/AbstractCmsEntryPoint.java @@ -1,5 +1,6 @@ package org.argeo.cms.ui; +import java.io.IOException; import java.security.PrivilegedAction; import java.util.HashMap; import java.util.Map; @@ -12,6 +13,11 @@ 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.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import javax.servlet.http.HttpServletRequest; @@ -54,7 +60,6 @@ public abstract class AbstractCmsEntryPoint extends AbstractEntryPoint implement private Node node; private String nodePath;// useful when changing auth private String state; - private String page; private Throwable exception; // Client services @@ -133,7 +138,7 @@ public abstract class AbstractCmsEntryPoint extends AbstractEntryPoint implement * The node to return when no node was found (for authenticated users and * anonymous) */ - protected Node getDefaultNode(Session session) throws RepositoryException { + private Node getDefaultNode(Session session) throws RepositoryException { if (!session.hasPermission(defaultPath, "read")) { String userId = session.getUserID(); if (userId.equals(NodeConstants.ROLE_ANONYMOUS)) @@ -248,8 +253,8 @@ public abstract class AbstractCmsEntryPoint extends AbstractEntryPoint implement protected synchronized String setState(String newState) { String previousState = this.state; - Node node = null; - page = null; + String newNodePath = null; + String prefix = null; this.state = newState; if (newState.equals("~")) this.state = ""; @@ -257,25 +262,53 @@ public abstract class AbstractCmsEntryPoint extends AbstractEntryPoint implement try { int firstSlash = state.indexOf('/'); if (firstSlash == 0) { - node = session.getNode(state); - page = ""; + newNodePath = state; + prefix = ""; } else if (firstSlash > 0) { - String prefix = state.substring(0, firstSlash); - String path = state.substring(firstSlash); - if (session.nodeExists(path)) - node = session.getNode(path); - else - throw new CmsException("Data " + path + " does not exist"); - page = prefix; + prefix = state.substring(0, firstSlash); + newNodePath = state.substring(firstSlash); } else { - node = getDefaultNode(session); - page = state; + newNodePath = defaultPath; + prefix = state; + + } + + // auth + int colonIndex = prefix.indexOf(':'); + if (colonIndex > 0) { + String user = prefix.substring(0, colonIndex); + // if (isAnonymous()) { + String token = prefix.substring(colonIndex + 1); + LoginContext lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, new CallbackHandler() { + + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof NameCallback) + ((NameCallback) callback).setName(user); + else if (callback instanceof PasswordCallback) + ((PasswordCallback) callback).setPassword(token.toCharArray()); + } + + } + }); + 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"); + setNode(newNode); } - setNode(node); - String title = publishMetaData(node); + String title = publishMetaData(getNode()); if (log.isTraceEnabled()) - log.trace("node=" + node + ", state=" + state + " (page=" + page + ")"); + log.trace("node=" + newNodePath + ", state=" + state + " (prefix=" + prefix + ")"); return title; } catch (Exception e) { diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/UxContext.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/UxContext.java index f03b88bbb..42d7ab3ef 100644 --- a/org.argeo.cms.ui/src/org/argeo/cms/ui/UxContext.java +++ b/org.argeo.cms.ui/src/org/argeo/cms/ui/UxContext.java @@ -2,8 +2,17 @@ package org.argeo.cms.ui; public interface UxContext { boolean isPortrait(); + boolean isLandscape(); + boolean isSquare(); - + boolean isSmall(); + + /** + * Is a production environment (must be false by default, and be explicitly + * set during the CMS deployment). When false, it can activate additional UI + * capabilities in order to facilitate QA. + */ + boolean isMasterData(); } diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/BundleResourceLoader.java b/org.argeo.cms.ui/src/org/argeo/cms/util/BundleResourceLoader.java index cda00efcd..c8fb8a40c 100644 --- a/org.argeo.cms.ui/src/org/argeo/cms/util/BundleResourceLoader.java +++ b/org.argeo.cms.ui/src/org/argeo/cms/util/BundleResourceLoader.java @@ -7,40 +7,20 @@ import java.net.URL; import org.argeo.cms.CmsException; import org.eclipse.rap.rwt.service.ResourceLoader; import org.osgi.framework.Bundle; -import org.osgi.framework.BundleContext; /** {@link ResourceLoader} implementation wrapping an {@link Bundle}. */ -public class BundleResourceLoader implements ResourceLoader { - private final BundleContext bundleContext; +class BundleResourceLoader implements ResourceLoader { + private final Bundle bundle; - public BundleResourceLoader(BundleContext bundleContext) { - this.bundleContext = bundleContext; + public BundleResourceLoader(Bundle bundle) { + this.bundle = bundle; } @Override - public InputStream getResourceAsStream(String resourceName) - throws IOException { - // TODO deal with other bundles - Bundle bundle = bundleContext.getBundle(); - // String location = - // bundle.getLocation().substring("initial@reference:".length()); - // if (location.startsWith("file:")) { - // Path path = null; - // try { - // path = Paths.get(new URI(location)); - // } catch (URISyntaxException e) { - // e.printStackTrace(); - // } - // if (path != null) { - // Path resourcePath = path.resolve(resourceName); - // if (Files.exists(resourcePath)) - // return Files.newInputStream(resourcePath); - // } - // } + public InputStream getResourceAsStream(String resourceName) throws IOException { URL res = bundle.getResource(resourceName); if (res == null) - throw new CmsException("Resource " + resourceName - + " not found in bundle " + bundle.getSymbolicName()); + throw new CmsException("Resource " + resourceName + " not found in bundle " + bundle.getSymbolicName()); return res.openStream(); } diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleApp.java b/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleApp.java index 780a01d18..b75c70007 100644 --- a/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleApp.java +++ b/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleApp.java @@ -2,13 +2,17 @@ package org.argeo.cms.util; import java.io.IOException; import java.io.InputStream; +import java.net.URL; import java.util.ArrayList; import java.util.Arrays; +import java.util.Enumeration; import java.util.HashMap; +import java.util.HashSet; import java.util.Hashtable; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import javax.jcr.Repository; import javax.jcr.RepositoryException; @@ -40,6 +44,7 @@ import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; +import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceRegistration; @@ -71,8 +76,7 @@ public class SimpleApp implements CmsConstants, ApplicationConfiguration { public void configure(Application application) { try { - StyleSheetResourceLoader styleSheetRL = new StyleSheetResourceLoader(bundleContext); - BundleResourceLoader bundleRL = new BundleResourceLoader(bundleContext); + BundleResourceLoader bundleRL = new BundleResourceLoader(bundleContext.getBundle()); application.setOperationMode(OperationMode.SWT_COMPATIBILITY); // application.setOperationMode(OperationMode.JEE_COMPATIBILITY); @@ -93,6 +97,7 @@ public class SimpleApp implements CmsConstants, ApplicationConfiguration { Map defaultBranding = null; if (branding.containsKey("*")) defaultBranding = branding.get("*"); + String defaultTheme = defaultBranding.get(WebClient.THEME_ID); // entry points for (String page : pages.keySet()) { @@ -103,8 +108,11 @@ public class SimpleApp implements CmsConstants, ApplicationConfiguration { } // favicon if (properties.containsKey(WebClient.FAVICON)) { + String themeId = defaultBranding.get(WebClient.THEME_ID); + Bundle themeBundle = findThemeBundle(themeId); String faviconRelPath = properties.get(WebClient.FAVICON); - application.addResource(faviconRelPath, new BundleResourceLoader(bundleContext)); + application.addResource(faviconRelPath, + new BundleResourceLoader(themeBundle != null ? themeBundle : bundleContext.getBundle())); if (log.isTraceEnabled()) log.trace("Favicon " + faviconRelPath); @@ -128,8 +136,14 @@ public class SimpleApp implements CmsConstants, ApplicationConfiguration { log.info("Page /" + page); } - // stylesheets + // stylesheets and themes + Set themeBundles = new HashSet<>(); for (String themeId : styleSheets.keySet()) { + Bundle themeBundle = findThemeBundle(themeId); + StyleSheetResourceLoader styleSheetRL = new StyleSheetResourceLoader( + themeBundle != null ? themeBundle : bundleContext.getBundle()); + if (themeBundle != null) + themeBundles.add(themeBundle); List cssLst = styleSheets.get(themeId); if (log.isDebugEnabled()) log.debug("Theme " + themeId); @@ -140,6 +154,12 @@ public class SimpleApp implements CmsConstants, ApplicationConfiguration { } } + for (Bundle themeBundle : themeBundles) { + BundleResourceLoader themeBRL = new BundleResourceLoader(themeBundle); + addThemeResources(application, themeBundle, themeBRL, "*.png"); + addThemeResources(application, themeBundle, themeBRL, "*.gif"); + addThemeResources(application, themeBundle, themeBRL, "*.jpg"); + } } catch (RuntimeException e) { // Easier access to initialisation errors log.error("Unexpected exception when configuring RWT application.", e); @@ -147,6 +167,41 @@ public class SimpleApp implements CmsConstants, ApplicationConfiguration { } } + private Bundle findThemeBundle(String themeId) { + if (themeId == null) + return null; + // TODO optimize + // TODO deal with multiple versions + Bundle themeBundle = null; + if (themeId != null) { + for (Bundle bundle : bundleContext.getBundles()) + if (themeId.equals(bundle.getSymbolicName())) { + themeBundle = bundle; + break; + } + } + return themeBundle; + } + + private void addThemeResources(Application application, Bundle themeBundle, BundleResourceLoader themeBRL, + String pattern) { + Enumeration themeResources = themeBundle.findEntries("/", pattern, true); + if (themeResources == null) + return; + while (themeResources.hasMoreElements()) { + String resource = themeResources.nextElement().getPath(); + // remove first '/' so that RWT registers it + resource = resource.substring(1); + if (!resource.endsWith("/")) { + application.addResource(resource, themeBRL); + if (log.isTraceEnabled()) + log.trace("Registered " + resource + " from theme " + themeBundle); + } + + } + + } + public void init() throws RepositoryException { Session session = null; try { diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleErgonomics.java b/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleErgonomics.java index b7b76e4e6..505d482f3 100644 --- a/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleErgonomics.java +++ b/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleErgonomics.java @@ -50,7 +50,9 @@ public class SimpleErgonomics extends AbstractCmsEntryPoint { parent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); parent.setLayout(CmsUtils.noSpaceGridLayout()); - // createAdminArea(parent); + 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); @@ -61,7 +63,6 @@ public class SimpleErgonomics extends AbstractCmsEntryPoint { bodyArea.setData(RWT.CUSTOM_VARIANT, CmsStyles.CMS_BODY); bodyArea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); bodyArea.setLayout(CmsUtils.noSpaceGridLayout()); - uxContext = new SimpleUxContext(); uiInitialized = true; refresh(); } diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleUxContext.java b/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleUxContext.java index 6cb37bde4..a38444658 100644 --- a/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleUxContext.java +++ b/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleUxContext.java @@ -41,4 +41,9 @@ public class SimpleUxContext implements UxContext { return size.x <= small.x || size.y <= small.y; } + @Override + public boolean isMasterData() { + return false; + } + } diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/StyleSheetResourceLoader.java b/org.argeo.cms.ui/src/org/argeo/cms/util/StyleSheetResourceLoader.java index a7e3b6e84..1df98d484 100644 --- a/org.argeo.cms.ui/src/org/argeo/cms/util/StyleSheetResourceLoader.java +++ b/org.argeo.cms.ui/src/org/argeo/cms/util/StyleSheetResourceLoader.java @@ -12,24 +12,21 @@ import org.apache.commons.io.IOUtils; import org.argeo.cms.CmsException; import org.eclipse.rap.rwt.service.ResourceLoader; import org.osgi.framework.Bundle; -import org.osgi.framework.BundleContext; /** {@link ResourceLoader} caching stylesheets. */ -public class StyleSheetResourceLoader implements ResourceLoader { - private final BundleContext bundleContext; - +class StyleSheetResourceLoader implements ResourceLoader { + private Bundle themeBundle; private Map stylesheets = new LinkedHashMap(); - public StyleSheetResourceLoader(BundleContext bundleContext) { - this.bundleContext = bundleContext; + public StyleSheetResourceLoader(Bundle themeBundle) { + this.themeBundle = themeBundle; } @Override - public InputStream getResourceAsStream(String resourceName) - throws IOException { + public InputStream getResourceAsStream(String resourceName) throws IOException { if (!stylesheets.containsKey(resourceName)) { // TODO deal with other bundles - Bundle bundle = bundleContext.getBundle(); + // Bundle bundle = bundleContext.getBundle(); // String location = // bundle.getLocation().substring("initial@reference:".length()); // if (location.startsWith("file:")) { @@ -45,10 +42,11 @@ public class StyleSheetResourceLoader implements ResourceLoader { // return Files.newInputStream(resourcePath); // } // } - URL res = bundle.getResource(resourceName); + + URL res = themeBundle.getResource(resourceName); if (res == null) - throw new CmsException("Resource " + resourceName - + " not found in bundle " + bundle.getSymbolicName()); + throw new CmsException( + "Resource " + resourceName + " not found in bundle " + themeBundle.getSymbolicName()); ByteArrayOutputStream out = new ByteArrayOutputStream(); IOUtils.copy(res.openStream(), out); stylesheets.put(resourceName, new StyleSheet(out.toByteArray())); diff --git a/org.argeo.cms/src/org/argeo/cms/internal/backup/SimpleBackupPurge.java b/org.argeo.cms/src/org/argeo/cms/internal/backup/SimpleBackupPurge.java index 64863759f..33b5b5ebe 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/backup/SimpleBackupPurge.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/backup/SimpleBackupPurge.java @@ -16,6 +16,9 @@ package org.argeo.cms.internal.backup; import java.text.DateFormat; +import java.time.Period; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Date; import java.util.SortedMap; import java.util.TreeMap; @@ -27,8 +30,6 @@ import org.apache.commons.vfs2.FileSystemManager; import org.apache.commons.vfs2.FileSystemOptions; import org.apache.commons.vfs2.Selectors; import org.argeo.cms.CmsException; -import org.joda.time.DateTime; -import org.joda.time.Period; /** Simple backup purge which keeps backups only for a given number of days */ public class SimpleBackupPurge implements BackupPurge { @@ -37,14 +38,13 @@ public class SimpleBackupPurge implements BackupPurge { private Integer daysKept = 30; @Override - public void purge(FileSystemManager fileSystemManager, String base, - String name, DateFormat dateFormat, FileSystemOptions opts) { + public void purge(FileSystemManager fileSystemManager, String base, String name, DateFormat dateFormat, + FileSystemOptions opts) { try { - DateTime nowDt = new DateTime(); - FileObject baseFo = fileSystemManager.resolveFile( - base + '/' + name, opts); + ZonedDateTime nowDt = ZonedDateTime.now(); + FileObject baseFo = fileSystemManager.resolveFile(base + '/' + name, opts); - SortedMap toDelete = new TreeMap(); + SortedMap toDelete = new TreeMap(); int backupCount = 0; // make sure base dir exists @@ -55,9 +55,9 @@ public class SimpleBackupPurge implements BackupPurge { String backupName = backupFo.getName().getBaseName(); Date backupDate = dateFormat.parse(backupName); backupCount++; - - DateTime backupDt = new DateTime(backupDate.getTime()); - Period sinceThen = new Period(backupDt, nowDt); + ZonedDateTime backupDt = ZonedDateTime.ofInstant(backupDate.toInstant(), ZoneId.systemDefault()); + Period sinceThen = Period.between(backupDt.toLocalDate(), nowDt.toLocalDate()); + // new Period(backupDt, nowDt); int days = sinceThen.getDays(); // int days = sinceThen.getMinutes(); if (days > daysKept) { @@ -68,11 +68,9 @@ public class SimpleBackupPurge implements BackupPurge { if (toDelete.size() != 0 && toDelete.size() == backupCount) { // all backups would be deleted // but we want to keep at least one - DateTime lastBackupDt = toDelete.firstKey(); + ZonedDateTime lastBackupDt = toDelete.firstKey(); FileObject keptFo = toDelete.remove(lastBackupDt); - log.warn("Backup " + keptFo - + " kept although it is older than " + daysKept - + " days."); + log.warn("Backup " + keptFo + " kept although it is older than " + daysKept + " days."); } // delete old backups diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/AsyncUiEventListener.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/AsyncUiEventListener.java index 5d5a06f58..88119b8ab 100644 --- a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/AsyncUiEventListener.java +++ b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/AsyncUiEventListener.java @@ -28,10 +28,12 @@ import org.apache.commons.logging.LogFactory; import org.argeo.eclipse.ui.EclipseUiException; import org.eclipse.swt.widgets.Display; -/** {@link EventListener} which simplifies running actions within the UI thread. */ +/** + * {@link EventListener} which simplifies running actions within the UI thread. + */ public abstract class AsyncUiEventListener implements EventListener { -// private final static Log logSuper = LogFactory -// .getLog(AsyncUiEventListener.class); + // private final static Log logSuper = LogFactory + // .getLog(AsyncUiEventListener.class); private final Log logThis = LogFactory.getLog(getClass()); private final Display display; @@ -42,15 +44,13 @@ public abstract class AsyncUiEventListener implements EventListener { } /** Called asynchronously in the UI thread. */ - protected abstract void onEventInUiThread(List events) - throws RepositoryException; + protected abstract void onEventInUiThread(List events) throws RepositoryException; /** * Whether these events should be processed in the UI or skipped with no UI * job created. */ - protected Boolean willProcessInUiThread(List events) - throws RepositoryException { + protected Boolean willProcessInUiThread(List events) throws RepositoryException { return true; } @@ -73,27 +73,27 @@ public abstract class AsyncUiEventListener implements EventListener { throw new EclipseUiException("Cannot test skip events " + events, e); } -// Job job = new Job("JCR Events") { -// protected IStatus run(IProgressMonitor monitor) { -// if (display.isDisposed()) { -// logSuper.warn("Display is disposed cannot update UI"); -// return Status.CANCEL_STATUS; -// } + // Job job = new Job("JCR Events") { + // protected IStatus run(IProgressMonitor monitor) { + // if (display.isDisposed()) { + // logSuper.warn("Display is disposed cannot update UI"); + // return Status.CANCEL_STATUS; + // } - display.asyncExec(new Runnable() { - public void run() { - try { - onEventInUiThread(events); - } catch (RepositoryException e) { - throw new EclipseUiException("Cannot process events " - + events, e); - } + if (!display.isDisposed()) + display.asyncExec(new Runnable() { + public void run() { + try { + onEventInUiThread(events); + } catch (RepositoryException e) { + throw new EclipseUiException("Cannot process events " + events, e); } - }); + } + }); -// return Status.OK_STATUS; -// } -// }; -// job.schedule(); + // return Status.OK_STATUS; + // } + // }; + // job.schedule(); } } -- 2.30.2