Move CMS to Commons
authorMathieu Baudier <mbaudier@argeo.org>
Mon, 24 Nov 2014 13:57:18 +0000 (13:57 +0000)
committerMathieu Baudier <mbaudier@argeo.org>
Mon, 24 Nov 2014 13:57:18 +0000 (13:57 +0000)
git-svn-id: https://svn.argeo.org/commons/trunk@7507 4cfe0d0a-d680-48aa-b62c-e0a02a3f76cc

74 files changed:
org.argeo.cms/.classpath [new file with mode: 0644]
org.argeo.cms/.project [new file with mode: 0644]
org.argeo.cms/.settings/org.eclipse.jdt.core.prefs [new file with mode: 0644]
org.argeo.cms/.settings/org.eclipse.pde.core.prefs [new file with mode: 0644]
org.argeo.cms/META-INF/spring/backend.xml [new file with mode: 0644]
org.argeo.cms/META-INF/spring/osgi.xml [new file with mode: 0644]
org.argeo.cms/bnd.bnd [new file with mode: 0644]
org.argeo.cms/build.properties [new file with mode: 0644]
org.argeo.cms/icons/loading.gif [new file with mode: 0644]
org.argeo.cms/icons/noPic-goldenRatio-640px.png [new file with mode: 0644]
org.argeo.cms/icons/noPic-square-640px.png [new file with mode: 0644]
org.argeo.cms/pom.xml [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/AbstractCmsEntryPoint.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/BundleResourceLoader.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsApplication.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsConstants.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsEditable.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsEditionEvent.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsEntryPointFactory.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsException.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsImageManager.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsInstallPage.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsLink.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsLogin.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsLoginRequiredException.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsMsg.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsMsg_fr.properties [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsNames.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsSession.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsStyles.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsTypes.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsUiProvider.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsUtils.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/DefaultsResourceBundle.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/IdentityTextInterpreter.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/LifeCycleUiProvider.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/MenuLink.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/Msg.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/OpenUserMenu.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/SimpleCmsHeader.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/SimpleDynamicPages.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/SimpleStaticPage.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/TextInterpreter.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/UrlResourceLoader.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/UserMenu.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/cms.cnd [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/ImageManagerImpl.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/JcrContentProvider.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/JcrFileUploadReceiver.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/SimpleEditableImage.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/text/AbstractTextViewer.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/text/SectionTitle.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/text/TextContextMenu.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/text/CustomTextEditor.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/text/Img.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/text/Paragraph.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/text/StandardTextEditor.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/text/TextEditorHeader.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/text/TextSection.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/text/TextStyles.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/text/WikiPage.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/viewers/AbstractPageViewer.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/viewers/EditablePart.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/viewers/ItemPart.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/viewers/JcrVersionCmsEditable.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/viewers/NodePart.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/viewers/PropertyPart.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/viewers/Section.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/viewers/SectionPart.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/widgets/EditableImage.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/widgets/EditableText.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/widgets/JcrComposite.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/widgets/ScrolledPage.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/widgets/StyledControl.java [new file with mode: 0644]

diff --git a/org.argeo.cms/.classpath b/org.argeo.cms/.classpath
new file mode 100644 (file)
index 0000000..ad32c83
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.argeo.cms/.project b/org.argeo.cms/.project
new file mode 100644 (file)
index 0000000..2e18c90
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.cms</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ManifestBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.SchemaBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
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 (file)
index 0000000..c537b63
--- /dev/null
@@ -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 (file)
index 0000000..f29e940
--- /dev/null
@@ -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 (file)
index 0000000..c32676f
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:p="http://www.springframework.org/schema/p"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans
+        http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
+        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd">
+
+       <!-- BACKEND -->
+
+       <bean id="cmsRepository" class="org.argeo.jackrabbit.JackrabbitWrapper"
+               init-method="init" destroy-method="destroy">
+               <property name="cndFiles">
+                       <list>
+                               <value>/org/argeo/cms/cms.cnd</value>
+                       </list>
+               </property>
+               <property name="repository" ref="repository" />
+               <property name="bundleContext" ref="bundleContext" />
+       </bean>
+
+       <!-- Execute initialization with a system authentication -->
+       <bean
+               class="org.argeo.security.core.AuthenticatedApplicationContextInitialization">
+               <property name="authenticationManager" ref="authenticationManager" />
+               <property name="beanNames">
+                       <list>
+                               <value>cmsRepository</value>
+                       </list>
+               </property>
+       </bean>
+</beans>
\ 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 (file)
index 0000000..3a103ce
--- /dev/null
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>\r
+<beans:beans xmlns="http://www.springframework.org/schema/osgi"\r
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans"\r
+       xsi:schemaLocation="http://www.springframework.org/schema/osgi  \r
+       http://www.springframework.org/schema/osgi/spring-osgi-1.1.xsd\r
+       http://www.springframework.org/schema/beans   \r
+       http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">\r
+\r
+       <!-- REFERENCES -->\r
+       <reference id="repository" interface="javax.jcr.Repository"\r
+               filter="(argeo.jcr.repository.alias=node)" />\r
+\r
+       <reference id="authenticationManager"\r
+               interface="org.springframework.security.AuthenticationManager" />\r
+\r
+       <!-- SERVICES -->\r
+       <service ref="cmsRepository" interface="javax.jcr.Repository">\r
+               <service-properties>\r
+                       <beans:entry key="argeo.jcr.repository.alias" value="cms" />\r
+               </service-properties>\r
+       </service>\r
+</beans:beans>
\ No newline at end of file
diff --git a/org.argeo.cms/bnd.bnd b/org.argeo.cms/bnd.bnd
new file mode 100644 (file)
index 0000000..848f45b
--- /dev/null
@@ -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 (file)
index 0000000..34d2e4d
--- /dev/null
@@ -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 (file)
index 0000000..3288d10
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 (file)
index 0000000..0396506
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 (file)
index 0000000..8e3abb5
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 (file)
index 0000000..96ad206
--- /dev/null
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.connect</groupId>
+               <artifactId>argeo-connect</artifactId>
+               <version>2.1.13-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.cms</artifactId>
+       <name>Connect Content Management System</name>
+       <packaging>jar</packaging>
+
+       <!-- TODO move to parent -->
+
+       <!-- <build> -->
+       <!-- <plugins> -->
+       <!-- <plugin> -->
+       <!-- <groupId>org.apache.felix</groupId> -->
+       <!-- <artifactId>maven-bundle-plugin</artifactId> -->
+       <!-- <configuration> -->
+       <!-- <instructions> -->
+       <!-- Different from plugin -->
+       <!-- <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName> -->
+       <!-- <Bundle-Activator>org.argeo.connect.files.web.Activator</Bundle-Activator> -->
+       <!-- <Require-Bundle>org.eclipse.rap.rwt;bundle-version="[2.2.0,3.0.0)"</Require-Bundle> -->
+       <!-- <Import-Package> -->
+       <!-- *, -->
+       <!-- org.springframework.core -->
+       <!-- </Import-Package> -->
+       <!-- </instructions> -->
+       <!-- </configuration> -->
+       <!-- </plugin> -->
+       <!-- </plugins> -->
+       <!-- </build> -->
+       <dependencies>
+               <dependency>
+                       <groupId>org.argeo.commons.server</groupId>
+                       <artifactId>org.argeo.server.jcr</artifactId>
+                       <version>${version.argeo-commons}</version>
+               </dependency>
+
+               <dependency>
+                       <groupId>org.argeo.commons.security</groupId>
+                       <artifactId>org.argeo.security.core</artifactId>
+                       <version>${version.argeo-commons}</version>
+               </dependency>
+
+               <!-- Files -->
+               <dependency>
+                       <groupId>org.argeo.tp.rap.addons</groupId>
+                       <artifactId>org.eclipse.rap.addons.fileupload</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.addons</groupId>
+                       <artifactId>org.eclipse.rap.addons.filedialog</artifactId>
+               </dependency>
+
+               <!-- RAP -->
+               <dependency>
+                       <groupId>org.argeo.tp.rap.platform</groupId>
+                       <artifactId>org.eclipse.rap.rwt</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.platform</groupId>
+                       <artifactId>org.eclipse.rap.jface</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.platform</groupId>
+                       <artifactId>org.eclipse.core.commands</artifactId>
+               </dependency>
+
+               <!-- OSGi -->
+               <dependency>
+                       <groupId>org.argeo.tp</groupId>
+                       <artifactId>org.springframework.osgi.core</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.platform</groupId>
+                       <artifactId>org.eclipse.osgi</artifactId>
+                       <scope>provided</scope>
+               </dependency>
+       </dependencies>
+</project>
\ 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 (file)
index 0000000..8d68fc1
--- /dev/null
@@ -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 (file)
index 0000000..e42a001
--- /dev/null
@@ -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 (file)
index 0000000..afb915d
--- /dev/null
@@ -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<String, EntryPointFactory> entryPoints = new HashMap<String, EntryPointFactory>();
+       private Map<String, Map<String, String>> entryPointsBranding = new HashMap<String, Map<String, String>>();
+       private Map<String, List<String>> styleSheets = new HashMap<String, List<String>>();
+
+       private List<String> resources = new ArrayList<String>();
+
+       // 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<String, String> properties = new HashMap<String, String>();
+                               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<String> 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<String, EntryPointFactory> entryPointFactories) {
+               this.entryPoints = entryPointFactories;
+       }
+
+       public void setEntryPointsBranding(
+                       Map<String, Map<String, String>> entryPointBranding) {
+               this.entryPointsBranding = entryPointBranding;
+       }
+
+       public void setStyleSheets(Map<String, List<String>> styleSheets) {
+               this.styleSheets = styleSheets;
+       }
+
+       // public void setClientScriptingBundle(Bundle clientScriptingBundle) {
+       // this.clientScriptingBundle = clientScriptingBundle;
+       // }
+
+       public void setBundleContext(BundleContext bundleContext) {
+               this.bundleContext = bundleContext;
+       }
+
+       public void setResources(List<String> 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 = "<div"
+                       + " style=\"position: absolute; left: 50%; top: 50%; margin: -32px -32px; width: 64px; height:64px\">"
+                       + "<img src=\"./rwt-resources/icons/loading.gif\" width=\"32\" height=\"32\" style=\"margin: 16px 16px\"/>"
+                       + "</div>";
+}
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 (file)
index 0000000..9d299dc
--- /dev/null
@@ -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 (file)
index 0000000..3c666ff
--- /dev/null
@@ -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 (file)
index 0000000..920f6d9
--- /dev/null
@@ -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 (file)
index 0000000..537363d
--- /dev/null
@@ -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<String> roPrincipals = Arrays.asList("anonymous", "everyone");
+       private List<String> rwPrincipals = Arrays.asList("everyone");
+
+       private CmsLogin cmsLogin;
+
+       private CmsUiProvider header;
+       // private CmsUiProvider dynamicPages;
+       // private Map<String, CmsUiProvider> staticPages;
+       private Map<String, CmsUiProvider> pages = new LinkedHashMap<String, CmsUiProvider>();
+
+       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<String, CmsUiProvider> 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<String, CmsUiProvider> staticPages) {
+               log.warn("'staticPages' is deprecated, use 'pages' instead");
+               pages.putAll(staticPages);
+       }
+
+       public void setBasePath(String basePath) {
+               this.basePath = basePath;
+       }
+
+       public void setRoPrincipals(List<String> roPrincipals) {
+               this.roPrincipals = roPrincipals;
+       }
+
+       public void setRwPrincipals(List<String> 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 (file)
index 0000000..285741d
--- /dev/null
@@ -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 (file)
index 0000000..2577dc7
--- /dev/null
@@ -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 <img tag, with src, width and height set. @return null if not
+        * available
+        */
+       public String getImageTag(Node node) throws RepositoryException;
+
+       /**
+        * The related <img tag, with url, width and height set. Caller must close
+        * the tag (or add additional attributes). @return null if not available
+        */
+       public StringBuilder getImageTagBuilder(Node node, Point size)
+                       throws RepositoryException;
+
+       /**
+        * Returns the remotely accessible URL of the image (registering it if
+        * needed) @return null if not available
+        */
+       public String getImageUrl(Node node) throws RepositoryException;
+
+       public Binary getImageBinary(Node node) throws RepositoryException;
+
+       public Image getSwtImage(Node node) throws RepositoryException;
+
+       /** @return URL */
+       public String uploadImage(Node parentNode, String fileName, InputStream in)
+                       throws RepositoryException;
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/CmsInstallPage.java b/org.argeo.cms/src/org/argeo/cms/CmsInstallPage.java
new file mode 100644 (file)
index 0000000..d7c69d6
--- /dev/null
@@ -0,0 +1,21 @@
+package org.argeo.cms;
+
+import javax.jcr.Node;
+
+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.Text;
+
+public class CmsInstallPage implements CmsUiProvider {
+       private Text text;
+
+       @Override
+       public Control createUi(Composite parent, Node context) {
+               text = new Text(parent, SWT.MULTI);
+               text.setData(RWT.CUSTOM_VARIANT, "cms_install");
+               return text;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/CmsLink.java b/org.argeo.cms/src/org/argeo/cms/CmsLink.java
new file mode 100644 (file)
index 0000000..753d7f5
--- /dev/null
@@ -0,0 +1,270 @@
+package org.argeo.cms;
+
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import javax.jcr.Node;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.service.ResourceManager;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.graphics.ImageData;
+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.osgi.framework.BundleContext;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.osgi.context.BundleContextAware;
+
+/** A link to an internal or external location. */
+public class CmsLink implements CmsUiProvider, InitializingBean,
+               BundleContextAware {
+       private final static Log log = LogFactory.getLog(CmsLink.class);
+
+       private String label;
+       private String custom;
+       private String target;
+       private String image;
+       private MouseListener mouseListener;
+
+       private int verticalAlignment = SWT.CENTER;
+
+       // internal
+       //private Boolean isUrl = false;
+       private Integer imageWidth, imageHeight;
+
+       private BundleContext bundleContext;
+
+       public CmsLink() {
+               super();
+       }
+
+       public CmsLink(String label, String target) {
+               this(label, target, null);
+       }
+
+       public CmsLink(String label, String target, String custom) {
+               super();
+               this.label = label;
+               this.target = target;
+               this.custom = custom;
+               afterPropertiesSet();
+       }
+
+       @Override
+       public void afterPropertiesSet() {
+//             if (target != null) {
+//                     if (target.startsWith("/")) {
+//                             isUrl = true;
+//                     } else {
+//                             try {
+//                                     new URL(target);
+//                                     isUrl = true;
+//                             } catch (MalformedURLException e1) {
+//                                     isUrl = false;
+//                             }
+//                     }
+//             }
+
+               if (image != null) {
+                       ImageData image = loadImage();
+                       imageWidth = image.width;
+                       imageHeight = image.height;
+               }
+       }
+
+       @Override
+       public Control createUi(final Composite parent, Node context) {
+               Composite comp = new Composite(parent, SWT.BOTTOM);
+               comp.setLayout(CmsUtils.noSpaceGridLayout());
+
+               Label link = new Label(comp, SWT.NONE);
+               link.setData(RWT.MARKUP_ENABLED, Boolean.TRUE);
+               GridData layoutData = new GridData(SWT.CENTER, verticalAlignment, true,
+                               true);
+               if (image != null) {
+                       layoutData.heightHint = imageHeight;
+                       if (label == null)
+                               layoutData.widthHint = imageWidth;
+               }
+
+               link.setLayoutData(layoutData);
+               if (custom != null) {
+                       comp.setData(RWT.CUSTOM_VARIANT, custom);
+                       link.setData(RWT.CUSTOM_VARIANT, custom);
+               } else {
+                       comp.setData(RWT.CUSTOM_VARIANT, CmsStyles.CMS_LINK);
+                       link.setData(RWT.CUSTOM_VARIANT, CmsStyles.CMS_LINK);
+               }
+
+               // label
+               StringBuilder labelText = new StringBuilder();
+               if (target != null) {
+                       labelText
+                                       .append("<a style='color:inherit;text-decoration:inherit;' href=\"");
+//                     if (!isUrl)
+//                             labelText.append('#');
+                       labelText.append(target);
+                       labelText.append("\">");
+               }
+               if (image != null) {
+                       registerImageIfNeeded();
+                       String imageLocation = RWT.getResourceManager().getLocation(image);
+                       labelText.append("<img width='").append(imageWidth)
+                                       .append("' height='").append(imageHeight)
+                                       .append("' src=\"").append(imageLocation).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("</a>");
+
+               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 (file)
index 0000000..7c4dd5f
--- /dev/null
@@ -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("<b>" + username + "</b>");
+       // 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 (file)
index 0000000..b9917e7
--- /dev/null
@@ -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 (file)
index 0000000..a69d209
--- /dev/null
@@ -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 (file)
index 0000000..f7e5b00
--- /dev/null
@@ -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 (file)
index 0000000..be10b76
--- /dev/null
@@ -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 (file)
index 0000000..0f4e541
--- /dev/null
@@ -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<CmsSession> current = new ThreadLocal<CmsSession>();
+
+       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 (file)
index 0000000..419fb55
--- /dev/null
@@ -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 (file)
index 0000000..a8ef076
--- /dev/null
@@ -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 (file)
index 0000000..27799b0
--- /dev/null
@@ -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 (file)
index 0000000..10e5238
--- /dev/null
@@ -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("<img width='").append(width)
+                               .append("' height='").append(height).append("' src='")
+                               .append(src).append("'");
+       }
+
+       public static String noImg(Point size) {
+               ResourceManager rm = RWT.getResourceManager();
+               return CmsUtils.img(rm.getLocation(NO_IMAGE), size);
+       }
+
+       public static String noImg() {
+               return noImg(NO_IMAGE_SIZE);
+       }
+
+       public static Image noImage(Point size) {
+               ResourceManager rm = RWT.getResourceManager();
+               InputStream in = null;
+               try {
+                       in = rm.getRegisteredContent(NO_IMAGE);
+                       ImageData id = new ImageData(in);
+                       ImageData scaled = id.scaledTo(size.x, size.y);
+                       Image image = new Image(Display.getCurrent(), scaled);
+                       return image;
+               } finally {
+                       IOUtils.closeQuietly(in);
+               }
+       }
+
+       private CmsUtils() {
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/DefaultsResourceBundle.java b/org.argeo.cms/src/org/argeo/cms/DefaultsResourceBundle.java
new file mode 100644 (file)
index 0000000..2d78758
--- /dev/null
@@ -0,0 +1,39 @@
+package org.argeo.cms;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.Enumeration;
+import java.util.ResourceBundle;
+import java.util.Vector;
+
+/** Expose the default values as a {@link ResourceBundle} */
+public class DefaultsResourceBundle extends ResourceBundle {
+
+       @Override
+       protected Object handleGetObject(String key) {
+               Object obj;
+               try {
+                       Field field = getClass().getField(key);
+                       obj = field.getType().getMethod("getDefault")
+                                       .invoke(field.get(null));
+               } catch (Exception e) {
+                       throw new CmsException("Cannot get default for " + key, e);
+               }
+               return obj;
+       }
+
+       @Override
+       public Enumeration<String> getKeys() {
+               Vector<String> res = new Vector<String>();
+               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 (file)
index 0000000..c66cd86
--- /dev/null
@@ -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 (file)
index 0000000..bb64b64
--- /dev/null
@@ -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 (file)
index 0000000..75be3f1
--- /dev/null
@@ -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 (file)
index 0000000..057b74a
--- /dev/null
@@ -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 (file)
index 0000000..55c149e
--- /dev/null
@@ -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 (file)
index 0000000..24c9242
--- /dev/null
@@ -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<CmsUiProvider> lead = new ArrayList<CmsUiProvider>();
+       private List<CmsUiProvider> center = new ArrayList<CmsUiProvider>();
+       private List<CmsUiProvider> end = new ArrayList<CmsUiProvider>();
+
+       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<CmsUiProvider> 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<CmsUiProvider> lead) {
+               this.lead = lead;
+       }
+
+       public void setCenter(List<CmsUiProvider> center) {
+               this.center = center;
+       }
+
+       public void setEnd(List<CmsUiProvider> 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 (file)
index 0000000..5de40bb
--- /dev/null
@@ -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("<b>" + context.getName() + "</b>");
+               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("<i>Children:</i>");
+               // 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("<i>Properties:</i>");
+               // 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 = "<binary>";
+                       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 (file)
index 0000000..1cb0300
--- /dev/null
@@ -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 (file)
index 0000000..f7cbca7
--- /dev/null
@@ -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 (file)
index 0000000..fb4e2cd
--- /dev/null
@@ -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 (file)
index 0000000..57de515
--- /dev/null
@@ -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("<b>" + username + "</b>");
+
+               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 (file)
index 0000000..67dcfb5
--- /dev/null
@@ -0,0 +1,21 @@
+<cms = 'http://www.argeo.org/ns/cms'>
+
+[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 (file)
index 0000000..db36f71
--- /dev/null
@@ -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 (file)
index 0000000..fd8eb2c
--- /dev/null
@@ -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<Node> arr = new ArrayList<Node>();
+                       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<Node> arr = new ArrayList<Node>();
+                       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 (file)
index 0000000..6b91295
--- /dev/null
@@ -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 (file)
index 0000000..e3211ef
--- /dev/null
@@ -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 (file)
index 0000000..cbed63e
--- /dev/null
@@ -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<Control> toLayout = new ArrayList<Control>();
+                               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<String, Section> parentSubsections = parentSection
+                                                       .getSubSections();
+                                       ArrayList<Section> lst = new ArrayList<Section>(
+                                                       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<Section> 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 (file)
index 0000000..160060e
--- /dev/null
@@ -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 (file)
index 0000000..8afad16
--- /dev/null
@@ -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<StyleButton> styleButtons = new ArrayList<TextContextMenu.StyleButton>();
+
+       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 (file)
index 0000000..6ff7810
--- /dev/null
@@ -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 (file)
index 0000000..83c3208
--- /dev/null
@@ -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 (file)
index 0000000..d917c45
--- /dev/null
@@ -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 (file)
index 0000000..7a3de3b
--- /dev/null
@@ -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 (file)
index 0000000..6821fcb
--- /dev/null
@@ -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 (file)
index 0000000..09456f2
--- /dev/null
@@ -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 (file)
index 0000000..44c3ad0
--- /dev/null
@@ -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 (file)
index 0000000..17c3d9c
--- /dev/null
@@ -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 (file)
index 0000000..5e6de37
--- /dev/null
@@ -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 (file)
index 0000000..99f8acf
--- /dev/null
@@ -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 (file)
index 0000000..52e5a88
--- /dev/null
@@ -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<T extends Item> {
+       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 (file)
index 0000000..bcd4285
--- /dev/null
@@ -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 (file)
index 0000000..db9a60a
--- /dev/null
@@ -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<Node> {
+       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 (file)
index 0000000..50fdd06
--- /dev/null
@@ -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<Property> {
+       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 (file)
index 0000000..c09b179
--- /dev/null
@@ -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<String, Section> getSubSections() throws RepositoryException {
+               LinkedHashMap<String, Section> result = new LinkedHashMap<String, Section>();
+               for (Control child : getChildren()) {
+                       if (child instanceof Composite) {
+                               collectDirectSubSections((Composite) child, result);
+                       }
+               }
+               return Collections.unmodifiableMap(result);
+       }
+
+       private void collectDirectSubSections(Composite composite,
+                       LinkedHashMap<String, Section> 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 (file)
index 0000000..6cd45c5
--- /dev/null
@@ -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 (file)
index 0000000..00a7c26
--- /dev/null
@@ -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 (file)
index 0000000..a117711
--- /dev/null
@@ -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 (file)
index 0000000..7704d40
--- /dev/null
@@ -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 (file)
index 0000000..c36ed20
--- /dev/null
@@ -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 (file)
index 0000000..0e0fd24
--- /dev/null
@@ -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);
+       }
+}