From: Mathieu Baudier Date: Mon, 24 Nov 2014 13:57:18 +0000 (+0000) Subject: Move CMS to Commons X-Git-Tag: argeo-commons-2.1.30~540 X-Git-Url: https://git.argeo.org/?a=commitdiff_plain;h=998cdf60d00ffc35d987bdb373a9676b095f16f8;p=lgpl%2Fargeo-commons.git Move CMS to Commons git-svn-id: https://svn.argeo.org/commons/trunk@7507 4cfe0d0a-d680-48aa-b62c-e0a02a3f76cc --- diff --git a/org.argeo.cms/.classpath b/org.argeo.cms/.classpath new file mode 100644 index 000000000..ad32c83a7 --- /dev/null +++ b/org.argeo.cms/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/org.argeo.cms/.project b/org.argeo.cms/.project new file mode 100644 index 000000000..2e18c90d0 --- /dev/null +++ b/org.argeo.cms/.project @@ -0,0 +1,28 @@ + + + org.argeo.cms + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/org.argeo.cms/.settings/org.eclipse.jdt.core.prefs b/org.argeo.cms/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..c537b6306 --- /dev/null +++ b/org.argeo.cms/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/org.argeo.cms/.settings/org.eclipse.pde.core.prefs b/org.argeo.cms/.settings/org.eclipse.pde.core.prefs new file mode 100644 index 000000000..f29e940a0 --- /dev/null +++ b/org.argeo.cms/.settings/org.eclipse.pde.core.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +pluginProject.extensions=false +resolve.requirebundle=false diff --git a/org.argeo.cms/META-INF/spring/backend.xml b/org.argeo.cms/META-INF/spring/backend.xml new file mode 100644 index 000000000..c32676f6b --- /dev/null +++ b/org.argeo.cms/META-INF/spring/backend.xml @@ -0,0 +1,32 @@ + + + + + + + + + /org/argeo/cms/cms.cnd + + + + + + + + + + + + cmsRepository + + + + \ No newline at end of file diff --git a/org.argeo.cms/META-INF/spring/osgi.xml b/org.argeo.cms/META-INF/spring/osgi.xml new file mode 100644 index 000000000..3a103ce87 --- /dev/null +++ b/org.argeo.cms/META-INF/spring/osgi.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms/bnd.bnd b/org.argeo.cms/bnd.bnd new file mode 100644 index 000000000..848f45b55 --- /dev/null +++ b/org.argeo.cms/bnd.bnd @@ -0,0 +1,6 @@ +Import-Package: org.springframework.core,\ + org.eclipse.core.commands,\ + org.eclipse.swt,\ + javax.jcr.security,\ + * +Private-Package: org.argeo.cam.internal.* \ No newline at end of file diff --git a/org.argeo.cms/build.properties b/org.argeo.cms/build.properties new file mode 100644 index 000000000..34d2e4d2d --- /dev/null +++ b/org.argeo.cms/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/org.argeo.cms/icons/loading.gif b/org.argeo.cms/icons/loading.gif new file mode 100644 index 000000000..3288d1035 Binary files /dev/null and b/org.argeo.cms/icons/loading.gif differ diff --git a/org.argeo.cms/icons/noPic-goldenRatio-640px.png b/org.argeo.cms/icons/noPic-goldenRatio-640px.png new file mode 100644 index 000000000..039650638 Binary files /dev/null and b/org.argeo.cms/icons/noPic-goldenRatio-640px.png differ diff --git a/org.argeo.cms/icons/noPic-square-640px.png b/org.argeo.cms/icons/noPic-square-640px.png new file mode 100644 index 000000000..8e3abb518 Binary files /dev/null and b/org.argeo.cms/icons/noPic-square-640px.png differ diff --git a/org.argeo.cms/pom.xml b/org.argeo.cms/pom.xml new file mode 100644 index 000000000..96ad206b1 --- /dev/null +++ b/org.argeo.cms/pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + + org.argeo.connect + argeo-connect + 2.1.13-SNAPSHOT + .. + + org.argeo.cms + Connect Content Management System + jar + + + + + + + + + + + + + + + + + + + + + + + + + + org.argeo.commons.server + org.argeo.server.jcr + ${version.argeo-commons} + + + + org.argeo.commons.security + org.argeo.security.core + ${version.argeo-commons} + + + + + org.argeo.tp.rap.addons + org.eclipse.rap.addons.fileupload + + + org.argeo.tp.rap.addons + org.eclipse.rap.addons.filedialog + + + + + org.argeo.tp.rap.platform + org.eclipse.rap.rwt + + + org.argeo.tp.rap.platform + org.eclipse.rap.jface + + + org.argeo.tp.rap.platform + org.eclipse.core.commands + + + + + org.argeo.tp + org.springframework.osgi.core + + + org.argeo.tp.rap.platform + org.eclipse.osgi + provided + + + \ No newline at end of file diff --git a/org.argeo.cms/src/org/argeo/cms/AbstractCmsEntryPoint.java b/org.argeo.cms/src/org/argeo/cms/AbstractCmsEntryPoint.java new file mode 100644 index 000000000..8d68fc120 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/AbstractCmsEntryPoint.java @@ -0,0 +1,260 @@ +package org.argeo.cms; + +import java.util.Locale; +import java.util.ResourceBundle; + +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.jcr.JcrUtils; +import org.eclipse.rap.rwt.RWT; +import org.eclipse.rap.rwt.application.AbstractEntryPoint; +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.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.springframework.security.context.SecurityContextHolder; + +/** Manages history and navigation */ +public abstract class AbstractCmsEntryPoint extends AbstractEntryPoint + implements CmsSession { + private final Log log = LogFactory.getLog(AbstractCmsEntryPoint.class); + + private Repository repository; + private String workspace; + private Session session; + + // current state + private Node node; + private String state; + private String page; + private Throwable exception; + + private BrowserNavigation history; + + public AbstractCmsEntryPoint(Repository repository, String workspace) { + if (SecurityContextHolder.getContext().getAuthentication() == null) + logAsAnonymous(); + + this.repository = repository; + this.workspace = workspace; + authChange(); + + history = RWT.getClient().getService(BrowserNavigation.class); + if (history != null) + history.addBrowserNavigationListener(new CmsNavigationListener()); + + // RWT.setLocale(Locale.FRANCE); + } + + @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; + } + + /** Recreate header UI */ + protected abstract void refreshHeader(); + + /** Recreate body UI */ + protected abstract void refreshBody(); + + /** Log as anonymous */ + protected abstract void logAsAnonymous(); + + /** + * The node to return when no node was found (for authenticated users and + * anonymous) + */ + protected abstract Node getDefaultNode(Session session) + throws RepositoryException; + + /** + * Reasonable default since it is a nt:hierarchyNode and is thus compatible + * with the obvious default folder type, nt:folder, conceptual equivalent of + * an empty text file in an operating system. To be overridden. + */ + protected String getDefaultNewNodeType() { + return CmsTypes.CMS_TEXT; + } + + /** Default new folder type (used in mkdirs) is nt:folder. To be overridden. */ + protected String getDefaultNewFolderType() { + return NodeType.NT_FOLDER; + } + + public void navigateTo(String state) { + exception = null; + setState(state); + refreshBody(); + if (history != null) + history.pushState(state, state); + } + + @Override + public void authChange() { + try { + String currentPath = null; + if (node != null) + currentPath = node.getPath(); + JcrUtils.logoutQuietly(session); + + if (SecurityContextHolder.getContext().getAuthentication() == null) + logAsAnonymous(); + session = repository.login(workspace); + if (currentPath != null) + node = session.getNode(currentPath); + + // refresh UI + refreshHeader(); + refreshBody(); + } catch (RepositoryException e) { + throw new CmsException("Cannot perform auth change", e); + } + + } + + @Override + public void exception(Throwable e) { + this.exception = e; + log.error("Unexpected exception in CMS", e); + refreshBody(); + } + + @Override + public Object local(Msg msg) { + String key = msg.getId(); + int lastDot = key.lastIndexOf('.'); + String className = key.substring(0, lastDot); + String fieldName = key.substring(lastDot + 1); + Locale locale = RWT.getLocale(); + ResourceBundle rb = ResourceBundle.getBundle(className, locale, + msg.getClassLoader()); + return rb.getString(fieldName); + } + + /** Sets the state of the entry point and retrieve the related JCR node. */ + protected synchronized void setState(String newState) { + String previousState = this.state; + + node = null; + page = null; + this.state = newState; + + try { + int firstSlash = state.indexOf('/'); + if (firstSlash == 0) { + if (!session.nodeExists(state)) + node = addNode(session, state, null); + else + node = session.getNode(state); + page = ""; + } else if (firstSlash > 0) { + String prefix = state.substring(0, firstSlash); + String path = state.substring(firstSlash); + if (session.getWorkspace().getNodeTypeManager() + .hasNodeType(prefix)) { + String nodeType = prefix; + if (!session.nodeExists(path)) + node = addNode(session, path, nodeType); + else { + node = session.getNode(path); + if (!node.isNodeType(nodeType)) + throw new CmsException("Node " + path + + " not of type " + nodeType); + } + } else if ("delete".equals(prefix)) { + if (session.itemExists(path)) { + Node nodeToDelete = session.getNode(path); + // TODO "Are you sure?" + nodeToDelete.remove(); + session.save(); + log.debug("Deleted " + path); + navigateTo(previousState); + } else + throw new CmsException("Data " + path + + " does not exist"); + } else { + if (session.itemExists(path)) + node = session.getNode(path); + else + throw new CmsException("Data " + path + + " does not exist"); + } + page = prefix; + } else { + node = getDefaultNode(session); + if (state.equals("~")) + page = ""; + else + page = state; + } + + if (log.isTraceEnabled()) + log.trace("page=" + page + ", node=" + node + ", state=" + + state); + + } catch (RepositoryException e) { + throw new CmsException("Cannot retrieve node", e); + } + } + + protected Node addNode(Session session, String path, String nodeType) + throws RepositoryException { + return JcrUtils.mkdirs(session, path, nodeType != null ? nodeType + : getDefaultNewNodeType(), getDefaultNewFolderType(), false); + // not saved, so that the UI can discard it later on + } + + protected Node getNode() { + return node; + } + + @Override + public String getState() { + return state; + } + + protected String getPage() { + return page; + } + + 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()); + refreshBody(); + } + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/BundleResourceLoader.java b/org.argeo.cms/src/org/argeo/cms/BundleResourceLoader.java new file mode 100644 index 000000000..e42a00184 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/BundleResourceLoader.java @@ -0,0 +1,31 @@ +package org.argeo.cms; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +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; + + public BundleResourceLoader(BundleContext bundleContext) { + this.bundleContext = bundleContext; + } + + @Override + public InputStream getResourceAsStream(String resourceName) + throws IOException { + // TODO deal with other bundles + Bundle bundle = bundleContext.getBundle(); + URL res = bundle.getResource(resourceName); + if (res == null) + throw new CmsException("Resource " + resourceName + + " not found in bundle " + bundle.getSymbolicName()); + return bundleContext.getBundle().getResource(resourceName).openStream(); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/CmsApplication.java b/org.argeo.cms/src/org/argeo/cms/CmsApplication.java new file mode 100644 index 000000000..afb915d24 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/CmsApplication.java @@ -0,0 +1,204 @@ +package org.argeo.cms; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.eclipse.rap.rwt.application.Application; +import org.eclipse.rap.rwt.application.Application.OperationMode; +import org.eclipse.rap.rwt.application.ApplicationConfiguration; +import org.eclipse.rap.rwt.application.EntryPointFactory; +import org.eclipse.rap.rwt.application.ExceptionHandler; +import org.eclipse.rap.rwt.client.WebClient; +import org.eclipse.rap.rwt.lifecycle.PhaseEvent; +import org.eclipse.rap.rwt.lifecycle.PhaseId; +import org.eclipse.rap.rwt.lifecycle.PhaseListener; +import org.eclipse.rap.rwt.service.ResourceLoader; +import org.osgi.framework.BundleContext; +import org.springframework.osgi.context.BundleContextAware; + +/** Configures an Argeo CMS RWT application. */ +public class CmsApplication implements CmsConstants, ApplicationConfiguration, + BundleContextAware { + final static Log log = LogFactory.getLog(CmsApplication.class); + + private Map entryPoints = new HashMap(); + private Map> entryPointsBranding = new HashMap>(); + private Map> styleSheets = new HashMap>(); + + private List resources = new ArrayList(); + + // private Bundle clientScriptingBundle; + private BundleContext bundleContext; + + public void configure(Application application) { + try { + application.setOperationMode(OperationMode.SWT_COMPATIBILITY); + application.setExceptionHandler(new CmsExceptionHandler()); + + // TODO load all pics under icons + // loading animated gif + application.addResource(LOADING_IMAGE, + createResourceLoader(LOADING_IMAGE)); + // empty image + application.addResource(NO_IMAGE, createResourceLoader(NO_IMAGE)); + + for (String resource : resources) { + // URL res = bundleContext.getBundle().getResource(resource); + // if (res == null) + // throw new CmsException("Resource " + resource + // + " not found"); + application.addResource(resource, new BundleResourceLoader( + bundleContext)); + if (log.isDebugEnabled()) + log.debug("Registered resource " + resource); + } + + // entry points + for (String entryPoint : entryPoints.keySet()) { + Map properties = new HashMap(); + if (entryPointsBranding.containsKey(entryPoint)) { + properties = entryPointsBranding.get(entryPoint); + if (properties.containsKey(WebClient.FAVICON)) { + String faviconRelPath = properties + .get(WebClient.FAVICON); + // URL res = bundleContext.getBundle().getResource( + // faviconRelPath); + application.addResource(faviconRelPath, + new BundleResourceLoader(bundleContext)); + if (log.isTraceEnabled()) + log.trace("Registered favicon " + faviconRelPath); + + } + + if (!properties.containsKey(WebClient.BODY_HTML)) + properties.put(WebClient.BODY_HTML, + DEFAULT_LOADING_BODY); + } + + application.addEntryPoint("/" + entryPoint, + entryPoints.get(entryPoint), properties); + log.info("Registered entry point /" + entryPoint); + } + + // stylesheets + for (String themeId : styleSheets.keySet()) { + List cssLst = styleSheets.get(themeId); + for (String css : cssLst) { + // URL res = bundleContext.getBundle().getResource(css); + // if (res == null) + // throw new CmsException("Stylesheet " + css + // + " not found"); + application.addStyleSheet(themeId, css, + new BundleResourceLoader(bundleContext)); + } + + } + + application.addPhaseListener(new CmsPhaseListener()); + + // registerClientScriptingResources(application); + } catch (RuntimeException e) { + // Easier access to initialisation errors + log.error("Unexpected exception when configuring RWT application.", + e); + throw e; + } + } + + // see Eclipse.org bug 369957 + // private void registerClientScriptingResources(Application application) { + // if (clientScriptingBundle != null) { + // String className = + // "org.eclipse.rap.clientscripting.internal.resources.ClientScriptingResources"; + // try { + // Class resourceClass = clientScriptingBundle + // .loadClass(className); + // Method registerMethod = resourceClass.getMethod("register", + // Application.class); + // registerMethod.invoke(null, application); + // } catch (Exception exception) { + // throw new RuntimeException(exception); + // } + // } + // } + + private static ResourceLoader createResourceLoader(final String resourceName) { + return new ResourceLoader() { + public InputStream getResourceAsStream(String resourceName) + throws IOException { + return getClass().getClassLoader().getResourceAsStream( + resourceName); + } + }; + } + + public void setEntryPoints( + Map entryPointFactories) { + this.entryPoints = entryPointFactories; + } + + public void setEntryPointsBranding( + Map> entryPointBranding) { + this.entryPointsBranding = entryPointBranding; + } + + public void setStyleSheets(Map> styleSheets) { + this.styleSheets = styleSheets; + } + + // public void setClientScriptingBundle(Bundle clientScriptingBundle) { + // this.clientScriptingBundle = clientScriptingBundle; + // } + + public void setBundleContext(BundleContext bundleContext) { + this.bundleContext = bundleContext; + } + + public void setResources(List resources) { + this.resources = resources; + } + + class CmsExceptionHandler implements ExceptionHandler { + + @Override + public void handleException(Throwable throwable) { + CmsSession.current.get().exception(throwable); + } + + } + + class CmsPhaseListener implements PhaseListener { + private static final long serialVersionUID = -1966645586738534609L; + + @Override + public PhaseId getPhaseId() { + return PhaseId.RENDER; + } + + @Override + public void beforePhase(PhaseEvent event) { + CmsSession cmsSession = CmsSession.current.get(); + String state = cmsSession.getState(); + if (state == null) + cmsSession.navigateTo("~"); + } + + @Override + public void afterPhase(PhaseEvent event) { + } + } + + /* + * TEXTS + */ + private static String DEFAULT_LOADING_BODY = "" + + "" + + ""; +} diff --git a/org.argeo.cms/src/org/argeo/cms/CmsConstants.java b/org.argeo.cms/src/org/argeo/cms/CmsConstants.java new file mode 100644 index 000000000..9d299dcbc --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/CmsConstants.java @@ -0,0 +1,18 @@ +package org.argeo.cms; + +import org.eclipse.rap.rwt.RWT; +import org.eclipse.swt.graphics.Point; + +/** Commons constants */ +public interface CmsConstants { + // DATAKEYS + public static final String STYLE = RWT.CUSTOM_VARIANT; + public static final String MARKUP = RWT.MARKUP_ENABLED; + + // STANDARD RESOURCES + public static final String LOADING_IMAGE = "icons/loading.gif"; + + public static final String NO_IMAGE = "icons/noPic-square-640px.png"; + public static final Point NO_IMAGE_SIZE = new Point(640, 640); + public static final Float NO_IMAGE_RATIO = 1f; +} diff --git a/org.argeo.cms/src/org/argeo/cms/CmsEditable.java b/org.argeo.cms/src/org/argeo/cms/CmsEditable.java new file mode 100644 index 000000000..3c666ff30 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/CmsEditable.java @@ -0,0 +1,36 @@ +package org.argeo.cms; + +/** API NOT STABLE (yet). */ +public interface CmsEditable { + + /** Whether the calling thread can edit, the value is immutable */ + public Boolean canEdit(); + + public Boolean isEditing(); + + public void startEditing(); + + public void stopEditing(); + + public static CmsEditable NON_EDITABLE = new CmsEditable() { + + @Override + public void stopEditing() { + } + + @Override + public void startEditing() { + } + + @Override + public Boolean isEditing() { + return false; + } + + @Override + public Boolean canEdit() { + return false; + } + }; + +} diff --git a/org.argeo.cms/src/org/argeo/cms/CmsEditionEvent.java b/org.argeo.cms/src/org/argeo/cms/CmsEditionEvent.java new file mode 100644 index 000000000..920f6d937 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/CmsEditionEvent.java @@ -0,0 +1,23 @@ +package org.argeo.cms; + +import java.util.EventObject; + +/** Notify of the edition lifecycle */ +public class CmsEditionEvent extends EventObject { + private static final long serialVersionUID = 950914736016693110L; + + public final static Integer START_EDITING = 0; + public final static Integer STOP_EDITING = 1; + + private final Integer type; + + public CmsEditionEvent(Object source, Integer type) { + super(source); + this.type = type; + } + + public Integer getType() { + return type; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/CmsEntryPointFactory.java b/org.argeo.cms/src/org/argeo/cms/CmsEntryPointFactory.java new file mode 100644 index 000000000..537363d31 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/CmsEntryPointFactory.java @@ -0,0 +1,295 @@ +package org.argeo.cms; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.security.Privilege; +import javax.jcr.version.VersionManager; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.cms.internal.ImageManagerImpl; +import org.argeo.jcr.JcrUtils; +import org.eclipse.rap.rwt.RWT; +import org.eclipse.rap.rwt.application.EntryPoint; +import org.eclipse.rap.rwt.application.EntryPointFactory; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +/** Creates and registers an {@link EntryPoint} */ +public class CmsEntryPointFactory implements EntryPointFactory { + private final static Log log = LogFactory + .getLog(CmsEntryPointFactory.class); + + private Repository repository; + private String workspace = null; + private String basePath = "/"; + private List roPrincipals = Arrays.asList("anonymous", "everyone"); + private List rwPrincipals = Arrays.asList("everyone"); + + private CmsLogin cmsLogin; + + private CmsUiProvider header; + // private CmsUiProvider dynamicPages; + // private Map staticPages; + private Map pages = new LinkedHashMap(); + + private Integer headerHeight = 40; + + // Managers + private CmsImageManager imageManager = new ImageManagerImpl(); + + @Override + public EntryPoint create() { + CmsEntryPoint cmsEntryPoint = new CmsEntryPoint(repository, workspace); + CmsSession.current.set(cmsEntryPoint); + return cmsEntryPoint; + } + + public void init() throws RepositoryException { + if (workspace == null) + throw new CmsException( + "Workspace must be set when calling initialization." + + " Please make sure that read-only and read-write roles" + + " have been properly configured:" + + " the defaults are open."); + + Session session = null; + try { + session = JcrUtils.loginOrCreateWorkspace(repository, workspace); + VersionManager vm = session.getWorkspace().getVersionManager(); + if (!vm.isCheckedOut("/")) + vm.checkout("/"); + // session = repository.login(workspace); + JcrUtils.mkdirs(session, basePath); + for (String principal : rwPrincipals) + JcrUtils.addPrivilege(session, basePath, principal, + Privilege.JCR_WRITE); + for (String principal : roPrincipals) + JcrUtils.addPrivilege(session, basePath, principal, + Privilege.JCR_READ); + + for (String pageName : pages.keySet()) { + try { + initPage(session, pages.get(pageName)); + session.save(); + } catch (Exception e) { + throw new CmsException( + "Cannot initialize page " + pageName, e); + } + } + + } finally { + JcrUtils.logoutQuietly(session); + } + } + + protected void initPage(Session adminSession, CmsUiProvider page) + throws RepositoryException { + if (page instanceof LifeCycleUiProvider) + ((LifeCycleUiProvider) page).init(adminSession); + } + + public void destroy() { + for (String pageName : pages.keySet()) { + try { + CmsUiProvider page = pages.get(pageName); + if (page instanceof LifeCycleUiProvider) + ((LifeCycleUiProvider) page).destroy(); + } catch (Exception e) { + log.error("Cannot destroy page " + pageName, e); + } + } + } + + public void setRepository(Repository repository) { + this.repository = repository; + } + + public void setWorkspace(String workspace) { + this.workspace = workspace; + } + + public void setCmsLogin(CmsLogin cmsLogin) { + this.cmsLogin = cmsLogin; + } + + public void setHeader(CmsUiProvider header) { + this.header = header; + } + + public void setPages(Map pages) { + this.pages = pages; + } + + @Deprecated + public void setDynamicPages(CmsUiProvider dynamicPages) { + log.warn("'dynamicPages' is deprecated, use 'pages' instead, with \"\" as key"); + pages.put("", dynamicPages); + } + + @Deprecated + public void setStaticPages(Map staticPages) { + log.warn("'staticPages' is deprecated, use 'pages' instead"); + pages.putAll(staticPages); + } + + public void setBasePath(String basePath) { + this.basePath = basePath; + } + + public void setRoPrincipals(List roPrincipals) { + this.roPrincipals = roPrincipals; + } + + public void setRwPrincipals(List rwPrincipals) { + this.rwPrincipals = rwPrincipals; + } + + public void setHeaderHeight(Integer headerHeight) { + this.headerHeight = headerHeight; + } + + private class CmsEntryPoint extends AbstractCmsEntryPoint { + private Composite headerArea; + private Composite bodyArea; + + public CmsEntryPoint(Repository repository, String workspace) { + super(repository, workspace); + } + + @Override + protected void createContents(Composite parent) { + try { + getShell().getDisplay().setData(CmsSession.KEY, this); + + parent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, + true)); + parent.setLayout(CmsUtils.noSpaceGridLayout()); + + headerArea = new Composite(parent, SWT.NONE); + headerArea.setLayout(new FillLayout()); + GridData headerData = new GridData(SWT.FILL, SWT.FILL, false, + false); + headerData.heightHint = headerHeight; + headerArea.setLayoutData(headerData); + refreshHeader(); + + 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.setBackgroundMode(SWT.INHERIT_DEFAULT); + bodyArea.setLayout(CmsUtils.noSpaceGridLayout()); + } catch (Exception e) { + throw new CmsException("Cannot create entrypoint contents", e); + } + } + + @Override + protected void refreshHeader() { + if (headerArea == 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); + } + + @Override + protected void refreshBody() { + if (bodyArea == null) + return; + // clear + for (Control child : bodyArea.getChildren()) + child.dispose(); + bodyArea.setLayout(CmsUtils.noSpaceGridLayout()); + + // Exception + Throwable exception = getException(); + if (exception != null) { + new Label(bodyArea, SWT.NONE).setText("Unreachable state : " + + getState()); + if (getNode() != null) + new Label(bodyArea, SWT.NONE).setText("Context : " + + getNode()); + + Text errorText = new Text(bodyArea, SWT.MULTI | SWT.H_SCROLL + | SWT.V_SCROLL); + errorText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, + true)); + StringWriter sw = new StringWriter(); + exception.printStackTrace(new PrintWriter(sw)); + errorText.setText(sw.toString()); + IOUtils.closeQuietly(sw); + resetException(); + // TODO report + } else { + String state = getState(); + String page = getPage(); + try { + if (state == null) + throw new CmsException("State cannot be null"); + if (page == null) + throw new CmsException("Page cannot be null"); + // else if (state.length() == 0) + // log.debug("empty state"); + else if (pages.containsKey(page)) + pages.get(page).createUi(bodyArea, getNode()); + else { + // try { + // RWT.getResponse().sendError(404); + // } catch (IOException e) { + // log.error("Cannot send 404 code", e); + // } + throw new CmsException("Unsupported state " + state); + } + } catch (RepositoryException e) { + throw new CmsException("Cannot refresh body", e); + } + } + bodyArea.layout(true, true); + } + + @Override + protected void logAsAnonymous() { + cmsLogin.logInAsAnonymous(); + } + + @Override + protected Node getDefaultNode(Session session) + throws RepositoryException { + if (!session.hasPermission(basePath, "read")) { + if (session.getUserID().equals("anonymous")) + throw new CmsLoginRequiredException(); + else + throw new CmsException("Unauthorized"); + } + return session.getNode(basePath); + } + + @Override + public CmsImageManager getImageManager() { + return imageManager; + } + + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/CmsException.java b/org.argeo.cms/src/org/argeo/cms/CmsException.java new file mode 100644 index 000000000..285741d7b --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/CmsException.java @@ -0,0 +1,17 @@ +package org.argeo.cms; + +import org.argeo.ArgeoException; + +/** CMS specific exceptions. */ +public class CmsException extends ArgeoException { + private static final long serialVersionUID = -5341764743356771313L; + + public CmsException(String message) { + super(message); + } + + public CmsException(String message, Throwable e) { + super(message, e); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/CmsImageManager.java b/org.argeo.cms/src/org/argeo/cms/CmsImageManager.java new file mode 100644 index 000000000..2577dc777 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/CmsImageManager.java @@ -0,0 +1,48 @@ +package org.argeo.cms; + +import java.io.InputStream; + +import javax.jcr.Binary; +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Control; + +/** Read and write access to images. */ +public interface CmsImageManager { + /** Load image in control */ + public Boolean load(Node node, Control control, Point size) + throws RepositoryException; + + /** @return (0,0) if not available */ + public Point getImageSize(Node node) throws RepositoryException; + + /** + * The related "); + } + if (image != null) { + registerImageIfNeeded(); + String imageLocation = RWT.getResourceManager().getLocation(image); + labelText.append(""); + + // final Image img = loadImage(parent.getDisplay()); + // link.setImage(img); + // link.addDisposeListener(new DListener(img)); + } + + if (label != null) { + // link.setText(label); + labelText.append(' ').append(label); + } + + if (target != null) + labelText.append(""); + + link.setText(labelText.toString()); + + // link.setCursor(link.getDisplay().getSystemCursor(SWT.CURSOR_HAND)); + // CmsSession cmsSession = (CmsSession) parent.getDisplay().getData( + // CmsSession.KEY); + if (mouseListener != null) + link.addMouseListener(mouseListener); + + return comp; + } + + private void registerImageIfNeeded() { + ResourceManager resourceManager = RWT.getResourceManager(); + if (!resourceManager.isRegistered(image)) { + URL res = getImageUrl(); + InputStream inputStream = null; + try { + IOUtils.closeQuietly(inputStream); + inputStream = res.openStream(); + resourceManager.register(image, inputStream); + if (log.isTraceEnabled()) + log.trace("Registered image " + image); + } catch (Exception e) { + throw new CmsException("Cannot load image " + image, e); + } finally { + IOUtils.closeQuietly(inputStream); + } + } + } + + private ImageData loadImage() { + URL url = getImageUrl(); + ImageData result = null; + InputStream inputStream = null; + try { + inputStream = url.openStream(); + result = new ImageData(inputStream); + if (log.isTraceEnabled()) + log.trace("Loaded image " + image); + } catch (Exception e) { + throw new CmsException("Cannot load image " + image, e); + } finally { + IOUtils.closeQuietly(inputStream); + } + return result; + } + + private URL getImageUrl() { + URL url; + try { + // pure URL + url = new URL(image); + } catch (MalformedURLException e1) { + // in OSGi bundle + if (bundleContext == null) + throw new CmsException("No bundle context available"); + url = bundleContext.getBundle().getResource(image); + } + + if (url == null) + throw new CmsException("No image " + image + " available."); + + return url; + } + + public void setLabel(String label) { + this.label = label; + } + + public void setCustom(String custom) { + this.custom = custom; + } + + public void setTarget(String target) { + this.target = target; + // try { + // new URL(target); + // isUrl = true; + // } catch (MalformedURLException e1) { + // isUrl = false; + // } + } + + public void setImage(String image) { + this.image = image; + } + + @Override + public void setBundleContext(BundleContext bundleContext) { + this.bundleContext = bundleContext; + } + + public void setMouseListener(MouseListener mouseListener) { + this.mouseListener = mouseListener; + } + + public void setvAlign(String vAlign) { + if ("bottom".equals(vAlign)) { + verticalAlignment = SWT.BOTTOM; + } else if ("top".equals(vAlign)) { + verticalAlignment = SWT.TOP; + } else if ("center".equals(vAlign)) { + verticalAlignment = SWT.CENTER; + } else { + throw new CmsException("Unsupported vertical allignment " + vAlign + + " (must be: top, bottom or center)"); + } + } + + // private class MListener extends MouseAdapter { + // private static final long serialVersionUID = 3634864186295639792L; + // + // @Override + // public void mouseDown(MouseEvent e) { + // if (e.button == 1) { + // } + // } + // } + // + // private class DListener implements DisposeListener { + // private static final long serialVersionUID = -3808587499269394812L; + // private final Image img; + // + // public DListener(Image img) { + // super(); + // this.img = img; + // } + // + // @Override + // public void widgetDisposed(DisposeEvent event) { + // img.dispose(); + // } + // + // } +} diff --git a/org.argeo.cms/src/org/argeo/cms/CmsLogin.java b/org.argeo.cms/src/org/argeo/cms/CmsLogin.java new file mode 100644 index 000000000..7c4dd5f4b --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/CmsLogin.java @@ -0,0 +1,217 @@ +package org.argeo.cms; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.security.Authentication; +import org.springframework.security.AuthenticationManager; +import org.springframework.security.GrantedAuthority; +import org.springframework.security.GrantedAuthorityImpl; +import org.springframework.security.context.SecurityContextHolder; +import org.springframework.security.providers.UsernamePasswordAuthenticationToken; +import org.springframework.security.providers.anonymous.AnonymousAuthenticationToken; +import org.springframework.security.userdetails.User; +import org.springframework.security.userdetails.UserDetails; + +/** Gateway for user login, can also generate the related UI. */ +public class CmsLogin { + private final static Log log = LogFactory.getLog(CmsLogin.class); + private AuthenticationManager authenticationManager; + private String systemKey = "argeo"; + + protected void logInAsAnonymous() { + // TODO Better deal with anonymous authentication + try { + GrantedAuthority[] anonAuthorities = { new GrantedAuthorityImpl( + "ROLE_ANONYMOUS") }; + UserDetails anonUser = new User("anonymous", "", true, true, true, + true, anonAuthorities); + AnonymousAuthenticationToken anonToken = new AnonymousAuthenticationToken( + systemKey, anonUser, anonAuthorities); + Authentication authentication = authenticationManager + .authenticate(anonToken); + SecurityContextHolder.getContext() + .setAuthentication(authentication); + } catch (Exception e) { + throw new CmsException("Cannot authenticate", e); + } + } + + protected void logInWithPassword(String username, char[] password) { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + username, new String(password)); + Authentication authentication = authenticationManager + .authenticate(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + if (log.isDebugEnabled()) + log.debug("Authenticated as " + authentication); + } + + /* + * UI + */ + + // @Override + // public Control createUi(Composite parent, Node context) + // throws RepositoryException { + // Composite comp = new Composite(parent, SWT.NONE); + // comp.setLayout(new GridLayout(1, true)); + // comp.setData(RWT.CUSTOM_VARIANT, CmsStyles.CMS_LOGIN); + // refreshUi(comp); + // return comp; + // } + + // protected void refreshUi(Composite comp) { + // String username = SecurityContextHolder.getContext() + // .getAuthentication().getName(); + // if (username.equals("anonymous")) + // username = null; + // + // for (Control child : comp.getChildren()) { + // child.dispose(); + // } + // + // Label l = new Label(comp, SWT.NONE); + // l.setData(RWT.CUSTOM_VARIANT, CmsStyles.CMS_LOGIN); + // l.setData(RWT.MARKUP_ENABLED, true); + // l.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false)); + // if (username != null) { + // l.setText("" + username + ""); + // l.addMouseListener(new UserListener()); + // } else { + // l.setText("Log in"); + // l.addMouseListener(new LoginListener()); + // } + // + // comp.pack(); + // } + + public void setAuthenticationManager( + AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + public void setSystemKey(String systemKey) { + this.systemKey = systemKey; + } + + // private class UserListener extends MouseAdapter { + // private static final long serialVersionUID = -3565359775509786183L; + // private Control source; + // private Shell dialog; + // + // @Override + // public void mouseDown(MouseEvent e) { + // source = ((Control) e.widget); + // if (dialog != null) { + // dialog.close(); + // dialog.dispose(); + // dialog = null; + // } else { + // dialog = createDialog(source); + // } + // } + // + // @SuppressWarnings("serial") + // protected Shell createDialog(Control source) { + // Shell dialog = new Shell(source.getDisplay(), SWT.NO_TRIM + // | SWT.BORDER | SWT.ON_TOP); + // dialog.setData(RWT.CUSTOM_VARIANT, CMS_USER_MENU); + // dialog.setLayout(new GridLayout(1, false)); + // + // final CmsSession cmsSession = (CmsSession) source.getDisplay() + // .getData(CmsSession.KEY); + // + // Label l = new Label(dialog, SWT.NONE); + // l.setData(RWT.CUSTOM_VARIANT, CMS_USER_MENU_ITEM); + // l.setText("Log out"); + // GridData lData = new GridData(SWT.FILL, SWT.FILL, true, false); + // lData.widthHint = 120; + // l.setLayoutData(lData); + // + // l.addMouseListener(new MouseAdapter() { + // public void mouseDown(MouseEvent e) { + // SecurityContextHolder.getContext().setAuthentication(null); + // UserListener.this.dialog.close(); + // UserListener.this.dialog.dispose(); + // cmsSession.authChange(); + // } + // }); + // + // dialog.pack(); + // dialog.layout(); + // dialog.setLocation(source.toDisplay( + // source.getSize().x - dialog.getSize().x, source.getSize().y)); + // dialog.open(); + // return dialog; + // } + // } + // + // private class LoginListener extends MouseAdapter { + // private static final long serialVersionUID = 677115566708451462L; + // private Control source; + // private Shell dialog; + // + // @Override + // public void mouseDown(MouseEvent e) { + // source = ((Control) e.widget); + // if (dialog != null) { + // dialog.close(); + // dialog.dispose(); + // dialog = null; + // } else { + // dialog = createDialog(source); + // } + // } + // + // @SuppressWarnings("serial") + // protected Shell createDialog(Control source) { + // Integer textWidth = 150; + // Shell dialog = new Shell(source.getDisplay(), SWT.NO_TRIM + // | SWT.BORDER | SWT.ON_TOP); + // dialog.setData(RWT.CUSTOM_VARIANT, CMS_LOGIN_DIALOG); + // dialog.setLayout(new GridLayout(2, false)); + // + // new Label(dialog, SWT.NONE).setText("Username"); + // final Text username = new Text(dialog, SWT.BORDER); + // username.setData(RWT.CUSTOM_VARIANT, CMS_LOGIN_DIALOG_USERNAME); + // GridData gd = new GridData(SWT.FILL, SWT.FILL, true, false); + // gd.widthHint = textWidth; + // username.setLayoutData(gd); + // + // new Label(dialog, SWT.NONE).setText("Password"); + // final Text password = new Text(dialog, SWT.BORDER | SWT.PASSWORD); + // password.setData(RWT.CUSTOM_VARIANT, CMS_LOGIN_DIALOG_PASSWORD); + // gd = new GridData(SWT.FILL, SWT.FILL, true, false); + // gd.widthHint = textWidth; + // password.setLayoutData(gd); + // + // dialog.pack(); + // dialog.layout(); + // dialog.setLocation(source.toDisplay( + // source.getSize().x - dialog.getSize().x, source.getSize().y)); + // dialog.open(); + // + // // Listeners + // TraverseListener tl = new TraverseListener() { + // public void keyTraversed(TraverseEvent e) { + // if (e.detail == SWT.TRAVERSE_RETURN) + // login(username.getText(), password.getTextChars()); + // } + // }; + // username.addTraverseListener(tl); + // password.addTraverseListener(tl); + // return dialog; + // } + // + // protected void login(String username, char[] password) { + // CmsSession cmsSession = (CmsSession) source.getDisplay().getData( + // CmsSession.KEY); + // logInWithPassword(username, password); + // dialog.close(); + // dialog.dispose(); + // refreshUi(source.getParent()); + // cmsSession.authChange(); + // } + // + // } +} diff --git a/org.argeo.cms/src/org/argeo/cms/CmsLoginRequiredException.java b/org.argeo.cms/src/org/argeo/cms/CmsLoginRequiredException.java new file mode 100644 index 000000000..b9917e785 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/CmsLoginRequiredException.java @@ -0,0 +1,19 @@ +package org.argeo.cms; + +/** Throwing this exception triggers redirection to a login page. */ +public class CmsLoginRequiredException extends CmsException { + private static final long serialVersionUID = 7009402894657958151L; + + public CmsLoginRequiredException() { + super("Login is required"); + } + + public CmsLoginRequiredException(String message, Throwable e) { + super(message, e); + } + + public CmsLoginRequiredException(String message) { + super(message); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/CmsMsg.java b/org.argeo.cms/src/org/argeo/cms/CmsMsg.java new file mode 100644 index 000000000..a69d20970 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/CmsMsg.java @@ -0,0 +1,12 @@ +package org.argeo.cms; + +/** Standard CMS messages. */ +public class CmsMsg extends DefaultsResourceBundle { + public final static Msg username = new Msg("username"); + public final static Msg password = new Msg("password"); + public final static Msg logout = new Msg("log out"); + + static { + Msg.init(CmsMsg.class); + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/CmsMsg_fr.properties b/org.argeo.cms/src/org/argeo/cms/CmsMsg_fr.properties new file mode 100644 index 000000000..f7e5b007e --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/CmsMsg_fr.properties @@ -0,0 +1,3 @@ +username=identifiant +password=mot de passe +logout=déconnexion \ No newline at end of file diff --git a/org.argeo.cms/src/org/argeo/cms/CmsNames.java b/org.argeo.cms/src/org/argeo/cms/CmsNames.java new file mode 100644 index 000000000..be10b7601 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/CmsNames.java @@ -0,0 +1,24 @@ +package org.argeo.cms; + +/** JCR names. */ +public interface CmsNames { + /* + * TEXT + */ + public final static String CMS_DRAFTS = "cms:drafts"; + + public final static String CMS_P = "cms:p"; + public final static String CMS_H = "cms:h"; + + public final static String CMS_CONTENT = "cms:content"; + public final static String CMS_STYLE = "cms:style"; + + public final static String CMS_INDEX = "cms:index"; + + /* + * IMAGES + */ + public final static String CMS_IMAGE_WIDTH = "cms:imageWidth"; + public final static String CMS_IMAGE_HEIGHT = "cms:imageHeight"; + public final static String CMS_DATA = "cms:data"; +} diff --git a/org.argeo.cms/src/org/argeo/cms/CmsSession.java b/org.argeo.cms/src/org/argeo/cms/CmsSession.java new file mode 100644 index 000000000..0f4e54109 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/CmsSession.java @@ -0,0 +1,20 @@ +package org.argeo.cms; + +/** Provides interaction with the CMS system. UNSTABLE API at this stage. */ +public interface CmsSession { + public final static String KEY = "org.argeo.connect.web.cmsSession"; + + final ThreadLocal current = new ThreadLocal(); + + public void navigateTo(String state); + + public void authChange(); + + public void exception(Throwable e); + + public Object local(Msg msg); + + public String getState(); + + public CmsImageManager getImageManager(); +} diff --git a/org.argeo.cms/src/org/argeo/cms/CmsStyles.java b/org.argeo.cms/src/org/argeo/cms/CmsStyles.java new file mode 100644 index 000000000..419fb553a --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/CmsStyles.java @@ -0,0 +1,26 @@ +package org.argeo.cms; + +/** Styles references in the CSS. */ +public interface CmsStyles { + // General + public final static String CMS_SHELL = "cms_shell"; + public final static String CMS_MENU_LINK = "cms_menu_link"; + + // Header + public final static String CMS_HEADER = "cms_header"; + public final static String CMS_HEADER_LEAD = "cms_header-lead"; + public final static String CMS_HEADER_CENTER = "cms_header-center"; + public final static String CMS_HEADER_END = "cms_header-end"; + public final static String CMS_LOGIN = "cms_login"; + public final static String CMS_LOGIN_DIALOG = "cms_login_dialog"; + public final static String CMS_LOGIN_DIALOG_USERNAME = "cms_login_dialog-username"; + public final static String CMS_LOGIN_DIALOG_PASSWORD = "cms_login_dialog-password"; + public final static String CMS_USER_MENU = "cms_user_menu"; + public final static String CMS_USER_MENU_ITEM = "cms_user_menu-item"; + + // Body + public final static String CMS_SCROLLED_AREA = "cms_scrolled_area"; + public final static String CMS_BODY = "cms_body"; + public final static String CMS_STATIC_TEXT = "cms_static-text"; + public final static String CMS_LINK = "cms_link"; +} diff --git a/org.argeo.cms/src/org/argeo/cms/CmsTypes.java b/org.argeo.cms/src/org/argeo/cms/CmsTypes.java new file mode 100644 index 000000000..a8ef07618 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/CmsTypes.java @@ -0,0 +1,10 @@ +package org.argeo.cms; + +/** JCR types. */ +public interface CmsTypes { + public final static String CMS_TEXT = "cms:text"; + public final static String CMS_IMAGE = "cms:image"; + public final static String CMS_SECTION = "cms:section"; + public final static String CMS_STYLED = "cms:styled"; + +} diff --git a/org.argeo.cms/src/org/argeo/cms/CmsUiProvider.java b/org.argeo.cms/src/org/argeo/cms/CmsUiProvider.java new file mode 100644 index 000000000..27799b09d --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/CmsUiProvider.java @@ -0,0 +1,21 @@ +package org.argeo.cms; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + +/** Stateless factory building an SWT user interface given a JCR context. */ +public interface CmsUiProvider { + /** + * Initialises a user interface. + * + * @param parent + * the parent composite + * @param a + * context node or null + */ + public Control createUi(Composite parent, Node context) + throws RepositoryException; +} diff --git a/org.argeo.cms/src/org/argeo/cms/CmsUtils.java b/org.argeo.cms/src/org/argeo/cms/CmsUtils.java new file mode 100644 index 000000000..10e523873 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/CmsUtils.java @@ -0,0 +1,176 @@ +package org.argeo.cms; + +import java.io.InputStream; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; + +import org.apache.commons.io.IOUtils; +import org.argeo.jcr.JcrUtils; +import org.eclipse.rap.rwt.RWT; +import org.eclipse.rap.rwt.service.ResourceManager; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.layout.RowData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Widget; + +/** Static utilities for the CMS framework. */ +public class CmsUtils implements CmsConstants { + /** @deprecated Use rowData16px() instead. GridData should not be reused. */ + @Deprecated + public static RowData ROW_DATA_16px = new RowData(16, 16); + + public static GridLayout noSpaceGridLayout() { + return noSpaceGridLayout(new GridLayout()); + } + + public static GridLayout noSpaceGridLayout(GridLayout layout) { + layout.horizontalSpacing = 0; + layout.verticalSpacing = 0; + layout.marginWidth = 0; + layout.marginHeight = 0; + return layout; + } + + // + // GRID DATA + // + public static GridData fillWidth() { + return grabWidth(SWT.FILL, SWT.FILL); + } + + public static GridData fillAll() { + return new GridData(SWT.FILL, SWT.FILL, true, true); + } + + public static GridData grabWidth(int horizontalAlignment, + int verticalAlignment) { + return new GridData(horizontalAlignment, horizontalAlignment, true, + false); + } + + public static RowData rowData16px() { + return new RowData(16, 16); + } + + public static void style(Widget widget, String style) { + widget.setData(CmsConstants.STYLE, style); + } + + public static void markup(Widget widget) { + widget.setData(CmsConstants.MARKUP, true); + } + + /** @return the path or null if not instrumented */ + public static String getDataPath(Widget widget) { + // JCR item + Object data = widget.getData(); + if (data != null && data instanceof Item) { + try { + return ((Item) data).getPath(); + } catch (RepositoryException e) { + throw new CmsException("Cannot find data path of " + data + + " for " + widget); + } + } + + // JCR path + data = widget.getData(Property.JCR_PATH); + if (data != null) + return data.toString(); + + return null; + } + + /** Dispose all children of a Composite */ + public static void clear(Composite composite) { + for (Control child : composite.getChildren()) + child.dispose(); + } + + // + // JCR + // + public static Node getOrAddEmptyFile(Node parent, Enum child) + throws RepositoryException { + if (has(parent, child)) + return child(parent, child); + return JcrUtils.copyBytesAsFile(parent, child.name(), new byte[0]); + } + + public static Node child(Node parent, Enum en) + throws RepositoryException { + return parent.getNode(en.name()); + } + + public static Boolean has(Node parent, Enum en) + throws RepositoryException { + return parent.hasNode(en.name()); + } + + public static Node getOrAdd(Node parent, Enum en) + throws RepositoryException { + return getOrAdd(parent, en, null); + } + + public static Node getOrAdd(Node parent, Enum en, String primaryType) + throws RepositoryException { + if (has(parent, en)) + return child(parent, en); + else if (primaryType == null) + return parent.addNode(en.name()); + else + return parent.addNode(en.name(), primaryType); + } + + // IMAGES + public static String img(String src, String width, String height) { + return imgBuilder(src, width, height).append("/>").toString(); + } + + public static String img(String src, Point size) { + return img(src, Integer.toString(size.x), Integer.toString(size.y)); + } + + public static StringBuilder imgBuilder(String src, String width, + String height) { + return new StringBuilder(64).append(" getKeys() { + Vector res = new Vector(); + final Field[] fieldArray = getClass().getDeclaredFields(); + + for (Field field : fieldArray) { + if (Modifier.isStatic(field.getModifiers()) + && field.getType().isAssignableFrom(Msg.class)) { + res.add(field.getName()); + } + } + return res.elements(); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/IdentityTextInterpreter.java b/org.argeo.cms/src/org/argeo/cms/IdentityTextInterpreter.java new file mode 100644 index 000000000..c66cd866d --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/IdentityTextInterpreter.java @@ -0,0 +1,79 @@ +package org.argeo.cms; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; + +/** Based on HTML with a few Wiki-like shortcuts. */ +public class IdentityTextInterpreter implements TextInterpreter, CmsNames { + + @Override + public void write(Item item, String content) { + try { + if (item instanceof Node) { + Node node = (Node) item; + if (node.isNodeType(CmsTypes.CMS_STYLED)) { + String raw = convertToStorage(node, content); + node.setProperty(CMS_CONTENT, raw); + } else { + throw new CmsException("Don't know how to interpret " + + node); + } + } else {// property + Property property = (Property) item; + property.setValue(content); + } + item.getSession().save(); + } catch (RepositoryException e) { + throw new CmsException("Cannot set content on " + item, e); + } + } + + @Override + public String read(Item item) { + try { + String raw = raw(item); + return convertFromStorage(item, raw); + } catch (RepositoryException e) { + throw new CmsException("Cannot get " + item + " for edit", e); + } + } + + @Override + public String raw(Item item) { + try { + if (item instanceof Node) { + Node node = (Node) item; + if (node.isNodeType(CmsTypes.CMS_STYLED)) { + // WORKAROUND FOR BROKEN PARARAPHS + if (!node.hasProperty(CMS_CONTENT)) { + node.setProperty(CMS_CONTENT, ""); + node.getSession().save(); + } + + return node.getProperty(CMS_CONTENT).getString(); + } else { + throw new CmsException("Don't know how to interpret " + + node); + } + } else {// property + Property property = (Property) item; + return property.getString(); + } + } catch (RepositoryException e) { + throw new CmsException("Cannot get " + item + " content", e); + } + } + + protected String convertToStorage(Item item, String content) + throws RepositoryException { + return content; + + } + + protected String convertFromStorage(Item item, String content) + throws RepositoryException { + return content; + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/LifeCycleUiProvider.java b/org.argeo.cms/src/org/argeo/cms/LifeCycleUiProvider.java new file mode 100644 index 000000000..bb64b649d --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/LifeCycleUiProvider.java @@ -0,0 +1,11 @@ +package org.argeo.cms; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** CmsUiProvider notified of initialisation with a system session. */ +public interface LifeCycleUiProvider extends CmsUiProvider { + public void init(Session adminSession) throws RepositoryException; + + public void destroy(); +} diff --git a/org.argeo.cms/src/org/argeo/cms/MenuLink.java b/org.argeo.cms/src/org/argeo/cms/MenuLink.java new file mode 100644 index 000000000..75be3f1ef --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/MenuLink.java @@ -0,0 +1,11 @@ +package org.argeo.cms; + +/** + * Convenience class setting the custom style {@link CmsStyles#CMS_MENU_LINK} on + * a {@link CmsLink} when simple menus are used. + */ +public class MenuLink extends CmsLink { + public MenuLink() { + setCustom(CmsStyles.CMS_MENU_LINK); + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/Msg.java b/org.argeo.cms/src/org/argeo/cms/Msg.java new file mode 100644 index 000000000..057b74ab0 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/Msg.java @@ -0,0 +1,84 @@ +package org.argeo.cms; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +import org.eclipse.rap.rwt.RWT; + +/** A single message to be internationalised. */ +public class Msg { + private String id; + private ClassLoader classLoader; + private final Object defaultLocal; + + public Msg() { + defaultLocal = null; + } + + public Msg(Object defaultMessage) { + this.defaultLocal = defaultMessage; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public ClassLoader getClassLoader() { + return classLoader; + } + + public void setClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + public Object getDefault() { + return defaultLocal; + } + + public String toString() { + return local().toString(); + } + + /** When used as the first word of a sentence. */ + public String lead() { + String raw = toString(); + return raw.substring(0, 1).toUpperCase(RWT.getLocale()) + + raw.substring(1); + } + + public Object local() { + CmsSession cmSession = CmsSession.current.get(); + Object local = cmSession.local(this); + if (local == null) + local = getDefault(); + if (local == null) + throw new CmsException("No translation found for " + id); + return local; + } + + public static void init(Class clss) { + final Field[] fieldArray = clss.getDeclaredFields(); + ClassLoader loader = clss.getClassLoader(); + + for (Field field : fieldArray) { + if (Modifier.isStatic(field.getModifiers()) + && field.getType().isAssignableFrom(Msg.class)) { + try { + Object obj = field.get(null); + String id = clss.getCanonicalName() + "." + field.getName(); + obj.getClass().getMethod("setId", String.class) + .invoke(obj, id); + obj.getClass() + .getMethod("setClassLoader", ClassLoader.class) + .invoke(obj, loader); + } catch (Exception e) { + throw new CmsException("Cannot prepare field " + field); + } + } + } + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/OpenUserMenu.java b/org.argeo.cms/src/org/argeo/cms/OpenUserMenu.java new file mode 100644 index 000000000..55c149ee1 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/OpenUserMenu.java @@ -0,0 +1,23 @@ +package org.argeo.cms; + +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.widgets.Control; + +/** Open the user menu when clicked */ +public class OpenUserMenu extends MouseAdapter { + private static final long serialVersionUID = 3634864186295639792L; + private CmsLogin cmsLogin; + + @Override + public void mouseDown(MouseEvent e) { + if (e.button == 1) { + new UserMenu(cmsLogin, (Control) e.getSource()); + } + } + + public void setCmsLogin(CmsLogin cmsLogin) { + this.cmsLogin = cmsLogin; + } + +} \ No newline at end of file diff --git a/org.argeo.cms/src/org/argeo/cms/SimpleCmsHeader.java b/org.argeo.cms/src/org/argeo/cms/SimpleCmsHeader.java new file mode 100644 index 000000000..24c924217 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/SimpleCmsHeader.java @@ -0,0 +1,85 @@ +package org.argeo.cms; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.eclipse.rap.rwt.RWT; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + +/** A header in three parts */ +public class SimpleCmsHeader implements CmsUiProvider { + private List lead = new ArrayList(); + private List center = new ArrayList(); + private List end = new ArrayList(); + + private Boolean subPartsSameWidth = false; + + @Override + public Control createUi(Composite parent, Node context) + throws RepositoryException { + Composite header = new Composite(parent, SWT.NONE); + header.setData(RWT.CUSTOM_VARIANT, CmsStyles.CMS_HEADER); + header.setBackgroundMode(SWT.INHERIT_DEFAULT); + header.setLayout(CmsUtils.noSpaceGridLayout(new GridLayout(3, false))); + + configurePart(context, header, lead); + configurePart(context, header, center); + configurePart(context, header, end); + return header; + } + + protected void configurePart(Node context, Composite parent, + List partProviders) throws RepositoryException { + final int style; + final String custom; + if (lead == partProviders) { + style = SWT.LEAD; + custom = CmsStyles.CMS_HEADER_LEAD; + } else if (center == partProviders) { + style = SWT.CENTER; + custom = CmsStyles.CMS_HEADER_CENTER; + } else if (end == partProviders) { + style = SWT.END; + custom = CmsStyles.CMS_HEADER_END; + } else { + throw new CmsException("Unsupported part providers " + + partProviders); + } + + Composite part = new Composite(parent, SWT.NONE); + part.setData(RWT.CUSTOM_VARIANT, custom); + GridData gridData = new GridData(style, SWT.FILL, true, true); + part.setLayoutData(gridData); + part.setLayout(CmsUtils.noSpaceGridLayout(new GridLayout(partProviders + .size(), subPartsSameWidth))); + for (CmsUiProvider uiProvider : partProviders) { + Control subPart = uiProvider.createUi(part, context); + subPart.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, + true)); + } + } + + public void setLead(List lead) { + this.lead = lead; + } + + public void setCenter(List center) { + this.center = center; + } + + public void setEnd(List end) { + this.end = end; + } + + public void setSubPartsSameWidth(Boolean subPartsSameWidth) { + this.subPartsSameWidth = subPartsSameWidth; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/SimpleDynamicPages.java b/org.argeo.cms/src/org/argeo/cms/SimpleDynamicPages.java new file mode 100644 index 000000000..5de40bb50 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/SimpleDynamicPages.java @@ -0,0 +1,116 @@ +package org.argeo.cms; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.argeo.jcr.JcrUtils; +import org.eclipse.rap.rwt.RWT; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; + +public class SimpleDynamicPages implements CmsUiProvider { + + @Override + public Control createUi(Composite parent, Node context) + throws RepositoryException { + if (context == null) + throw new CmsException("Context cannot be null"); + parent.setLayout(new GridLayout(2, false)); + + // parent + if (!context.getPath().equals("/")) { + new CmsLink("..", context.getParent().getPath()).createUi(parent, + context); + new Label(parent, SWT.NONE).setText(context.getParent() + .getPrimaryNodeType().getName()); + } + + // context + Label contextL = new Label(parent, SWT.NONE); + contextL.setData(RWT.MARKUP_ENABLED, true); + contextL.setText("" + context.getName() + ""); + new Label(parent, SWT.NONE).setText(context.getPrimaryNodeType() + .getName()); + + // children + // Label childrenL = new Label(parent, SWT.NONE); + // childrenL.setData(RWT.MARKUP_ENABLED, true); + // childrenL.setText("Children:"); + // childrenL.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, + // false, 2, 1)); + + for (NodeIterator nIt = context.getNodes(); nIt.hasNext();) { + Node child = nIt.nextNode(); + new CmsLink(child.getName(), child.getPath()).createUi(parent, + context); + + new Label(parent, SWT.NONE).setText(child.getPrimaryNodeType() + .getName()); + } + + // properties + // Label propsL = new Label(parent, SWT.NONE); + // propsL.setData(RWT.MARKUP_ENABLED, true); + // propsL.setText("Properties:"); + // propsL.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false, + // 2, 1)); + for (PropertyIterator pIt = context.getProperties(); pIt.hasNext();) { + Property property = pIt.nextProperty(); + + Label label = new Label(parent, SWT.NONE); + label.setText(property.getName()); + label.setToolTipText(JcrUtils + .getPropertyDefinitionAsString(property)); + + new Label(parent, SWT.NONE).setText(getPropAsString(property)); + } + + return null; + } + + private String getPropAsString(Property property) + throws RepositoryException { + String result = ""; + DateFormat timeFormatter = new SimpleDateFormat(""); + if (property.isMultiple()) { + result = getMultiAsString(property, ", "); + } else { + Value value = property.getValue(); + if (value.getType() == PropertyType.BINARY) + result = ""; + else if (value.getType() == PropertyType.DATE) + result = timeFormatter.format(value.getDate().getTime()); + else + result = value.getString(); + } + return result; + } + + private String getMultiAsString(Property property, String separator) + throws RepositoryException { + if (separator == null) + separator = "; "; + Value[] values = property.getValues(); + StringBuilder builder = new StringBuilder(); + for (Value val : values) { + String currStr = val.getString(); + if (!"".equals(currStr.trim())) + builder.append(currStr).append(separator); + } + if (builder.lastIndexOf(separator) >= 0) + return builder.substring(0, builder.length() - separator.length()); + else + return builder.toString(); + } +} \ No newline at end of file diff --git a/org.argeo.cms/src/org/argeo/cms/SimpleStaticPage.java b/org.argeo.cms/src/org/argeo/cms/SimpleStaticPage.java new file mode 100644 index 000000000..1cb030028 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/SimpleStaticPage.java @@ -0,0 +1,30 @@ +package org.argeo.cms; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.eclipse.rap.rwt.RWT; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; + +public class SimpleStaticPage implements CmsUiProvider { + private String text; + + @Override + public Control createUi(Composite parent, Node context) + throws RepositoryException { + Label textC = new Label(parent, SWT.WRAP); + textC.setData(RWT.CUSTOM_VARIANT, CmsStyles.CMS_STATIC_TEXT); + textC.setData(RWT.MARKUP_ENABLED, Boolean.TRUE); + textC.setText(text); + + return textC; + } + + public void setText(String text) { + this.text = text; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/TextInterpreter.java b/org.argeo.cms/src/org/argeo/cms/TextInterpreter.java new file mode 100644 index 000000000..f7cbca759 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/TextInterpreter.java @@ -0,0 +1,12 @@ +package org.argeo.cms; + +import javax.jcr.Item; + +/** Convert from/to data layer to/from presentation layer. */ +public interface TextInterpreter { + public String raw(Item item); + + public String read(Item item); + + public void write(Item item, String content); +} diff --git a/org.argeo.cms/src/org/argeo/cms/UrlResourceLoader.java b/org.argeo.cms/src/org/argeo/cms/UrlResourceLoader.java new file mode 100644 index 000000000..fb4e2cd51 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/UrlResourceLoader.java @@ -0,0 +1,25 @@ +package org.argeo.cms; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import org.eclipse.rap.rwt.service.ResourceLoader; + +/** {@link ResourceLoader} implementation wrapping an {@link URL}. */ +@Deprecated +public class UrlResourceLoader implements ResourceLoader { + private final URL url; + + public UrlResourceLoader(URL url) { + super(); + this.url = url; + } + + @Override + public InputStream getResourceAsStream(String resourceName) + throws IOException { + return url.openStream(); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/UserMenu.java b/org.argeo.cms/src/org/argeo/cms/UserMenu.java new file mode 100644 index 000000000..57de51526 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/UserMenu.java @@ -0,0 +1,133 @@ +package org.argeo.cms; + +import org.eclipse.rap.rwt.RWT; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.ShellAdapter; +import org.eclipse.swt.events.ShellEvent; +import org.eclipse.swt.events.TraverseEvent; +import org.eclipse.swt.events.TraverseListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; +import org.springframework.security.context.SecurityContextHolder; + +/** The site-related user menu */ +public class UserMenu extends Shell implements CmsStyles { + private static final long serialVersionUID = -5788157651532106301L; + + private CmsLogin cmsLogin; + private String username = null; + + public UserMenu(CmsLogin cmsLogin, Control source) { + super(source.getDisplay(), SWT.NO_TRIM | SWT.BORDER | SWT.ON_TOP); + this.cmsLogin = cmsLogin; + + setData(RWT.CUSTOM_VARIANT, CMS_USER_MENU); + + username = SecurityContextHolder.getContext().getAuthentication() + .getName(); + if (username.equals("anonymous")) { + username = null; + anonymousUi(); + } else { + userUi(); + } + + pack(); + layout(); + setLocation(source.toDisplay(source.getSize().x - getSize().x, + source.getSize().y)); + + addShellListener(new ShellAdapter() { + private static final long serialVersionUID = 5178980294808435833L; + + @Override + public void shellDeactivated(ShellEvent e) { + close(); + dispose(); + } + + }); + + open(); + + } + + protected void userUi() { + setLayout(new GridLayout()); + + Label l = new Label(this, SWT.NONE); + l.setData(RWT.CUSTOM_VARIANT, CMS_USER_MENU_ITEM); + l.setData(RWT.MARKUP_ENABLED, true); + l.setLayoutData(CmsUtils.fillWidth()); + l.setText("" + username + ""); + + final CmsSession cmsSession = (CmsSession) getDisplay().getData( + CmsSession.KEY); + l = new Label(this, SWT.NONE); + l.setData(RWT.CUSTOM_VARIANT, CMS_USER_MENU_ITEM); + l.setText(CmsMsg.logout.lead()); + GridData lData = CmsUtils.fillWidth(); + lData.widthHint = 120; + l.setLayoutData(lData); + + l.addMouseListener(new MouseAdapter() { + private static final long serialVersionUID = 6444395812777413116L; + + public void mouseDown(MouseEvent e) { + SecurityContextHolder.getContext().setAuthentication(null); + close(); + dispose(); + cmsSession.authChange(); + } + }); + } + + protected void anonymousUi() { + Integer textWidth = 150; + setData(RWT.CUSTOM_VARIANT, CMS_USER_MENU); + setLayout(new GridLayout(2, false)); + + new Label(this, SWT.NONE).setText(CmsMsg.username.lead()); + final Text username = new Text(this, SWT.BORDER); + username.setData(RWT.CUSTOM_VARIANT, CMS_LOGIN_DIALOG_USERNAME); + GridData gd = CmsUtils.fillWidth(); + gd.widthHint = textWidth; + username.setLayoutData(gd); + + new Label(this, SWT.NONE).setText(CmsMsg.password.lead()); + final Text password = new Text(this, SWT.BORDER | SWT.PASSWORD); + password.setData(RWT.CUSTOM_VARIANT, CMS_LOGIN_DIALOG_PASSWORD); + gd = CmsUtils.fillWidth(); + gd.widthHint = textWidth; + password.setLayoutData(gd); + + // Listeners + TraverseListener tl = new TraverseListener() { + private static final long serialVersionUID = -1158892811534971856L; + + public void keyTraversed(TraverseEvent e) { + if (e.detail == SWT.TRAVERSE_RETURN) + login(username.getText(), password.getTextChars()); + } + }; + username.addTraverseListener(tl); + password.addTraverseListener(tl); + } + + protected void login(String username, char[] password) { + CmsSession cmsSession = (CmsSession) getDisplay().getData( + CmsSession.KEY); + cmsLogin.logInWithPassword(username, password); + close(); + dispose(); + // refreshUi(source.getParent()); + cmsSession.authChange(); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/cms.cnd b/org.argeo.cms/src/org/argeo/cms/cms.cnd new file mode 100644 index 000000000..67dcfb518 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/cms.cnd @@ -0,0 +1,21 @@ + + +[cms:styled] +mixin +- cms:style (STRING) +- cms:content (STRING) +- cms:data (BINARY) + +[cms:image] > mix:title, mix:mimeType +mixin +- cms:imageWidth (STRING) +- cms:imageHeight (STRING) + +[cms:section] > nt:folder, mix:created, mix:lastModified, mix:title +orderable ++ cms:p (nt:base) = nt:unstructured * ++ cms:h (cms:section) * ++ cms:attached (nt:folder) + +[cms:text] > cms:section ++ cms:history (nt:folder) diff --git a/org.argeo.cms/src/org/argeo/cms/internal/ImageManagerImpl.java b/org.argeo.cms/src/org/argeo/cms/internal/ImageManagerImpl.java new file mode 100644 index 000000000..db36f719f --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/internal/ImageManagerImpl.java @@ -0,0 +1,261 @@ +package org.argeo.cms.internal; + +import static javax.jcr.Node.JCR_CONTENT; +import static javax.jcr.Property.JCR_DATA; +import static javax.jcr.nodetype.NodeType.NT_FILE; +import static javax.jcr.nodetype.NodeType.NT_RESOURCE; +import static org.argeo.cms.CmsConstants.NO_IMAGE_SIZE; +import static org.argeo.cms.CmsTypes.CMS_STYLED; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.activation.MimetypesFileTypeMap; +import javax.jcr.Binary; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.cms.CmsException; +import org.argeo.cms.CmsImageManager; +import org.argeo.cms.CmsNames; +import org.argeo.cms.CmsTypes; +import org.argeo.cms.CmsUtils; +import org.argeo.jcr.JcrUtils; +import org.eclipse.rap.rwt.RWT; +import org.eclipse.rap.rwt.service.ResourceManager; +import org.eclipse.rap.rwt.widgets.FileUpload; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; + +/** Manages only public images so far. */ +public class ImageManagerImpl implements CmsImageManager, CmsNames { + private final static Log log = LogFactory.getLog(ImageManagerImpl.class); + private MimetypesFileTypeMap fileTypeMap = new MimetypesFileTypeMap(); + + public Boolean load(Node node, Control control, Point preferredSize) + throws RepositoryException { + Point imageSize = getImageSize(node); + Point size; + String imgTag = null; + if (preferredSize == null || imageSize.x == 0 || imageSize.y == 0 + || (preferredSize.x == 0 && preferredSize.y == 0)) { + if (imageSize.x != 0 && imageSize.y != 0) { + // actual image size if completely known + size = imageSize; + } else { + // no image if not completely known + size = resizeTo(NO_IMAGE_SIZE, + preferredSize != null ? preferredSize : imageSize); + imgTag = CmsUtils.noImg(size); + } + + } else if (preferredSize.x != 0 && preferredSize.y != 0) { + // given size if completely provided + size = preferredSize; + } else { + // at this stage : + // image is completely known + assert imageSize.x != 0 && imageSize.y != 0; + // one and only one of the dimension as been specified + assert preferredSize.x == 0 || preferredSize.y == 0; + size = resizeTo(imageSize, preferredSize); + } + + boolean loaded = false; + if (control == null) + return loaded; + + if (control instanceof Label) { + if (imgTag == null) { + // IMAGE RETRIEVED HERE + imgTag = getImageTag(node, size); + // + if (imgTag == null) + imgTag = CmsUtils.noImg(size); + else + loaded = true; + } + + Label lbl = (Label) control; + lbl.setText(imgTag); + // lbl.setSize(size); + } else if (control instanceof FileUpload) { + FileUpload lbl = (FileUpload) control; + lbl.setImage(CmsUtils.noImage(size)); + lbl.setSize(size); + return loaded; + } else + loaded = false; + + return loaded; + } + + private Point resizeTo(Point orig, Point constraints) { + if (constraints.x != 0 && constraints.y != 0) { + return constraints; + } else if (constraints.x == 0 && constraints.y == 0) { + return orig; + } else if (constraints.y == 0) {// force width + return new Point(constraints.x, + scale(orig.y, orig.x, constraints.x)); + } else if (constraints.x == 0) {// force height + return new Point(scale(orig.x, orig.y, constraints.y), + constraints.y); + } + throw new CmsException("Cannot resize " + orig + " to " + constraints); + } + + private int scale(int origDimension, int otherDimension, int otherConstraint) { + return Math.round(origDimension + * divide(otherConstraint, otherDimension)); + } + + private float divide(int a, int b) { + return ((float) a) / ((float) b); + } + + public Point getImageSize(Node node) throws RepositoryException { + return new Point(node.hasProperty(CMS_IMAGE_WIDTH) ? (int) node + .getProperty(CMS_IMAGE_WIDTH).getLong() : 0, + node.hasProperty(CMS_IMAGE_WIDTH) ? (int) node.getProperty( + CMS_IMAGE_HEIGHT).getLong() : 0); + } + + /** @return null if not available */ + @Override + public String getImageTag(Node node) throws RepositoryException { + return getImageTag(node, getImageSize(node)); + } + + private String getImageTag(Node node, Point size) + throws RepositoryException { + StringBuilder buf = getImageTagBuilder(node, size); + if (buf == null) + return null; + return buf.append("/>").toString(); + } + + /** @return null if not available */ + @Override + public StringBuilder getImageTagBuilder(Node node, Point size) + throws RepositoryException { + return getImageTagBuilder(node, Integer.toString(size.x), + Integer.toString(size.y)); + } + + /** @return null if not available */ + private StringBuilder getImageTagBuilder(Node node, String width, + String height) throws RepositoryException { + String url = getImageUrl(node); + if (url == null) + return null; + return CmsUtils.imgBuilder(url, width, height); + } + + /** @return null if not available */ + @Override + public String getImageUrl(Node node) throws RepositoryException { + String name = getResourceName(node); + ResourceManager resourceManager = RWT.getResourceManager(); + if (!resourceManager.isRegistered(name)) { + InputStream inputStream = null; + Binary binary = getImageBinary(node); + if (binary == null) + return null; + try { + inputStream = binary.getStream(); + resourceManager.register(name, inputStream); + } finally { + IOUtils.closeQuietly(inputStream); + JcrUtils.closeQuietly(binary); + } + if (log.isDebugEnabled()) + log.debug("Registered image " + name); + } + return resourceManager.getLocation(name); + } + + protected String getResourceName(Node node) throws RepositoryException { + String workspace = node.getSession().getWorkspace().getName(); + if (node.hasNode(JCR_CONTENT)) + return workspace + '_' + node.getNode(JCR_CONTENT).getIdentifier(); + else + return workspace + '_' + node.getIdentifier(); + } + + public Binary getImageBinary(Node node) throws RepositoryException { + if (node.isNodeType(NT_FILE)) + return node.getNode(JCR_CONTENT).getProperty(JCR_DATA).getBinary(); + else if (node.isNodeType(CMS_STYLED) && node.hasProperty(CMS_DATA)) { + return node.getProperty(CMS_DATA).getBinary(); + } else { + return null; + } + } + + public Image getSwtImage(Node node) throws RepositoryException { + InputStream inputStream = null; + Binary binary = getImageBinary(node); + if (binary == null) + return null; + try { + inputStream = binary.getStream(); + return new Image(Display.getCurrent(), inputStream); + } finally { + IOUtils.closeQuietly(inputStream); + JcrUtils.closeQuietly(binary); + } + } + + @Override + public String uploadImage(Node parentNode, String fileName, InputStream in) + throws RepositoryException { + InputStream inputStream = null; + try { + String previousResourceName = null; + if (parentNode.hasNode(fileName)) { + Node node = parentNode.getNode(fileName); + previousResourceName = getResourceName(node); + if (node.hasNode(JCR_CONTENT)){ + node.getNode(JCR_CONTENT).remove(); + node.addNode(JCR_CONTENT, NT_RESOURCE); + } + } + + byte[] arr = IOUtils.toByteArray(in); + Node fileNode = JcrUtils.copyBytesAsFile(parentNode, fileName, arr); + fileNode.addMixin(CmsTypes.CMS_IMAGE); + + inputStream = new ByteArrayInputStream(arr); + ImageData id = new ImageData(inputStream); + fileNode.setProperty(CMS_IMAGE_WIDTH, id.width); + fileNode.setProperty(CMS_IMAGE_HEIGHT, id.height); + fileNode.setProperty(Property.JCR_MIMETYPE, + fileTypeMap.getContentType(fileName)); + fileNode.getSession().save(); + + // reset resource manager + ResourceManager resourceManager = RWT.getResourceManager(); + if (resourceManager.isRegistered(previousResourceName)) { + resourceManager.unregister(previousResourceName); + if (log.isDebugEnabled()) + log.debug("Unregistered image " + previousResourceName); + } + return getImageUrl(fileNode); + } catch (IOException e) { + throw new CmsException("Cannot upload image " + fileName + " in " + + parentNode, e); + } finally { + IOUtils.closeQuietly(inputStream); + } + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/internal/JcrContentProvider.java b/org.argeo.cms/src/org/argeo/cms/internal/JcrContentProvider.java new file mode 100644 index 000000000..fd8eb2c75 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/internal/JcrContentProvider.java @@ -0,0 +1,82 @@ +package org.argeo.cms.internal; + +import java.util.ArrayList; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; + +import org.argeo.cms.CmsException; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.Viewer; + +@Deprecated +class JcrContentProvider implements ITreeContentProvider { + private static final long serialVersionUID = -1333678161322488674L; + + @Override + public void dispose() { + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + if (newInput == null) + return; + if (!(newInput instanceof Node)) + throw new CmsException("Input " + newInput + " must be a node"); + } + + @Override + public Object[] getElements(Object inputElement) { + try { + Node node = (Node) inputElement; + ArrayList arr = new ArrayList(); + NodeIterator nit = node.getNodes(); + while (nit.hasNext()) { + arr.add(nit.nextNode()); + } + return arr.toArray(); + } catch (RepositoryException e) { + throw new CmsException("Cannot get elements", e); + } + } + + @Override + public Object[] getChildren(Object parentElement) { + try { + Node node = (Node) parentElement; + ArrayList arr = new ArrayList(); + NodeIterator nit = node.getNodes(); + while (nit.hasNext()) { + arr.add(nit.nextNode()); + } + return arr.toArray(); + } catch (RepositoryException e) { + throw new CmsException("Cannot get elements", e); + } + } + + @Override + public Object getParent(Object element) { + try { + Node node = (Node) element; + if (node.getName().equals("")) + return null; + else + return node.getParent(); + } catch (RepositoryException e) { + throw new CmsException("Cannot get elements", e); + } + } + + @Override + public boolean hasChildren(Object element) { + try { + Node node = (Node) element; + return node.hasNodes(); + } catch (RepositoryException e) { + throw new CmsException("Cannot get elements", e); + } + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/internal/JcrFileUploadReceiver.java b/org.argeo.cms/src/org/argeo/cms/internal/JcrFileUploadReceiver.java new file mode 100644 index 000000000..6b91295ec --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/internal/JcrFileUploadReceiver.java @@ -0,0 +1,82 @@ +package org.argeo.cms.internal; + +import static javax.jcr.nodetype.NodeType.NT_FILE; + +import java.io.IOException; +import java.io.InputStream; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; + +import org.apache.commons.io.FilenameUtils; +import org.argeo.cms.CmsException; +import org.argeo.cms.CmsImageManager; +import org.argeo.cms.CmsNames; +import org.argeo.jcr.JcrUtils; +import org.eclipse.rap.addons.fileupload.FileDetails; +import org.eclipse.rap.addons.fileupload.FileUploadReceiver; + +public class JcrFileUploadReceiver extends FileUploadReceiver implements + CmsNames { + private final Node parentNode; + private final String nodeName; + private final CmsImageManager imageManager; + + /** If nodeName is null, use the uploaded file name */ + public JcrFileUploadReceiver(Node parentNode, String nodeName, + CmsImageManager imageManager) { + super(); + this.parentNode = parentNode; + this.nodeName = nodeName; + this.imageManager = imageManager; + } + + @Override + public void receive(InputStream stream, FileDetails details) + throws IOException { + try { + String fileName = nodeName != null ? nodeName : details + .getFileName(); + String contentType = details.getContentType(); + if (isImage(details.getFileName(), contentType)) { + imageManager.uploadImage(parentNode, fileName, stream); + return; + // InputStream inputStream = new ByteArrayInputStream(arr); + // ImageData id = new ImageData(inputStream); + // fileNode.addMixin(CmsTypes.CMS_IMAGE); + // fileNode.setProperty(CMS_IMAGE_WIDTH, id.width); + // fileNode.setProperty(CMS_IMAGE_HEIGHT, id.height); + } + + Node fileNode; + if (parentNode.hasNode(fileName)) { + fileNode = parentNode.getNode(fileName); + if (!fileNode.isNodeType(NT_FILE)) + fileNode.remove(); + } + fileNode = JcrUtils.copyStreamAsFile(parentNode, fileName, stream); + + if (contentType != null) { + fileNode.addMixin(NodeType.MIX_MIMETYPE); + fileNode.setProperty(Property.JCR_MIMETYPE, contentType); + } + processNewFile(fileNode); + fileNode.getSession().save(); + } catch (RepositoryException e) { + throw new CmsException("cannot receive " + details, e); + } + } + + protected Boolean isImage(String fileName, String contentType) { + String ext = FilenameUtils.getExtension(fileName); + return ext != null + && (ext.equals("png") || ext.equalsIgnoreCase("jpg")); + } + + protected void processNewFile(Node node) { + + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/internal/SimpleEditableImage.java b/org.argeo.cms/src/org/argeo/cms/internal/SimpleEditableImage.java new file mode 100644 index 000000000..e3211ef13 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/internal/SimpleEditableImage.java @@ -0,0 +1,73 @@ +package org.argeo.cms.internal; + +import javax.jcr.RepositoryException; + +import org.argeo.cms.CmsUtils; +import org.argeo.cms.widgets.EditableImage; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Text; + +/** NOT working yet. */ +public class SimpleEditableImage extends EditableImage { + private static final long serialVersionUID = -5689145523114022890L; + + private String src; + private Point imageSize; + + public SimpleEditableImage(Composite parent, int swtStyle) { + super(parent, swtStyle); + // load(getControl()); + getParent().layout(); + } + + public SimpleEditableImage(Composite parent, int swtStyle, String src, + Point imageSize) { + super(parent, swtStyle); + this.src = src; + this.imageSize = imageSize; + } + + @Override + protected Control createControl(Composite box, String style) { + if (isEditing()) { + return createText(box, style); + } else { + return createLabel(box, style); + } + } + + protected String createImgTag() throws RepositoryException { + String imgTag; + if (src != null) + imgTag = CmsUtils.img(src, imageSize); + else + imgTag = CmsUtils.noImg(imageSize != null ? imageSize + : NO_IMAGE_SIZE); + return imgTag; + } + + protected Text createText(Composite box, String style) { + Text text = new Text(box, getStyle()); + CmsUtils.style(text, style); + return text; + } + + public String getSrc() { + return src; + } + + public void setSrc(String src) { + this.src = src; + } + + public Point getImageSize() { + return imageSize; + } + + public void setImageSize(Point imageSize) { + this.imageSize = imageSize; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/internal/text/AbstractTextViewer.java b/org.argeo.cms/src/org/argeo/cms/internal/text/AbstractTextViewer.java new file mode 100644 index 000000000..cbed63e8c --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/internal/text/AbstractTextViewer.java @@ -0,0 +1,890 @@ +package org.argeo.cms.internal.text; + +import static javax.jcr.Property.JCR_TITLE; +import static org.argeo.cms.CmsUtils.fillWidth; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; +import java.util.Observer; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.cms.CmsEditable; +import org.argeo.cms.CmsException; +import org.argeo.cms.CmsImageManager; +import org.argeo.cms.CmsNames; +import org.argeo.cms.CmsSession; +import org.argeo.cms.CmsTypes; +import org.argeo.cms.CmsUtils; +import org.argeo.cms.IdentityTextInterpreter; +import org.argeo.cms.TextInterpreter; +import org.argeo.cms.text.Img; +import org.argeo.cms.text.Paragraph; +import org.argeo.cms.text.TextSection; +import org.argeo.cms.viewers.AbstractPageViewer; +import org.argeo.cms.viewers.EditablePart; +import org.argeo.cms.viewers.NodePart; +import org.argeo.cms.viewers.PropertyPart; +import org.argeo.cms.viewers.Section; +import org.argeo.cms.viewers.SectionPart; +import org.argeo.cms.widgets.EditableImage; +import org.argeo.cms.widgets.EditableText; +import org.argeo.cms.widgets.StyledControl; +import org.argeo.jcr.JcrUtils; +import org.eclipse.rap.addons.fileupload.FileDetails; +import org.eclipse.rap.addons.fileupload.FileUploadEvent; +import org.eclipse.rap.addons.fileupload.FileUploadHandler; +import org.eclipse.rap.addons.fileupload.FileUploadListener; +import org.eclipse.rap.rwt.RWT; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseListener; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Text; + +/** Base class for text viewers and editors. */ +public abstract class AbstractTextViewer extends AbstractPageViewer implements + CmsNames, KeyListener, Observer { + private static final long serialVersionUID = -2401274679492339668L; + private final static Log log = LogFactory.getLog(AbstractTextViewer.class); + + private final Section mainSection; + + private TextInterpreter textInterpreter = new IdentityTextInterpreter(); + private CmsImageManager imageManager = CmsSession.current.get() + .getImageManager(); + + private FileUploadListener fileUploadListener; + private TextContextMenu styledTools; + + private final boolean flat; + + protected AbstractTextViewer(Section parent, int style, + CmsEditable cmsEditable) { + super(parent, style, cmsEditable); + flat = SWT.FLAT == (style & SWT.FLAT); + + if (getCmsEditable().canEdit()) { + fileUploadListener = new FUL(); + styledTools = new TextContextMenu(this, parent.getDisplay()); + } + this.mainSection = parent; + initModelIfNeeded(mainSection.getNode()); + // layout(this.mainSection); + } + + @Override + public Control getControl() { + return mainSection; + } + + protected void refresh(Control control) throws RepositoryException { + if (!(control instanceof Section)) + return; + Section section = (Section) control; + if (section instanceof TextSection) { + CmsUtils.clear(section); + Node node = section.getNode(); + TextSection textSection = (TextSection) section; + if (node.hasProperty(Property.JCR_TITLE)) { + if (section.getHeader() == null) + section.createHeader(); + if (node.hasProperty(Property.JCR_TITLE)) { + SectionTitle title = newSectionTitle(textSection, node); + title.setLayoutData(CmsUtils.fillWidth()); + updateContent(title); + } + } + + for (NodeIterator ni = node.getNodes(CMS_P); ni.hasNext();) { + Node child = ni.nextNode(); + final SectionPart sectionPart; + if (child.isNodeType(CmsTypes.CMS_IMAGE) + || child.isNodeType(NodeType.NT_FILE)) { + sectionPart = newImg(textSection, child); + } else if (child.isNodeType(CmsTypes.CMS_STYLED)) { + sectionPart = newParagraph(textSection, child); + } else { + sectionPart = newSectionPart(textSection, child); + if (sectionPart == null) + throw new CmsException("Unsupported node " + child); + // TODO list node types in exception + } + if (sectionPart instanceof Control) + ((Control) sectionPart).setLayoutData(CmsUtils.fillWidth()); + } + + if (!flat) + for (NodeIterator ni = section.getNode().getNodes(CMS_H); ni + .hasNext();) { + Node child = ni.nextNode(); + if (child.isNodeType(CmsTypes.CMS_SECTION)) { + TextSection newSection = new TextSection(section, + SWT.NONE, child); + newSection.setLayoutData(CmsUtils.fillWidth()); + refresh(newSection); + } + } + } else { + for (Section s : section.getSubSections().values()) + refresh(s); + } + // section.layout(); + } + + /** To be overridden in order to provide additional SectionPart types */ + protected SectionPart newSectionPart(TextSection textSection, Node node) { + return null; + } + + // CRUD + protected Paragraph newParagraph(TextSection parent, Node node) + throws RepositoryException { + Paragraph paragraph = new Paragraph(parent, parent.getStyle(), node); + updateContent(paragraph); + paragraph.setLayoutData(fillWidth()); + paragraph.setMouseListener(getMouseListener()); + return paragraph; + } + + protected Img newImg(TextSection parent, Node node) + throws RepositoryException { + Img img = new Img(parent, parent.getStyle(), node) { + private static final long serialVersionUID = 1297900641952417540L; + + @Override + protected void setContainerLayoutData(Composite composite) { + composite.setLayoutData(CmsUtils.grabWidth(SWT.CENTER, + SWT.DEFAULT)); + } + + @Override + protected void setControlLayoutData(Control control) { + control.setLayoutData(CmsUtils.grabWidth(SWT.CENTER, + SWT.DEFAULT)); + } + }; + img.setLayoutData(CmsUtils.grabWidth(SWT.CENTER, SWT.DEFAULT)); + updateContent(img); + img.setMouseListener(getMouseListener()); + return img; + } + + protected SectionTitle newSectionTitle(TextSection parent, Node node) + throws RepositoryException { + SectionTitle title = new SectionTitle(parent.getHeader(), + parent.getStyle(), node.getProperty(JCR_TITLE)); + updateContent(title); + title.setMouseListener(getMouseListener()); + return title; + } + + protected SectionTitle prepareSectionTitle(Section newSection, + String titleText) throws RepositoryException { + Node sectionNode = newSection.getNode(); + if (!sectionNode.hasProperty(JCR_TITLE)) + sectionNode.setProperty(Property.JCR_TITLE, ""); + getTextInterpreter().write(sectionNode.getProperty(Property.JCR_TITLE), + titleText); + if (newSection.getHeader() == null) + newSection.createHeader(); + SectionTitle sectionTitle = newSectionTitle((TextSection) newSection, + sectionNode); + return sectionTitle; + } + + protected void updateContent(EditablePart part) throws RepositoryException { + if (part instanceof SectionPart) { + SectionPart sectionPart = (SectionPart) part; + Node partNode = sectionPart.getNode(); + + if (part instanceof StyledControl + && (sectionPart.getSection() instanceof TextSection)) { + TextSection section = (TextSection) sectionPart.getSection(); + StyledControl styledControl = (StyledControl) part; + if (partNode.isNodeType(CmsTypes.CMS_STYLED)) { + String style = partNode.hasProperty(CMS_STYLE) ? partNode + .getProperty(CMS_STYLE).getString() : section + .getDefaultTextStyle(); + styledControl.setStyle(style); + } + } + // use control AFTER setting style, since it may have been reset + + if (part instanceof EditableText) { + EditableText paragraph = (EditableText) part; + if (paragraph == getEdited()) + paragraph.setText(textInterpreter.read(partNode)); + else + paragraph.setText(textInterpreter.raw(partNode)); + } else if (part instanceof EditableImage) { + EditableImage editableImage = (EditableImage) part; + imageManager.load(partNode, part.getControl(), + editableImage.getPreferredImageSize()); + } + } else if (part instanceof SectionTitle) { + SectionTitle title = (SectionTitle) part; + title.setStyle(title.getSection().getTitleStyle()); + // use control AFTER setting style + if (title == getEdited()) + title.setText(textInterpreter.read(title.getProperty())); + else + title.setText(textInterpreter.raw(title.getProperty())); + } + } + + // OVERRIDDEN FROM PARENT VIEWER + @Override + protected void save(EditablePart part) throws RepositoryException { + if (part instanceof EditableText) { + EditableText et = (EditableText) part; + String text = ((Text) et.getControl()).getText(); + + String[] lines = text.split("[\r\n]+"); + assert lines.length != 0; + saveLine(part, lines[0]); + if (lines.length > 1) { + ArrayList toLayout = new ArrayList(); + if (part instanceof Paragraph) { + Paragraph currentParagraph = (Paragraph) et; + Section section = currentParagraph.getSection(); + Node sectionNode = section.getNode(); + Node currentParagraphN = currentParagraph.getNode(); + for (int i = 1; i < lines.length; i++) { + Node newNode = sectionNode.addNode(CMS_P); + newNode.addMixin(CmsTypes.CMS_STYLED); + saveLine(newNode, lines[i]); + // second node was create as last, if it is not the next + // one, it + // means there are some in between and we can take the + // one at + // index+1 for the re-order + if (newNode.getIndex() > currentParagraphN.getIndex() + 1) { + sectionNode.orderBefore(p(newNode.getIndex()), + p(currentParagraphN.getIndex() + 1)); + } + Paragraph newParagraph = newParagraph( + (TextSection) section, newNode); + newParagraph.moveBelow(currentParagraph); + toLayout.add(newParagraph); + + currentParagraph = newParagraph; + currentParagraphN = newNode; + } + } + // TODO or rather return the created paragarphs? + layout(toLayout.toArray(new Control[toLayout.size()])); + } + } + } + + protected void saveLine(EditablePart part, String line) { + if (part instanceof NodePart) { + saveLine(((NodePart) part).getNode(), line); + } else if (part instanceof PropertyPart) { + saveLine(((PropertyPart) part).getProperty(), line); + } else { + throw new CmsException("Unsupported part " + part); + } + } + + protected void saveLine(Item item, String line) { + line = line.trim(); + textInterpreter.write(item, line); + } + + @Override + protected void prepare(EditablePart part, Object caretPosition) { + Control control = part.getControl(); + if (control instanceof Text) { + Text text = (Text) control; + if (caretPosition != null) + if (caretPosition instanceof Integer) + text.setSelection((Integer) caretPosition); + else if (caretPosition instanceof Point) { + // TODO find a way to position the caret at the right place + } + text.setData(RWT.ACTIVE_KEYS, new String[] { "BACKSPACE", "ESC", + "TAB", "SHIFT+TAB", "ALT+ARROW_LEFT", "ALT+ARROW_RIGHT", + "ALT+ARROW_UP", "ALT+ARROW_DOWN", "RETURN", "CTRL+RETURN", + "ENTER", "DELETE" }); + text.setData(RWT.CANCEL_KEYS, new String[] { "ALT+ARROW_LEFT", + "ALT+ARROW_RIGHT" }); + text.addKeyListener(this); + } else if (part instanceof Img) { + ((Img) part).setFileUploadListener(fileUploadListener); + } + } + + // REQUIRED BY CONTEXT MENU + void setParagraphStyle(Paragraph paragraph, String style) { + try { + Node paragraphNode = paragraph.getNode(); + paragraphNode.setProperty(CMS_STYLE, style); + paragraphNode.getSession().save(); + updateContent(paragraph); + layout(paragraph); + } catch (RepositoryException e1) { + throw new CmsException("Cannot set style " + style + " on " + + paragraph, e1); + } + } + + void deletePart(SectionPart paragraph) { + try { + Node paragraphNode = paragraph.getNode(); + Section section = paragraph.getSection(); + Session session = paragraphNode.getSession(); + paragraphNode.remove(); + session.save(); + if (paragraph instanceof Control) + ((Control) paragraph).dispose(); + layout(section); + } catch (RepositoryException e1) { + throw new CmsException("Cannot delete " + paragraph, e1); + } + } + + String getRawParagraphText(Paragraph paragraph) { + return textInterpreter.raw(paragraph.getNode()); + } + + // COMMANDS + protected void splitEdit() { + checkEdited(); + try { + if (getEdited() instanceof Paragraph) { + Paragraph paragraph = (Paragraph) getEdited(); + Text text = (Text) paragraph.getControl(); + int caretPosition = text.getCaretPosition(); + String txt = text.getText(); + String first = txt.substring(0, caretPosition); + String second = txt.substring(caretPosition); + Node firstNode = paragraph.getNode(); + Node sectionNode = firstNode.getParent(); + firstNode.setProperty(CMS_CONTENT, first); + Node secondNode = sectionNode.addNode(CMS_P); + secondNode.addMixin(CmsTypes.CMS_STYLED); + // second node was create as last, if it is not the next one, it + // means there are some in between and we can take the one at + // index+1 for the re-order + if (secondNode.getIndex() > firstNode.getIndex() + 1) { + sectionNode.orderBefore(p(secondNode.getIndex()), + p(firstNode.getIndex() + 1)); + } + + // if we die in between, at least we still have the whole text + // in the first node + textInterpreter.write(secondNode, second); + textInterpreter.write(firstNode, first); + + Paragraph secondParagraph = paragraphSplitted(paragraph, + secondNode); + edit(secondParagraph, 0); + } else if (getEdited() instanceof SectionTitle) { + SectionTitle sectionTitle = (SectionTitle) getEdited(); + Text text = (Text) sectionTitle.getControl(); + String txt = text.getText(); + int caretPosition = text.getCaretPosition(); + Section section = sectionTitle.getSection(); + Node sectionNode = section.getNode(); + Node paragraphNode = sectionNode.addNode(CMS_P); + paragraphNode.addMixin(CmsTypes.CMS_STYLED); + textInterpreter.write(paragraphNode, + txt.substring(caretPosition)); + textInterpreter.write( + sectionNode.getProperty(Property.JCR_TITLE), + txt.substring(0, caretPosition)); + sectionNode.orderBefore(p(paragraphNode.getIndex()), p(1)); + sectionNode.getSession().save(); + + Paragraph paragraph = sectionTitleSplitted(sectionTitle, + paragraphNode); + // section.layout(); + edit(paragraph, 0); + } + } catch (RepositoryException e) { + throw new CmsException("Cannot split " + getEdited(), e); + } + } + + protected void mergeWithPrevious() { + checkEdited(); + try { + Paragraph paragraph = (Paragraph) getEdited(); + Text text = (Text) paragraph.getControl(); + String txt = text.getText(); + Node paragraphNode = paragraph.getNode(); + if (paragraphNode.getIndex() == 1) + return;// do nothing + Node sectionNode = paragraphNode.getParent(); + Node previousNode = sectionNode + .getNode(p(paragraphNode.getIndex() - 1)); + String previousTxt = textInterpreter.read(previousNode); + textInterpreter.write(previousNode, previousTxt + txt); + paragraphNode.remove(); + sectionNode.getSession().save(); + + Paragraph previousParagraph = paragraphMergedWithPrevious( + paragraph, previousNode); + edit(previousParagraph, previousTxt.length()); + } catch (RepositoryException e) { + throw new CmsException("Cannot stop editing", e); + } + } + + protected void mergeWithNext() { + checkEdited(); + try { + Paragraph paragraph = (Paragraph) getEdited(); + Text text = (Text) paragraph.getControl(); + String txt = text.getText(); + Node paragraphNode = paragraph.getNode(); + Node sectionNode = paragraphNode.getParent(); + NodeIterator paragraphNodes = sectionNode.getNodes(CMS_P); + long size = paragraphNodes.getSize(); + if (paragraphNode.getIndex() == size) + return;// do nothing + Node nextNode = sectionNode + .getNode(p(paragraphNode.getIndex() + 1)); + String nextTxt = textInterpreter.read(nextNode); + textInterpreter.write(paragraphNode, txt + nextTxt); + + Section section = paragraph.getSection(); + Paragraph removed = (Paragraph) section.getSectionPart(nextNode + .getIdentifier()); + + nextNode.remove(); + sectionNode.getSession().save(); + + paragraphMergedWithNext(paragraph, removed); + edit(paragraph, txt.length()); + } catch (RepositoryException e) { + throw new CmsException("Cannot stop editing", e); + } + } + + protected synchronized void upload(EditablePart part) { + try { + if (part instanceof SectionPart) { + SectionPart sectionPart = (SectionPart) part; + Node partNode = sectionPart.getNode(); + int partIndex = partNode.getIndex(); + Section section = sectionPart.getSection(); + Node sectionNode = section.getNode(); + + if (part instanceof Paragraph) { + Node newNode = sectionNode.addNode(CMS_P, NodeType.NT_FILE); + newNode.addNode(Node.JCR_CONTENT, NodeType.NT_RESOURCE); + JcrUtils.copyBytesAsFile(sectionNode, + p(newNode.getIndex()), new byte[0]); + if (partIndex < newNode.getIndex() - 1) { + // was not last + sectionNode.orderBefore(p(newNode.getIndex()), + p(partIndex - 1)); + } + // sectionNode.orderBefore(p(partNode.getIndex()), + // p(newNode.getIndex())); + sectionNode.getSession().save(); + Img img = newImg((TextSection) section, newNode); + edit(img, null); + layout(img.getControl()); + } else if (part instanceof Img) { + if (getEdited() == part) + return; + edit(part, null); + layout(part.getControl()); + } + } + } catch (RepositoryException e) { + throw new CmsException("Cannot upload", e); + } + } + + protected void deepen() { + if (flat) + return; + checkEdited(); + try { + if (getEdited() instanceof Paragraph) { + Paragraph paragraph = (Paragraph) getEdited(); + Text text = (Text) paragraph.getControl(); + String txt = text.getText(); + Node paragraphNode = paragraph.getNode(); + Section section = paragraph.getSection(); + Node sectionNode = section.getNode(); + // main title + if (section == mainSection && section instanceof TextSection + && paragraphNode.getIndex() == 1 + && !sectionNode.hasProperty(JCR_TITLE)) { + SectionTitle sectionTitle = prepareSectionTitle(section, + txt); + edit(sectionTitle, 0); + return; + } + Node newSectionNode = sectionNode.addNode(CMS_H, + CmsTypes.CMS_SECTION); + sectionNode.orderBefore(h(newSectionNode.getIndex()), h(1)); + + int paragraphIndex = paragraphNode.getIndex(); + String sectionPath = sectionNode.getPath(); + String newSectionPath = newSectionNode.getPath(); + while (sectionNode.hasNode(p(paragraphIndex + 1))) { + Node parag = sectionNode.getNode(p(paragraphIndex + 1)); + sectionNode.getSession().move( + sectionPath + '/' + p(paragraphIndex + 1), + newSectionPath + '/' + CMS_P); + SectionPart sp = section.getSectionPart(parag + .getIdentifier()); + if (sp instanceof Control) + ((Control) sp).dispose(); + } + // create property + newSectionNode.setProperty(Property.JCR_TITLE, ""); + getTextInterpreter().write( + newSectionNode.getProperty(Property.JCR_TITLE), txt); + + TextSection newSection = new TextSection(section, + section.getStyle(), newSectionNode); + newSection.setLayoutData(CmsUtils.fillWidth()); + newSection.moveBelow(paragraph); + + // dispose + paragraphNode.remove(); + paragraph.dispose(); + + refresh(newSection); + newSection.getParent().layout(); + layout(newSection); + newSectionNode.getSession().save(); + } else if (getEdited() instanceof SectionTitle) { + SectionTitle sectionTitle = (SectionTitle) getEdited(); + Section section = sectionTitle.getSection(); + Section parentSection = section.getParentSection(); + if (parentSection == null) + return;// cannot deepen main section + Node sectionN = section.getNode(); + Node parentSectionN = parentSection.getNode(); + if (sectionN.getIndex() == 1) + return;// cannot deepen first section + Node previousSectionN = parentSectionN.getNode(h(sectionN + .getIndex() - 1)); + NodeIterator subSections = previousSectionN.getNodes(CMS_H); + int subsectionsCount = (int) subSections.getSize(); + previousSectionN.getSession().move( + sectionN.getPath(), + previousSectionN.getPath() + "/" + + h(subsectionsCount + 1)); + section.dispose(); + TextSection newSection = new TextSection(section, + section.getStyle(), sectionN); + refresh(newSection); + previousSectionN.getSession().save(); + } + } catch (RepositoryException e) { + throw new CmsException("Cannot deepen " + getEdited(), e); + } + } + + protected void undeepen() { + if (flat) + return; + checkEdited(); + try { + if (getEdited() instanceof Paragraph) { + upload(getEdited()); + } else if (getEdited() instanceof SectionTitle) { + SectionTitle sectionTitle = (SectionTitle) getEdited(); + Section section = sectionTitle.getSection(); + Node sectionNode = section.getNode(); + Section parentSection = section.getParentSection(); + if (parentSection == null) + return;// cannot undeepen main section + + // choose in which section to merge + Section mergedSection; + if (sectionNode.getIndex() == 1) + mergedSection = section.getParentSection(); + else { + Map parentSubsections = parentSection + .getSubSections(); + ArrayList
lst = new ArrayList
( + parentSubsections.values()); + mergedSection = lst.get(sectionNode.getIndex() - 1); + } + Node mergedNode = mergedSection.getNode(); + boolean mergedHasSubSections = mergedNode.hasNode(CMS_H); + + // title as paragraph + Node newParagrapheNode = mergedNode.addNode(CMS_P); + newParagrapheNode.addMixin(CmsTypes.CMS_STYLED); + if (mergedHasSubSections) + mergedNode.orderBefore(p(newParagrapheNode.getIndex()), + h(1)); + String txt = getTextInterpreter().read( + sectionNode.getProperty(Property.JCR_TITLE)); + getTextInterpreter().write(newParagrapheNode, txt); + // move + NodeIterator paragraphs = sectionNode.getNodes(CMS_P); + while (paragraphs.hasNext()) { + Node p = paragraphs.nextNode(); + SectionPart sp = section.getSectionPart(p.getIdentifier()); + if (sp instanceof Control) + ((Control) sp).dispose(); + mergedNode.getSession().move(p.getPath(), + mergedNode.getPath() + '/' + CMS_P); + if (mergedHasSubSections) + mergedNode.orderBefore(p(p.getIndex()), h(1)); + } + + Iterator
subsections = section.getSubSections() + .values().iterator(); + // NodeIterator sections = sectionNode.getNodes(CMS_H); + while (subsections.hasNext()) { + Section subsection = subsections.next(); + Node s = subsection.getNode(); + mergedNode.getSession().move(s.getPath(), + mergedNode.getPath() + '/' + CMS_H); + subsection.dispose(); + } + + // remove section + section.getNode().remove(); + section.dispose(); + + refresh(mergedSection); + mergedSection.getParent().layout(); + layout(mergedSection); + mergedNode.getSession().save(); + } + } catch (RepositoryException e) { + throw new CmsException("Cannot undeepen " + getEdited(), e); + } + } + + // UI CHANGES + protected Paragraph paragraphSplitted(Paragraph paragraph, Node newNode) + throws RepositoryException { + Section section = paragraph.getSection(); + updateContent(paragraph); + Paragraph newParagraph = newParagraph((TextSection) section, newNode); + newParagraph.setLayoutData(CmsUtils.fillWidth()); + newParagraph.moveBelow(paragraph); + layout(paragraph.getControl(), newParagraph.getControl()); + return newParagraph; + } + + protected Paragraph sectionTitleSplitted(SectionTitle sectionTitle, + Node newNode) throws RepositoryException { + updateContent(sectionTitle); + Paragraph newParagraph = newParagraph(sectionTitle.getSection(), + newNode); + // we assume beforeFirst is not null since there was a sectionTitle + newParagraph.moveBelow(sectionTitle.getSection().getHeader()); + layout(sectionTitle.getControl(), newParagraph.getControl()); + return newParagraph; + } + + protected Paragraph paragraphMergedWithPrevious(Paragraph removed, + Node remaining) throws RepositoryException { + Section section = removed.getSection(); + removed.dispose(); + + Paragraph paragraph = (Paragraph) section.getSectionPart(remaining + .getIdentifier()); + updateContent(paragraph); + layout(paragraph.getControl()); + return paragraph; + } + + protected void paragraphMergedWithNext(Paragraph remaining, + Paragraph removed) throws RepositoryException { + removed.dispose(); + updateContent(remaining); + layout(remaining.getControl()); + } + + // UTILITIES + protected String p(Integer index) { + StringBuilder sb = new StringBuilder(6); + sb.append(CMS_P).append('[').append(index).append(']'); + return sb.toString(); + } + + protected String h(Integer index) { + StringBuilder sb = new StringBuilder(5); + sb.append(CMS_H).append('[').append(index).append(']'); + return sb.toString(); + } + + // GETTERS / SETTERS + public Section getMainSection() { + return mainSection; + } + + public boolean isFlat() { + return flat; + } + + public TextInterpreter getTextInterpreter() { + return textInterpreter; + } + + // KEY LISTENER + @Override + public void keyPressed(KeyEvent e) { + if (log.isTraceEnabled()) + log.trace(e); + + if (getEdited() == null) + return; + boolean altPressed = (e.stateMask & SWT.ALT) != 0; + boolean shiftPressed = (e.stateMask & SWT.SHIFT) != 0; + boolean ctrlPressed = (e.stateMask & SWT.CTRL) != 0; + + // Common + if (e.keyCode == SWT.ESC) { + cancelEdit(); + } else if (e.character == '\r') { + splitEdit(); + } else if (e.character == 'S') { + if (ctrlPressed) + saveEdit(); + } else if (e.character == '\t') { + if (!shiftPressed) { + deepen(); + } else if (shiftPressed) { + undeepen(); + } + } else { + if (getEdited() instanceof Paragraph) { + Paragraph paragraph = (Paragraph) getEdited(); + Section section = paragraph.getSection(); + if (altPressed && e.keyCode == SWT.ARROW_RIGHT) { + edit(section.nextSectionPart(paragraph), 0); + } else if (altPressed && e.keyCode == SWT.ARROW_LEFT) { + edit(section.previousSectionPart(paragraph), 0); + } else if (e.character == SWT.BS) { + Text text = (Text) paragraph.getControl(); + int caretPosition = text.getCaretPosition(); + if (caretPosition == 0) { + mergeWithPrevious(); + } + } else if (e.character == SWT.DEL) { + Text text = (Text) paragraph.getControl(); + int caretPosition = text.getCaretPosition(); + int charcount = text.getCharCount(); + if (caretPosition == charcount) { + mergeWithNext(); + } + } + } + } + } + + @Override + public void keyReleased(KeyEvent e) { + } + + // MOUSE LISTENER + @Override + protected MouseListener createMouseListener() { + return new ML(); + } + + private class ML extends MouseAdapter { + private static final long serialVersionUID = 8526890859876770905L; + + @Override + public void mouseDoubleClick(MouseEvent e) { + if (e.button == 1) { + Control source = (Control) e.getSource(); + if (getCmsEditable().canEdit()) { + if (getCmsEditable().isEditing() + && !(getEdited() instanceof Img)) { + if (source == mainSection) + return; + EditablePart part = findDataParent(source); + upload(part); + } else { + getCmsEditable().startEditing(); + } + } + } + } + + @Override + public void mouseDown(MouseEvent e) { + if (getCmsEditable().isEditing()) { + if (e.button == 1) { + Control source = (Control) e.getSource(); + EditablePart composite = findDataParent(source); + Point point = new Point(e.x, e.y); + if (!(composite instanceof Img)) + edit(composite, source.toDisplay(point)); + } else if (e.button == 3) { + EditablePart composite = findDataParent((Control) e + .getSource()); + if (styledTools != null) + styledTools.show(composite, new Point(e.x, e.y)); + } + } + } + + private EditablePart findDataParent(Control parent) { + if (parent instanceof EditablePart) { + return (EditablePart) parent; + } + if (parent.getParent() != null) + return findDataParent(parent.getParent()); + else + throw new CmsException("No data parent found"); + } + + @Override + public void mouseUp(MouseEvent e) { + } + } + + // FILE UPLOAD LISTENER + private class FUL implements FileUploadListener { + public void uploadProgress(FileUploadEvent event) { + // TODO Monitor upload progress + } + + public void uploadFailed(FileUploadEvent event) { + throw new CmsException("Upload failed " + event, + event.getException()); + } + + public void uploadFinished(FileUploadEvent event) { + for (FileDetails file : event.getFileDetails()) { + if (log.isDebugEnabled()) + log.debug("Received: " + file.getFileName()); + } + mainSection.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + saveEdit(); + } + }); + FileUploadHandler uploadHandler = (FileUploadHandler) event + .getSource(); + uploadHandler.dispose(); + } + } +} \ No newline at end of file diff --git a/org.argeo.cms/src/org/argeo/cms/internal/text/SectionTitle.java b/org.argeo.cms/src/org/argeo/cms/internal/text/SectionTitle.java new file mode 100644 index 000000000..160060ea3 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/internal/text/SectionTitle.java @@ -0,0 +1,39 @@ +package org.argeo.cms.internal.text; + +import javax.jcr.Property; +import javax.jcr.RepositoryException; + +import org.argeo.cms.text.TextSection; +import org.argeo.cms.viewers.EditablePart; +import org.argeo.cms.viewers.PropertyPart; +import org.argeo.cms.widgets.EditableText; +import org.eclipse.swt.widgets.Composite; + +/** The title of a section. */ +public class SectionTitle extends EditableText implements EditablePart, + PropertyPart { + private static final long serialVersionUID = -1787983154946583171L; + + private final TextSection section; + + public SectionTitle(Composite parent, int swtStyle, Property title) + throws RepositoryException { + super(parent, swtStyle, title); + section = (TextSection) TextSection.findSection(this); + } + + public TextSection getSection() { + return section; + } + + // @Override + // public Property getProperty() throws RepositoryException { + // return getSection().getNode().getProperty(Property.JCR_TITLE); + // } + + @Override + public Property getItem() throws RepositoryException { + return getProperty(); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/internal/text/TextContextMenu.java b/org.argeo.cms/src/org/argeo/cms/internal/text/TextContextMenu.java new file mode 100644 index 000000000..8afad1629 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/internal/text/TextContextMenu.java @@ -0,0 +1,135 @@ +package org.argeo.cms.internal.text; + +import java.util.ArrayList; +import java.util.List; + +import org.argeo.cms.CmsNames; +import org.argeo.cms.text.Paragraph; +import org.argeo.cms.text.TextStyles; +import org.argeo.cms.viewers.EditablePart; +import org.argeo.cms.viewers.SectionPart; +import org.eclipse.rap.rwt.RWT; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.ShellEvent; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +/** Dialog to edit a text part. */ +class TextContextMenu extends Shell implements CmsNames, TextStyles { + private final static String[] DEFAULT_TEXT_STYLES = { + TextStyles.TEXT_DEFAULT, TextStyles.TEXT_PRE, TextStyles.TEXT_QUOTE }; + + private final AbstractTextViewer textViewer; + + private static final long serialVersionUID = -3826246895162050331L; + private List styleButtons = new ArrayList(); + + private Label deleteButton, publishButton, editButton; + + private EditablePart currentTextPart; + + public TextContextMenu(AbstractTextViewer textViewer, Display display) { + super(display, SWT.NO_TRIM | SWT.BORDER | SWT.ON_TOP); + this.textViewer = textViewer; + setLayout(new GridLayout()); + setData(RWT.CUSTOM_VARIANT, TEXT_STYLED_TOOLS_DIALOG); + + StyledToolMouseListener stml = new StyledToolMouseListener(); + if (textViewer.getCmsEditable().isEditing()) { + for (String style : DEFAULT_TEXT_STYLES) { + StyleButton styleButton = new StyleButton(this, SWT.WRAP); + styleButton.setData(RWT.CUSTOM_VARIANT, style); + styleButton.setData(RWT.MARKUP_ENABLED, true); + styleButton.addMouseListener(stml); + styleButtons.add(styleButton); + } + + // Delete + deleteButton = new Label(this, SWT.NONE); + deleteButton.setText("Delete"); + deleteButton.addMouseListener(stml); + + // Publish + publishButton = new Label(this, SWT.NONE); + publishButton.setText("Publish"); + publishButton.addMouseListener(stml); + } else if (textViewer.getCmsEditable().canEdit()) { + // Edit + editButton = new Label(this, SWT.NONE); + editButton.setText("Edit"); + editButton.addMouseListener(stml); + } + addShellListener(new ToolsShellListener()); + } + + public void show(EditablePart source, Point location) { + if (isVisible()) + setVisible(false); + + this.currentTextPart = source; + + if (currentTextPart instanceof Paragraph) { + final int size = 32; + String text = textViewer + .getRawParagraphText((Paragraph) currentTextPart); + String textToShow = text.length() > size ? text.substring(0, + size - 3) + "..." : text; + for (StyleButton styleButton : styleButtons) { + styleButton.setText(textToShow); + } + } + pack(); + layout(); + if (source instanceof Control) + setLocation(((Control) source).toDisplay(location.x, location.y)); + open(); + } + + class StyleButton extends Label { + private static final long serialVersionUID = 7731102609123946115L; + + public StyleButton(Composite parent, int swtStyle) { + super(parent, swtStyle); + } + + } + + class StyledToolMouseListener extends MouseAdapter { + private static final long serialVersionUID = 8516297091549329043L; + + @Override + public void mouseDown(MouseEvent e) { + Object eventSource = e.getSource(); + if (eventSource instanceof StyleButton) { + StyleButton sb = (StyleButton) e.getSource(); + String style = sb.getData(RWT.CUSTOM_VARIANT).toString(); + textViewer + .setParagraphStyle((Paragraph) currentTextPart, style); + } else if (eventSource == deleteButton) { + textViewer.deletePart((SectionPart) currentTextPart); + } else if (eventSource == editButton) { + textViewer.getCmsEditable().startEditing(); + } else if (eventSource == publishButton) { + textViewer.getCmsEditable().stopEditing(); + } + setVisible(false); + } + } + + class ToolsShellListener extends org.eclipse.swt.events.ShellAdapter { + private static final long serialVersionUID = 8432350564023247241L; + + @Override + public void shellDeactivated(ShellEvent e) { + setVisible(false); + } + + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/text/CustomTextEditor.java b/org.argeo.cms/src/org/argeo/cms/text/CustomTextEditor.java new file mode 100644 index 000000000..6ff78108f --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/text/CustomTextEditor.java @@ -0,0 +1,35 @@ +package org.argeo.cms.text; + +import static org.argeo.cms.CmsUtils.fillWidth; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.cms.CmsEditable; +import org.argeo.cms.internal.text.AbstractTextViewer; +import org.argeo.cms.viewers.Section; +import org.eclipse.swt.widgets.Composite; + +/** + * Manages hardcoded sections as an arbitrary hierarchy under the main section, + * which contains no text and no title. + */ +public class CustomTextEditor extends AbstractTextViewer { + private static final long serialVersionUID = 5277789504209413500L; + + public CustomTextEditor(Composite parent, int style, Node textNode, + CmsEditable cmsEditable) throws RepositoryException { + this(new Section(parent, style, textNode), style, cmsEditable); + } + + public CustomTextEditor(Section mainSection, int style, + CmsEditable cmsEditable) throws RepositoryException { + super(mainSection, style, cmsEditable); + mainSection.setLayoutData(fillWidth()); + } + + @Override + public Section getMainSection() { + return super.getMainSection(); + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/text/Img.java b/org.argeo.cms/src/org/argeo/cms/text/Img.java new file mode 100644 index 000000000..83c32087f --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/text/Img.java @@ -0,0 +1,153 @@ +package org.argeo.cms.text; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.cms.CmsException; +import org.argeo.cms.CmsImageManager; +import org.argeo.cms.CmsSession; +import org.argeo.cms.CmsUtils; +import org.argeo.cms.internal.JcrFileUploadReceiver; +import org.argeo.cms.viewers.NodePart; +import org.argeo.cms.viewers.Section; +import org.argeo.cms.viewers.SectionPart; +import org.argeo.cms.widgets.EditableImage; +import org.eclipse.rap.addons.fileupload.FileUploadHandler; +import org.eclipse.rap.addons.fileupload.FileUploadListener; +import org.eclipse.rap.addons.fileupload.FileUploadReceiver; +import org.eclipse.rap.rwt.service.ServerPushSession; +import org.eclipse.rap.rwt.widgets.FileUpload; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + +/** An image within the Argeo Text framework */ +public class Img extends EditableImage implements SectionPart, NodePart { + private static final long serialVersionUID = 6233572783968188476L; + + private final Section section; + + private final CmsImageManager imageManager; + private FileUploadHandler currentUploadHandler = null; + private FileUploadListener fileUploadListener; + + public Img(Composite parent, int swtStyle, Node imgNode, + Point preferredImageSize) throws RepositoryException { + this(Section.findSection(parent), parent, swtStyle, imgNode, + preferredImageSize); + setStyle(TextStyles.TEXT_IMAGE); + } + + public Img(Composite parent, int swtStyle, Node imgNode) + throws RepositoryException { + this(Section.findSection(parent), parent, swtStyle, imgNode, null); + setStyle(TextStyles.TEXT_IMAGE); + } + + Img(Section section, Composite parent, int swtStyle, Node imgNode, + Point preferredImageSize) throws RepositoryException { + super(parent, swtStyle, imgNode, false, preferredImageSize); + this.section = section; + imageManager = CmsSession.current.get().getImageManager(); + CmsUtils.style(this, TextStyles.TEXT_IMG); + } + + @Override + protected Control createControl(Composite box, String style) { + if (isEditing()) { + try { + return createImageChooser(box, style); + } catch (RepositoryException e) { + throw new CmsException("Cannot create image chooser", e); + } + } else { + return createLabel(box, style); + } + } + + @Override + public synchronized void stopEditing() { + super.stopEditing(); + fileUploadListener = null; + } + + @Override + protected synchronized Boolean load(Control lbl) { + try { + Node imgNode = getNode(); + boolean loaded = imageManager.load(imgNode, lbl, + getPreferredImageSize()); + // getParent().layout(); + return loaded; + } catch (RepositoryException e) { + throw new CmsException("Cannot load " + getNodeId() + + " from image manager", e); + } + } + + protected Control createImageChooser(Composite box, String style) + throws RepositoryException { + // FileDialog fileDialog = new FileDialog(getShell()); + // fileDialog.open(); + // String fileName = fileDialog.getFileName(); + CmsImageManager imageManager = CmsSession.current.get() + .getImageManager(); + Node node = getNode(); + JcrFileUploadReceiver receiver = new JcrFileUploadReceiver( + node.getParent(), node.getName() + '[' + node.getIndex() + ']', + imageManager); + if (currentUploadHandler != null) + currentUploadHandler.dispose(); + currentUploadHandler = prepareUpload(receiver); + final ServerPushSession pushSession = new ServerPushSession(); + final FileUpload fileUpload = new FileUpload(box, SWT.NONE); + CmsUtils.style(fileUpload, style); + fileUpload.addSelectionListener(new SelectionAdapter() { + private static final long serialVersionUID = -9158471843941668562L; + + @Override + public void widgetSelected(SelectionEvent e) { + pushSession.start(); + fileUpload.submit(currentUploadHandler.getUploadUrl()); + } + }); + return fileUpload; + } + + protected FileUploadHandler prepareUpload(FileUploadReceiver receiver) { + final FileUploadHandler uploadHandler = new FileUploadHandler(receiver); + if (fileUploadListener != null) + uploadHandler.addUploadListener(fileUploadListener); + return uploadHandler; + } + + @Override + public Section getSection() { + return section; + } + + public void setFileUploadListener(FileUploadListener fileUploadListener) { + this.fileUploadListener = fileUploadListener; + if (currentUploadHandler != null) + currentUploadHandler.addUploadListener(fileUploadListener); + } + + @Override + public Node getItem() throws RepositoryException { + return getNode(); + } + + @Override + public String getPartId() { + return getNodeId(); + } + + @Override + public String toString() { + return "Img #" + getPartId(); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/text/Paragraph.java b/org.argeo.cms/src/org/argeo/cms/text/Paragraph.java new file mode 100644 index 000000000..d917c45d4 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/text/Paragraph.java @@ -0,0 +1,41 @@ +package org.argeo.cms.text; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.cms.CmsUtils; +import org.argeo.cms.viewers.Section; +import org.argeo.cms.viewers.SectionPart; +import org.argeo.cms.widgets.EditableText; + +public class Paragraph extends EditableText implements SectionPart { + private static final long serialVersionUID = 3746457776229542887L; + + private final TextSection section; + + public Paragraph(TextSection section, int style, Node node) + throws RepositoryException { + super(section, style, node); + this.section = section; + CmsUtils.style(this, TextStyles.TEXT_PARAGRAPH); + } + + public Section getSection() { + return section; + } + + @Override + public String getPartId() { + return getNodeId(); + } + + @Override + public Node getItem() throws RepositoryException { + return getNode(); + } + + @Override + public String toString() { + return "Paragraph #" + getPartId(); + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/text/StandardTextEditor.java b/org.argeo.cms/src/org/argeo/cms/text/StandardTextEditor.java new file mode 100644 index 000000000..7a3de3be9 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/text/StandardTextEditor.java @@ -0,0 +1,48 @@ +package org.argeo.cms.text; + +import static javax.jcr.Property.JCR_TITLE; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; + +import org.argeo.cms.CmsEditable; +import org.argeo.cms.CmsTypes; +import org.argeo.cms.CmsUtils; +import org.argeo.cms.internal.text.AbstractTextViewer; +import org.argeo.cms.viewers.Section; +import org.eclipse.swt.widgets.Composite; + +/** Text editor where sections and subsections can be managed by the user. */ +public class StandardTextEditor extends AbstractTextViewer { + private static final long serialVersionUID = 6049661610883342325L; + + public StandardTextEditor(Composite parent, int style, Node textNode, + CmsEditable cmsEditable) throws RepositoryException { + super(new TextSection(parent, style, textNode), style, cmsEditable); + refresh(); + getMainSection().setLayoutData(CmsUtils.fillWidth()); + } + + @Override + protected void initModel(Node textNode) throws RepositoryException { + if (isFlat()) + textNode.addNode(CMS_P).addMixin(CmsTypes.CMS_STYLED); + else + textNode.setProperty(JCR_TITLE, textNode.getName()); + } + + @Override + protected Boolean isModelInitialized(Node textNode) + throws RepositoryException { + return textNode.hasProperty(Property.JCR_TITLE) + || textNode.hasNode(CMS_P) + || (!isFlat() && textNode.hasNode(CMS_H)); + } + + @Override + public Section getMainSection() { + // TODO Auto-generated method stub + return super.getMainSection(); + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/text/TextEditorHeader.java b/org.argeo.cms/src/org/argeo/cms/text/TextEditorHeader.java new file mode 100644 index 000000000..6821fcb53 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/text/TextEditorHeader.java @@ -0,0 +1,90 @@ +package org.argeo.cms.text; + +import java.util.Observable; +import java.util.Observer; + +import org.argeo.cms.CmsEditable; +import org.argeo.cms.CmsUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; + +/** Adds editing capabilities to a page editing text */ +public class TextEditorHeader implements SelectionListener, Observer { + private static final long serialVersionUID = 4186756396045701253L; + + private final CmsEditable cmsEditable; + private Button publish; + + private Composite parent; + private Composite display; + private Object layoutData; + + public TextEditorHeader(CmsEditable cmsEditable, Composite parent, int style) { + this.cmsEditable = cmsEditable; + this.parent = parent; + if (this.cmsEditable instanceof Observable) + ((Observable) this.cmsEditable).addObserver(this); + refresh(); + } + + protected void refresh() { + if (display != null && !display.isDisposed()) + display.dispose(); + display = null; + publish = null; + if (cmsEditable.isEditing()) { + display = new Composite(parent, SWT.NONE); + // display.setBackgroundMode(SWT.INHERIT_NONE); + display.setLayoutData(layoutData); + display.setLayout(CmsUtils.noSpaceGridLayout()); + CmsUtils.style(display, TextStyles.TEXT_EDITOR_HEADER); + publish = new Button(display, SWT.FLAT | SWT.PUSH); + publish.setText(getPublishButtonLabel()); + CmsUtils.style(publish, TextStyles.TEXT_EDITOR_HEADER); + publish.addSelectionListener(this); + display.moveAbove(null); + } + parent.layout(); + } + + private String getPublishButtonLabel() { + if (cmsEditable.isEditing()) + return "Publish"; + else + return "Edit"; + } + + @Override + public void widgetSelected(SelectionEvent e) { + if (e.getSource() == publish) { + if (cmsEditable.isEditing()) { + cmsEditable.stopEditing(); + } else { + cmsEditable.startEditing(); + } + // publish.setText(getPublishButtonLabel()); + } + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } + + @Override + public void update(Observable o, Object arg) { + if (o == cmsEditable) { + // publish.setText(getPublishButtonLabel()); + refresh(); + } + } + + public void setLayoutData(Object layoutData) { + this.layoutData = layoutData; + if (display != null && !display.isDisposed()) + display.setLayoutData(layoutData); + } + +} \ No newline at end of file diff --git a/org.argeo.cms/src/org/argeo/cms/text/TextSection.java b/org.argeo.cms/src/org/argeo/cms/text/TextSection.java new file mode 100644 index 000000000..09456f2c7 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/text/TextSection.java @@ -0,0 +1,52 @@ +package org.argeo.cms.text; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.cms.CmsNames; +import org.argeo.cms.CmsUtils; +import org.argeo.cms.viewers.Section; +import org.eclipse.swt.widgets.Composite; + +public class TextSection extends Section implements CmsNames { + private static final long serialVersionUID = -8625209546243220689L; + private String defaultTextStyle = TextStyles.TEXT_DEFAULT; + private String titleStyle; + + public TextSection(Composite parent, int style, Node node) + throws RepositoryException { + this(parent, findSection(parent), style, node); + } + + public TextSection(TextSection section, int style, Node node) + throws RepositoryException { + this(section, section.getParentSection(), style, node); + } + + private TextSection(Composite parent, Section parentSection, int style, + Node node) throws RepositoryException { + super(parent, parentSection, style, node); + CmsUtils.style(this, TextStyles.TEXT_SECTION); + } + + public String getDefaultTextStyle() { + return defaultTextStyle; + } + + public String getTitleStyle() { + if (titleStyle != null) + return titleStyle; + // TODO make base H styles configurable + Integer relativeDepth = getRelativeDepth(); + return relativeDepth == 0 ? TextStyles.TEXT_TITLE : TextStyles.TEXT_H + + relativeDepth; + } + + public void setDefaultTextStyle(String defaultTextStyle) { + this.defaultTextStyle = defaultTextStyle; + } + + public void setTitleStyle(String titleStyle) { + this.titleStyle = titleStyle; + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/text/TextStyles.java b/org.argeo.cms/src/org/argeo/cms/text/TextStyles.java new file mode 100644 index 000000000..44c3ad054 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/text/TextStyles.java @@ -0,0 +1,37 @@ +package org.argeo.cms.text; + +/** Styles references in the CSS. */ +public interface TextStyles { + /** The whole page area */ + public final static String TEXT_AREA = "text_area"; + /** Area providing controls for editing text */ + public final static String TEXT_EDITOR_HEADER = "text_editor_header"; + /** The styled composite for editing the text */ + public final static String TEXT_STYLED_COMPOSITE = "text_styled_composite"; + /** A section */ + public final static String TEXT_SECTION = "text_section"; + /** A paragraph */ + public final static String TEXT_PARAGRAPH = "text_paragraph"; + /** An image */ + public final static String TEXT_IMG = "text_img"; + /** The dialog to edit styled paragraph */ + public final static String TEXT_STYLED_TOOLS_DIALOG = "text_styled_tools_dialog"; + + /* + * DEFAULT TEXT STYLES + */ + /** Default style for text body */ + public final static String TEXT_DEFAULT = "text_default"; + /** Fixed-width, typically code */ + public final static String TEXT_PRE = "text_pre"; + /** Quote */ + public final static String TEXT_QUOTE = "text_quote"; + /** Title */ + public final static String TEXT_TITLE = "text_title"; + /** Header (to be dynamically completed with the depth, e.g. text_h1) */ + public final static String TEXT_H = "text_h"; + + /** Default style for images */ + public final static String TEXT_IMAGE = "text_image"; + +} diff --git a/org.argeo.cms/src/org/argeo/cms/text/WikiPage.java b/org.argeo.cms/src/org/argeo/cms/text/WikiPage.java new file mode 100644 index 000000000..17c3d9c3a --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/text/WikiPage.java @@ -0,0 +1,63 @@ +package org.argeo.cms.text; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; + +import org.argeo.cms.CmsEditable; +import org.argeo.cms.CmsLink; +import org.argeo.cms.CmsNames; +import org.argeo.cms.CmsTypes; +import org.argeo.cms.CmsUiProvider; +import org.argeo.cms.CmsUtils; +import org.argeo.cms.viewers.JcrVersionCmsEditable; +import org.argeo.cms.widgets.ScrolledPage; +import org.argeo.jcr.JcrUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + +/** Display the text of the context, and provide an editor if the user can edit. */ +public class WikiPage implements CmsUiProvider, CmsNames { + @Override + public Control createUi(Composite parent, Node context) + throws RepositoryException { + CmsEditable cmsEditable = new JcrVersionCmsEditable(context); + if (cmsEditable.canEdit()) + new TextEditorHeader(cmsEditable, parent, SWT.NONE) + .setLayoutData(CmsUtils.fillWidth()); + + ScrolledPage page = new ScrolledPage(parent, SWT.NONE); + page.setLayout(CmsUtils.noSpaceGridLayout()); + GridData textGd = CmsUtils.fillAll(); + page.setLayoutData(textGd); + + if (context.isNodeType(CmsTypes.CMS_TEXT)) { + new StandardTextEditor(page, SWT.NONE, context, cmsEditable); + } else if (context.isNodeType(NodeType.NT_FOLDER) + || context.getPath().equals("/")) { + parent.setBackgroundMode(SWT.INHERIT_NONE); + Node indexNode = JcrUtils.getOrAdd(context, CMS_INDEX, + CmsTypes.CMS_TEXT); + new StandardTextEditor(page, SWT.NONE, indexNode, cmsEditable); + textGd.heightHint = 400; + + for (NodeIterator ni = context.getNodes(); ni.hasNext();) { + Node textNode = ni.nextNode(); + if (textNode.isNodeType(NodeType.NT_FOLDER)) + new CmsLink(textNode.getName() + "/", textNode.getPath()) + .createUi(parent, textNode); + } + for (NodeIterator ni = context.getNodes(); ni.hasNext();) { + Node textNode = ni.nextNode(); + if (textNode.isNodeType(CmsTypes.CMS_TEXT) + && !textNode.getName().equals(CMS_INDEX)) + new CmsLink(textNode.getName(), textNode.getPath()) + .createUi(parent, textNode); + } + } + return page; + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/viewers/AbstractPageViewer.java b/org.argeo.cms/src/org/argeo/cms/viewers/AbstractPageViewer.java new file mode 100644 index 000000000..5e6de3709 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/viewers/AbstractPageViewer.java @@ -0,0 +1,239 @@ +package org.argeo.cms.viewers; + +import java.util.Observable; +import java.util.Observer; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.cms.CmsEditable; +import org.argeo.cms.CmsException; +import org.argeo.cms.widgets.ScrolledPage; +import org.eclipse.jface.viewers.ContentViewer; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseListener; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Widget; + +/** Base class for viewers related to a page */ +public abstract class AbstractPageViewer extends ContentViewer implements + Observer { + private static final long serialVersionUID = 5438688173410341485L; + + private final static Log log = LogFactory.getLog(AbstractPageViewer.class); + + private final boolean readOnly; + /** The basis for the layouts, typically a ScrolledPage. */ + private final Composite page; + private final CmsEditable cmsEditable; + + private MouseListener mouseListener; + + private EditablePart edited; + private ISelection selection = StructuredSelection.EMPTY; + + protected AbstractPageViewer(Section parent, int style, + CmsEditable cmsEditable) { + // read only at UI level + readOnly = SWT.READ_ONLY == (style & SWT.READ_ONLY); + + this.cmsEditable = cmsEditable == null ? CmsEditable.NON_EDITABLE + : cmsEditable; + if (this.cmsEditable instanceof Observable) + ((Observable) this.cmsEditable).addObserver(this); + + if (cmsEditable.canEdit()) { + mouseListener = createMouseListener(); + } + page = findPage(parent); + } + + /** + * Can be called to simplify the called to isModelInitialized() and + * initModel() + */ + protected void initModelIfNeeded(Node node) { + try { + if (!isModelInitialized(node)) + if (getCmsEditable().canEdit()) { + initModel(node); + node.getSession().save(); + } + } catch (Exception e) { + throw new CmsException("Cannot initialize model", e); + } + } + + /** Called if user can edit and model is not initialized */ + protected Boolean isModelInitialized(Node node) throws RepositoryException { + return true; + } + + /** Called if user can edit and model is not initialized */ + protected void initModel(Node node) throws RepositoryException { + } + + /** Create (retrieve) the MouseListener to use. */ + protected MouseListener createMouseListener() { + return new MouseAdapter() { + private static final long serialVersionUID = 1L; + }; + } + + protected Composite findPage(Composite composite) { + if (composite instanceof ScrolledPage) { + return (ScrolledPage) composite; + } else { + if (composite.getParent() == null) + return composite; + return findPage(composite.getParent()); + } + } + + @Override + public void update(Observable o, Object arg) { + if (o == cmsEditable) + editingStateChanged(cmsEditable); + } + + /** To be overridden in order to provide the actual refresh */ + protected void refresh(Control control) throws RepositoryException { + } + + /** To be overridden.Save the edited part. */ + protected void save(EditablePart part) throws RepositoryException { + } + + /** Prepare the edited part */ + protected void prepare(EditablePart part, Object caretPosition) { + } + + /** Notified when the editing state changed. Does nothing, to be overridden */ + protected void editingStateChanged(CmsEditable cmsEditable) { + } + + @Override + public void refresh() { + try { + if (cmsEditable.canEdit() && !readOnly) + mouseListener = createMouseListener(); + else + mouseListener = null; + refresh(getControl()); + layout(getControl()); + } catch (RepositoryException e) { + throw new CmsException("Cannot refresh", e); + } + } + + @Override + public void setSelection(ISelection selection, boolean reveal) { + this.selection = selection; + } + + protected void updateContent(EditablePart part) throws RepositoryException { + } + + // LOW LEVEL EDITION + protected void edit(EditablePart part, Object caretPosition) { + try { + if (edited == part) + return; + + if (edited != null && edited != part) + stopEditing(true); + + part.startEditing(); + updateContent(part); + prepare(part, caretPosition); + edited = part; + layout(part.getControl()); + } catch (RepositoryException e) { + throw new CmsException("Cannot edit " + part, e); + } + } + + private void stopEditing(Boolean save) throws RepositoryException { + if (edited instanceof Widget && ((Widget) edited).isDisposed()) { + edited = null; + return; + } + + assert edited != null; + if (edited == null) { + if (log.isTraceEnabled()) + log.warn("Told to stop editing while not editing anything"); + return; + } + + if (save) + save(edited); + + edited.stopEditing(); + updateContent(edited); + layout(((EditablePart) edited).getControl()); + edited = null; + } + + // METHODS AVAILABLE TO EXTENDING CLASSES + protected void saveEdit() { + try { + if (edited != null) + stopEditing(true); + } catch (RepositoryException e) { + throw new CmsException("Cannot stop editing", e); + } + } + + protected void cancelEdit() { + try { + if (edited != null) + stopEditing(false); + } catch (RepositoryException e) { + throw new CmsException("Cannot cancel editing", e); + } + } + + /** Layout this controls from the related base page. */ + public void layout(Control... controls) { + page.layout(controls); + } + + // UTILITIES + /** Check whether the edited part is in a proper state */ + protected void checkEdited() { + if (edited == null || (edited instanceof Widget) + && ((Widget) edited).isDisposed()) + throw new CmsException( + "Edited should not be null or disposed at this stage"); + } + + // GETTERS / SETTERS + public boolean isReadOnly() { + return readOnly; + } + + protected EditablePart getEdited() { + return edited; + } + + public MouseListener getMouseListener() { + return mouseListener; + } + + public CmsEditable getCmsEditable() { + return cmsEditable; + } + + @Override + public ISelection getSelection() { + return selection; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/viewers/EditablePart.java b/org.argeo.cms/src/org/argeo/cms/viewers/EditablePart.java new file mode 100644 index 000000000..99f8acf9a --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/viewers/EditablePart.java @@ -0,0 +1,11 @@ +package org.argeo.cms.viewers; + +import org.eclipse.swt.widgets.Control; + +public interface EditablePart { + public void startEditing(); + + public void stopEditing(); + + public Control getControl(); +} diff --git a/org.argeo.cms/src/org/argeo/cms/viewers/ItemPart.java b/org.argeo.cms/src/org/argeo/cms/viewers/ItemPart.java new file mode 100644 index 000000000..52e5a88eb --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/viewers/ItemPart.java @@ -0,0 +1,9 @@ +package org.argeo.cms.viewers; + +import javax.jcr.Item; +import javax.jcr.RepositoryException; + +/** An editable part related to a JCR Item */ +public interface ItemPart { + public Item getItem() throws RepositoryException; +} diff --git a/org.argeo.cms/src/org/argeo/cms/viewers/JcrVersionCmsEditable.java b/org.argeo.cms/src/org/argeo/cms/viewers/JcrVersionCmsEditable.java new file mode 100644 index 000000000..bcd42851f --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/viewers/JcrVersionCmsEditable.java @@ -0,0 +1,100 @@ +package org.argeo.cms.viewers; + +import java.util.Observable; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; +import javax.jcr.version.VersionManager; + +import org.argeo.cms.CmsEditable; +import org.argeo.cms.CmsEditionEvent; +import org.argeo.cms.CmsException; +import org.eclipse.rap.rwt.RWT; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; + +/** Provides the CmsEditable semantic based on JCR versioning. */ +public class JcrVersionCmsEditable extends Observable implements CmsEditable { + private final String nodePath;// cache + private final VersionManager versionManager; + private final Boolean canEdit; + + public JcrVersionCmsEditable(Node node) throws RepositoryException { + this.nodePath = node.getPath(); + if (node.getSession().hasPermission(node.getPath(), + Session.ACTION_ADD_NODE)) { + canEdit = true; + if (!node.isNodeType(NodeType.MIX_VERSIONABLE)) { + node.addMixin(NodeType.MIX_VERSIONABLE); + node.getSession().save(); + } + versionManager = node.getSession().getWorkspace() + .getVersionManager(); + } else { + canEdit = false; + versionManager = null; + } + + // bind keys + if (canEdit) { + Display display = Display.getCurrent(); + display.setData(RWT.ACTIVE_KEYS, new String[] { "CTRL+RETURN", + "CTRL+E" }); + display.addFilter(SWT.KeyDown, new Listener() { + private static final long serialVersionUID = -4378653870463187318L; + + public void handleEvent(Event e) { + boolean ctrlPressed = (e.stateMask & SWT.CTRL) != 0; + if (ctrlPressed && e.keyCode == '\r') + stopEditing(); + else if (ctrlPressed && e.keyCode == 'E') + stopEditing(); + } + }); + } + } + + @Override + public Boolean canEdit() { + return canEdit; + } + + public Boolean isEditing() { + try { + if (!canEdit()) + return false; + return versionManager.isCheckedOut(nodePath); + } catch (RepositoryException e) { + throw new CmsException("Cannot check whether " + nodePath + + " is editing", e); + } + } + + @Override + public void startEditing() { + try { + versionManager.checkout(nodePath); + setChanged(); + } catch (RepositoryException e1) { + throw new CmsException("Cannot publish " + nodePath); + } + notifyObservers(new CmsEditionEvent(nodePath, + CmsEditionEvent.START_EDITING)); + } + + @Override + public void stopEditing() { + try { + versionManager.checkin(nodePath); + setChanged(); + } catch (RepositoryException e1) { + throw new CmsException("Cannot publish " + nodePath, e1); + } + notifyObservers(new CmsEditionEvent(nodePath, + CmsEditionEvent.STOP_EDITING)); + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/viewers/NodePart.java b/org.argeo.cms/src/org/argeo/cms/viewers/NodePart.java new file mode 100644 index 000000000..db9a60a87 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/viewers/NodePart.java @@ -0,0 +1,8 @@ +package org.argeo.cms.viewers; + +import javax.jcr.Node; + +/** An editable part related to a node */ +public interface NodePart extends ItemPart { + public Node getNode(); +} diff --git a/org.argeo.cms/src/org/argeo/cms/viewers/PropertyPart.java b/org.argeo.cms/src/org/argeo/cms/viewers/PropertyPart.java new file mode 100644 index 000000000..50fdd0601 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/viewers/PropertyPart.java @@ -0,0 +1,8 @@ +package org.argeo.cms.viewers; + +import javax.jcr.Property; + +/** An editable part related to a JCR Property */ +public interface PropertyPart extends ItemPart { + public Property getProperty(); +} diff --git a/org.argeo.cms/src/org/argeo/cms/viewers/Section.java b/org.argeo.cms/src/org/argeo/cms/viewers/Section.java new file mode 100644 index 000000000..c09b17987 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/viewers/Section.java @@ -0,0 +1,155 @@ +package org.argeo.cms.viewers; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.cms.CmsException; +import org.argeo.cms.CmsNames; +import org.argeo.cms.CmsUtils; +import org.argeo.cms.widgets.JcrComposite; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + +public class Section extends JcrComposite implements CmsNames { + private static final long serialVersionUID = -5933796173755739207L; + + private final Section parentSection; + private Composite sectionHeader; + private final Integer relativeDepth; + + public Section(Composite parent, int style, Node node) + throws RepositoryException { + this(parent, findSection(parent), style, node); + } + + public Section(Section section, int style, Node node) + throws RepositoryException { + this(section, section, style, node); + } + + protected Section(Composite parent, Section parentSection, int style, + Node node) throws RepositoryException { + super(parent, style, node); + this.parentSection = parentSection; + if (parentSection != null) { + relativeDepth = getNode().getDepth() + - parentSection.getNode().getDepth(); + } else { + relativeDepth = 0; + } + setLayout(CmsUtils.noSpaceGridLayout()); + } + + public Map getSubSections() throws RepositoryException { + LinkedHashMap result = new LinkedHashMap(); + for (Control child : getChildren()) { + if (child instanceof Composite) { + collectDirectSubSections((Composite) child, result); + } + } + return Collections.unmodifiableMap(result); + } + + private void collectDirectSubSections(Composite composite, + LinkedHashMap subSections) + throws RepositoryException { + if (composite == sectionHeader || composite instanceof EditablePart) + return; + if (composite instanceof Section) { + Section section = (Section) composite; + subSections.put(section.getNodeId(), section); + return; + } + + for (Control child : composite.getChildren()) + if (child instanceof Composite) + collectDirectSubSections((Composite) child, subSections); + } + + public void createHeader() { + if (sectionHeader != null) + throw new CmsException("Section header was already created"); + + sectionHeader = new Composite(this, SWT.NONE); + sectionHeader.setLayoutData(CmsUtils.fillWidth()); + sectionHeader.setLayout(CmsUtils.noSpaceGridLayout()); + // sectionHeader.moveAbove(null); + // layout(); + } + + public Composite getHeader() { + if (sectionHeader != null && sectionHeader.isDisposed()) + sectionHeader = null; + return sectionHeader; + } + + // SECTION PARTS + public SectionPart getSectionPart(String partId) { + for (Control child : getChildren()) { + if (child instanceof SectionPart) { + SectionPart paragraph = (SectionPart) child; + if (paragraph.getPartId().equals(partId)) + return paragraph; + } + } + return null; + } + + public SectionPart nextSectionPart(SectionPart sectionPart) { + Control[] children = getChildren(); + for (int i = 0; i < children.length; i++) { + if (sectionPart == children[i]) + if (i + 1 < children.length) { + Composite next = (Composite) children[i + 1]; + return (SectionPart) next; + } else { + // next section + } + } + return null; + } + + public SectionPart previousSectionPart(SectionPart sectionPart) { + Control[] children = getChildren(); + for (int i = 0; i < children.length; i++) { + if (sectionPart == children[i]) + if (i != 0) { + Composite previous = (Composite) children[i - 1]; + return (SectionPart) previous; + } else { + // previous section + } + } + return null; + } + + @Override + public String toString() { + if (parentSection == null) + return "Main section " + getNode(); + return "Section " + getNode(); + } + + public Section getParentSection() { + return parentSection; + } + + public Integer getRelativeDepth() { + return relativeDepth; + } + + /** Recursively finds the related section in the parents (can be itself) */ + public static Section findSection(Control control) { + if (control == null) + return null; + if (control instanceof Section) + return (Section) control; + else + return findSection(control.getParent()); + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/viewers/SectionPart.java b/org.argeo.cms/src/org/argeo/cms/viewers/SectionPart.java new file mode 100644 index 000000000..6cd45c5ba --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/viewers/SectionPart.java @@ -0,0 +1,9 @@ +package org.argeo.cms.viewers; + + +/** An editable part dynamically related to a Section */ +public interface SectionPart extends EditablePart, NodePart { + public String getPartId(); + + public Section getSection(); +} diff --git a/org.argeo.cms/src/org/argeo/cms/widgets/EditableImage.java b/org.argeo.cms/src/org/argeo/cms/widgets/EditableImage.java new file mode 100644 index 000000000..00a7c2669 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/widgets/EditableImage.java @@ -0,0 +1,112 @@ +package org.argeo.cms.widgets; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.cms.CmsUtils; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +/** A stylable and editable image. */ +public abstract class EditableImage extends StyledControl { + private static final long serialVersionUID = -5689145523114022890L; + private final static Log log = LogFactory.getLog(EditableImage.class); + + private Point preferredImageSize; + private Boolean loaded = false; + + public EditableImage(Composite parent, int swtStyle) { + super(parent, swtStyle); + } + + public EditableImage(Composite parent, int swtStyle, + Point preferredImageSize) { + super(parent, swtStyle); + this.preferredImageSize = preferredImageSize; + } + + public EditableImage(Composite parent, int style, Node node, + boolean cacheImmediately, Point preferredImageSize) + throws RepositoryException { + super(parent, style, node, cacheImmediately); + this.preferredImageSize = preferredImageSize; + } + + @Override + protected void setContainerLayoutData(Composite composite) { + // composite.setLayoutData(fillWidth()); + } + + @Override + protected void setControlLayoutData(Control control) { + // control.setLayoutData(fillWidth()); + } + + /** To be overriden. */ + protected String createImgTag() throws RepositoryException { + return CmsUtils.noImg(preferredImageSize != null ? preferredImageSize + : getSize()); + } + + protected Label createLabel(Composite box, String style) { + Label lbl = new Label(box, getStyle()); + // lbl.setLayoutData(CmsUtils.fillWidth()); + CmsUtils.markup(lbl); + CmsUtils.style(lbl, style); + if (mouseListener != null) + lbl.addMouseListener(mouseListener); + load(lbl); + return lbl; + } + + /** To be overriden. */ + protected synchronized Boolean load(Control control) { + String imgTag; + try { + imgTag = createImgTag(); + } catch (Exception e) { + // throw new CmsException("Cannot retrieve image", e); + log.error("Cannot retrieve image", e); + imgTag = CmsUtils.noImg(preferredImageSize); + loaded = false; + } + + if (imgTag == null) { + loaded = false; + imgTag = CmsUtils.noImg(preferredImageSize); + } else + loaded = true; + if (control != null) { + ((Label) control).setText(imgTag); + control.setSize(preferredImageSize != null ? preferredImageSize + : getSize()); + } else { + loaded = false; + } + getParent().layout(); + return loaded; + } + + public void setPreferredSize(Point size) { + this.preferredImageSize = size; + if (!loaded) { + load((Label) getControl()); + } + } + + protected Text createText(Composite box, String style) { + Text text = new Text(box, getStyle()); + CmsUtils.style(text, style); + return text; + } + + public Point getPreferredImageSize() { + return preferredImageSize; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/widgets/EditableText.java b/org.argeo.cms/src/org/argeo/cms/widgets/EditableText.java new file mode 100644 index 000000000..a117711fc --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/widgets/EditableText.java @@ -0,0 +1,76 @@ +package org.argeo.cms.widgets; + +import javax.jcr.Item; +import javax.jcr.RepositoryException; + +import org.argeo.cms.CmsUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +/** Editable text part displaying styled text. */ +public class EditableText extends StyledControl { + private static final long serialVersionUID = -6372283442330912755L; + + public EditableText(Composite parent, int swtStyle) { + super(parent, swtStyle); + } + + public EditableText(Composite parent, int style, Item item) + throws RepositoryException { + this(parent, style, item, false); + } + + public EditableText(Composite parent, int style, Item item, + boolean cacheImmediately) throws RepositoryException { + super(parent, style, item, cacheImmediately); + } + + @Override + protected Control createControl(Composite box, String style) { + if (isEditing()) + return createText(box, style); + else + return createLabel(box, style); + } + + protected Label createLabel(Composite box, String style) { + Label lbl = new Label(box, getStyle() | SWT.WRAP); + lbl.setLayoutData(CmsUtils.fillWidth()); + CmsUtils.style(lbl, style); + CmsUtils.markup(lbl); + if (mouseListener != null) + lbl.addMouseListener(mouseListener); + return lbl; + } + + protected Text createText(Composite box, String style) { + final Text text = new Text(box, getStyle() | SWT.MULTI | SWT.WRAP); + GridData textLayoutData = CmsUtils.fillWidth(); + // textLayoutData.heightHint = preferredHeight; + text.setLayoutData(textLayoutData); + CmsUtils.style(text, style); + text.setFocus(); + return text; + } + + public void setText(String text) { + Control child = getControl(); + if (child instanceof Label) + ((Label) child).setText(text); + else if (child instanceof Text) + ((Text) child).setText(text); + } + + public Text getAsText() { + return (Text) getControl(); + } + + public Label getAsLabel() { + return (Label) getControl(); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/widgets/JcrComposite.java b/org.argeo.cms/src/org/argeo/cms/widgets/JcrComposite.java new file mode 100644 index 000000000..7704d4092 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/widgets/JcrComposite.java @@ -0,0 +1,175 @@ +package org.argeo.cms.widgets; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.argeo.cms.CmsException; +import org.argeo.cms.CmsUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; + +/** A composite which can (optionally) manage a JCR Item. */ +public class JcrComposite extends Composite { + private static final long serialVersionUID = -1447009015451153367L; + + private final Session session; + + private String nodeId; + private String property = null; + private Node cache; + + /** Regular composite constructor. No layout is set. */ + public JcrComposite(Composite parent, int style) { + super(parent, style); + session = null; + nodeId = null; + } + + public JcrComposite(Composite parent, int style, Item item) + throws RepositoryException { + this(parent, style, item, false); + } + + public JcrComposite(Composite parent, int style, Item item, + boolean cacheImmediately) throws RepositoryException { + super(parent, style); + this.session = item.getSession(); + if (!cacheImmediately && (SWT.READ_ONLY == (style & SWT.READ_ONLY))) { + // (useless?) optimization: we only save a pointer to the session, + // not even a reference to the item + this.nodeId = null; + } else { + Node node; + Property property = null; + if (item instanceof Node) { + node = (Node) item; + } else {// Property + property = (Property) item; + if (property.isMultiple())// TODO manage property index + throw new CmsException( + "Multiple properties not supported yet."); + this.property = property.getName(); + node = property.getParent(); + } + this.nodeId = node.getIdentifier(); + if (cacheImmediately) + this.cache = node; + } + setLayout(CmsUtils.noSpaceGridLayout()); + } + + public synchronized Node getNode() { + try { + if (!itemIsNode()) + throw new CmsException("Item is not a Node"); + return getNodeInternal(); + } catch (RepositoryException e) { + throw new CmsException("Cannot get node " + nodeId, e); + } + } + + private synchronized Node getNodeInternal() throws RepositoryException { + if (cache != null) + return cache; + else if (session != null) + if (nodeId != null) + return session.getNodeByIdentifier(nodeId); + else + return null; + else + return null; + } + + public synchronized Property getProperty() { + try { + if (itemIsNode()) + throw new CmsException("Item is not a Property"); + Node node = getNodeInternal(); + if (!node.hasProperty(property)) + throw new CmsException("Property " + property + + " is not set on " + node); + return node.getProperty(property); + } catch (RepositoryException e) { + throw new CmsException("Cannot get property " + property + + " from node " + nodeId, e); + } + } + + public synchronized Boolean itemIsNode() { + return property == null; + } + + /** Set/update the cache or change the node */ + public synchronized void setNode(Node node) throws RepositoryException { + if (!itemIsNode()) + throw new CmsException("Cannot set a Node on a Property"); + + if (node == null) {// clear cache + this.cache = null; + return; + } + + if (session == null || session != node.getSession())// check session + throw new CmsException("Uncompatible session"); + + if (nodeId == null || !nodeId.equals(node.getIdentifier())) { + nodeId = node.getIdentifier(); + cache = node; + itemUpdated(); + } else { + cache = node;// set/update cache + } + } + + /** Set/update the cache or change the property */ + public synchronized void setProperty(Property prop) + throws RepositoryException { + if (itemIsNode()) + throw new CmsException("Cannot set a Property on a Node"); + + if (prop == null) {// clear cache + this.cache = null; + return; + } + + if (session == null || session != prop.getSession())// check session + throw new CmsException("Uncompatible session"); + + Node node = prop.getNode(); + if (nodeId == null || !nodeId.equals(node.getIdentifier()) + || !property.equals(prop.getName())) { + nodeId = node.getIdentifier(); + property = prop.getName(); + cache = node; + itemUpdated(); + } else { + cache = node;// set/update cache + } + } + + public synchronized String getNodeId() { + return nodeId; + } + + /** Change the node, does nothing if same. */ + public synchronized void setNodeId(String nodeId) + throws RepositoryException { + if (this.nodeId != null && this.nodeId.equals(nodeId)) + return; + this.nodeId = nodeId; + if (cache != null) + cache = session.getNodeByIdentifier(this.nodeId); + itemUpdated(); + } + + protected synchronized void itemUpdated() { + layout(); + } + + public Session getSession() { + return session; + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/widgets/ScrolledPage.java b/org.argeo.cms/src/org/argeo/cms/widgets/ScrolledPage.java new file mode 100644 index 000000000..c36ed2052 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/widgets/ScrolledPage.java @@ -0,0 +1,60 @@ +package org.argeo.cms.widgets; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Composite; + +/** + * A composite that can be scrolled vertically. It wraps a + * {@link ScrolledComposite} (and is being wrapped by it), simplifying its + * configuration. + */ +public class ScrolledPage extends Composite { + private static final long serialVersionUID = 1593536965663574437L; + + private ScrolledComposite scrolledComposite; + + public ScrolledPage(Composite parent, int style) { + super(new ScrolledComposite(parent, SWT.V_SCROLL), style); + scrolledComposite = (ScrolledComposite) getParent(); + scrolledComposite.setContent(this); + + scrolledComposite.setExpandVertical(true); + scrolledComposite.setExpandHorizontal(true); + scrolledComposite.addControlListener(new ScrollControlListener()); + } + + @Override + public void layout(boolean changed, boolean all) { + updateScroll(); + super.layout(changed, all); + } + + protected void updateScroll() { + Rectangle r = scrolledComposite.getClientArea(); + Point preferredSize = computeSize(r.width, SWT.DEFAULT); + scrolledComposite.setMinHeight(preferredSize.y); + } + + // public ScrolledComposite getScrolledComposite() { + // return this.scrolledComposite; + // } + + /** Set it on the wrapping scrolled composite */ + @Override + public void setLayoutData(Object layoutData) { + scrolledComposite.setLayoutData(layoutData); + } + + private class ScrollControlListener extends + org.eclipse.swt.events.ControlAdapter { + private static final long serialVersionUID = -3586986238567483316L; + + public void controlResized(ControlEvent e) { + updateScroll(); + } + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/widgets/StyledControl.java b/org.argeo.cms/src/org/argeo/cms/widgets/StyledControl.java new file mode 100644 index 000000000..0e0fd24ae --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/widgets/StyledControl.java @@ -0,0 +1,124 @@ +package org.argeo.cms.widgets; + +import javax.jcr.Item; +import javax.jcr.RepositoryException; + +import org.argeo.cms.CmsConstants; +import org.argeo.cms.CmsNames; +import org.argeo.cms.CmsUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.MouseListener; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + +/** Editable text part displaying styled text. */ +public abstract class StyledControl extends JcrComposite implements + CmsConstants, CmsNames { + private static final long serialVersionUID = -6372283442330912755L; + private Control control; + + private Composite container; + private Composite box; + + protected MouseListener mouseListener; + + private Boolean editing = Boolean.FALSE; + + public StyledControl(Composite parent, int swtStyle) { + super(parent, swtStyle); + setLayout(CmsUtils.noSpaceGridLayout()); + } + + public StyledControl(Composite parent, int style, Item item) + throws RepositoryException { + super(parent, style, item); + } + + public StyledControl(Composite parent, int style, Item item, + boolean cacheImmediately) throws RepositoryException { + super(parent, style, item, cacheImmediately); + } + + protected abstract Control createControl(Composite box, String style); + + protected Composite createBox(Composite parent) { + Composite box = new Composite(parent, SWT.INHERIT_DEFAULT); + setContainerLayoutData(box); + box.setLayout(CmsUtils.noSpaceGridLayout()); + // new Label(box, SWT.NONE).setText("BOX"); + return box; + } + + public Control getControl() { + return control; + } + + protected synchronized Boolean isEditing() { + return editing; + } + + public synchronized void startEditing() { + assert !isEditing(); + editing = true; + // int height = control.getSize().y; + String style = (String) control.getData(STYLE); + clear(false); + control = createControl(box, style); + setControlLayoutData(control); + } + + public synchronized void stopEditing() { + assert isEditing(); + editing = false; + String style = (String) control.getData(STYLE); + clear(false); + control = createControl(box, style); + setControlLayoutData(control); + } + + public void setStyle(String style) { + Object currentStyle = null; + if (control != null) + currentStyle = control.getData(STYLE); + if (currentStyle != null && currentStyle.equals(style)) + return; + + // Integer preferredHeight = control != null ? control.getSize().y : + // null; + clear(true); + control = createControl(box, style); + setControlLayoutData(control); + + control.getParent().setData(STYLE, style + "_box"); + control.getParent().getParent().setData(STYLE, style + "_container"); + } + + /** To be overridden */ + protected void setControlLayoutData(Control control) { + control.setLayoutData(CmsUtils.fillWidth()); + } + + /** To be overridden */ + protected void setContainerLayoutData(Composite composite) { + composite.setLayoutData(CmsUtils.fillWidth()); + } + + protected void clear(boolean deep) { + if (deep) { + for (Control control : getChildren()) + control.dispose(); + container = createBox(this); + box = createBox(container); + } else { + control.dispose(); + } + } + + public void setMouseListener(MouseListener mouseListener) { + if (this.mouseListener != null && control != null) + control.removeMouseListener(this.mouseListener); + this.mouseListener = mouseListener; + if (control != null && this.mouseListener != null) + control.addMouseListener(mouseListener); + } +}