--- /dev/null
+**/bin/
+**/MANIFEST.MF
+/sdk.mk
+/output/
--- /dev/null
+[submodule "argeo-build"]
+ path = sdk/argeo-build
+ url = http://git.argeo.org/cc0/argeo-build.git
+ branch = unstable
--- /dev/null
+include sdk.mk
+
+all: osgi
+
+A2_CATEGORY = org.argeo.cms.jcr
+
+BUNDLES = \
+org.argeo.cms.jcr \
+org.argeo.cms.jcr.ui \
+
+DEP_CATEGORIES = \
+org.argeo.tp \
+org.argeo.tp.apache \
+org.argeo.tp.jetty \
+osgi/api/org.argeo.tp.osgi \
+osgi/equinox/org.argeo.tp.eclipse \
+swt/rap/org.argeo.tp.swt \
+swt/rap/org.argeo.tp.swt.workbench \
+org.argeo.tp.jcr \
+org.argeo.cms \
+swt/org.argeo.cms \
+swt/rap/org.argeo.cms \
+$(A2_CATEGORY)
+
+clean:
+ rm -rf $(BUILD_BASE)
+
+include $(SDK_SRC_BASE)/sdk/argeo-build/osgi.mk
\ No newline at end of file
--- /dev/null
+include $(SDK_SRC_BASE)/cnf/unstable.bnd
--- /dev/null
+MAJOR=2
+MINOR=3
+MICRO=9
+qualifier=.next
+
+Bundle-RequiredExecutionEnvironment=JavaSE-11
--- /dev/null
+#!/bin/sh
+
+# Source are located where this script is
+SDK_SRC_BASE="$(cd "$(dirname "$0")"; pwd -P)"
+
+# Source the configure script
+. $SDK_SRC_BASE/sdk/argeo-build/configure
--- /dev/null
+<?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-17"/>
+ <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>org.argeo.cms.jcr.ui</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>
--- /dev/null
+/MANIFEST.MF
--- /dev/null
+Bundle-Activator: org.argeo.cms.ui.internal.Activator
+Bundle-ActivationPolicy: lazy
+
+Import-Package: org.eclipse.swt,\
+org.eclipse.jface.window,\
+org.eclipse.core.commands,\
+javax.jcr.security,\
+org.eclipse.rap.fileupload;version="[2.1,4)",\
+org.eclipse.rap.rwt;version="[2.1,4)",\
+org.eclipse.rap.rwt.application;version="[2.1,4)",\
+org.eclipse.rap.rwt.client;version="[2.1,4)",\
+org.eclipse.rap.rwt.client.service;version="[2.1,4)",\
+org.eclipse.rap.rwt.service;version="[2.1,4)",\
+org.eclipse.rap.rwt.widgets;version="[2.1,4)",\
+org.osgi.*;version=0.0.0,\
+javax.servlet.*;version="[3,5)",\
+*
+
+## TODO: in order to enable single sourcing, we have introduced dummy RAP packages
+# in the RCP specific bundle org.argeo.eclipse.ui.rap.
+# this was working with RAP 2.x but since we upgrade Rap to 3.1.x,
+# there is a version range issue cause org.argeo.eclipse.ui.rap bundle is still in 2.x
+# We enable large RAP version range as a workaround that must be fixed
\ No newline at end of file
--- /dev/null
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+ .,\
+ icons/
--- /dev/null
+package org.argeo.cms.ui;
+
+/** Commons constants */
+@Deprecated
+public interface CmsUiConstants {
+ // DATAKEYS
+// public final static String STYLE = EclipseUiConstants.CSS_CLASS;
+// public final static String MARKUP = EclipseUiConstants.MARKUP_SUPPORT;
+ @Deprecated
+ /* RWT.CUSTOM_ITEM_HEIGHT */
+ public final static String ITEM_HEIGHT = "org.eclipse.rap.rwt.customItemHeight";
+
+ // EVENT DETAILS
+ @Deprecated
+ /* RWT.HYPERLINK */
+ public final static int HYPERLINK = 1 << 26;
+
+ // STANDARD RESOURCES
+ public final static String LOADING_IMAGE = "icons/loading.gif";
+
+ // MISCEALLENEOUS
+ String DATE_TIME_FORMAT = "dd/MM/yyyy, HH:mm";
+}
--- /dev/null
+package org.argeo.cms.ui;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.api.acr.Content;
+import org.argeo.cms.jcr.acr.JcrContent;
+import org.argeo.cms.swt.acr.SwtUiProvider;
+import org.argeo.jcr.JcrException;
+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 extends SwtUiProvider {
+ /**
+ * Initialises a user interface.
+ *
+ * @param parent the parent composite
+ * @param context a context node (holding the JCR underlying session), or null
+ */
+ default Control createUi(Composite parent, Node context) throws RepositoryException {
+ // does nothing by default
+ return null;
+ }
+
+ default Control createUiPart(Composite parent, Node context) {
+ try {
+ return createUi(parent, context);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot create UI for context " + context, e);
+ }
+ }
+
+ @Override
+ default Control createUiPart(Composite parent, Content context) {
+ if (context == null)
+ return createUiPart(parent, (Node) null);
+ if (context instanceof JcrContent) {
+ Node node = ((JcrContent) context).getJcrNode();
+ return createUiPart(parent, node);
+ } else {
+// CmsLog.getLog(CmsUiProvider.class)
+// .warn("In " + getClass() + ", content " + context + " is not compatible with JCR.");
+// return createUiPart(parent, (Node) null);
+
+ throw new IllegalArgumentException(
+ "In " + getClass() + ", content " + context + " is not compatible with JCR");
+ }
+ }
+
+}
--- /dev/null
+package org.argeo.cms.ui;
+
+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();
+}
--- /dev/null
+package org.argeo.cms.ui.eclipse.forms;
+/**
+ * AbstractFormPart implements IFormPart interface and can be used as a
+ * convenient base class for concrete form parts. If a method contains
+ * code that must be called, look for instructions to call 'super'
+ * when overriding.
+ *
+ * @see org.eclipse.ui.forms.widgets.Section
+ * @since 1.0
+ */
+public abstract class AbstractFormPart implements IFormPart {
+ private IManagedForm managedForm;
+ private boolean dirty = false;
+ private boolean stale = true;
+ /**
+ * @see org.eclipse.ui.forms.IFormPart#initialize(org.eclipse.ui.forms.IManagedForm)
+ */
+ public void initialize(IManagedForm form) {
+ this.managedForm = form;
+ }
+ /**
+ * Returns the form that manages this part.
+ *
+ * @return the managed form
+ */
+ public IManagedForm getManagedForm() {
+ return managedForm;
+ }
+ /**
+ * Disposes the part. Subclasses should override to release any system
+ * resources.
+ */
+ public void dispose() {
+ }
+ /**
+ * Commits the part. Subclasses should call 'super' when overriding.
+ *
+ * @param onSave
+ * <code>true</code> if the request to commit has arrived as a
+ * result of the 'save' action.
+ */
+ public void commit(boolean onSave) {
+ dirty = false;
+ }
+ /**
+ * Sets the overall form input. Subclases may elect to override the method
+ * and adjust according to the form input.
+ *
+ * @param input
+ * the form input object
+ * @return <code>false</code>
+ */
+ public boolean setFormInput(Object input) {
+ return false;
+ }
+ /**
+ * Instructs the part to grab keyboard focus.
+ */
+ public void setFocus() {
+ }
+ /**
+ * Refreshes the section after becoming stale (falling behind data in the
+ * model). Subclasses must call 'super' when overriding this method.
+ */
+ public void refresh() {
+ stale = false;
+ // since we have refreshed, any changes we had in the
+ // part are gone and we are not dirty
+ dirty = false;
+ }
+ /**
+ * Marks the part dirty. Subclasses should call this method as a result of
+ * user interaction with the widgets in the section.
+ */
+ public void markDirty() {
+ dirty = true;
+ managedForm.dirtyStateChanged();
+ }
+ /**
+ * Tests whether the part is dirty i.e. its widgets have state that is
+ * newer than the data in the model.
+ *
+ * @return <code>true</code> if the part is dirty, <code>false</code>
+ * otherwise.
+ */
+ public boolean isDirty() {
+ return dirty;
+ }
+ /**
+ * Tests whether the part is stale i.e. its widgets have state that is
+ * older than the data in the model.
+ *
+ * @return <code>true</code> if the part is stale, <code>false</code>
+ * otherwise.
+ */
+ public boolean isStale() {
+ return stale;
+ }
+ /**
+ * Marks the part stale. Subclasses should call this method as a result of
+ * model notification that indicates that the content of the section is no
+ * longer in sync with the model.
+ */
+ public void markStale() {
+ stale = true;
+ managedForm.staleStateChanged();
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.eclipse.forms;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.resource.LocalResourceManager;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.RGB;
+//import org.eclipse.swt.internal.graphics.Graphics;
+import org.eclipse.swt.widgets.Display;
+
+/**
+ * Manages colors that will be applied to forms and form widgets. The colors are
+ * chosen to make the widgets look correct in the editor area. If a different
+ * set of colors is needed, subclass this class and override 'initialize' and/or
+ * 'initializeColors'.
+ *
+ * @since 1.0
+ */
+public class FormColors {
+ /**
+ * Key for the form title foreground color.
+ *
+ * @deprecated use <code>IFormColors.TITLE</code>.
+ */
+ public static final String TITLE = IFormColors.TITLE;
+
+ /**
+ * Key for the tree/table border color.
+ *
+ * @deprecated use <code>IFormColors.BORDER</code>
+ */
+ public static final String BORDER = IFormColors.BORDER;
+
+ /**
+ * Key for the section separator color.
+ *
+ * @deprecated use <code>IFormColors.SEPARATOR</code>.
+ */
+ public static final String SEPARATOR = IFormColors.SEPARATOR;
+
+ /**
+ * Key for the section title bar background.
+ *
+ * @deprecated use <code>IFormColors.TB_BG
+ */
+ public static final String TB_BG = IFormColors.TB_BG;
+
+ /**
+ * Key for the section title bar foreground.
+ *
+ * @deprecated use <code>IFormColors.TB_FG</code>
+ */
+ public static final String TB_FG = IFormColors.TB_FG;
+
+ /**
+ * Key for the section title bar gradient.
+ *
+ * @deprecated use <code>IFormColors.TB_GBG</code>
+ */
+ public static final String TB_GBG = IFormColors.TB_GBG;
+
+ /**
+ * Key for the section title bar border.
+ *
+ * @deprecated use <code>IFormColors.TB_BORDER</code>.
+ */
+ public static final String TB_BORDER = IFormColors.TB_BORDER;
+
+ /**
+ * Key for the section toggle color. Since 3.1, this color is used for all
+ * section styles.
+ *
+ * @deprecated use <code>IFormColors.TB_TOGGLE</code>.
+ */
+ public static final String TB_TOGGLE = IFormColors.TB_TOGGLE;
+
+ /**
+ * Key for the section toggle hover color.
+ *
+ * @deprecated use <code>IFormColors.TB_TOGGLE_HOVER</code>.
+ */
+ public static final String TB_TOGGLE_HOVER = IFormColors.TB_TOGGLE_HOVER;
+
+ protected Map colorRegistry = new HashMap(10);
+
+ private LocalResourceManager resources;
+
+ protected Color background;
+
+ protected Color foreground;
+
+ private boolean shared;
+
+ protected Display display;
+
+ protected Color border;
+
+ /**
+ * Creates form colors using the provided display.
+ *
+ * @param display
+ * the display to use
+ */
+ public FormColors(Display display) {
+ this.display = display;
+ initialize();
+ }
+
+ /**
+ * Returns the display used to create colors.
+ *
+ * @return the display
+ */
+ public Display getDisplay() {
+ return display;
+ }
+
+ /**
+ * Initializes the colors. Subclasses can override this method to change the
+ * way colors are created. Alternatively, only the color table can be
+ * modified by overriding <code>initializeColorTable()</code>.
+ *
+ * @see #initializeColorTable
+ */
+ protected void initialize() {
+ background = display.getSystemColor(SWT.COLOR_LIST_BACKGROUND);
+ foreground = display.getSystemColor(SWT.COLOR_LIST_FOREGROUND);
+ initializeColorTable();
+ updateBorderColor();
+ }
+
+ /**
+ * Allocates colors for the following keys: BORDER, SEPARATOR and
+ * TITLE. Subclasses can override to allocate these colors differently.
+ */
+ protected void initializeColorTable() {
+ createTitleColor();
+ createColor(IFormColors.SEPARATOR, getColor(IFormColors.TITLE).getRGB());
+ RGB black = getSystemColor(SWT.COLOR_BLACK);
+ RGB borderRGB = getSystemColor(SWT.COLOR_TITLE_INACTIVE_BACKGROUND_GRADIENT);
+ createColor(IFormColors.BORDER, blend(borderRGB, black, 80));
+ }
+
+ /**
+ * Allocates colors for the section tool bar (all the keys that start with
+ * TB). Since these colors are only needed when TITLE_BAR style is used with
+ * the Section widget, they are not needed all the time and are allocated on
+ * demand. Consequently, this method will do nothing if the colors have been
+ * already initialized. Call this method prior to using colors with the TB
+ * keys to ensure they are available.
+ */
+ public void initializeSectionToolBarColors() {
+ if (colorRegistry.containsKey(IFormColors.TB_BG))
+ return;
+ createTitleBarGradientColors();
+ createTitleBarOutlineColors();
+ createTwistieColors();
+ }
+
+ /**
+ * Allocates additional colors for the form header, namely background
+ * gradients, bottom separator keylines and DND highlights. Since these
+ * colors are only needed for clients that want to use these particular
+ * style of header rendering, they are not needed all the time and are
+ * allocated on demand. Consequently, this method will do nothing if the
+ * colors have been already initialized. Call this method prior to using
+ * color keys with the H_ prefix to ensure they are available.
+ */
+ protected void initializeFormHeaderColors() {
+ if (colorRegistry.containsKey(IFormColors.H_BOTTOM_KEYLINE2))
+ return;
+ createFormHeaderColors();
+ }
+
+ /**
+ * Returns the RGB value of the system color represented by the code
+ * argument, as defined in <code>SWT</code> class.
+ *
+ * @param code
+ * the system color constant as defined in <code>SWT</code>
+ * class.
+ * @return the RGB value of the system color
+ */
+ public RGB getSystemColor(int code) {
+ return getDisplay().getSystemColor(code).getRGB();
+ }
+
+ /**
+ * Creates the color for the specified key using the provided RGB object.
+ * The color object will be returned and also put into the registry. When
+ * the class is disposed, the color will be disposed with it.
+ *
+ * @param key
+ * the unique color key
+ * @param rgb
+ * the RGB object
+ * @return the allocated color object
+ */
+ public Color createColor(String key, RGB rgb) {
+ // RAP [rh] changes due to missing Color constructor
+// Color c = getResourceManager().createColor(rgb);
+// Color prevC = (Color) colorRegistry.get(key);
+// if (prevC != null && !prevC.isDisposed())
+// getResourceManager().destroyColor(prevC.getRGB());
+// Color c = Graphics.getColor(rgb);
+ Color c = new Color(display, rgb);
+ colorRegistry.put(key, c);
+ return c;
+ }
+
+ /**
+ * Creates a color that can be used for areas of the form that is inactive.
+ * These areas can contain images, links, controls and other content but are
+ * considered auxilliary to the main content area.
+ *
+ * <p>
+ * The color should not be disposed because it is managed by this class.
+ *
+ * @return the inactive form color
+ */
+ public Color getInactiveBackground() {
+ String key = "__ncbg__"; //$NON-NLS-1$
+ Color color = getColor(key);
+ if (color == null) {
+ RGB sel = getSystemColor(SWT.COLOR_LIST_SELECTION);
+ // a blend of 95% white and 5% list selection system color
+ RGB ncbg = blend(sel, getSystemColor(SWT.COLOR_WHITE), 5);
+ color = createColor(key, ncbg);
+ }
+ return color;
+ }
+
+ /**
+ * Creates the color for the specified key using the provided RGB values.
+ * The color object will be returned and also put into the registry. If
+ * there is already another color object under the same key in the registry,
+ * the existing object will be disposed. When the class is disposed, the
+ * color will be disposed with it.
+ *
+ * @param key
+ * the unique color key
+ * @param r
+ * red value
+ * @param g
+ * green value
+ * @param b
+ * blue value
+ * @return the allocated color object
+ */
+ public Color createColor(String key, int r, int g, int b) {
+ return createColor(key, new RGB(r,g,b));
+ }
+
+ /**
+ * Computes the border color relative to the background. Allocated border
+ * color is designed to work well with white. Otherwise, stanard widget
+ * background color will be used.
+ */
+ protected void updateBorderColor() {
+ if (isWhiteBackground())
+ border = getColor(IFormColors.BORDER);
+ else {
+ border = display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
+ Color bg = getImpliedBackground();
+ if (border.getRed() == bg.getRed()
+ && border.getGreen() == bg.getGreen()
+ && border.getBlue() == bg.getBlue())
+ border = display.getSystemColor(SWT.COLOR_WIDGET_DARK_SHADOW);
+ }
+ }
+
+ /**
+ * Sets the background color. All the toolkits that use this class will
+ * share the same background.
+ *
+ * @param bg
+ * background color
+ */
+ public void setBackground(Color bg) {
+ this.background = bg;
+ updateBorderColor();
+ updateFormHeaderColors();
+ }
+
+ /**
+ * Sets the foreground color. All the toolkits that use this class will
+ * share the same foreground.
+ *
+ * @param fg
+ * foreground color
+ */
+ public void setForeground(Color fg) {
+ this.foreground = fg;
+ }
+
+ /**
+ * Returns the current background color.
+ *
+ * @return the background color
+ */
+ public Color getBackground() {
+ return background;
+ }
+
+ /**
+ * Returns the current foreground color.
+ *
+ * @return the foreground color
+ */
+ public Color getForeground() {
+ return foreground;
+ }
+
+ /**
+ * Returns the computed border color. Border color depends on the background
+ * and is recomputed whenever the background changes.
+ *
+ * @return the current border color
+ */
+ public Color getBorderColor() {
+ return border;
+ }
+
+ /**
+ * Tests if the background is white. White background has RGB value
+ * 255,255,255.
+ *
+ * @return <samp>true</samp> if background is white, <samp>false</samp>
+ * otherwise.
+ */
+ public boolean isWhiteBackground() {
+ Color bg = getImpliedBackground();
+ return bg.getRed() == 255 && bg.getGreen() == 255
+ && bg.getBlue() == 255;
+ }
+
+ /**
+ * Returns the color object for the provided key or <samp>null </samp> if
+ * not in the registry.
+ *
+ * @param key
+ * the color key
+ * @return color object if found, or <samp>null </samp> if not.
+ */
+ public Color getColor(String key) {
+ if (key.startsWith(IFormColors.TB_PREFIX))
+ initializeSectionToolBarColors();
+ else if (key.startsWith(IFormColors.H_PREFIX))
+ initializeFormHeaderColors();
+ return (Color) colorRegistry.get(key);
+ }
+
+ /**
+ * Disposes all the colors in the registry.
+ */
+ public void dispose() {
+ if (resources != null)
+ resources.dispose();
+ resources = null;
+ colorRegistry = null;
+ }
+
+ /**
+ * Marks the colors shared. This prevents toolkits that share this object
+ * from disposing it.
+ */
+ public void markShared() {
+ this.shared = true;
+ }
+
+ /**
+ * Tests if the colors are shared.
+ *
+ * @return <code>true</code> if shared, <code>false</code> otherwise.
+ */
+ public boolean isShared() {
+ return shared;
+ }
+
+ /**
+ * Blends c1 and c2 based in the provided ratio.
+ *
+ * @param c1
+ * first color
+ * @param c2
+ * second color
+ * @param ratio
+ * percentage of the first color in the blend (0-100)
+ * @return the RGB value of the blended color
+ */
+ public static RGB blend(RGB c1, RGB c2, int ratio) {
+ int r = blend(c1.red, c2.red, ratio);
+ int g = blend(c1.green, c2.green, ratio);
+ int b = blend(c1.blue, c2.blue, ratio);
+ return new RGB(r, g, b);
+ }
+
+ /**
+ * Tests the source RGB for range.
+ *
+ * @param rgb
+ * the tested RGB
+ * @param from
+ * range start (excluding the value itself)
+ * @param to
+ * range end (excluding the value itself)
+ * @return <code>true</code> if at least one of the primary colors in the
+ * source RGB are within the provided range, <code>false</code>
+ * otherwise.
+ */
+ public static boolean testAnyPrimaryColor(RGB rgb, int from, int to) {
+ if (testPrimaryColor(rgb.red, from, to))
+ return true;
+ if (testPrimaryColor(rgb.green, from, to))
+ return true;
+ if (testPrimaryColor(rgb.blue, from, to))
+ return true;
+ return false;
+ }
+
+ /**
+ * Tests the source RGB for range.
+ *
+ * @param rgb
+ * the tested RGB
+ * @param from
+ * range start (excluding the value itself)
+ * @param to
+ * tange end (excluding the value itself)
+ * @return <code>true</code> if at least two of the primary colors in the
+ * source RGB are within the provided range, <code>false</code>
+ * otherwise.
+ */
+ public static boolean testTwoPrimaryColors(RGB rgb, int from, int to) {
+ int total = 0;
+ if (testPrimaryColor(rgb.red, from, to))
+ total++;
+ if (testPrimaryColor(rgb.green, from, to))
+ total++;
+ if (testPrimaryColor(rgb.blue, from, to))
+ total++;
+ return total >= 2;
+ }
+
+ /**
+ * Blends two primary color components based on the provided ratio.
+ *
+ * @param v1
+ * first component
+ * @param v2
+ * second component
+ * @param ratio
+ * percentage of the first component in the blend
+ * @return
+ */
+ private static int blend(int v1, int v2, int ratio) {
+ int b = (ratio * v1 + (100 - ratio) * v2) / 100;
+ return Math.min(255, b);
+ }
+
+ private Color getImpliedBackground() {
+ if (getBackground() != null)
+ return getBackground();
+ return getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
+ }
+
+ private static boolean testPrimaryColor(int value, int from, int to) {
+ return value > from && value < to;
+ }
+
+ private void createTitleColor() {
+ /*
+ * RGB rgb = getSystemColor(SWT.COLOR_LIST_SELECTION); // test too light
+ * if (testTwoPrimaryColors(rgb, 120, 151)) rgb = blend(rgb, BLACK, 80);
+ * else if (testTwoPrimaryColors(rgb, 150, 256)) rgb = blend(rgb, BLACK,
+ * 50); createColor(TITLE, rgb);
+ */
+ RGB bg = getImpliedBackground().getRGB();
+ RGB listSelection = getSystemColor(SWT.COLOR_LIST_SELECTION);
+ RGB listForeground = getSystemColor(SWT.COLOR_LIST_FOREGROUND);
+ RGB rgb = listSelection;
+
+ // Group 1
+ // Rule: If at least 2 of the LIST_SELECTION RGB values are equal to or
+ // between 0 and 120, then use 100% LIST_SELECTION as it is (no
+ // additions)
+ // Examples: XP Default, Win Classic Standard, Win High Con White, Win
+ // Classic Marine
+ if (testTwoPrimaryColors(listSelection, -1, 121))
+ rgb = listSelection;
+ // Group 2
+ // When LIST_BACKGROUND = white (255, 255, 255) or not black, text
+ // colour = LIST_SELECTION @ 100% Opacity + 50% LIST_FOREGROUND over
+ // LIST_BACKGROUND
+ // Rule: If at least 2 of the LIST_SELECTION RGB values are equal to or
+ // between 121 and 255, then add 50% LIST_FOREGROUND to LIST_SELECTION
+ // foreground colour
+ // Examples: Win Vista, XP Silver, XP Olive , Win Classic Plum, OSX
+ // Aqua, OSX Graphite, Linux GTK
+ else if (testTwoPrimaryColors(listSelection, 120, 256)
+ || (bg.red == 0 && bg.green == 0 && bg.blue == 0))
+ rgb = blend(listSelection, listForeground, 50);
+ // Group 3
+ // When LIST_BACKGROUND = black (0, 0, 0), text colour = LIST_SELECTION
+ // @ 100% Opacity + 50% LIST_FOREGROUND over LIST_BACKGROUND
+ // Rule: If LIST_BACKGROUND = 0, 0, 0, then add 50% LIST_FOREGROUND to
+ // LIST_SELECTION foreground colour
+ // Examples: Win High Con Black, Win High Con #1, Win High Con #2
+ // (covered in the second part of the OR clause above)
+ createColor(IFormColors.TITLE, rgb);
+ }
+
+ private void createTwistieColors() {
+ RGB rgb = getColor(IFormColors.TITLE).getRGB();
+ RGB white = getSystemColor(SWT.COLOR_WHITE);
+ createColor(TB_TOGGLE, rgb);
+ rgb = blend(rgb, white, 60);
+ createColor(TB_TOGGLE_HOVER, rgb);
+ }
+
+ private void createTitleBarGradientColors() {
+ RGB tbBg = getSystemColor(SWT.COLOR_TITLE_BACKGROUND);
+ RGB bg = getImpliedBackground().getRGB();
+
+ // Group 1
+ // Rule: If at least 2 of the RGB values are equal to or between 180 and
+ // 255, then apply specified opacity for Group 1
+ // Examples: Vista, XP Silver, Wn High Con #2
+ // Gradient Bottom = TITLE_BACKGROUND @ 30% Opacity over LIST_BACKGROUND
+ // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND
+ if (testTwoPrimaryColors(tbBg, 179, 256))
+ tbBg = blend(tbBg, bg, 30);
+
+ // Group 2
+ // Rule: If at least 2 of the RGB values are equal to or between 121 and
+ // 179, then apply specified opacity for Group 2
+ // Examples: XP Olive, OSX Graphite, Linux GTK, Wn High Con Black
+ // Gradient Bottom = TITLE_BACKGROUND @ 20% Opacity over LIST_BACKGROUND
+ // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND
+ else if (testTwoPrimaryColors(tbBg, 120, 180))
+ tbBg = blend(tbBg, bg, 20);
+
+ // Group 3
+ // Rule: Everything else
+ // Examples: XP Default, Wn Classic Standard, Wn Marine, Wn Plum, OSX
+ // Aqua, Wn High Con White, Wn High Con #1
+ // Gradient Bottom = TITLE_BACKGROUND @ 10% Opacity over LIST_BACKGROUND
+ // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND
+ else {
+ tbBg = blend(tbBg, bg, 10);
+ }
+
+ createColor(IFormColors.TB_BG, tbBg);
+
+ // for backward compatibility
+ createColor(TB_GBG, tbBg);
+ }
+
+ private void createTitleBarOutlineColors() {
+ // title bar outline - border color
+ RGB tbBorder = getSystemColor(SWT.COLOR_TITLE_BACKGROUND);
+ RGB bg = getImpliedBackground().getRGB();
+ // Group 1
+ // Rule: If at least 2 of the RGB values are equal to or between 180 and
+ // 255, then apply specified opacity for Group 1
+ // Examples: Vista, XP Silver, Wn High Con #2
+ // Keyline = TITLE_BACKGROUND @ 70% Opacity over LIST_BACKGROUND
+ if (testTwoPrimaryColors(tbBorder, 179, 256))
+ tbBorder = blend(tbBorder, bg, 70);
+
+ // Group 2
+ // Rule: If at least 2 of the RGB values are equal to or between 121 and
+ // 179, then apply specified opacity for Group 2
+ // Examples: XP Olive, OSX Graphite, Linux GTK, Wn High Con Black
+
+ // Keyline = TITLE_BACKGROUND @ 50% Opacity over LIST_BACKGROUND
+ else if (testTwoPrimaryColors(tbBorder, 120, 180))
+ tbBorder = blend(tbBorder, bg, 50);
+
+ // Group 3
+ // Rule: Everything else
+ // Examples: XP Default, Wn Classic Standard, Wn Marine, Wn Plum, OSX
+ // Aqua, Wn High Con White, Wn High Con #1
+
+ // Keyline = TITLE_BACKGROUND @ 30% Opacity over LIST_BACKGROUND
+ else {
+ tbBorder = blend(tbBorder, bg, 30);
+ }
+ createColor(FormColors.TB_BORDER, tbBorder);
+ }
+
+ private void updateFormHeaderColors() {
+ if (colorRegistry.containsKey(IFormColors.H_GRADIENT_END)) {
+ disposeIfFound(IFormColors.H_GRADIENT_END);
+ disposeIfFound(IFormColors.H_GRADIENT_START);
+ disposeIfFound(IFormColors.H_BOTTOM_KEYLINE1);
+ disposeIfFound(IFormColors.H_BOTTOM_KEYLINE2);
+ disposeIfFound(IFormColors.H_HOVER_LIGHT);
+ disposeIfFound(IFormColors.H_HOVER_FULL);
+ initializeFormHeaderColors();
+ }
+ }
+
+ private void disposeIfFound(String key) {
+ Color color = getColor(key);
+ if (color != null) {
+ colorRegistry.remove(key);
+ // RAP [rh] changes due to missing Color#dispose()
+// color.dispose();
+ }
+ }
+
+ private void createFormHeaderColors() {
+ createFormHeaderGradientColors();
+ createFormHeaderKeylineColors();
+ createFormHeaderDNDColors();
+ }
+
+ private void createFormHeaderGradientColors() {
+ RGB titleBg = getSystemColor(SWT.COLOR_TITLE_BACKGROUND);
+ Color bgColor = getImpliedBackground();
+ RGB bg = bgColor.getRGB();
+ RGB bottom, top;
+ // Group 1
+ // Rule: If at least 2 of the RGB values are equal to or between 180 and
+ // 255, then apply specified opacity for Group 1
+ // Examples: Vista, XP Silver, Wn High Con #2
+ // Gradient Bottom = TITLE_BACKGROUND @ 30% Opacity over LIST_BACKGROUND
+ // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND
+ if (testTwoPrimaryColors(titleBg, 179, 256)) {
+ bottom = blend(titleBg, bg, 30);
+ top = bg;
+ }
+
+ // Group 2
+ // Rule: If at least 2 of the RGB values are equal to or between 121 and
+ // 179, then apply specified opacity for Group 2
+ // Examples: XP Olive, OSX Graphite, Linux GTK, Wn High Con Black
+ // Gradient Bottom = TITLE_BACKGROUND @ 20% Opacity over LIST_BACKGROUND
+ // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND
+ else if (testTwoPrimaryColors(titleBg, 120, 180)) {
+ bottom = blend(titleBg, bg, 20);
+ top = bg;
+ }
+
+ // Group 3
+ // Rule: If at least 2 of the RGB values are equal to or between 0 and
+ // 120, then apply specified opacity for Group 3
+ // Examples: XP Default, Wn Classic Standard, Wn Marine, Wn Plum, OSX
+ // Aqua, Wn High Con White, Wn High Con #1
+ // Gradient Bottom = TITLE_BACKGROUND @ 10% Opacity over LIST_BACKGROUND
+ // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND
+ else {
+ bottom = blend(titleBg, bg, 10);
+ top = bg;
+ }
+ createColor(IFormColors.H_GRADIENT_END, top);
+ createColor(IFormColors.H_GRADIENT_START, bottom);
+ }
+
+ private void createFormHeaderKeylineColors() {
+ RGB titleBg = getSystemColor(SWT.COLOR_TITLE_BACKGROUND);
+ Color bgColor = getImpliedBackground();
+ RGB bg = bgColor.getRGB();
+ RGB keyline2;
+ // H_BOTTOM_KEYLINE1
+ createColor(IFormColors.H_BOTTOM_KEYLINE1, new RGB(255, 255, 255));
+
+ // H_BOTTOM_KEYLINE2
+ // Group 1
+ // Rule: If at least 2 of the RGB values are equal to or between 180 and
+ // 255, then apply specified opacity for Group 1
+ // Examples: Vista, XP Silver, Wn High Con #2
+ // Keyline = TITLE_BACKGROUND @ 70% Opacity over LIST_BACKGROUND
+ if (testTwoPrimaryColors(titleBg, 179, 256))
+ keyline2 = blend(titleBg, bg, 70);
+
+ // Group 2
+ // Rule: If at least 2 of the RGB values are equal to or between 121 and
+ // 179, then apply specified opacity for Group 2
+ // Examples: XP Olive, OSX Graphite, Linux GTK, Wn High Con Black
+ // Keyline = TITLE_BACKGROUND @ 50% Opacity over LIST_BACKGROUND
+ else if (testTwoPrimaryColors(titleBg, 120, 180))
+ keyline2 = blend(titleBg, bg, 50);
+
+ // Group 3
+ // Rule: If at least 2 of the RGB values are equal to or between 0 and
+ // 120, then apply specified opacity for Group 3
+ // Examples: XP Default, Wn Classic Standard, Wn Marine, Wn Plum, OSX
+ // Aqua, Wn High Con White, Wn High Con #1
+
+ // Keyline = TITLE_BACKGROUND @ 30% Opacity over LIST_BACKGROUND
+ else
+ keyline2 = blend(titleBg, bg, 30);
+ // H_BOTTOM_KEYLINE2
+ createColor(IFormColors.H_BOTTOM_KEYLINE2, keyline2);
+ }
+
+ private void createFormHeaderDNDColors() {
+ RGB titleBg = getSystemColor(SWT.COLOR_TITLE_BACKGROUND_GRADIENT);
+ Color bgColor = getImpliedBackground();
+ RGB bg = bgColor.getRGB();
+ RGB light, full;
+ // ALL Themes
+ //
+ // Light Highlight
+ // When *near* the 'hot' area
+ // Rule: If near the title in the 'hot' area, show background highlight
+ // TITLE_BACKGROUND_GRADIENT @ 40%
+ light = blend(titleBg, bg, 40);
+ // Full Highlight
+ // When *on* the title area (regions 1 and 2)
+ // Rule: If near the title in the 'hot' area, show background highlight
+ // TITLE_BACKGROUND_GRADIENT @ 60%
+ full = blend(titleBg, bg, 60);
+ // H_DND_LIGHT
+ // H_DND_FULL
+ createColor(IFormColors.H_HOVER_LIGHT, light);
+ createColor(IFormColors.H_HOVER_FULL, full);
+ }
+
+ private LocalResourceManager getResourceManager() {
+ if (resources == null)
+ resources = new LocalResourceManager(JFaceResources.getResources());
+ return resources;
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.eclipse.forms;
+
+import java.util.HashMap;
+
+import org.eclipse.jface.resource.DeviceResourceException;
+import org.eclipse.jface.resource.FontDescriptor;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.resource.LocalResourceManager;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+//import org.eclipse.swt.internal.graphics.Graphics;
+import org.eclipse.swt.widgets.Display;
+
+public class FormFonts {
+ private static FormFonts instance;
+
+ public static FormFonts getInstance() {
+ if (instance == null)
+ instance = new FormFonts();
+ return instance;
+ }
+
+ private LocalResourceManager resources;
+ private HashMap descriptors;
+
+ private FormFonts() {
+ }
+
+ private class BoldFontDescriptor extends FontDescriptor {
+ private FontData[] fFontData;
+
+ BoldFontDescriptor(Font font) {
+ // RAP [if] Changes due to different way of creating fonts
+ // fFontData = font.getFontData();
+ // for (int i = 0; i < fFontData.length; i++) {
+ // fFontData[i].setStyle(fFontData[i].getStyle() | SWT.BOLD);
+ // }
+ FontData fontData = font.getFontData()[0];
+ // Font boldFont = Graphics.getFont( fontData.getName(),
+ // fontData.getHeight(),
+ // fontData.getStyle() | SWT.BOLD );
+ Font boldFont = new Font(Display.getCurrent(), fontData.getName(), fontData.getHeight(),
+ fontData.getStyle() | SWT.BOLD);
+ fFontData = boldFont.getFontData();
+ }
+
+ public boolean equals(Object obj) {
+ if (obj instanceof BoldFontDescriptor) {
+ BoldFontDescriptor desc = (BoldFontDescriptor) obj;
+ if (desc.fFontData.length != fFontData.length)
+ return false;
+ for (int i = 0; i < fFontData.length; i++)
+ if (!fFontData[i].equals(desc.fFontData[i]))
+ return false;
+ return true;
+ }
+ return false;
+ }
+
+ public int hashCode() {
+ int hash = 0;
+ for (int i = 0; i < fFontData.length; i++)
+ hash = hash * 7 + fFontData[i].hashCode();
+ return hash;
+ }
+
+ public Font createFont(Device device) throws DeviceResourceException {
+ // RAP [if] Changes due to different way of creating fonts
+ return new Font(device, fFontData[0]);
+ // return Graphics.getFont( fFontData[ 0 ] );
+ }
+
+ public void destroyFont(Font previouslyCreatedFont) {
+ // RAP [if] unnecessary
+ // previouslyCreatedFont.dispose();
+ }
+ }
+
+ public Font getBoldFont(Display display, Font font) {
+ checkHashMaps();
+ BoldFontDescriptor desc = new BoldFontDescriptor(font);
+ Font result = getResourceManager().createFont(desc);
+ descriptors.put(result, desc);
+ return result;
+ }
+
+ public boolean markFinished(Font boldFont) {
+ checkHashMaps();
+ BoldFontDescriptor desc = (BoldFontDescriptor) descriptors.get(boldFont);
+ if (desc != null) {
+ getResourceManager().destroyFont(desc);
+ if (getResourceManager().find(desc) == null) {
+ descriptors.remove(boldFont);
+ validateHashMaps();
+ }
+ return true;
+
+ }
+ // if the image was not found, dispose of it for the caller
+ // RAP [if] unnecessary
+ // boldFont.dispose();
+ return false;
+ }
+
+ private LocalResourceManager getResourceManager() {
+ if (resources == null)
+ resources = new LocalResourceManager(JFaceResources.getResources());
+ return resources;
+ }
+
+ private void checkHashMaps() {
+ if (descriptors == null)
+ descriptors = new HashMap();
+ }
+
+ private void validateHashMaps() {
+ if (descriptors.size() == 0)
+ descriptors = null;
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.eclipse.forms;
+
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+//import org.eclipse.swt.custom.CCombo;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.FocusAdapter;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.KeyAdapter;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+// RAP [rh] Paint events missing
+//import org.eclipse.swt.events.PaintEvent;
+//import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+//RAP [rh] GC missing
+//import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Point;
+//import org.eclipse.swt.graphics.RGB;
+//import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+//import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+//import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.Widget;
+//import org.eclipse.ui.forms.FormColors;
+//import org.eclipse.ui.forms.HyperlinkGroup;
+//import org.eclipse.ui.forms.IFormColors;
+//import org.eclipse.ui.internal.forms.widgets.FormFonts;
+//import org.eclipse.ui.internal.forms.widgets.FormUtil;
+
+/**
+ * The toolkit is responsible for creating SWT controls adapted to work in
+ * Eclipse forms. In addition to changing their presentation properties (fonts,
+ * colors etc.), various listeners are attached to make them behave correctly in
+ * the form context.
+ * <p>
+ * In addition to being the control factory, the toolkit is also responsible for
+ * painting flat borders for select controls, managing hyperlink groups and
+ * control colors.
+ * <p>
+ * The toolkit creates some of the most common controls used to populate Eclipse
+ * forms. Controls that must be created using their constructors,
+ * <code>adapt()</code> method is available to change its properties in the
+ * same way as with the supported toolkit controls.
+ * <p>
+ * Typically, one toolkit object is created per workbench part (for example, an
+ * editor or a form wizard). The toolkit is disposed when the part is disposed.
+ * To conserve resources, it is possible to create one color object for the
+ * entire plug-in and share it between several toolkits. The plug-in is
+ * responsible for disposing the colors (disposing the toolkit that uses shared
+ * color object will not dispose the colors).
+ * <p>
+ * FormToolkit is normally instantiated, but can also be subclassed if some of
+ * the methods needs to be modified. In those cases, <code>super</code> must
+ * be called to preserve normal behaviour.
+ *
+ * @since 1.0
+ */
+public class FormToolkit {
+ public static final String KEY_DRAW_BORDER = "FormWidgetFactory.drawBorder"; //$NON-NLS-1$
+
+ public static final String TREE_BORDER = "treeBorder"; //$NON-NLS-1$
+
+ public static final String TEXT_BORDER = "textBorder"; //$NON-NLS-1$
+
+ private int borderStyle = SWT.NULL;
+
+ private FormColors colors;
+
+ private int orientation = Window.getDefaultOrientation();
+
+ // private KeyListener deleteListener;
+ // RAP [rh] Paint events missing
+// private BorderPainter borderPainter;
+
+ private BoldFontHolder boldFontHolder;
+
+// private HyperlinkGroup hyperlinkGroup;
+
+ private boolean isDisposed = false;
+
+ /* default */
+ VisibilityHandler visibilityHandler;
+
+ /* default */
+ KeyboardHandler keyboardHandler;
+
+ // RAP [rh] Paint events missing
+// private class BorderPainter implements PaintListener {
+// public void paintControl(PaintEvent event) {
+// Composite composite = (Composite) event.widget;
+// Control[] children = composite.getChildren();
+// for (int i = 0; i < children.length; i++) {
+// Control c = children[i];
+// boolean inactiveBorder = false;
+// boolean textBorder = false;
+// if (!c.isVisible())
+// continue;
+// /*
+// * if (c.getEnabled() == false && !(c instanceof CCombo))
+// * continue;
+// */
+// if (c instanceof Hyperlink)
+// continue;
+// Object flag = c.getData(KEY_DRAW_BORDER);
+// if (flag != null) {
+// if (flag.equals(Boolean.FALSE))
+// continue;
+// if (flag.equals(TREE_BORDER))
+// inactiveBorder = true;
+// else if (flag.equals(TEXT_BORDER))
+// textBorder = true;
+// }
+// if (getBorderStyle() == SWT.BORDER) {
+// if (!inactiveBorder && !textBorder) {
+// continue;
+// }
+// if (c instanceof Text || c instanceof Table
+// || c instanceof Tree)
+// continue;
+// }
+// if (!inactiveBorder
+// && (c instanceof Text || c instanceof CCombo || textBorder)) {
+// Rectangle b = c.getBounds();
+// GC gc = event.gc;
+// gc.setForeground(c.getBackground());
+// gc.drawRectangle(b.x - 1, b.y - 1, b.width + 1,
+// b.height + 1);
+// // gc.setForeground(getBorderStyle() == SWT.BORDER ? colors
+// // .getBorderColor() : colors.getForeground());
+// gc.setForeground(colors.getBorderColor());
+// if (c instanceof CCombo)
+// gc.drawRectangle(b.x - 1, b.y - 1, b.width + 1,
+// b.height + 1);
+// else
+// gc.drawRectangle(b.x - 1, b.y - 2, b.width + 1,
+// b.height + 3);
+// } else if (inactiveBorder || c instanceof Table
+// || c instanceof Tree) {
+// Rectangle b = c.getBounds();
+// GC gc = event.gc;
+// gc.setForeground(colors.getBorderColor());
+// gc.drawRectangle(b.x - 1, b.y - 1, b.width + 1,
+// b.height + 1);
+// }
+// }
+// }
+// }
+
+ private static class VisibilityHandler extends FocusAdapter {
+ public void focusGained(FocusEvent e) {
+ Widget w = e.widget;
+ if (w instanceof Control) {
+ FormUtil.ensureVisible((Control) w);
+ }
+ }
+ }
+
+ private static class KeyboardHandler extends KeyAdapter {
+ public void keyPressed(KeyEvent e) {
+ Widget w = e.widget;
+ if (w instanceof Control) {
+ if (e.doit)
+ FormUtil.processKey(e.keyCode, (Control) w);
+ }
+ }
+ }
+
+ private class BoldFontHolder {
+ private Font normalFont;
+
+ private Font boldFont;
+
+ public BoldFontHolder() {
+ }
+
+ public Font getBoldFont(Font font) {
+ createBoldFont(font);
+ return boldFont;
+ }
+
+ private void createBoldFont(Font font) {
+ if (normalFont == null || !normalFont.equals(font)) {
+ normalFont = font;
+ dispose();
+ }
+ if (boldFont == null) {
+ boldFont = FormFonts.getInstance().getBoldFont(colors.getDisplay(),
+ normalFont);
+ }
+ }
+
+ public void dispose() {
+ if (boldFont != null) {
+ FormFonts.getInstance().markFinished(boldFont);
+ boldFont = null;
+ }
+ }
+ }
+
+ /**
+ * Creates a toolkit that is self-sufficient (will manage its own colors).
+ * <p>
+ * Clients that call this method must call {@link #dispose()} when they
+ * are finished using the toolkit.
+ *
+ */
+ public FormToolkit(Display display) {
+ this(new FormColors(display));
+ }
+
+ /**
+ * Creates a toolkit that will use the provided (shared) colors. The toolkit
+ * will dispose the colors if and only if they are <b>not</b> marked as
+ * shared via the <code>markShared()</code> method.
+ * <p>
+ * Clients that call this method must call {@link #dispose()} when they
+ * are finished using the toolkit.
+ *
+ * @param colors
+ * the shared colors
+ */
+ public FormToolkit(FormColors colors) {
+ this.colors = colors;
+ initialize();
+ }
+
+ /**
+ * Creates a button as a part of the form.
+ *
+ * @param parent
+ * the button parent
+ * @param text
+ * an optional text for the button (can be <code>null</code>)
+ * @param style
+ * the button style (for example, <code>SWT.PUSH</code>)
+ * @return the button widget
+ */
+ public Button createButton(Composite parent, String text, int style) {
+ Button button = new Button(parent, style | SWT.FLAT | orientation);
+ if (text != null)
+ button.setText(text);
+ adapt(button, true, true);
+ return button;
+ }
+
+ /**
+ * Creates the composite as a part of the form.
+ *
+ * @param parent
+ * the composite parent
+ * @return the composite widget
+ */
+ public Composite createComposite(Composite parent) {
+ return createComposite(parent, SWT.NULL);
+ }
+
+ /**
+ * Creates the composite as part of the form using the provided style.
+ *
+ * @param parent
+ * the composite parent
+ * @param style
+ * the composite style
+ * @return the composite widget
+ */
+ public Composite createComposite(Composite parent, int style) {
+// Composite composite = new LayoutComposite(parent, style | orientation);
+ Composite composite = new Composite(parent, style | orientation);
+ adapt(composite);
+ return composite;
+ }
+
+ /**
+ * Creats the composite that can server as a separator between various parts
+ * of a form. Separator height should be controlled by setting the height
+ * hint on the layout data for the composite.
+ *
+ * @param parent
+ * the separator parent
+ * @return the separator widget
+ */
+// RAP [rh] createCompositeSeparator: currently no useful implementation possible, delete?
+ public Composite createCompositeSeparator(Composite parent) {
+ final Composite composite = new Composite(parent, orientation);
+// RAP [rh] GC and paint events missing
+// composite.addListener(SWT.Paint, new Listener() {
+// public void handleEvent(Event e) {
+// if (composite.isDisposed())
+// return;
+// Rectangle bounds = composite.getBounds();
+// GC gc = e.gc;
+// gc.setForeground(colors.getColor(IFormColors.SEPARATOR));
+// if (colors.getBackground() != null)
+// gc.setBackground(colors.getBackground());
+// gc.fillGradientRectangle(0, 0, bounds.width, bounds.height,
+// false);
+// }
+// });
+// if (parent instanceof Section)
+// ((Section) parent).setSeparatorControl(composite);
+ return composite;
+ }
+
+ /**
+ * Creates a label as a part of the form.
+ *
+ * @param parent
+ * the label parent
+ * @param text
+ * the label text
+ * @return the label widget
+ */
+ public Label createLabel(Composite parent, String text) {
+ return createLabel(parent, text, SWT.NONE);
+ }
+
+ /**
+ * Creates a label as a part of the form.
+ *
+ * @param parent
+ * the label parent
+ * @param text
+ * the label text
+ * @param style
+ * the label style
+ * @return the label widget
+ */
+ public Label createLabel(Composite parent, String text, int style) {
+ Label label = new Label(parent, style | orientation);
+ if (text != null)
+ label.setText(text);
+ adapt(label, false, false);
+ return label;
+ }
+
+ /**
+ * Creates a hyperlink as a part of the form. The hyperlink will be added to
+ * the hyperlink group that belongs to this toolkit.
+ *
+ * @param parent
+ * the hyperlink parent
+ * @param text
+ * the text of the hyperlink
+ * @param style
+ * the hyperlink style
+ * @return the hyperlink widget
+ */
+// public Hyperlink createHyperlink(Composite parent, String text, int style) {
+// Hyperlink hyperlink = new Hyperlink(parent, style | orientation);
+// if (text != null)
+// hyperlink.setText(text);
+// hyperlink.addFocusListener(visibilityHandler);
+// hyperlink.addKeyListener(keyboardHandler);
+// hyperlinkGroup.add(hyperlink);
+// return hyperlink;
+// }
+
+ /**
+ * Creates an image hyperlink as a part of the form. The hyperlink will be
+ * added to the hyperlink group that belongs to this toolkit.
+ *
+ * @param parent
+ * the hyperlink parent
+ * @param style
+ * the hyperlink style
+ * @return the image hyperlink widget
+ */
+// public ImageHyperlink createImageHyperlink(Composite parent, int style) {
+// ImageHyperlink hyperlink = new ImageHyperlink(parent, style
+// | orientation);
+// hyperlink.addFocusListener(visibilityHandler);
+// hyperlink.addKeyListener(keyboardHandler);
+// hyperlinkGroup.add(hyperlink);
+// return hyperlink;
+// }
+
+ /**
+ * Creates a rich text as a part of the form.
+ *
+ * @param parent
+ * the rich text parent
+ * @param trackFocus
+ * if <code>true</code>, the toolkit will monitor focus
+ * transfers to ensure that the hyperlink in focus is visible in
+ * the form.
+ * @return the rich text widget
+ * @since 1.2
+ */
+// public FormText createFormText(Composite parent, boolean trackFocus) {
+// FormText engine = new FormText(parent, SWT.WRAP | orientation);
+// engine.marginWidth = 1;
+// engine.marginHeight = 0;
+// engine.setHyperlinkSettings(getHyperlinkGroup());
+// adapt(engine, trackFocus, true);
+// engine.setMenu(parent.getMenu());
+// return engine;
+// }
+
+ /**
+ * Adapts a control to be used in a form that is associated with this
+ * toolkit. This involves adjusting colors and optionally adding handlers to
+ * ensure focus tracking and keyboard management.
+ *
+ * @param control
+ * a control to adapt
+ * @param trackFocus
+ * if <code>true</code>, form will be scrolled horizontally
+ * and/or vertically if needed to ensure that the control is
+ * visible when it gains focus. Set it to <code>false</code> if
+ * the control is not capable of gaining focus.
+ * @param trackKeyboard
+ * if <code>true</code>, the control that is capable of
+ * gaining focus will be tracked for certain keys that are
+ * important to the underlying form (for example, PageUp,
+ * PageDown, ScrollUp, ScrollDown etc.). Set it to
+ * <code>false</code> if the control is not capable of gaining
+ * focus or these particular key event are already used by the
+ * control.
+ */
+ public void adapt(Control control, boolean trackFocus, boolean trackKeyboard) {
+ control.setBackground(colors.getBackground());
+ control.setForeground(colors.getForeground());
+// if (control instanceof ExpandableComposite) {
+// ExpandableComposite ec = (ExpandableComposite) control;
+// if (ec.toggle != null) {
+// if (trackFocus)
+// ec.toggle.addFocusListener(visibilityHandler);
+// if (trackKeyboard)
+// ec.toggle.addKeyListener(keyboardHandler);
+// }
+// if (ec.textLabel != null) {
+// if (trackFocus)
+// ec.textLabel.addFocusListener(visibilityHandler);
+// if (trackKeyboard)
+// ec.textLabel.addKeyListener(keyboardHandler);
+// }
+// return;
+// }
+ if (trackFocus)
+ control.addFocusListener(visibilityHandler);
+ if (trackKeyboard)
+ control.addKeyListener(keyboardHandler);
+ }
+
+ /**
+ * Adapts a composite to be used in a form associated with this toolkit.
+ *
+ * @param composite
+ * the composite to adapt
+ */
+ public void adapt(Composite composite) {
+ composite.setBackground(colors.getBackground());
+ composite.addMouseListener(new MouseAdapter() {
+ public void mouseDown(MouseEvent e) {
+ ((Control) e.widget).setFocus();
+ }
+ });
+ if (composite.getParent() != null)
+ composite.setMenu(composite.getParent().getMenu());
+ }
+
+ /**
+ * A helper method that ensures the provided control is visible when
+ * ScrolledComposite is somewhere in the parent chain. If scroll bars are
+ * visible and the control is clipped, the client of the scrolled composite
+ * will be scrolled to reveal the control.
+ *
+ * @param c
+ * the control to reveal
+ */
+ public static void ensureVisible(Control c) {
+ FormUtil.ensureVisible(c);
+ }
+
+ /**
+ * Creates a section as a part of the form.
+ *
+ * @param parent
+ * the section parent
+ * @param sectionStyle
+ * the section style
+ * @return the section widget
+ */
+// public Section createSection(Composite parent, int sectionStyle) {
+// Section section = new Section(parent, orientation, sectionStyle);
+// section.setMenu(parent.getMenu());
+// adapt(section, true, true);
+// if (section.toggle != null) {
+// section.toggle.setHoverDecorationColor(colors
+// .getColor(IFormColors.TB_TOGGLE_HOVER));
+// section.toggle.setDecorationColor(colors
+// .getColor(IFormColors.TB_TOGGLE));
+// }
+// section.setFont(boldFontHolder.getBoldFont(parent.getFont()));
+// if ((sectionStyle & Section.TITLE_BAR) != 0
+// || (sectionStyle & Section.SHORT_TITLE_BAR) != 0) {
+// colors.initializeSectionToolBarColors();
+// section.setTitleBarBackground(colors.getColor(IFormColors.TB_BG));
+// section.setTitleBarBorderColor(colors
+// .getColor(IFormColors.TB_BORDER));
+// }
+// // call setTitleBarForeground regardless as it also sets the label color
+// section.setTitleBarForeground(colors
+// .getColor(IFormColors.TB_TOGGLE));
+// return section;
+// }
+
+ /**
+ * Creates an expandable composite as a part of the form.
+ *
+ * @param parent
+ * the expandable composite parent
+ * @param expansionStyle
+ * the expandable composite style
+ * @return the expandable composite widget
+ */
+// public ExpandableComposite createExpandableComposite(Composite parent,
+// int expansionStyle) {
+// ExpandableComposite ec = new ExpandableComposite(parent, orientation,
+// expansionStyle);
+// ec.setMenu(parent.getMenu());
+// adapt(ec, true, true);
+// ec.setFont(boldFontHolder.getBoldFont(ec.getFont()));
+// return ec;
+// }
+
+ /**
+ * Creates a separator label as a part of the form.
+ *
+ * @param parent
+ * the separator parent
+ * @param style
+ * the separator style
+ * @return the separator label
+ */
+ public Label createSeparator(Composite parent, int style) {
+ Label label = new Label(parent, SWT.SEPARATOR | style | orientation);
+ label.setBackground(colors.getBackground());
+ label.setForeground(colors.getBorderColor());
+ return label;
+ }
+
+ /**
+ * Creates a table as a part of the form.
+ *
+ * @param parent
+ * the table parent
+ * @param style
+ * the table style
+ * @return the table widget
+ */
+ public Table createTable(Composite parent, int style) {
+ Table table = new Table(parent, style | borderStyle | orientation);
+ adapt(table, false, false);
+ // hookDeleteListener(table);
+ return table;
+ }
+
+ /**
+ * Creates a text as a part of the form.
+ *
+ * @param parent
+ * the text parent
+ * @param value
+ * the text initial value
+ * @return the text widget
+ */
+ public Text createText(Composite parent, String value) {
+ return createText(parent, value, SWT.SINGLE);
+ }
+
+ /**
+ * Creates a text as a part of the form.
+ *
+ * @param parent
+ * the text parent
+ * @param value
+ * the text initial value
+ * @param style
+ * the text style
+ * @return the text widget
+ */
+ public Text createText(Composite parent, String value, int style) {
+ Text text = new Text(parent, borderStyle | style | orientation);
+ if (value != null)
+ text.setText(value);
+ text.setForeground(colors.getForeground());
+ text.setBackground(colors.getBackground());
+ text.addFocusListener(visibilityHandler);
+ return text;
+ }
+
+ /**
+ * Creates a tree widget as a part of the form.
+ *
+ * @param parent
+ * the tree parent
+ * @param style
+ * the tree style
+ * @return the tree widget
+ */
+ public Tree createTree(Composite parent, int style) {
+ Tree tree = new Tree(parent, borderStyle | style | orientation);
+ adapt(tree, false, false);
+ // hookDeleteListener(tree);
+ return tree;
+ }
+
+ /**
+ * Creates a scrolled form widget in the provided parent. If you do not
+ * require scrolling because there is already a scrolled composite up the
+ * parent chain, use 'createForm' instead.
+ *
+ * @param parent
+ * the scrolled form parent
+ * @return the form that can scroll itself
+ * @see #createForm
+ */
+ public ScrolledComposite createScrolledForm(Composite parent) {
+ ScrolledComposite form = new ScrolledComposite(parent, SWT.V_SCROLL
+ | SWT.H_SCROLL | orientation);
+ form.setExpandHorizontal(true);
+ form.setExpandVertical(true);
+ form.setBackground(colors.getBackground());
+ form.setForeground(colors.getColor(IFormColors.TITLE));
+ form.setFont(JFaceResources.getHeaderFont());
+ return form;
+ }
+
+ /**
+ * Creates a form widget in the provided parent. Note that this widget does
+ * not scroll its content, so make sure there is a scrolled composite up the
+ * parent chain. If you require scrolling, use 'createScrolledForm' instead.
+ *
+ * @param parent
+ * the form parent
+ * @return the form that does not scroll
+ * @see #createScrolledForm
+ */
+// public Form createForm(Composite parent) {
+// Form formContent = new Form(parent, orientation);
+// formContent.setBackground(colors.getBackground());
+// formContent.setForeground(colors.getColor(IFormColors.TITLE));
+// formContent.setFont(JFaceResources.getHeaderFont());
+// return formContent;
+// }
+
+ /**
+ * Takes advantage of the gradients and other capabilities to decorate the
+ * form heading using colors computed based on the current skin and
+ * operating system.
+ *
+ * @param form
+ * the form to decorate
+ */
+
+// public void decorateFormHeading(Form form) {
+// Color top = colors.getColor(IFormColors.H_GRADIENT_END);
+// Color bot = colors.getColor(IFormColors.H_GRADIENT_START);
+// form.setTextBackground(new Color[] { top, bot }, new int[] { 100 },
+// true);
+// form.setHeadColor(IFormColors.H_BOTTOM_KEYLINE1, colors
+// .getColor(IFormColors.H_BOTTOM_KEYLINE1));
+// form.setHeadColor(IFormColors.H_BOTTOM_KEYLINE2, colors
+// .getColor(IFormColors.H_BOTTOM_KEYLINE2));
+// form.setHeadColor(IFormColors.H_HOVER_LIGHT, colors
+// .getColor(IFormColors.H_HOVER_LIGHT));
+// form.setHeadColor(IFormColors.H_HOVER_FULL, colors
+// .getColor(IFormColors.H_HOVER_FULL));
+// form.setHeadColor(IFormColors.TB_TOGGLE, colors
+// .getColor(IFormColors.TB_TOGGLE));
+// form.setHeadColor(IFormColors.TB_TOGGLE_HOVER, colors
+// .getColor(IFormColors.TB_TOGGLE_HOVER));
+// form.setSeparatorVisible(true);
+// }
+
+ /**
+ * Creates a scrolled page book widget as a part of the form.
+ *
+ * @param parent
+ * the page book parent
+ * @param style
+ * the text style
+ * @return the scrolled page book widget
+ */
+// public ScrolledPageBook createPageBook(Composite parent, int style) {
+// ScrolledPageBook book = new ScrolledPageBook(parent, style
+// | orientation);
+// adapt(book, true, true);
+// book.setMenu(parent.getMenu());
+// return book;
+// }
+
+ /**
+ * Disposes the toolkit.
+ */
+ public void dispose() {
+ if (isDisposed) {
+ return;
+ }
+ isDisposed = true;
+ if (colors.isShared() == false) {
+ colors.dispose();
+ colors = null;
+ }
+ boldFontHolder.dispose();
+ }
+
+ /**
+ * Returns the hyperlink group that manages hyperlinks for this toolkit.
+ *
+ * @return the hyperlink group
+ */
+// public HyperlinkGroup getHyperlinkGroup() {
+// return hyperlinkGroup;
+// }
+
+ /**
+ * Sets the background color for the entire toolkit. The method delegates
+ * the call to the FormColors object and also updates the hyperlink group so
+ * that hyperlinks and other objects are in sync.
+ *
+ * @param bg
+ * the new background color
+ */
+ public void setBackground(Color bg) {
+// hyperlinkGroup.setBackground(bg);
+ colors.setBackground(bg);
+ }
+
+ /**
+ * Refreshes the hyperlink colors by loading from JFace settings.
+ */
+// public void refreshHyperlinkColors() {
+// hyperlinkGroup.initializeDefaultForegrounds(colors.getDisplay());
+// }
+
+// RAP [rh] paintBordersFor not useful as no GC to actually paint borders
+// /**
+// * Paints flat borders for widgets created by this toolkit within the
+// * provided parent. Borders will not be painted if the global border style
+// * is SWT.BORDER (i.e. if native borders are used). Call this method during
+// * creation of a form composite to get the borders of its children painted.
+// * Care should be taken when selection layout margins. At least one pixel
+// * pargin width and height must be chosen to allow the toolkit to paint the
+// * border on the parent around the widgets.
+// * <p>
+// * Borders are painted for some controls that are selected by the toolkit by
+// * default. If a control needs a border but is not on its list, it is
+// * possible to force border in the following way:
+// *
+// * <pre>
+// *
+// *
+// *
+// * widget.setData(FormToolkit.KEY_DRAW_BORDER, FormToolkit.TREE_BORDER);
+// *
+// * or
+// *
+// * widget.setData(FormToolkit.KEY_DRAW_BORDER, FormToolkit.TEXT_BORDER);
+// *
+// *
+// *
+// * </pre>
+// *
+// * @param parent
+// * the parent that owns the children for which the border needs
+// * to be painted.
+// */
+// public void paintBordersFor(Composite parent) {
+// // if (borderStyle == SWT.BORDER)
+// // return;
+// if (borderPainter == null)
+// borderPainter = new BorderPainter();
+// parent.addPaintListener(borderPainter);
+// }
+
+ /**
+ * Returns the colors used by this toolkit.
+ *
+ * @return the color object
+ */
+ public FormColors getColors() {
+ return colors;
+ }
+
+ /**
+ * Returns the border style used for various widgets created by this
+ * toolkit. The intent of the toolkit is to create controls with styles that
+ * yield a 'flat' appearance. On systems where the native borders are
+ * already flat, we set the style to SWT.BORDER and don't paint the borders
+ * ourselves. Otherwise, the style is set to SWT.NULL, and borders are
+ * painted by the toolkit.
+ *
+ * @return the global border style
+ */
+ public int getBorderStyle() {
+ return borderStyle;
+ }
+
+ /**
+ * Returns the margin required around the children whose border is being
+ * painted by the toolkit using {@link #paintBordersFor(Composite)}. Since
+ * the border is painted around the controls on the parent, a number of
+ * pixels needs to be reserved for this border. For windowing systems where
+ * the native border is used, this margin is 0.
+ *
+ * @return the margin in the parent when children have their border painted
+ */
+ public int getBorderMargin() {
+ return getBorderStyle() == SWT.BORDER ? 0 : 2;
+ }
+
+ /**
+ * Sets the border style to be used when creating widgets. The toolkit
+ * chooses the correct style based on the platform but this value can be
+ * changed using this method.
+ *
+ * @param style
+ * <code>SWT.BORDER</code> or <code>SWT.NULL</code>
+ * @see #getBorderStyle
+ */
+ public void setBorderStyle(int style) {
+ this.borderStyle = style;
+ }
+
+ /**
+ * A utility method that ensures that the control is visible in the scrolled
+ * composite. The prerequisite for this method is that the control has a
+ * class that extends ScrolledComposite somewhere in the parent chain. If
+ * the control is partially or fully clipped, the composite is scrolled to
+ * set by setting the origin to the control origin.
+ *
+ * @param c
+ * the control to make visible
+ * @param verticalOnly
+ * if <code>true</code>, the scrolled composite will be
+ * scrolled only vertically if needed. Otherwise, the scrolled
+ * composite origin will be set to the control origin.
+ */
+ public static void setControlVisible(Control c, boolean verticalOnly) {
+ ScrolledComposite scomp = FormUtil.getScrolledComposite(c);
+ if (scomp == null)
+ return;
+ Point location = FormUtil.getControlLocation(scomp, c);
+ scomp.setOrigin(location);
+ }
+
+ private void initialize() {
+ initializeBorderStyle();
+// hyperlinkGroup = new HyperlinkGroup(colors.getDisplay());
+// hyperlinkGroup.setBackground(colors.getBackground());
+ visibilityHandler = new VisibilityHandler();
+ keyboardHandler = new KeyboardHandler();
+ boldFontHolder = new BoldFontHolder();
+ }
+
+// RAP [rh] revise detection of border style: can't ask OS here
+ private void initializeBorderStyle() {
+// String osname = System.getProperty("os.name"); //$NON-NLS-1$
+// String osversion = System.getProperty("os.version"); //$NON-NLS-1$
+// if (osname.startsWith("Windows") && "5.1".compareTo(osversion) <= 0) { //$NON-NLS-1$ //$NON-NLS-2$
+// // Skinned widgets used on newer Windows (e.g. XP (5.1), Vista
+// // (6.0))
+// // Check for Windows Classic. If not used, set the style to BORDER
+// RGB rgb = colors.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
+// if (rgb.red != 212 || rgb.green != 208 || rgb.blue != 200)
+// borderStyle = SWT.BORDER;
+// } else if (osname.startsWith("Mac")) //$NON-NLS-1$
+// borderStyle = SWT.BORDER;
+
+ borderStyle = SWT.BORDER;
+ }
+
+ /**
+ * Returns the orientation that all the widgets created by this toolkit will
+ * inherit, if set. Can be <code>SWT.NULL</code>,
+ * <code>SWT.LEFT_TO_RIGHT</code> and <code>SWT.RIGHT_TO_LEFT</code>.
+ *
+ * @return orientation style for this toolkit, or <code>SWT.NULL</code> if
+ * not set. The default orientation is inherited from the Window
+ * default orientation.
+ * @see org.eclipse.jface.window.Window#getDefaultOrientation()
+ */
+
+ public int getOrientation() {
+ return orientation;
+ }
+
+ /**
+ * Sets the orientation that all the widgets created by this toolkit will
+ * inherit. Can be <code>SWT.NULL</code>, <code>SWT.LEFT_TO_RIGHT</code>
+ * and <code>SWT.RIGHT_TO_LEFT</code>.
+ *
+ * @param orientation
+ * style for this toolkit.
+ */
+
+ public void setOrientation(int orientation) {
+ this.orientation = orientation;
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.eclipse.forms;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.MouseEvent;
+//import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.FontMetrics;
+import org.eclipse.swt.graphics.GC;
+//import org.eclipse.swt.graphics.Image;
+//import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Layout;
+//import org.eclipse.swt.widgets.ScrollBar;
+import org.eclipse.swt.widgets.Text;
+//import org.eclipse.ui.forms.widgets.ColumnLayout;
+//import org.eclipse.ui.forms.widgets.Form;
+//import org.eclipse.ui.forms.widgets.FormText;
+//import org.eclipse.ui.forms.widgets.FormToolkit;
+//import org.eclipse.ui.forms.widgets.ILayoutExtension;
+//
+//import com.ibm.icu.text.BreakIterator;
+
+public class FormUtil {
+ public static final String PLUGIN_ID = "org.eclipse.ui.forms"; //$NON-NLS-1$
+
+ static final int H_SCROLL_INCREMENT = 5;
+
+ static final int V_SCROLL_INCREMENT = 64;
+
+ public static final String DEBUG = PLUGIN_ID + "/debug"; //$NON-NLS-1$
+
+ public static final String DEBUG_TEXT = DEBUG + "/text"; //$NON-NLS-1$
+ public static final String DEBUG_TEXTSIZE = DEBUG + "/textsize"; //$NON-NLS-1$
+
+ public static final String DEBUG_FOCUS = DEBUG + "/focus"; //$NON-NLS-1$
+
+ public static final String FOCUS_SCROLLING = "focusScrolling"; //$NON-NLS-1$
+
+ public static final String IGNORE_BODY = "__ignore_body__"; //$NON-NLS-1$
+
+ public static Text createText(Composite parent, String label,
+ FormToolkit factory) {
+ return createText(parent, label, factory, 1);
+ }
+
+ public static Text createText(Composite parent, String label,
+ FormToolkit factory, int span) {
+ factory.createLabel(parent, label);
+ Text text = factory.createText(parent, ""); //$NON-NLS-1$
+ int hfill = span == 1 ? GridData.FILL_HORIZONTAL
+ : GridData.HORIZONTAL_ALIGN_FILL;
+ GridData gd = new GridData(hfill | GridData.VERTICAL_ALIGN_CENTER);
+ gd.horizontalSpan = span;
+ text.setLayoutData(gd);
+ return text;
+ }
+
+ public static Text createText(Composite parent, String label,
+ FormToolkit factory, int span, int style) {
+ Label l = factory.createLabel(parent, label);
+ if ((style & SWT.MULTI) != 0) {
+ GridData gd = new GridData(GridData.VERTICAL_ALIGN_BEGINNING);
+ l.setLayoutData(gd);
+ }
+ Text text = factory.createText(parent, "", style); //$NON-NLS-1$
+ int hfill = span == 1 ? GridData.FILL_HORIZONTAL
+ : GridData.HORIZONTAL_ALIGN_FILL;
+ GridData gd = new GridData(hfill | GridData.VERTICAL_ALIGN_CENTER);
+ gd.horizontalSpan = span;
+ text.setLayoutData(gd);
+ return text;
+ }
+
+ public static Text createText(Composite parent, FormToolkit factory,
+ int span) {
+ Text text = factory.createText(parent, ""); //$NON-NLS-1$
+ int hfill = span == 1 ? GridData.FILL_HORIZONTAL
+ : GridData.HORIZONTAL_ALIGN_FILL;
+ GridData gd = new GridData(hfill | GridData.VERTICAL_ALIGN_CENTER);
+ gd.horizontalSpan = span;
+ text.setLayoutData(gd);
+ return text;
+ }
+
+ public static int computeMinimumWidth(GC gc, String text) {
+// BreakIterator wb = BreakIterator.getWordInstance();
+// wb.setText(text);
+// int last = 0;
+//
+// int width = 0;
+//
+// for (int loc = wb.first(); loc != BreakIterator.DONE; loc = wb.next()) {
+// String word = text.substring(last, loc);
+// Point extent = gc.textExtent(word);
+// width = Math.max(width, extent.x);
+// last = loc;
+// }
+// String lastWord = text.substring(last);
+// Point extent = gc.textExtent(lastWord);
+// width = Math.max(width, extent.x);
+// return width;
+ return 0;
+ }
+
+ public static Point computeWrapSize(GC gc, String text, int wHint) {
+// BreakIterator wb = BreakIterator.getWordInstance();
+// wb.setText(text);
+ FontMetrics fm = gc.getFontMetrics();
+ int lineHeight = fm.getHeight();
+
+ int saved = 0;
+ int last = 0;
+ int height = lineHeight;
+ int maxWidth = 0;
+// for (int loc = wb.first(); loc != BreakIterator.DONE; loc = wb.next()) {
+// String word = text.substring(saved, loc);
+// Point extent = gc.textExtent(word);
+// if (extent.x > wHint) {
+// // overflow
+// saved = last;
+// height += extent.y;
+// // switch to current word so maxWidth will accommodate very long single words
+// word = text.substring(last, loc);
+// extent = gc.textExtent(word);
+// }
+// maxWidth = Math.max(maxWidth, extent.x);
+// last = loc;
+// }
+ /*
+ * Correct the height attribute in case it was calculated wrong due to wHint being less than maxWidth.
+ * The recursive call proved to be the only thing that worked in all cases. Some attempts can be made
+ * to estimate the height, but the algorithm needs to be run again to be sure.
+ */
+ if (maxWidth > wHint)
+ return computeWrapSize(gc, text, maxWidth);
+ return new Point(maxWidth, height);
+ }
+
+// RAP [rh] paintWrapText unnecessary
+// public static void paintWrapText(GC gc, String text, Rectangle bounds) {
+// paintWrapText(gc, text, bounds, false);
+// }
+
+// RAP [rh] paintWrapText unnecessary
+// public static void paintWrapText(GC gc, String text, Rectangle bounds,
+// boolean underline) {
+// BreakIterator wb = BreakIterator.getWordInstance();
+// wb.setText(text);
+// FontMetrics fm = gc.getFontMetrics();
+// int lineHeight = fm.getHeight();
+// int descent = fm.getDescent();
+//
+// int saved = 0;
+// int last = 0;
+// int y = bounds.y;
+// int width = bounds.width;
+//
+// for (int loc = wb.first(); loc != BreakIterator.DONE; loc = wb.next()) {
+// String line = text.substring(saved, loc);
+// Point extent = gc.textExtent(line);
+//
+// if (extent.x > width) {
+// // overflow
+// String prevLine = text.substring(saved, last);
+// gc.drawText(prevLine, bounds.x, y, true);
+// if (underline) {
+// Point prevExtent = gc.textExtent(prevLine);
+// int lineY = y + lineHeight - descent + 1;
+// gc
+// .drawLine(bounds.x, lineY, bounds.x + prevExtent.x,
+// lineY);
+// }
+//
+// saved = last;
+// y += lineHeight;
+// }
+// last = loc;
+// }
+// // paint the last line
+// String lastLine = text.substring(saved, last);
+// gc.drawText(lastLine, bounds.x, y, true);
+// if (underline) {
+// int lineY = y + lineHeight - descent + 1;
+// Point lastExtent = gc.textExtent(lastLine);
+// gc.drawLine(bounds.x, lineY, bounds.x + lastExtent.x, lineY);
+// }
+// }
+
+ public static ScrolledComposite getScrolledComposite(Control c) {
+ Composite parent = c.getParent();
+
+ while (parent != null) {
+ if (parent instanceof ScrolledComposite) {
+ return (ScrolledComposite) parent;
+ }
+ parent = parent.getParent();
+ }
+ return null;
+ }
+
+ public static void ensureVisible(Control c) {
+ ScrolledComposite scomp = getScrolledComposite(c);
+ if (scomp != null) {
+ Object data = scomp.getData(FOCUS_SCROLLING);
+ if (data == null || !data.equals(Boolean.FALSE))
+ FormUtil.ensureVisible(scomp, c);
+ }
+ }
+
+ public static void ensureVisible(ScrolledComposite scomp, Control control) {
+ // if the control is a FormText we do not need to scroll since it will
+ // ensure visibility of its segments as necessary
+// if (control instanceof FormText)
+// return;
+ Point controlSize = control.getSize();
+ Point controlOrigin = getControlLocation(scomp, control);
+ ensureVisible(scomp, controlOrigin, controlSize);
+ }
+
+ public static void ensureVisible(ScrolledComposite scomp,
+ Point controlOrigin, Point controlSize) {
+ Rectangle area = scomp.getClientArea();
+ Point scompOrigin = scomp.getOrigin();
+
+ int x = scompOrigin.x;
+ int y = scompOrigin.y;
+
+ // horizontal right, but only if the control is smaller
+ // than the client area
+ if (controlSize.x < area.width
+ && (controlOrigin.x + controlSize.x > scompOrigin.x
+ + area.width)) {
+ x = controlOrigin.x + controlSize.x - area.width;
+ }
+ // horizontal left - make sure the left edge of
+ // the control is showing
+ if (controlOrigin.x < x) {
+ if (controlSize.x < area.width)
+ x = controlOrigin.x + controlSize.x - area.width;
+ else
+ x = controlOrigin.x;
+ }
+ // vertical bottom
+ if (controlSize.y < area.height
+ && (controlOrigin.y + controlSize.y > scompOrigin.y
+ + area.height)) {
+ y = controlOrigin.y + controlSize.y - area.height;
+ }
+ // vertical top - make sure the top of
+ // the control is showing
+ if (controlOrigin.y < y) {
+ if (controlSize.y < area.height)
+ y = controlOrigin.y + controlSize.y - area.height;
+ else
+ y = controlOrigin.y;
+ }
+
+ if (scompOrigin.x != x || scompOrigin.y != y) {
+ // scroll to reveal
+ scomp.setOrigin(x, y);
+ }
+ }
+
+ public static void ensureVisible(ScrolledComposite scomp, Control control,
+ MouseEvent e) {
+ Point controlOrigin = getControlLocation(scomp, control);
+ int rX = controlOrigin.x + e.x;
+ int rY = controlOrigin.y + e.y;
+ Rectangle area = scomp.getClientArea();
+ Point scompOrigin = scomp.getOrigin();
+
+ int x = scompOrigin.x;
+ int y = scompOrigin.y;
+ // System.out.println("Ensure: area="+area+", origin="+scompOrigin+",
+ // cloc="+controlOrigin+", csize="+controlSize+", x="+x+", y="+y);
+
+ // horizontal right
+ if (rX > scompOrigin.x + area.width) {
+ x = rX - area.width;
+ }
+ // horizontal left
+ else if (rX < x) {
+ x = rX;
+ }
+ // vertical bottom
+ if (rY > scompOrigin.y + area.height) {
+ y = rY - area.height;
+ }
+ // vertical top
+ else if (rY < y) {
+ y = rY;
+ }
+
+ if (scompOrigin.x != x || scompOrigin.y != y) {
+ // scroll to reveal
+ scomp.setOrigin(x, y);
+ }
+ }
+
+ public static Point getControlLocation(ScrolledComposite scomp,
+ Control control) {
+ int x = 0;
+ int y = 0;
+ Control content = scomp.getContent();
+ Control currentControl = control;
+ for (;;) {
+ if (currentControl == content)
+ break;
+ Point location = currentControl.getLocation();
+ // if (location.x > 0)
+ // x += location.x;
+ // if (location.y > 0)
+ // y += location.y;
+ x += location.x;
+ y += location.y;
+ currentControl = currentControl.getParent();
+ }
+ return new Point(x, y);
+ }
+
+ static void scrollVertical(ScrolledComposite scomp, boolean up) {
+ scroll(scomp, 0, up ? -V_SCROLL_INCREMENT : V_SCROLL_INCREMENT);
+ }
+
+ static void scrollHorizontal(ScrolledComposite scomp, boolean left) {
+ scroll(scomp, left ? -H_SCROLL_INCREMENT : H_SCROLL_INCREMENT, 0);
+ }
+
+ static void scrollPage(ScrolledComposite scomp, boolean up) {
+ Rectangle clientArea = scomp.getClientArea();
+ int increment = up ? -clientArea.height : clientArea.height;
+ scroll(scomp, 0, increment);
+ }
+
+ static void scroll(ScrolledComposite scomp, int xoffset, int yoffset) {
+ Point origin = scomp.getOrigin();
+ Point contentSize = scomp.getContent().getSize();
+ int xorigin = origin.x + xoffset;
+ int yorigin = origin.y + yoffset;
+ xorigin = Math.max(xorigin, 0);
+ xorigin = Math.min(xorigin, contentSize.x - 1);
+ yorigin = Math.max(yorigin, 0);
+ yorigin = Math.min(yorigin, contentSize.y - 1);
+ scomp.setOrigin(xorigin, yorigin);
+ }
+
+// RAP [rh] FormUtil#updatePageIncrement: empty implementation
+ public static void updatePageIncrement(ScrolledComposite scomp) {
+// ScrollBar vbar = scomp.getVerticalBar();
+// if (vbar != null) {
+// Rectangle clientArea = scomp.getClientArea();
+// int increment = clientArea.height - 5;
+// vbar.setPageIncrement(increment);
+// }
+// ScrollBar hbar = scomp.getHorizontalBar();
+// if (hbar != null) {
+// Rectangle clientArea = scomp.getClientArea();
+// int increment = clientArea.width - 5;
+// hbar.setPageIncrement(increment);
+// }
+ }
+
+ public static void processKey(int keyCode, Control c) {
+ if (c.isDisposed()) {
+ return;
+ }
+ ScrolledComposite scomp = FormUtil.getScrolledComposite(c);
+ if (scomp != null) {
+ if (c instanceof Combo)
+ return;
+ switch (keyCode) {
+ case SWT.ARROW_DOWN:
+ if (scomp.getData("novarrows") == null) //$NON-NLS-1$
+ FormUtil.scrollVertical(scomp, false);
+ break;
+ case SWT.ARROW_UP:
+ if (scomp.getData("novarrows") == null) //$NON-NLS-1$
+ FormUtil.scrollVertical(scomp, true);
+ break;
+ case SWT.ARROW_LEFT:
+ FormUtil.scrollHorizontal(scomp, true);
+ break;
+ case SWT.ARROW_RIGHT:
+ FormUtil.scrollHorizontal(scomp, false);
+ break;
+ case SWT.PAGE_UP:
+ FormUtil.scrollPage(scomp, true);
+ break;
+ case SWT.PAGE_DOWN:
+ FormUtil.scrollPage(scomp, false);
+ break;
+ }
+ }
+ }
+
+ public static boolean isWrapControl(Control c) {
+ if ((c.getStyle() & SWT.WRAP) != 0)
+ return true;
+ if (c instanceof Composite) {
+ return false;
+// return ((Composite) c).getLayout() instanceof ILayoutExtension;
+ }
+ return false;
+ }
+
+ public static int getWidthHint(int wHint, Control c) {
+ boolean wrap = isWrapControl(c);
+ return wrap ? wHint : SWT.DEFAULT;
+ }
+
+ public static int getHeightHint(int hHint, Control c) {
+ if (c instanceof Composite) {
+ Layout layout = ((Composite) c).getLayout();
+// if (layout instanceof ColumnLayout)
+// return hHint;
+ }
+ return SWT.DEFAULT;
+ }
+
+ public static int computeMinimumWidth(Control c, boolean changed) {
+ if (c instanceof Composite) {
+ Layout layout = ((Composite) c).getLayout();
+// if (layout instanceof ILayoutExtension)
+// return ((ILayoutExtension) layout).computeMinimumWidth(
+// (Composite) c, changed);
+ }
+ return c.computeSize(FormUtil.getWidthHint(5, c), SWT.DEFAULT, changed).x;
+ }
+
+ public static int computeMaximumWidth(Control c, boolean changed) {
+ if (c instanceof Composite) {
+ Layout layout = ((Composite) c).getLayout();
+// if (layout instanceof ILayoutExtension)
+// return ((ILayoutExtension) layout).computeMaximumWidth(
+// (Composite) c, changed);
+ }
+ return c.computeSize(SWT.DEFAULT, SWT.DEFAULT, changed).x;
+ }
+
+// public static Form getForm(Control c) {
+// Composite parent = c.getParent();
+// while (parent != null) {
+// if (parent instanceof Form) {
+// return (Form) parent;
+// }
+// parent = parent.getParent();
+// }
+// return null;
+// }
+
+// RAP [rh] FormUtil#createAlphaMashImage unnecessary
+// public static Image createAlphaMashImage(Device device, Image srcImage) {
+// Rectangle bounds = srcImage.getBounds();
+// int alpha = 0;
+// int calpha = 0;
+// ImageData data = srcImage.getImageData();
+// // Create a new image with alpha values alternating
+// // between fully transparent (0) and fully opaque (255).
+// // This image will show the background through the
+// // transparent pixels.
+// for (int i = 0; i < bounds.height; i++) {
+// // scan line
+// alpha = calpha;
+// for (int j = 0; j < bounds.width; j++) {
+// // column
+// data.setAlpha(j, i, alpha);
+// alpha = alpha == 255 ? 0 : 255;
+// }
+// calpha = calpha == 255 ? 0 : 255;
+// }
+// return new Image(device, data);
+// }
+
+ public static boolean mnemonicMatch(String text, char key) {
+ char mnemonic = findMnemonic(text);
+ if (mnemonic == '\0')
+ return false;
+ return Character.toUpperCase(key) == Character.toUpperCase(mnemonic);
+ }
+
+ private static char findMnemonic(String string) {
+ int index = 0;
+ int length = string.length();
+ do {
+ while (index < length && string.charAt(index) != '&')
+ index++;
+ if (++index >= length)
+ return '\0';
+ if (string.charAt(index) != '&')
+ return string.charAt(index);
+ index++;
+ } while (index < length);
+ return '\0';
+ }
+
+ public static void setFocusScrollingEnabled(Control c, boolean enabled) {
+ ScrolledComposite scomp = null;
+
+ if (c instanceof ScrolledComposite)
+ scomp = (ScrolledComposite)c;
+ else
+ scomp = getScrolledComposite(c);
+ if (scomp!=null)
+ scomp.setData(FormUtil.FOCUS_SCROLLING, enabled?null:Boolean.FALSE);
+ }
+
+ // RAP [rh] FormUtil#setAntialias unnecessary
+// public static void setAntialias(GC gc, int style) {
+// if (!gc.getAdvanced()) {
+// gc.setAdvanced(true);
+// if (!gc.getAdvanced())
+// return;
+// }
+// gc.setAntialias(style);
+// }
+}
--- /dev/null
+package org.argeo.cms.ui.eclipse.forms;
+
+/**
+ * A place to hold all the color constants used in the forms package.
+ *
+ * @since 1.0
+ */
+
+public interface IFormColors {
+ /**
+ * A prefix for all the keys.
+ */
+ String PREFIX = "org.eclipse.ui.forms."; //$NON-NLS-1$
+ /**
+ * Key for the form title foreground color.
+ */
+ String TITLE = PREFIX + "TITLE"; //$NON-NLS-1$
+
+ /**
+ * A prefix for the header color constants.
+ */
+ String H_PREFIX = PREFIX + "H_"; //$NON-NLS-1$
+ /*
+ * A prefix for the section title bar color constants.
+ */
+ String TB_PREFIX = PREFIX + "TB_"; //$NON-NLS-1$
+ /**
+ * Key for the form header background gradient ending color.
+ */
+ String H_GRADIENT_END = H_PREFIX + "GRADIENT_END"; //$NON-NLS-1$
+
+ /**
+ * Key for the form header background gradient starting color.
+ *
+ */
+ String H_GRADIENT_START = H_PREFIX + "GRADIENT_START"; //$NON-NLS-1$
+ /**
+ * Key for the form header bottom keyline 1 color.
+ *
+ */
+ String H_BOTTOM_KEYLINE1 = H_PREFIX + "BOTTOM_KEYLINE1"; //$NON-NLS-1$
+ /**
+ * Key for the form header bottom keyline 2 color.
+ *
+ */
+ String H_BOTTOM_KEYLINE2 = H_PREFIX + "BOTTOM_KEYLINE2"; //$NON-NLS-1$
+ /**
+ * Key for the form header light hover color.
+ *
+ */
+ String H_HOVER_LIGHT = H_PREFIX + "H_HOVER_LIGHT"; //$NON-NLS-1$
+ /**
+ * Key for the form header full hover color.
+ *
+ */
+ String H_HOVER_FULL = H_PREFIX + "H_HOVER_FULL"; //$NON-NLS-1$
+
+ /**
+ * Key for the tree/table border color.
+ */
+ String BORDER = PREFIX + "BORDER"; //$NON-NLS-1$
+
+ /**
+ * Key for the section separator color.
+ */
+ String SEPARATOR = PREFIX + "SEPARATOR"; //$NON-NLS-1$
+
+ /**
+ * Key for the section title bar background.
+ */
+ String TB_BG = TB_PREFIX + "BG"; //$NON-NLS-1$
+
+ /**
+ * Key for the section title bar foreground.
+ */
+ String TB_FG = TB_PREFIX + "FG"; //$NON-NLS-1$
+
+ /**
+ * Key for the section title bar gradient.
+ * @deprecated Since 3.3, this color is not used any more. The
+ * tool bar gradient is created starting from {@link #TB_BG} to
+ * the section background color.
+ */
+ String TB_GBG = TB_BG;
+
+ /**
+ * Key for the section title bar border.
+ */
+ String TB_BORDER = TB_PREFIX + "BORDER"; //$NON-NLS-1$
+
+ /**
+ * Key for the section toggle color. Since 3.1, this color is used for all
+ * section styles.
+ */
+ String TB_TOGGLE = TB_PREFIX + "TOGGLE"; //$NON-NLS-1$
+
+ /**
+ * Key for the section toggle hover color.
+ *
+ */
+ String TB_TOGGLE_HOVER = TB_PREFIX + "TOGGLE_HOVER"; //$NON-NLS-1$
+}
\ No newline at end of file
--- /dev/null
+package org.argeo.cms.ui.eclipse.forms;
+
+/**
+ * Classes that implement this interface can be added to the managed form and
+ * take part in the form life cycle. The part is initialized with the form and
+ * will be asked to accept focus. The part can receive form input and can elect
+ * to do something according to it (for example, select an object that matches
+ * the input).
+ * <p>
+ * The form part has two 'out of sync' states in respect to the model(s) that
+ * feed the form: <b>dirty</b> and <b>stale</b>. When a part is dirty, it
+ * means that the user interacted with it and now its widgets contain state that
+ * is newer than the model. In order to sync up with the model, 'commit' needs
+ * to be called. In contrast, the model can change 'under' the form (as a result
+ * of some actions outside the form), resulting in data in the model being
+ * 'newer' than the content presented in the form. A 'stale' form part is
+ * brought in sync with the model by calling 'refresh'. The part is responsible
+ * for notifying the form when one of these states change in the part. The form
+ * reserves the right to handle this notification in the most appropriate way
+ * for the situation (for example, if the form is in a page of the multi-page
+ * editor, it may do nothing for stale parts if the page is currently not
+ * showing).
+ * <p>
+ * When the form is disposed, each registered part is disposed as well. Parts
+ * are responsible for releasing any system resources they created and for
+ * removing themselves as listeners from all event providers.
+ *
+ * @see IManagedForm
+ * @since 1.0
+ *
+ */
+public interface IFormPart {
+ /**
+ * Initializes the part.
+ *
+ * @param form
+ * the managed form that manages the part
+ */
+ void initialize(IManagedForm form);
+
+ /**
+ * Disposes the part allowing it to release allocated resources.
+ */
+ void dispose();
+
+ /**
+ * Returns true if the part has been modified with respect to the data
+ * loaded from the model.
+ *
+ * @return true if the part has been modified with respect to the data
+ * loaded from the model
+ */
+ boolean isDirty();
+
+ /**
+ * If part is displaying information loaded from a model, this method
+ * instructs it to commit the new (modified) data back into the model.
+ *
+ * @param onSave
+ * indicates if commit is called during 'save' operation or for
+ * some other reason (for example, if form is contained in a
+ * wizard or a multi-page editor and the user is about to leave
+ * the page).
+ */
+ void commit(boolean onSave);
+
+ /**
+ * Notifies the part that an object has been set as overall form's input.
+ * The part can elect to react by revealing or selecting the object, or do
+ * nothing if not applicable.
+ *
+ * @return <code>true</code> if the part has selected and revealed the
+ * input object, <code>false</code> otherwise.
+ */
+ boolean setFormInput(Object input);
+
+ /**
+ * Instructs form part to transfer focus to the widget that should has focus
+ * in that part. The method can do nothing (if it has no widgets capable of
+ * accepting focus).
+ */
+ void setFocus();
+
+ /**
+ * Tests whether the form part is stale and needs refreshing. Parts can
+ * receive notification from models that will make their content stale, but
+ * may need to delay refreshing to improve performance (for example, there
+ * is no need to immediately refresh a part on a form that is current on a
+ * hidden page).
+ * <p>
+ * It is important to differentiate 'stale' and 'dirty' states. Part is
+ * 'dirty' if user interacted with its editable widgets and changed the
+ * values. In contrast, part is 'stale' when the data it presents in the
+ * widgets has been changed in the model without direct user interaction.
+ *
+ * @return <code>true</code> if the part needs refreshing,
+ * <code>false</code> otherwise.
+ */
+ boolean isStale();
+
+ /**
+ * Refreshes the part completely from the information freshly obtained from
+ * the model. The method will not be called if the part is not stale.
+ * Otherwise, the part is responsible for clearing the 'stale' flag after
+ * refreshing itself.
+ */
+ void refresh();
+}
--- /dev/null
+package org.argeo.cms.ui.eclipse.forms;
+
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.swt.custom.ScrolledComposite;
+//import org.eclipse.ui.forms.widgets.FormToolkit;
+//import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+/**
+ * Managed form wraps a form widget and adds life cycle methods for form parts.
+ * A form part is a portion of the form that participates in form life cycle
+ * events.
+ * <p>
+ * There is no 1/1 mapping between widgets and form parts. A widget like Section
+ * can be a part by itself, but a number of widgets can gather around one form
+ * part.
+ * <p>
+ * This interface should not be extended or implemented. New form instances
+ * should be created using ManagedForm.
+ *
+ * @see ManagedForm
+ * @since 1.0
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface IManagedForm {
+ /**
+ * Initializes the form by looping through the managed parts and
+ * initializing them. Has no effect if already called once.
+ */
+ public void initialize();
+
+ /**
+ * Returns the toolkit used by this form.
+ *
+ * @return the toolkit
+ */
+ public FormToolkit getToolkit();
+
+ /**
+ * Returns the form widget managed by this form.
+ *
+ * @return the form widget
+ */
+ public ScrolledComposite getForm();
+
+ /**
+ * Reflows the form as a result of the layout change.
+ *
+ * @param changed
+ * if <code>true</code>, discard cached layout information
+ */
+ public void reflow(boolean changed);
+
+ /**
+ * A part can use this method to notify other parts that implement
+ * IPartSelectionListener about selection changes.
+ *
+ * @param part
+ * the part that broadcasts the selection
+ * @param selection
+ * the selection in the part
+ */
+ public void fireSelectionChanged(IFormPart part, ISelection selection);
+
+ /**
+ * Returns all the parts currently managed by this form.
+ *
+ * @return the managed parts
+ */
+ IFormPart[] getParts();
+
+ /**
+ * Adds the new part to the form.
+ *
+ * @param part
+ * the part to add
+ */
+ void addPart(IFormPart part);
+
+ /**
+ * Removes the part from the form.
+ *
+ * @param part
+ * the part to remove
+ */
+ void removePart(IFormPart part);
+
+ /**
+ * Sets the input of this page to the provided object.
+ *
+ * @param input
+ * the new page input
+ * @return <code>true</code> if the form contains this object,
+ * <code>false</code> otherwise.
+ */
+ boolean setInput(Object input);
+
+ /**
+ * Returns the current page input.
+ *
+ * @return page input object or <code>null</code> if not applicable.
+ */
+ Object getInput();
+
+ /**
+ * Tests if form is dirty. A managed form is dirty if at least one managed
+ * part is dirty.
+ *
+ * @return <code>true</code> if at least one managed part is dirty,
+ * <code>false</code> otherwise.
+ */
+ boolean isDirty();
+
+ /**
+ * Notifies the form that the dirty state of one of its parts has changed.
+ * The global dirty state of the form can be obtained by calling 'isDirty'.
+ *
+ * @see #isDirty
+ */
+ void dirtyStateChanged();
+
+ /**
+ * Commits the dirty form. All pending changes in the widgets are flushed
+ * into the model.
+ *
+ * @param onSave
+ */
+ void commit(boolean onSave);
+
+ /**
+ * Tests if form is stale. A managed form is stale if at least one managed
+ * part is stale. This can happen when the underlying model changes,
+ * resulting in the presentation of the part being out of sync with the
+ * model and needing refreshing.
+ *
+ * @return <code>true</code> if the form is stale, <code>false</code>
+ * otherwise.
+ */
+ boolean isStale();
+
+ /**
+ * Notifies the form that the stale state of one of its parts has changed.
+ * The global stale state of the form can be obtained by calling 'isStale'.
+ */
+ void staleStateChanged();
+
+ /**
+ * Refreshes the form by refreshing every part that is stale.
+ */
+ void refresh();
+
+ /**
+ * Sets the container that owns this form. Depending on the context, the
+ * container may be wizard, editor page, editor etc.
+ *
+ * @param container
+ * the container of this form
+ */
+ void setContainer(Object container);
+
+ /**
+ * Returns the container of this form.
+ *
+ * @return the form container
+ */
+ Object getContainer();
+
+ /**
+ * Returns the message manager that will keep track of messages in this
+ * form.
+ *
+ * @return the message manager instance
+ */
+// IMessageManager getMessageManager();
+}
--- /dev/null
+package org.argeo.cms.ui.eclipse.forms;
+
+import org.eclipse.jface.viewers.ISelection;
+
+/**
+ * Form parts can implement this interface if they want to be
+ * notified when another part on the same form changes selection
+ * state.
+ *
+ * @see IFormPart
+ * @since 1.0
+ */
+public interface IPartSelectionListener {
+ /**
+ * Called when the provided part has changed selection state.
+ *
+ * @param part
+ * the selection source
+ * @param selection
+ * the new selection
+ */
+ public void selectionChanged(IFormPart part, ISelection selection);
+}
--- /dev/null
+package org.argeo.cms.ui.eclipse.forms;
+
+import java.util.Vector;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.widgets.Composite;
+//import org.eclipse.ui.forms.widgets.FormToolkit;
+//import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+/**
+ * Managed form wraps a form widget and adds life cycle methods for form parts.
+ * A form part is a portion of the form that participates in form life cycle
+ * events.
+ * <p>
+ * There is requirement for 1/1 mapping between widgets and form parts. A widget
+ * like Section can be a part by itself, but a number of widgets can join around
+ * one form part.
+ * <p>
+ * Note to developers: this class is left public to allow its use beyond the
+ * original intention (inside a multi-page editor's page). You should limit the
+ * use of this class to make new instances inside a form container (wizard page,
+ * dialog etc.). Clients that need access to the class should not do it
+ * directly. Instead, they should do it through IManagedForm interface as much
+ * as possible.
+ *
+ * @since 1.0
+ */
+public class ManagedForm implements IManagedForm {
+ private Object input;
+
+ private ScrolledComposite form;
+
+ private FormToolkit toolkit;
+
+ private Object container;
+
+ private boolean ownsToolkit;
+
+ private boolean initialized;
+
+ private Vector parts = new Vector();
+
+ /**
+ * Creates a managed form in the provided parent. Form toolkit and widget
+ * will be created and owned by this object.
+ *
+ * @param parent
+ * the parent widget
+ */
+ public ManagedForm(Composite parent) {
+ toolkit = new FormToolkit(parent.getDisplay());
+ ownsToolkit = true;
+ form = toolkit.createScrolledForm(parent);
+
+ }
+
+ /**
+ * Creates a managed form that will use the provided toolkit and
+ *
+ * @param toolkit
+ * @param form
+ */
+ public ManagedForm(FormToolkit toolkit, ScrolledComposite form) {
+ this.form = form;
+ this.toolkit = toolkit;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.forms.IManagedForm#addPart(org.eclipse.ui.forms.IFormPart)
+ */
+ public void addPart(IFormPart part) {
+ parts.add(part);
+ part.initialize(this);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.forms.IManagedForm#removePart(org.eclipse.ui.forms.IFormPart)
+ */
+ public void removePart(IFormPart part) {
+ parts.remove(part);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.forms.IManagedForm#getParts()
+ */
+ public IFormPart[] getParts() {
+ return (IFormPart[]) parts.toArray(new IFormPart[parts.size()]);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.forms.IManagedForm#getToolkit()
+ */
+ public FormToolkit getToolkit() {
+ return toolkit;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.forms.IManagedForm#getForm()
+ */
+ public ScrolledComposite getForm() {
+ return form;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.forms.IManagedForm#reflow(boolean)
+ */
+ public void reflow(boolean changed) {
+// form.reflow(changed);
+ }
+
+ /**
+ * A part can use this method to notify other parts that implement
+ * IPartSelectionListener about selection changes.
+ *
+ * @param part
+ * the part that broadcasts the selection
+ * @param selection
+ * the selection in the part
+ * @see IPartSelectionListener
+ */
+ public void fireSelectionChanged(IFormPart part, ISelection selection) {
+ for (int i = 0; i < parts.size(); i++) {
+ IFormPart cpart = (IFormPart) parts.get(i);
+ if (part.equals(cpart))
+ continue;
+// if (cpart instanceof IPartSelectionListener) {
+// ((IPartSelectionListener) cpart).selectionChanged(part,
+// selection);
+// }
+ }
+ }
+
+ /**
+ * Initializes the form by looping through the managed parts and
+ * initializing them. Has no effect if already called once.
+ */
+ public void initialize() {
+ if (initialized)
+ return;
+ for (int i = 0; i < parts.size(); i++) {
+ IFormPart part = (IFormPart) parts.get(i);
+ part.initialize(this);
+ }
+ initialized = true;
+ }
+
+ /**
+ * Disposes all the parts in this form.
+ */
+ public void dispose() {
+ for (int i = 0; i < parts.size(); i++) {
+ IFormPart part = (IFormPart) parts.get(i);
+ part.dispose();
+ }
+ if (ownsToolkit) {
+ toolkit.dispose();
+ }
+ }
+
+ /**
+ * Refreshes the form by refreshes all the stale parts. Since 3.1, this
+ * method is performed on a UI thread when called from another thread so it
+ * is not needed to wrap the call in <code>Display.syncExec</code> or
+ * <code>asyncExec</code>.
+ */
+ public void refresh() {
+ Thread t = Thread.currentThread();
+ Thread dt = toolkit.getColors().getDisplay().getThread();
+ if (t.equals(dt))
+ doRefresh();
+ else {
+ toolkit.getColors().getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ doRefresh();
+ }
+ });
+ }
+ }
+
+ private void doRefresh() {
+ int nrefreshed = 0;
+ for (int i = 0; i < parts.size(); i++) {
+ IFormPart part = (IFormPart) parts.get(i);
+ if (part.isStale()) {
+ part.refresh();
+ nrefreshed++;
+ }
+ }
+// if (nrefreshed > 0)
+// form.reflow(true);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.forms.IManagedForm#commit(boolean)
+ */
+ public void commit(boolean onSave) {
+ for (int i = 0; i < parts.size(); i++) {
+ IFormPart part = (IFormPart) parts.get(i);
+ if (part.isDirty())
+ part.commit(onSave);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.forms.IManagedForm#setInput(java.lang.Object)
+ */
+ public boolean setInput(Object input) {
+ boolean pageResult = false;
+
+ this.input = input;
+ for (int i = 0; i < parts.size(); i++) {
+ IFormPart part = (IFormPart) parts.get(i);
+ boolean result = part.setFormInput(input);
+ if (result)
+ pageResult = true;
+ }
+ return pageResult;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.forms.IManagedForm#getInput()
+ */
+ public Object getInput() {
+ return input;
+ }
+
+ /**
+ * Transfers the focus to the first form part.
+ */
+ public void setFocus() {
+ if (parts.size() > 0) {
+ IFormPart part = (IFormPart) parts.get(0);
+ part.setFocus();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.forms.IManagedForm#isDirty()
+ */
+ public boolean isDirty() {
+ for (int i = 0; i < parts.size(); i++) {
+ IFormPart part = (IFormPart) parts.get(i);
+ if (part.isDirty())
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.forms.IManagedForm#isStale()
+ */
+ public boolean isStale() {
+ for (int i = 0; i < parts.size(); i++) {
+ IFormPart part = (IFormPart) parts.get(i);
+ if (part.isStale())
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.forms.IManagedForm#dirtyStateChanged()
+ */
+ public void dirtyStateChanged() {
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.forms.IManagedForm#staleStateChanged()
+ */
+ public void staleStateChanged() {
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.forms.IManagedForm#getContainer()
+ */
+ public Object getContainer() {
+ return container;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.forms.IManagedForm#setContainer(java.lang.Object)
+ */
+ public void setContainer(Object container) {
+ this.container = container;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.ui.forms.IManagedForm#getMessageManager()
+ */
+// public IMessageManager getMessageManager() {
+// return form.getMessageManager();
+// }
+}
--- /dev/null
+package org.argeo.cms.ui.eclipse.forms.editor;
+
+import org.argeo.cms.ui.eclipse.forms.FormToolkit;
+import org.eclipse.jface.dialogs.IPageChangeProvider;
+
+/**
+ * This class forms a base of multi-page form editors that typically use one or
+ * more pages with forms and one page for raw source of the editor input.
+ * <p>
+ * Pages are added 'lazily' i.e. adding a page reserves a tab for it but does
+ * not cause the page control to be created. Page control is created when an
+ * attempt is made to select the page in question. This allows editors with
+ * several tabs and complex pages to open quickly.
+ * <p>
+ * Subclasses should extend this class and implement <code>addPages</code>
+ * method. One of the two <code>addPage</code> methods should be called to
+ * contribute pages to the editor. One adds complete (standalone) editors as
+ * nested tabs. These editors will be created right away and will be hooked so
+ * that key bindings, selection service etc. is compatible with the one for the
+ * standalone case. The other method adds classes that implement
+ * <code>IFormPage</code> interface. These pages will be created lazily and
+ * they will share the common key binding and selection service. Since 3.1,
+ * FormEditor is a page change provider. It allows listeners to attach to it and
+ * get notified when pages are changed. This new API in JFace allows dynamic
+ * help to update on page changes.
+ *
+ * @since 1.0
+ */
+// RAP [if] As RAP is still using workbench 3.4, the implementation of
+// IPageChangeProvider is missing from MultiPageEditorPart. Remove this code
+// with the adoption of workbench > 3.5
+//public abstract class FormEditor extends MultiPageEditorPart {
+public abstract class FormEditor implements
+ IPageChangeProvider {
+ private FormToolkit formToolkit;
+
+
+public FormToolkit getToolkit() {
+ return formToolkit;
+ }
+
+public void editorDirtyStateChanged() {
+
+}
+
+public FormPage getActivePageInstance() {
+ return null;
+}
+
+ // RAP [if] As RAP is still using workbench 3.4, the implementation of
+// IPageChangeProvider is missing from MultiPageEditorPart. Remove this code
+// with the adoption of workbench > 3.5
+// private ListenerList pageListeners = new ListenerList();
+//
+// /*
+// * (non-Javadoc)
+// *
+// * @see org.eclipse.jface.dialogs.IPageChangeProvider#addPageChangedListener(org.eclipse.jface.dialogs.IPageChangedListener)
+// */
+// public void addPageChangedListener(IPageChangedListener listener) {
+// pageListeners.add(listener);
+// }
+//
+// /*
+// * (non-Javadoc)
+// *
+// * @see org.eclipse.jface.dialogs.IPageChangeProvider#removePageChangedListener(org.eclipse.jface.dialogs.IPageChangedListener)
+// */
+// public void removePageChangedListener(IPageChangedListener listener) {
+// pageListeners.remove(listener);
+// }
+//
+// private void firePageChanged(final PageChangedEvent event) {
+// Object[] listeners = pageListeners.getListeners();
+// for (int i = 0; i < listeners.length; ++i) {
+// final IPageChangedListener l = (IPageChangedListener) listeners[i];
+// SafeRunnable.run(new SafeRunnable() {
+// public void run() {
+// l.pageChanged(event);
+// }
+// });
+// }
+// }
+// RAPEND [if]
+}
--- /dev/null
+package org.argeo.cms.ui.eclipse.forms.editor;
+import org.argeo.cms.ui.eclipse.forms.IManagedForm;
+import org.argeo.cms.ui.eclipse.forms.ManagedForm;
+import org.eclipse.swt.custom.BusyIndicator;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+/**
+ * A base class that all pages that should be added to FormEditor must subclass.
+ * Form page has an instance of PageForm that extends managed form. Subclasses
+ * should override method 'createFormContent(ManagedForm)' to fill the form with
+ * content. Note that page itself can be loaded lazily (on first open).
+ * Consequently, the call to create the form content can come after the editor
+ * has been opened for a while (in fact, it is possible to open and close the
+ * editor and never create the form because no attempt has been made to show the
+ * page).
+ *
+ * @since 1.0
+ */
+public class FormPage implements IFormPage {
+ private FormEditor editor;
+ private PageForm mform;
+ private int index;
+ private String id;
+
+ private String partName;
+
+
+
+ public void setPartName(String partName) {
+ this.partName = partName;
+ }
+ private static class PageForm extends ManagedForm {
+ public PageForm(FormPage page, ScrolledComposite form) {
+ super(page.getEditor().getToolkit(), form);
+ setContainer(page);
+ }
+
+ public FormPage getPage() {
+ return (FormPage)getContainer();
+ }
+ public void dirtyStateChanged() {
+ getPage().getEditor().editorDirtyStateChanged();
+ }
+ public void staleStateChanged() {
+ if (getPage().isActive())
+ refresh();
+ }
+ }
+ /**
+ * A constructor that creates the page and initializes it with the editor.
+ *
+ * @param editor
+ * the parent editor
+ * @param id
+ * the unique identifier
+ * @param title
+ * the page title
+ */
+ public FormPage(FormEditor editor, String id, String title) {
+ this(id, title);
+ initialize(editor);
+ }
+ /**
+ * The constructor. The parent editor need to be passed in the
+ * <code>initialize</code> method if this constructor is used.
+ *
+ * @param id
+ * a unique page identifier
+ * @param title
+ * a user-friendly page title
+ */
+ public FormPage(String id, String title) {
+ this.id = id;
+ setPartName(title);
+ }
+ /**
+ * Initializes the form page.
+ *
+ * @see IEditorPart#init
+ */
+// public void init(IEditorSite site, IEditorInput input) {
+// setSite(site);
+// setInput(input);
+// }
+ /**
+ * Primes the form page with the parent editor instance.
+ *
+ * @param editor
+ * the parent editor
+ */
+ public void initialize(FormEditor editor) {
+ this.editor = editor;
+ }
+ /**
+ * Returns the parent editor.
+ *
+ * @return parent editor instance
+ */
+ public FormEditor getEditor() {
+ return editor;
+ }
+ /**
+ * Returns the managed form owned by this page.
+ *
+ * @return the managed form
+ */
+ public IManagedForm getManagedForm() {
+ return mform;
+ }
+ /**
+ * Implements the required method by refreshing the form when set active.
+ * Subclasses must call super when overriding this method.
+ */
+ public void setActive(boolean active) {
+ if (active) {
+ // We are switching to this page - refresh it
+ // if needed.
+ if (mform != null)
+ mform.refresh();
+ }
+ }
+ /**
+ * Tests if the page is active by asking the parent editor if this page is
+ * the currently active page.
+ *
+ * @return <code>true</code> if the page is currently active,
+ * <code>false</code> otherwise.
+ */
+ public boolean isActive() {
+ return this.equals(editor.getActivePageInstance());
+ }
+ /**
+ * Creates the part control by creating the managed form using the parent
+ * editor's toolkit. Subclasses should override
+ * <code>createFormContent(IManagedForm)</code> to populate the form with
+ * content.
+ *
+ * @param parent
+ * the page parent composite
+ */
+ public void createPartControl(Composite parent) {
+ ScrolledComposite form = editor.getToolkit().createScrolledForm(parent);
+ mform = new PageForm(this, form);
+ BusyIndicator.showWhile(parent.getDisplay(), new Runnable() {
+ public void run() {
+ createFormContent(mform);
+ }
+ });
+ }
+ /**
+ * Subclasses should override this method to create content in the form
+ * hosted in this page.
+ *
+ * @param managedForm
+ * the form hosted in this page.
+ */
+ protected void createFormContent(IManagedForm managedForm) {
+ }
+ /**
+ * Returns the form page control.
+ *
+ * @return managed form's control
+ */
+ public Control getPartControl() {
+ return mform != null ? mform.getForm() : null;
+ }
+ /**
+ * Disposes the managed form.
+ */
+ public void dispose() {
+ if (mform != null)
+ mform.dispose();
+ }
+ /**
+ * Returns the unique identifier that can be used to reference this page.
+ *
+ * @return the unique page identifier
+ */
+ public String getId() {
+ return id;
+ }
+ /**
+ * Returns <code>null</code>- form page has no title image. Subclasses
+ * may override.
+ *
+ * @return <code>null</code>
+ */
+ public Image getTitleImage() {
+ return null;
+ }
+ /**
+ * Sets the focus by delegating to the managed form.
+ */
+ public void setFocus() {
+ if (mform != null)
+ mform.setFocus();
+ }
+ /**
+ * @see org.eclipse.ui.ISaveablePart#doSave(org.eclipse.core.runtime.IProgressMonitor)
+ */
+// public void doSave(IProgressMonitor monitor) {
+// if (mform != null)
+// mform.commit(true);
+// }
+ /**
+ * @see org.eclipse.ui.ISaveablePart#doSaveAs()
+ */
+ public void doSaveAs() {
+ }
+ /**
+ * @see org.eclipse.ui.ISaveablePart#isSaveAsAllowed()
+ */
+ public boolean isSaveAsAllowed() {
+ return false;
+ }
+ /**
+ * Implemented by testing if the managed form is dirty.
+ *
+ * @return <code>true</code> if the managed form is dirty,
+ * <code>false</code> otherwise.
+ *
+ * @see org.eclipse.ui.ISaveablePart#isDirty()
+ */
+ public boolean isDirty() {
+ return mform != null ? mform.isDirty() : false;
+ }
+ /**
+ * Preserves the page index.
+ *
+ * @param index
+ * the assigned page index
+ */
+ public void setIndex(int index) {
+ this.index = index;
+ }
+ /**
+ * Returns the saved page index.
+ *
+ * @return the page index
+ */
+ public int getIndex() {
+ return index;
+ }
+ /**
+ * Form pages are not editors.
+ *
+ * @return <code>false</code>
+ */
+ public boolean isEditor() {
+ return false;
+ }
+ /**
+ * Attempts to select and reveal the given object by passing the request to
+ * the managed form.
+ *
+ * @param object
+ * the object to select and reveal in the page if possible.
+ * @return <code>true</code> if the page has been successfully selected
+ * and revealed by one of the managed form parts, <code>false</code>
+ * otherwise.
+ */
+ public boolean selectReveal(Object object) {
+ if (mform != null)
+ return mform.setInput(object);
+ return false;
+ }
+ /**
+ * By default, editor will be allowed to flip the page.
+ * @return <code>true</code>
+ */
+ public boolean canLeaveThePage() {
+ return true;
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.eclipse.forms.editor;
+import org.argeo.cms.ui.eclipse.forms.IManagedForm;
+import org.eclipse.swt.widgets.Control;
+/**
+ * Interface that all GUI pages need to implement in order
+ * to be added to FormEditor part. The interface makes
+ * several assumptions:
+ * <ul>
+ * <li>The form page has a managed form</li>
+ * <li>The form page has a unique id</li>
+ * <li>The form page can be GUI but can also wrap a complete
+ * editor class (in that case, it should return <code>true</code>
+ * from <code>isEditor()</code> method).</li>
+ * <li>The form page is lazy i.e. understands that
+ * its part control will be created at the last possible
+ * moment.</li>.
+ * </ul>
+ * <p>Existing editors can be wrapped by implementing
+ * this interface. In this case, 'isEditor' should return <code>true</code>.
+ * A common editor to wrap in <code>TextEditor</code> that is
+ * often added to show the raw source code of the file open into
+ * the multi-page editor.
+ *
+ * @since 1.0
+ */
+public interface IFormPage {
+ /**
+ * @param editor
+ * the form editor that this page belongs to
+ */
+ void initialize(FormEditor editor);
+ /**
+ * Returns the editor this page belongs to.
+ *
+ * @return the form editor
+ */
+ FormEditor getEditor();
+ /**
+ * Returns the managed form of this page, unless this is a source page.
+ *
+ * @return the managed form or <samp>null </samp> if this is a source page.
+ */
+ IManagedForm getManagedForm();
+ /**
+ * Indicates whether the page has become the active in the editor. Classes
+ * that implement this interface may use this method to commit the page (on
+ * <code>false</code>) or lazily create and/or populate the content on
+ * <code>true</code>.
+ *
+ * @param active
+ * <code>true</code> if page should be visible, <code>false</code>
+ * otherwise.
+ */
+ void setActive(boolean active);
+ /**
+ * Returns <samp>true </samp> if page is currently active, false if not.
+ *
+ * @return <samp>true </samp> for active page.
+ */
+ boolean isActive();
+ /**
+ * Tests if the content of the page is in a state that allows the
+ * editor to flip to another page. Typically, pages that contain
+ * raw source with syntax errors should not allow editors to
+ * leave them until errors are corrected.
+ * @return <code>true</code> if the editor can flip to another page,
+ * <code>false</code> otherwise.
+ */
+ boolean canLeaveThePage();
+ /**
+ * Returns the control associated with this page.
+ *
+ * @return the control of this page if created or <samp>null </samp> if the
+ * page has not been shown yet.
+ */
+ Control getPartControl();
+ /**
+ * Page must have a unique id that can be used to show it without knowing
+ * its relative position in the editor.
+ *
+ * @return the unique page identifier
+ */
+ String getId();
+ /**
+ * Returns the position of the page in the editor.
+ *
+ * @return the zero-based index of the page in the editor.
+ */
+ int getIndex();
+ /**
+ * Sets the position of the page in the editor.
+ *
+ * @param index
+ * the zero-based index of the page in the editor.
+ */
+ void setIndex(int index);
+ /**
+ * Tests whether this page wraps a complete editor that
+ * can be registered on its own, or represents a page
+ * that cannot exist outside the multi-page editor context.
+ *
+ * @return <samp>true </samp> if the page wraps an editor,
+ * <samp>false </samp> if this is a form page.
+ */
+ boolean isEditor();
+ /**
+ * A hint to bring the provided object into focus. If the object is in a
+ * tree or table control, select it. If it is shown on a scrollable page,
+ * ensure that it is visible. If the object is not presented in
+ * the page, <code>false</code> should be returned to allow another
+ * page to try.
+ *
+ * @param object
+ * object to select and reveal
+ * @return <code>true</code> if the request was successful, <code>false</code>
+ * otherwise.
+ */
+ boolean selectReveal(Object object);
+}
--- /dev/null
+package org.argeo.cms.ui.forms;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.swt.SwtEditablePart;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.swt.SWT;
+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 String that displays a browsable link when read-only */
+public class EditableLink extends EditablePropertyString implements
+ SwtEditablePart {
+ private static final long serialVersionUID = 5055000749992803591L;
+
+ private String type;
+ private String message;
+ private boolean readOnly;
+
+ public EditableLink(Composite parent, int style, Node node,
+ String propertyName, String type, String message)
+ throws RepositoryException {
+ super(parent, style, node, propertyName, message);
+ this.message = message;
+ this.type = type;
+
+ readOnly = SWT.READ_ONLY == (style & SWT.READ_ONLY);
+ if (node.hasProperty(propertyName)) {
+ this.setStyle(FormStyle.propertyText.style());
+ this.setText(node.getProperty(propertyName).getString());
+ } else {
+ this.setStyle(FormStyle.propertyMessage.style());
+ this.setText("");
+ }
+ }
+
+ public void setText(String text) {
+ Control child = getControl();
+ if (child instanceof Label) {
+ Label lbl = (Label) child;
+ if (EclipseUiUtils.isEmpty(text))
+ lbl.setText(message);
+ else if (readOnly)
+ setLinkValue(lbl, text);
+ else
+ // if canEdit() we put only the value with no link
+ // to avoid glitches of the edition life cycle
+ lbl.setText(text);
+ } else if (child instanceof Text) {
+ Text txt = (Text) child;
+ if (EclipseUiUtils.isEmpty(text)) {
+ txt.setText("");
+ txt.setMessage(message);
+ } else
+ txt.setText(text);
+ }
+ }
+
+ private void setLinkValue(Label lbl, String text) {
+ if (FormStyle.email.style().equals(type))
+ lbl.setText(FormUtils.getMailLink(text));
+ else if (FormStyle.phone.style().equals(type))
+ lbl.setText(FormUtils.getPhoneLink(text));
+ else if (FormStyle.website.style().equals(type))
+ lbl.setText(FormUtils.getUrlLink(text));
+ else if (FormStyle.facebook.style().equals(type)
+ || FormStyle.instagram.style().equals(type)
+ || FormStyle.linkedIn.style().equals(type)
+ || FormStyle.twitter.style().equals(type))
+ lbl.setText(FormUtils.getUrlLink(text));
+ }
+}
\ No newline at end of file
--- /dev/null
+package org.argeo.cms.ui.forms;
+
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.cms.swt.SwtEditablePart;
+import org.argeo.cms.ui.widgets.StyledControl;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionListener;
+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.layout.RowLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+/** Display, add or remove values from a list in a CMS context */
+public class EditableMultiStringProperty extends StyledControl implements SwtEditablePart {
+ private static final long serialVersionUID = -7044614381252178595L;
+
+ private String propertyName;
+ private String message;
+ // TODO implement the ability to provide a list of possible values
+// private String[] possibleValues;
+ private boolean canEdit;
+ private SelectionListener removeValueSL;
+ private List<String> values;
+
+ // TODO manage within the CSS
+ private int rowSpacing = 5;
+ private int rowMarging = 0;
+ private int oneValueMargingRight = 5;
+ private int btnWidth = 16;
+ private int btnHeight = 16;
+ private int btnHorizontalIndent = 3;
+
+ public EditableMultiStringProperty(Composite parent, int style, Node node, String propertyName, List<String> values,
+ String[] possibleValues, String addValueMsg, SelectionListener removeValueSelectionListener)
+ throws RepositoryException {
+ super(parent, style, node, true);
+
+ this.propertyName = propertyName;
+ this.values = values;
+// this.possibleValues = new String[] { "Un", "Deux", "Trois" };
+ this.message = addValueMsg;
+ this.canEdit = removeValueSelectionListener != null;
+ this.removeValueSL = removeValueSelectionListener;
+ }
+
+ public List<String> getValues() {
+ return values;
+ }
+
+ public void setValues(List<String> values) {
+ this.values = values;
+ }
+
+ // Row layout items do not need explicit layout data
+ protected void setControlLayoutData(Control control) {
+ }
+
+ /** To be overridden */
+ protected void setContainerLayoutData(Composite composite) {
+ composite.setLayoutData(CmsSwtUtils.fillWidth());
+ }
+
+ @Override
+ public Control getControl() {
+ return super.getControl();
+ }
+
+ @Override
+ protected Control createControl(Composite box, String style) {
+ Composite row = new Composite(box, SWT.NO_FOCUS);
+ row.setLayoutData(EclipseUiUtils.fillAll());
+
+ RowLayout rl = new RowLayout(SWT.HORIZONTAL);
+ rl.wrap = true;
+ rl.spacing = rowSpacing;
+ rl.marginRight = rl.marginLeft = rl.marginBottom = rl.marginTop = rowMarging;
+ row.setLayout(rl);
+
+ if (values != null) {
+ for (final String value : values) {
+ if (canEdit)
+ createRemovableValue(row, SWT.SINGLE, value);
+ else
+ createValueLabel(row, SWT.SINGLE, value);
+ }
+ }
+
+ if (!canEdit)
+ return row;
+ else if (isEditing())
+ return createText(row, style);
+ else
+ return createLabel(row, style);
+ }
+
+ /**
+ * Override to provide specific layout for the existing values, typically adding
+ * a pound (#) char for tags or anchor info for browsable links. We assume the
+ * parent composite already has a layout and it is the caller responsibility to
+ * apply corresponding layout data
+ */
+ protected Label createValueLabel(Composite parent, int style, String value) {
+ Label label = new Label(parent, style);
+ label.setText("#" + value);
+ CmsSwtUtils.markup(label);
+ CmsSwtUtils.style(label, FormStyle.propertyText.style());
+ return label;
+ }
+
+ private Composite createRemovableValue(Composite parent, int style, String value) {
+ Composite valCmp = new Composite(parent, SWT.NO_FOCUS);
+ GridLayout gl = EclipseUiUtils.noSpaceGridLayout(new GridLayout(2, false));
+ gl.marginRight = oneValueMargingRight;
+ valCmp.setLayout(gl);
+
+ createValueLabel(valCmp, SWT.WRAP, value);
+
+ Button deleteBtn = new Button(valCmp, SWT.FLAT);
+ deleteBtn.setData(FormConstants.LINKED_VALUE, value);
+ deleteBtn.addSelectionListener(removeValueSL);
+ CmsSwtUtils.style(deleteBtn, FormStyle.delete.style() + FormStyle.BUTTON_SUFFIX);
+ GridData gd = new GridData();
+ gd.heightHint = btnHeight;
+ gd.widthHint = btnWidth;
+ gd.horizontalIndent = btnHorizontalIndent;
+ deleteBtn.setLayoutData(gd);
+
+ return valCmp;
+ }
+
+ protected Text createText(Composite box, String style) {
+ final Text text = new Text(box, getStyle());
+ // The "add new value" text is not meant to change, so we can set it on
+ // creation
+ text.setMessage(message);
+ CmsSwtUtils.style(text, style);
+ text.setFocus();
+
+ text.addTraverseListener(new TraverseListener() {
+ private static final long serialVersionUID = 1L;
+
+ public void keyTraversed(TraverseEvent e) {
+ if (e.keyCode == SWT.CR) {
+ addValue(text);
+ e.doit = false;
+ }
+ }
+ });
+
+ // The OK button does not work with the focusOut listener
+ // because focus out is called before the OK button is pressed
+
+ // // we must call layout() now so that the row data can compute the
+ // height
+ // // of the other controls.
+ // text.getParent().layout();
+ // int height = text.getSize().y;
+ //
+ // Button okBtn = new Button(box, SWT.BORDER | SWT.PUSH | SWT.BOTTOM);
+ // okBtn.setText("OK");
+ // RowData rd = new RowData(SWT.DEFAULT, height - 2);
+ // okBtn.setLayoutData(rd);
+ //
+ // okBtn.addSelectionListener(new SelectionAdapter() {
+ // private static final long serialVersionUID = 2780819012423622369L;
+ //
+ // @Override
+ // public void widgetSelected(SelectionEvent e) {
+ // addValue(text);
+ // }
+ // });
+
+ return text;
+ }
+
+ /** Performs the real addition, overwrite to make further sanity checks */
+ protected void addValue(Text text) {
+ String value = text.getText();
+ String errMsg = null;
+
+ if (EclipseUiUtils.isEmpty(value))
+ return;
+
+ if (values.contains(value))
+ errMsg = "Dupplicated value: " + value + ", please correct and try again";
+ if (errMsg != null)
+ MessageDialog.openError(this.getShell(), "Addition not allowed", errMsg);
+ else {
+ values.add(value);
+ Composite newCmp = createRemovableValue(text.getParent(), SWT.SINGLE, value);
+ newCmp.moveAbove(text);
+ text.setText("");
+ newCmp.getParent().layout();
+ }
+ }
+
+ protected Label createLabel(Composite box, String style) {
+ if (canEdit) {
+ Label lbl = new Label(box, getStyle());
+ lbl.setText(message);
+ CmsSwtUtils.style(lbl, style);
+ CmsSwtUtils.markup(lbl);
+ if (mouseListener != null)
+ lbl.addMouseListener(mouseListener);
+ return lbl;
+ }
+ return null;
+ }
+
+ protected void clear(boolean deep) {
+ Control child = getControl();
+ if (deep)
+ super.clear(deep);
+ else {
+ child.getParent().dispose();
+ }
+ }
+
+ public void setText(String text) {
+ Control child = getControl();
+ if (child instanceof Label) {
+ Label lbl = (Label) child;
+ if (canEdit)
+ lbl.setText(text);
+ else
+ lbl.setText("");
+ } else if (child instanceof Text) {
+ Text txt = (Text) child;
+ txt.setText(text);
+ }
+ }
+
+ public synchronized void startEditing() {
+ CmsSwtUtils.style(getControl(), FormStyle.propertyText.style());
+// getControl().setData(STYLE, FormStyle.propertyText.style());
+ super.startEditing();
+ }
+
+ public synchronized void stopEditing() {
+ CmsSwtUtils.style(getControl(), FormStyle.propertyMessage.style());
+// getControl().setData(STYLE, FormStyle.propertyMessage.style());
+ super.stopEditing();
+ }
+
+ public String getPropertyName() {
+ return propertyName;
+ }
+}
\ No newline at end of file
--- /dev/null
+package org.argeo.cms.ui.forms;
+
+import java.text.DateFormat;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.cms.swt.SwtEditablePart;
+import org.argeo.cms.ui.widgets.StyledControl;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.ShellAdapter;
+import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.DateTime;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+/** CMS form part to display and edit a date */
+public class EditablePropertyDate extends StyledControl implements SwtEditablePart {
+ private static final long serialVersionUID = 2500215515778162468L;
+
+ // Context
+ private String propertyName;
+ private String message;
+ private DateFormat dateFormat;
+
+ // UI Objects
+ private Text dateTxt;
+ private Button openCalBtn;
+
+ // TODO manage within the CSS
+ private int fieldBtnSpacing = 5;
+
+ /**
+ *
+ * @param parent
+ * @param style
+ * @param node
+ * @param propertyName
+ * @param message
+ * @param dateFormat provide a {@link DateFormat} as contract to be able to
+ * read/write dates as strings
+ * @throws RepositoryException
+ */
+ public EditablePropertyDate(Composite parent, int style, Node node, String propertyName, String message,
+ DateFormat dateFormat) throws RepositoryException {
+ super(parent, style, node, false);
+
+ this.propertyName = propertyName;
+ this.message = message;
+ this.dateFormat = dateFormat;
+
+ if (node.hasProperty(propertyName)) {
+ this.setStyle(FormStyle.propertyText.style());
+ this.setText(dateFormat.format(node.getProperty(propertyName).getDate().getTime()));
+ } else {
+ this.setStyle(FormStyle.propertyMessage.style());
+ this.setText(message);
+ }
+ }
+
+ public void setText(String text) {
+ Control child = getControl();
+ if (child instanceof Label) {
+ Label lbl = (Label) child;
+ if (EclipseUiUtils.isEmpty(text))
+ lbl.setText(message);
+ else
+ lbl.setText(text);
+ } else if (child instanceof Text) {
+ Text txt = (Text) child;
+ if (EclipseUiUtils.isEmpty(text)) {
+ txt.setText("");
+ } else
+ txt.setText(text);
+ }
+ }
+
+ public synchronized void startEditing() {
+ // if (dateTxt != null && !dateTxt.isDisposed())
+ CmsSwtUtils.style(getControl(), FormStyle.propertyText);
+// getControl().setData(STYLE, FormStyle.propertyText.style());
+ super.startEditing();
+ }
+
+ public synchronized void stopEditing() {
+ if (EclipseUiUtils.isEmpty(dateTxt.getText()))
+ CmsSwtUtils.style(getControl(), FormStyle.propertyMessage);
+// getControl().setData(STYLE, FormStyle.propertyMessage.style());
+ else
+ CmsSwtUtils.style(getControl(), FormStyle.propertyText);
+// getControl().setData(STYLE, FormStyle.propertyText.style());
+ super.stopEditing();
+ }
+
+ public String getPropertyName() {
+ return propertyName;
+ }
+
+ @Override
+ protected Control createControl(Composite box, String style) {
+ if (isEditing()) {
+ return createCustomEditableControl(box, style);
+ } else
+ return createLabel(box, style);
+ }
+
+ protected Label createLabel(Composite box, String style) {
+ Label lbl = new Label(box, getStyle() | SWT.WRAP);
+ lbl.setLayoutData(CmsSwtUtils.fillWidth());
+ CmsSwtUtils.style(lbl, style);
+ CmsSwtUtils.markup(lbl);
+ if (mouseListener != null)
+ lbl.addMouseListener(mouseListener);
+ return lbl;
+ }
+
+ private Control createCustomEditableControl(Composite box, String style) {
+ box.setLayoutData(CmsSwtUtils.fillWidth());
+ Composite dateComposite = new Composite(box, SWT.NONE);
+ GridLayout gl = EclipseUiUtils.noSpaceGridLayout(new GridLayout(2, false));
+ gl.horizontalSpacing = fieldBtnSpacing;
+ dateComposite.setLayout(gl);
+ dateTxt = new Text(dateComposite, SWT.BORDER);
+ CmsSwtUtils.style(dateTxt, style);
+ dateTxt.setLayoutData(new GridData(120, SWT.DEFAULT));
+ dateTxt.setToolTipText(
+ "Enter a date with form \"" + FormUtils.DEFAULT_SHORT_DATE_FORMAT + "\" or use the calendar");
+ openCalBtn = new Button(dateComposite, SWT.FLAT);
+ CmsSwtUtils.style(openCalBtn, FormStyle.calendar.style() + FormStyle.BUTTON_SUFFIX);
+ GridData gd = new GridData(SWT.CENTER, SWT.CENTER, false, false);
+ gd.heightHint = 17;
+ openCalBtn.setLayoutData(gd);
+ // openCalBtn.setImage(PeopleRapImages.CALENDAR_BTN);
+
+ openCalBtn.addSelectionListener(new SelectionAdapter() {
+ private static final long serialVersionUID = 1L;
+
+ public void widgetSelected(SelectionEvent event) {
+ CalendarPopup popup = new CalendarPopup(dateTxt);
+ popup.open();
+ }
+ });
+
+ // dateTxt.addFocusListener(new FocusListener() {
+ // private static final long serialVersionUID = 1L;
+ //
+ // @Override
+ // public void focusLost(FocusEvent event) {
+ // String newVal = dateTxt.getText();
+ // // Enable reset of the field
+ // if (FormUtils.notNull(newVal))
+ // calendar = null;
+ // else {
+ // try {
+ // Calendar newCal = parseDate(newVal);
+ // // DateText.this.setText(newCal);
+ // calendar = newCal;
+ // } catch (ParseException pe) {
+ // // Silent. Manage error popup?
+ // if (calendar != null)
+ // EditablePropertyDate.this.setText(calendar);
+ // }
+ // }
+ // }
+ //
+ // @Override
+ // public void focusGained(FocusEvent event) {
+ // }
+ // });
+ return dateTxt;
+ }
+
+ protected void clear(boolean deep) {
+ Control child = getControl();
+ if (deep || child instanceof Label)
+ super.clear(deep);
+ else {
+ child.getParent().dispose();
+ }
+ }
+
+ /** Enable setting a custom tooltip on the underlying text */
+ @Deprecated
+ public void setToolTipText(String toolTipText) {
+ dateTxt.setToolTipText(toolTipText);
+ }
+
+ @Deprecated
+ /** Enable setting a custom message on the underlying text */
+ public void setMessage(String message) {
+ dateTxt.setMessage(message);
+ }
+
+ @Deprecated
+ public void setText(Calendar cal) {
+ String newValueStr = "";
+ if (cal != null)
+ newValueStr = dateFormat.format(cal.getTime());
+ if (!newValueStr.equals(dateTxt.getText()))
+ dateTxt.setText(newValueStr);
+ }
+
+ // UTILITIES TO MANAGE THE CALENDAR POPUP
+ // TODO manage the popup shell in a cleaner way
+ private class CalendarPopup extends Shell {
+ private static final long serialVersionUID = 1L;
+ private DateTime dateTimeCtl;
+
+ public CalendarPopup(Control source) {
+ super(source.getDisplay(), SWT.NO_TRIM | SWT.BORDER | SWT.ON_TOP);
+ populate();
+ // Add border and shadow style
+ CmsSwtUtils.markup(CalendarPopup.this);
+ CmsSwtUtils.style(CalendarPopup.this, FormStyle.popupCalendar.style());
+ pack();
+ layout();
+ setLocation(source.toDisplay((source.getLocation().x - 2), (source.getSize().y) + 3));
+
+ addShellListener(new ShellAdapter() {
+ private static final long serialVersionUID = 5178980294808435833L;
+
+ @Override
+ public void shellDeactivated(ShellEvent e) {
+ close();
+ dispose();
+ }
+ });
+ open();
+ }
+
+ private void setProperty() {
+ // Direct set does not seems to work. investigate
+ // cal.set(dateTimeCtl.getYear(), dateTimeCtl.getMonth(),
+ // dateTimeCtl.getDay(), 12, 0);
+ Calendar cal = new GregorianCalendar();
+ cal.set(Calendar.YEAR, dateTimeCtl.getYear());
+ cal.set(Calendar.MONTH, dateTimeCtl.getMonth());
+ cal.set(Calendar.DAY_OF_MONTH, dateTimeCtl.getDay());
+ String dateStr = dateFormat.format(cal.getTime());
+ dateTxt.setText(dateStr);
+ }
+
+ protected void populate() {
+ setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+ dateTimeCtl = new DateTime(this, SWT.CALENDAR);
+ dateTimeCtl.setLayoutData(EclipseUiUtils.fillAll());
+
+ Calendar calendar = FormUtils.parseDate(dateFormat, dateTxt.getText());
+
+ if (calendar != null)
+ dateTimeCtl.setDate(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH),
+ calendar.get(Calendar.DAY_OF_MONTH));
+
+ dateTimeCtl.addSelectionListener(new SelectionAdapter() {
+ private static final long serialVersionUID = -8414377364434281112L;
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ setProperty();
+ }
+ });
+
+ dateTimeCtl.addMouseListener(new MouseListener() {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void mouseUp(MouseEvent e) {
+ }
+
+ @Override
+ public void mouseDown(MouseEvent e) {
+ }
+
+ @Override
+ public void mouseDoubleClick(MouseEvent e) {
+ setProperty();
+ close();
+ dispose();
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+package org.argeo.cms.ui.forms;
+
+import static org.argeo.cms.ui.forms.FormStyle.propertyMessage;
+import static org.argeo.cms.ui.forms.FormStyle.propertyText;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.cms.swt.SwtEditablePart;
+import org.argeo.cms.ui.widgets.EditableText;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+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 String in a CMS context */
+public class EditablePropertyString extends EditableText implements SwtEditablePart {
+ private static final long serialVersionUID = 5055000749992803591L;
+
+ private String propertyName;
+ private String message;
+
+ // encode the '&' character in rap
+ private final static String AMPERSAND = "&";
+ private final static String AMPERSAND_REGEX = "&(?![#a-zA-Z0-9]+;)";
+
+ public EditablePropertyString(Composite parent, int style, Node node, String propertyName, String message)
+ throws RepositoryException {
+ super(parent, style, node, true);
+ //setUseTextAsLabel(true);
+ this.propertyName = propertyName;
+ this.message = message;
+
+ if (node.hasProperty(propertyName)) {
+ this.setStyle(propertyText.style());
+ this.setText(node.getProperty(propertyName).getString());
+ } else {
+ this.setStyle(propertyMessage.style());
+ this.setText(message + " ");
+ }
+ }
+
+ public void setText(String text) {
+ Control child = getControl();
+ if (child instanceof Label) {
+ Label lbl = (Label) child;
+ if (EclipseUiUtils.isEmpty(text))
+ lbl.setText(message + " ");
+ else
+ // TODO enhance this
+ lbl.setText(text.replaceAll(AMPERSAND_REGEX, AMPERSAND));
+ } else if (child instanceof Text) {
+ Text txt = (Text) child;
+ if (EclipseUiUtils.isEmpty(text)) {
+ txt.setText("");
+ txt.setMessage(message + " ");
+ } else
+ txt.setText(text.replaceAll("<br/>", "\n"));
+ }
+ }
+
+ public synchronized void startEditing() {
+ CmsSwtUtils.style(getControl(), FormStyle.propertyText);
+ super.startEditing();
+ }
+
+ public synchronized void stopEditing() {
+ if (EclipseUiUtils.isEmpty(((Text) getControl()).getText()))
+ CmsSwtUtils.style(getControl(), FormStyle.propertyMessage);
+ else
+ CmsSwtUtils.style(getControl(), FormStyle.propertyText);
+ super.stopEditing();
+ }
+
+ public String getPropertyName() {
+ return propertyName;
+ }
+}
\ No newline at end of file
--- /dev/null
+package org.argeo.cms.ui.forms;
+
+/** Constants used in the various CMS Forms */
+public interface FormConstants {
+ // DATAKEYS
+ public final static String LINKED_VALUE = "LinkedValue";
+}
--- /dev/null
+package org.argeo.cms.ui.forms;
+
+import java.util.Observable;
+import java.util.Observer;
+
+import javax.jcr.Node;
+
+import org.argeo.api.cms.ux.CmsEditable;
+import org.argeo.cms.swt.CmsSwtUtils;
+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;
+
+/** Add life cycle management abilities to an editable form page */
+public class FormEditorHeader implements SelectionListener, Observer {
+ private static final long serialVersionUID = 7392898696542484282L;
+
+ // private final Node context;
+ private final CmsEditable cmsEditable;
+ private Button publishBtn;
+
+ // Should we provide here the ability to switch from read only to edition
+ // mode?
+ // private Button editBtn;
+ // private boolean readOnly;
+
+ // TODO add information about the current node status, typically if it is
+ // dirty or not
+
+ private Composite parent;
+ private Composite display;
+ private Object layoutData;
+
+ public FormEditorHeader(Composite parent, int style, Node context,
+ CmsEditable cmsEditable) {
+ this.cmsEditable = cmsEditable;
+ this.parent = parent;
+ // readOnly = SWT.READ_ONLY == (style & SWT.READ_ONLY);
+ // this.context = context;
+ if (this.cmsEditable instanceof Observable)
+ ((Observable) this.cmsEditable).addObserver(this);
+ refresh();
+ }
+
+ public void setLayoutData(Object layoutData) {
+ this.layoutData = layoutData;
+ if (display != null && !display.isDisposed())
+ display.setLayoutData(layoutData);
+ }
+
+ protected void refresh() {
+ if (display != null && !display.isDisposed())
+ display.dispose();
+
+ display = new Composite(parent, SWT.NONE);
+ display.setLayoutData(layoutData);
+
+ CmsSwtUtils.style(display, FormStyle.header.style());
+ display.setBackgroundMode(SWT.INHERIT_FORCE);
+
+ display.setLayout(CmsSwtUtils.noSpaceGridLayout());
+
+ publishBtn = createSimpleBtn(display, getPublishButtonLabel());
+ display.moveAbove(null);
+ parent.layout();
+ }
+
+ private Button createSimpleBtn(Composite parent, String label) {
+ Button button = new Button(parent, SWT.FLAT | SWT.PUSH);
+ button.setText(label);
+ CmsSwtUtils.style(button, FormStyle.header.style());
+ button.addSelectionListener(this);
+ return button;
+ }
+
+ private String getPublishButtonLabel() {
+ // Rather check if the current node differs from what has been
+ // previously committed
+ // For the time being, we always reach here, the underlying CmsEditable
+ // is always editing.
+ if (cmsEditable.isEditing())
+ return " Publish ";
+ else
+ return " Edit ";
+ }
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (e.getSource() == publishBtn) {
+ // For the time being, the underlying CmsEditable
+ // is always editing when we reach this point
+ if (cmsEditable.isEditing()) {
+ // we always leave the node in a check outed state
+ cmsEditable.stopEditing();
+ cmsEditable.startEditing();
+ } else {
+ cmsEditable.startEditing();
+ }
+ }
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ }
+
+ @Override
+ public void update(Observable o, Object arg) {
+ if (o == cmsEditable) {
+ refresh();
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+package org.argeo.cms.ui.forms;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.ValueFormatException;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.api.cms.ux.Cms2DSize;
+import org.argeo.api.cms.ux.CmsEditable;
+import org.argeo.api.cms.ux.CmsImageManager;
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.cms.swt.SwtEditablePart;
+import org.argeo.cms.ui.viewers.AbstractPageViewer;
+import org.argeo.cms.ui.viewers.Section;
+import org.argeo.cms.ui.viewers.SectionPart;
+import org.argeo.cms.ui.widgets.EditableImage;
+import org.argeo.cms.ui.widgets.Img;
+import org.argeo.cms.ui.widgets.StyledControl;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.jcr.JcrException;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.rap.fileupload.FileDetails;
+import org.eclipse.rap.fileupload.FileUploadEvent;
+import org.eclipse.rap.fileupload.FileUploadHandler;
+import org.eclipse.rap.fileupload.FileUploadListener;
+import org.eclipse.rap.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.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+/** Manage life cycle of a form page that is linked to a given node */
+public class FormPageViewer extends AbstractPageViewer {
+ private final static CmsLog log = CmsLog.getLog(FormPageViewer.class);
+ private static final long serialVersionUID = 5277789504209413500L;
+
+ private final Section mainSection;
+
+ // TODO manage within the CSS
+ private Integer labelColWidth = null;
+ private int rowLayoutHSpacing = 8;
+
+ // Context cached in the viewer
+ // The reference to translate from text to calendar and reverse
+ private DateFormat dateFormat = new SimpleDateFormat(FormUtils.DEFAULT_SHORT_DATE_FORMAT);
+ private CmsImageManager<Control, Node> imageManager;
+ private FileUploadListener fileUploadListener;
+
+ public FormPageViewer(Section mainSection, int style, CmsEditable cmsEditable) throws RepositoryException {
+ super(mainSection, style, cmsEditable);
+ this.mainSection = mainSection;
+
+ if (getCmsEditable().canEdit()) {
+ fileUploadListener = new FUL();
+ }
+ }
+
+ @Override
+ protected void prepare(SwtEditablePart part, Object caretPosition) {
+ if (part instanceof Img) {
+ ((Img) part).setFileUploadListener(fileUploadListener);
+ }
+ }
+
+ /** To be overridden.Save the edited part. */
+ protected void save(SwtEditablePart part) throws RepositoryException {
+ Node node = null;
+ if (part instanceof EditableMultiStringProperty) {
+ EditableMultiStringProperty ept = (EditableMultiStringProperty) part;
+ // SWT : View
+ List<String> values = ept.getValues();
+ // JCR : Model
+ node = ept.getNode();
+ String propName = ept.getPropertyName();
+ if (values.isEmpty()) {
+ if (node.hasProperty(propName))
+ node.getProperty(propName).remove();
+ } else {
+ node.setProperty(propName, values.toArray(new String[0]));
+ }
+ // => Viewer : Controller
+ } else if (part instanceof EditablePropertyString) {
+ EditablePropertyString ept = (EditablePropertyString) part;
+ // SWT : View
+ String txt = ((Text) ept.getControl()).getText();
+ // JCR : Model
+ node = ept.getNode();
+ String propName = ept.getPropertyName();
+ if (EclipseUiUtils.isEmpty(txt)) {
+ if (node.hasProperty(propName))
+ node.getProperty(propName).remove();
+ } else {
+ setPropertySilently(node, propName, txt);
+ // node.setProperty(propName, txt);
+ }
+ // node.getSession().save();
+ // => Viewer : Controller
+ } else if (part instanceof EditablePropertyDate) {
+ EditablePropertyDate ept = (EditablePropertyDate) part;
+ Calendar cal = FormUtils.parseDate(dateFormat, ((Text) ept.getControl()).getText());
+ node = ept.getNode();
+ String propName = ept.getPropertyName();
+ if (cal == null) {
+ if (node.hasProperty(propName))
+ node.getProperty(propName).remove();
+ } else {
+ node.setProperty(propName, cal);
+ }
+ // node.getSession().save();
+ // => Viewer : Controller
+ }
+ // TODO: make this configurable, sometimes we do not want to save the
+ // current session at this stage
+ if (node != null && node.getSession().hasPendingChanges()) {
+ JcrUtils.updateLastModified(node, true);
+ node.getSession().save();
+ }
+ }
+
+ @Override
+ protected void updateContent(SwtEditablePart part) throws RepositoryException {
+ if (part instanceof EditableMultiStringProperty) {
+ EditableMultiStringProperty ept = (EditableMultiStringProperty) part;
+ // SWT : View
+ Node node = ept.getNode();
+ String propName = ept.getPropertyName();
+ List<String> valStrings = new ArrayList<String>();
+ if (node.hasProperty(propName)) {
+ Value[] values = node.getProperty(propName).getValues();
+ for (Value val : values)
+ valStrings.add(val.getString());
+ }
+ ept.setValues(valStrings);
+ } else if (part instanceof EditablePropertyString) {
+ // || part instanceof EditableLink
+ EditablePropertyString ept = (EditablePropertyString) part;
+ // JCR : Model
+ Node node = ept.getNode();
+ String propName = ept.getPropertyName();
+ if (node.hasProperty(propName)) {
+ String value = node.getProperty(propName).getString();
+ ept.setText(value);
+ } else
+ ept.setText("");
+ // => Viewer : Controller
+ } else if (part instanceof EditablePropertyDate) {
+ EditablePropertyDate ept = (EditablePropertyDate) part;
+ // JCR : Model
+ Node node = ept.getNode();
+ String propName = ept.getPropertyName();
+ if (node.hasProperty(propName))
+ ept.setText(dateFormat.format(node.getProperty(propName).getDate().getTime()));
+ else
+ ept.setText("");
+ } else if (part instanceof SectionPart) {
+ SectionPart sectionPart = (SectionPart) part;
+ Node partNode = sectionPart.getNode();
+ // use control AFTER setting style, since it may have been reset
+ if (part instanceof EditableImage) {
+ EditableImage editableImage = (EditableImage) part;
+ imageManager().load(partNode, part.getControl(), editableImage.getPreferredImageSize());
+ }
+ }
+ }
+
+ // FILE UPLOAD LISTENER
+ protected class FUL implements FileUploadListener {
+
+ public FUL() {
+ }
+
+ public void uploadProgress(FileUploadEvent event) {
+ // TODO Monitor upload progress
+ }
+
+ public void uploadFailed(FileUploadEvent event) {
+ throw new IllegalStateException("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();
+ }
+ }
+
+ // FOCUS OUT LISTENER
+ protected FocusListener createFocusListener() {
+ return new FocusOutListener();
+ }
+
+ private class FocusOutListener implements FocusListener {
+ private static final long serialVersionUID = -6069205786732354186L;
+
+ @Override
+ public void focusLost(FocusEvent event) {
+ saveEdit();
+ }
+
+ @Override
+ public void focusGained(FocusEvent event) {
+ // does nothing;
+ }
+ }
+
+ // 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;
+ SwtEditablePart 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();
+ SwtEditablePart 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));
+ }
+ }
+ }
+
+ protected synchronized void upload(SwtEditablePart part) {
+ if (part instanceof SectionPart) {
+ if (part instanceof Img) {
+ if (getEdited() == part)
+ return;
+ edit(part, null);
+ layout(part.getControl());
+ }
+ }
+ }
+ }
+
+ @Override
+ public Control getControl() {
+ return mainSection;
+ }
+
+ protected CmsImageManager<Control, Node> imageManager() {
+ if (imageManager == null)
+ imageManager = (CmsImageManager<Control, Node>) CmsSwtUtils.getCmsView(mainSection).getImageManager();
+ return imageManager;
+ }
+
+ // LOCAL UI HELPERS
+ protected Section createSectionIfNeeded(Composite body, Node node) throws RepositoryException {
+ Section section = null;
+ if (node != null) {
+ section = new Section(body, SWT.NO_FOCUS, node);
+ section.setLayoutData(CmsSwtUtils.fillWidth());
+ section.setLayout(CmsSwtUtils.noSpaceGridLayout());
+ }
+ return section;
+ }
+
+ protected void createSimpleLT(Composite bodyRow, Node node, String propName, String label, String msg)
+ throws RepositoryException {
+ if (getCmsEditable().canEdit() || node.hasProperty(propName)) {
+ createPropertyLbl(bodyRow, label);
+ EditablePropertyString eps = new EditablePropertyString(bodyRow, SWT.WRAP | SWT.LEFT, node, propName, msg);
+ eps.setMouseListener(getMouseListener());
+ eps.setFocusListener(getFocusListener());
+ eps.setLayoutData(CmsSwtUtils.fillWidth());
+ }
+ }
+
+ protected void createMultiStringLT(Composite bodyRow, Node node, String propName, String label, String msg)
+ throws RepositoryException {
+ boolean canEdit = getCmsEditable().canEdit();
+ if (canEdit || node.hasProperty(propName)) {
+ createPropertyLbl(bodyRow, label);
+
+ List<String> valueStrings = new ArrayList<String>();
+
+ if (node.hasProperty(propName)) {
+ Value[] values = node.getProperty(propName).getValues();
+ for (Value value : values)
+ valueStrings.add(value.getString());
+ }
+
+ // TODO use a drop down to display possible values to the end user
+ EditableMultiStringProperty emsp = new EditableMultiStringProperty(bodyRow, SWT.SINGLE | SWT.LEAD, node,
+ propName, valueStrings, new String[] { "Implement this" }, msg,
+ canEdit ? getRemoveValueSelListener() : null);
+ addListeners(emsp);
+ // emsp.setMouseListener(getMouseListener());
+ emsp.setStyle(FormStyle.propertyMessage.style());
+ emsp.setLayoutData(CmsSwtUtils.fillWidth());
+ }
+ }
+
+ protected Label createPropertyLbl(Composite parent, String value) {
+ return createPropertyLbl(parent, value, SWT.NONE);
+ }
+
+ protected Label createPropertyLbl(Composite parent, String value, int vAlign) {
+ // boolean isSmall = CmsView.getCmsView(parent).getUxContext().isSmall();
+ Label label = new Label(parent, SWT.LEAD | SWT.WRAP);
+ label.setText(value + " ");
+ CmsSwtUtils.style(label, FormStyle.propertyLabel.style());
+ GridData gd = new GridData(SWT.LEAD, vAlign, false, false);
+ if (labelColWidth != null)
+ gd.widthHint = labelColWidth;
+ label.setLayoutData(gd);
+ return label;
+ }
+
+ protected Label newStyledLabel(Composite parent, String style, String value) {
+ Label label = new Label(parent, SWT.NONE);
+ label.setText(value);
+ CmsSwtUtils.style(label, style);
+ return label;
+ }
+
+ protected Composite createRowLayoutComposite(Composite parent) throws RepositoryException {
+ Composite bodyRow = new Composite(parent, SWT.NO_FOCUS);
+ bodyRow.setLayoutData(CmsSwtUtils.fillWidth());
+ RowLayout rl = new RowLayout(SWT.WRAP);
+ rl.type = SWT.HORIZONTAL;
+ rl.spacing = rowLayoutHSpacing;
+ rl.marginHeight = rl.marginWidth = 0;
+ rl.marginTop = rl.marginBottom = rl.marginLeft = rl.marginRight = 0;
+ bodyRow.setLayout(rl);
+ return bodyRow;
+ }
+
+ protected Composite createAddImgComposite(final Section section, Composite parent, final Node parentNode)
+ throws RepositoryException {
+
+ Composite body = new Composite(parent, SWT.NO_FOCUS);
+ body.setLayout(new GridLayout());
+
+ FormFileUploadReceiver receiver = new FormFileUploadReceiver(section, parentNode, null);
+ final FileUploadHandler currentUploadHandler = new FileUploadHandler(receiver);
+ if (fileUploadListener != null)
+ currentUploadHandler.addUploadListener(fileUploadListener);
+
+ // Button creation
+ final FileUpload fileUpload = new FileUpload(body, SWT.BORDER);
+ fileUpload.setText("Import an image");
+ fileUpload.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true));
+ fileUpload.addSelectionListener(new SelectionAdapter() {
+ private static final long serialVersionUID = 4869523412991968759L;
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ ServerPushSession pushSession = new ServerPushSession();
+ pushSession.start();
+ String uploadURL = currentUploadHandler.getUploadUrl();
+ fileUpload.submit(uploadURL);
+ }
+ });
+
+ return body;
+ }
+
+ protected class FormFileUploadReceiver extends FileUploadReceiver {
+
+ private Node context;
+ private Section section;
+ private String name;
+
+ public FormFileUploadReceiver(Section section, Node context, String name) {
+ this.context = context;
+ this.section = section;
+ this.name = name;
+ }
+
+ @Override
+ public void receive(InputStream stream, FileDetails details) throws IOException {
+
+ if (name == null)
+ name = details.getFileName();
+
+ // TODO clean image name more carefully
+ String cleanedName = name.replaceAll("[^a-zA-Z0-9-.]", "");
+ // We add a unique prefix to workaround the cache issue: when
+ // deleting and re-adding a new image with same name, the end user
+ // browser will use the cache and the image will remain unchanged
+ // for a while
+ cleanedName = System.currentTimeMillis() % 100000 + "_" + cleanedName;
+
+ imageManager().uploadImage(context, context, cleanedName, stream, details.getContentType());
+ // TODO clean refresh strategy
+ section.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ FormPageViewer.this.refresh(section);
+ section.layout();
+ section.getParent().layout();
+ } catch (RepositoryException re) {
+ throw new JcrException("Unable to refresh " + "image section for " + context, re);
+ }
+ }
+ });
+ }
+ }
+
+ protected void addListeners(StyledControl control) {
+ control.setMouseListener(getMouseListener());
+ control.setFocusListener(getFocusListener());
+ }
+
+ protected Img createImgComposite(Composite parent, Node node, Point preferredSize) throws RepositoryException {
+ Img img = new Img(parent, SWT.NONE, node, new Cms2DSize(preferredSize.x, preferredSize.y)) {
+ private static final long serialVersionUID = 1297900641952417540L;
+
+ @Override
+ protected void setContainerLayoutData(Composite composite) {
+ composite.setLayoutData(CmsSwtUtils.grabWidth(SWT.CENTER, SWT.DEFAULT));
+ }
+
+ @Override
+ protected void setControlLayoutData(Control control) {
+ control.setLayoutData(CmsSwtUtils.grabWidth(SWT.CENTER, SWT.DEFAULT));
+ }
+ };
+ img.setLayoutData(CmsSwtUtils.grabWidth(SWT.CENTER, SWT.DEFAULT));
+ updateContent(img);
+ addListeners(img);
+ return img;
+ }
+
+ protected Composite addDeleteAbility(final Section section, final Node sessionNode, int topWeight,
+ int rightWeight) {
+ Composite comp = new Composite(section, SWT.NONE);
+ comp.setLayoutData(CmsSwtUtils.fillAll());
+ comp.setLayout(new FormLayout());
+
+ // The body to be populated
+ Composite body = new Composite(comp, SWT.NO_FOCUS);
+ body.setLayoutData(EclipseUiUtils.fillFormData());
+
+ if (getCmsEditable().canEdit()) {
+ // the delete button
+ Button deleteBtn = new Button(comp, SWT.FLAT);
+ CmsSwtUtils.style(deleteBtn, FormStyle.deleteOverlay.style());
+ FormData formData = new FormData();
+ formData.right = new FormAttachment(rightWeight, 0);
+ formData.top = new FormAttachment(topWeight, 0);
+ deleteBtn.setLayoutData(formData);
+ deleteBtn.moveAbove(body);
+
+ deleteBtn.addSelectionListener(new SelectionAdapter() {
+ private static final long serialVersionUID = 4304223543657238462L;
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ if (MessageDialog.openConfirm(section.getShell(), "Confirm deletion",
+ "Are you really you want to remove this?")) {
+ Session session;
+ try {
+ session = sessionNode.getSession();
+ Section parSection = section.getParentSection();
+ sessionNode.remove();
+ session.save();
+ refresh(parSection);
+ layout(parSection);
+ } catch (RepositoryException re) {
+ throw new JcrException("Unable to delete " + sessionNode, re);
+ }
+
+ }
+
+ }
+ });
+ }
+ return body;
+ }
+
+// // LOCAL HELPERS FOR NODE MANAGEMENT
+// private Node getOrCreateNode(Node parent, String nodeName, String nodeType) throws RepositoryException {
+// Node node = null;
+// if (getCmsEditable().canEdit() && !parent.hasNode(nodeName)) {
+// node = JcrUtils.mkdirs(parent, nodeName, nodeType);
+// parent.getSession().save();
+// }
+//
+// if (getCmsEditable().canEdit() || parent.hasNode(nodeName))
+// node = parent.getNode(nodeName);
+//
+// return node;
+// }
+
+ private SelectionListener getRemoveValueSelListener() {
+ return new SelectionAdapter() {
+ private static final long serialVersionUID = 9022259089907445195L;
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ Object source = e.getSource();
+ if (source instanceof Button) {
+ Button btn = (Button) source;
+ Object obj = btn.getData(FormConstants.LINKED_VALUE);
+ SwtEditablePart ep = findDataParent(btn);
+ if (ep != null && ep instanceof EditableMultiStringProperty) {
+ EditableMultiStringProperty emsp = (EditableMultiStringProperty) ep;
+ List<String> values = emsp.getValues();
+ if (values.contains(obj)) {
+ values.remove(values.indexOf(obj));
+ emsp.setValues(values);
+ try {
+ save(emsp);
+ // TODO workaround to force refresh
+ edit(emsp, 0);
+ cancelEdit();
+ } catch (RepositoryException e1) {
+ throw new JcrException("Unable to remove value " + obj, e1);
+ }
+ layout(emsp);
+ }
+ }
+ }
+ }
+ };
+ }
+
+ protected void setPropertySilently(Node node, String propName, String value) throws RepositoryException {
+ try {
+ // TODO Clean this:
+ // Format strings to replace \n
+ value = value.replaceAll("\n", "<br/>");
+ // Do not make the update if validation fails
+ try {
+ MarkupValidatorCopy.getInstance().validate(value);
+ } catch (Exception e) {
+ log.warn("Cannot set [" + value + "] on prop " + propName + "of " + node
+ + ", String cannot be validated - " + e.getMessage());
+ return;
+ }
+ // TODO check if the newly created property is of the correct type,
+ // otherwise the property will be silently created with a STRING
+ // property type.
+ node.setProperty(propName, value);
+ } catch (ValueFormatException vfe) {
+ log.warn("Cannot set [" + value + "] on prop " + propName + "of " + node + " - " + vfe.getMessage());
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+package org.argeo.cms.ui.forms;
+
+import org.argeo.api.cms.ux.CmsStyle;
+
+/** Syles used */
+public enum FormStyle implements CmsStyle {
+ // Main
+ form, title,
+ // main part
+ header, headerBtn, headerCombo, section, sectionHeader,
+ // Property fields
+ propertyLabel, propertyText, propertyMessage, errorMessage,
+ // Date
+ popupCalendar,
+ // Buttons
+ starred, unstarred, starOverlay, editOverlay, deleteOverlay, updateOverlay, deleteOverlaySmall, calendar, delete,
+ // Contacts
+ email, address, phone, website,
+ // Social Media
+ facebook, twitter, linkedIn, instagram;
+
+ @Override
+ public String getClassPrefix() {
+ return "argeo-form";
+ }
+
+ // TODO clean button style management
+ public final static String BUTTON_SUFFIX = "_btn";
+}
--- /dev/null
+package org.argeo.cms.ui.forms;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.api.cms.ux.CmsView;
+import org.argeo.cms.swt.CmsException;
+import org.argeo.cms.ui.util.CmsUiUtils;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.jface.fieldassist.ControlDecoration;
+import org.eclipse.jface.fieldassist.FieldDecorationRegistry;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+/** Utilitary methods to ease implementation of CMS forms */
+public class FormUtils {
+ private final static CmsLog log = CmsLog.getLog(FormUtils.class);
+
+ public final static String DEFAULT_SHORT_DATE_FORMAT = "dd/MM/yyyy";
+
+ /** Best effort to convert a String to a calendar. Fails silently */
+ public static Calendar parseDate(DateFormat dateFormat, String calStr) {
+ Calendar cal = null;
+ if (EclipseUiUtils.notEmpty(calStr)) {
+ try {
+ Date date = dateFormat.parse(calStr);
+ cal = new GregorianCalendar();
+ cal.setTime(date);
+ } catch (ParseException pe) {
+ // Silent
+ log.warn("Unable to parse date: " + calStr + " - msg: "
+ + pe.getMessage());
+ }
+ }
+ return cal;
+ }
+
+ /** Add a double click listener on tables that display a JCR node list */
+ public static void addCanonicalDoubleClickListener(final TableViewer v) {
+ v.addDoubleClickListener(new IDoubleClickListener() {
+
+ @Override
+ public void doubleClick(DoubleClickEvent event) {
+ CmsView cmsView = CmsUiUtils.getCmsView();
+ Node node = (Node) ((IStructuredSelection) event.getSelection())
+ .getFirstElement();
+ try {
+ cmsView.navigateTo(node.getPath());
+ } catch (RepositoryException e) {
+ throw new CmsException("Unable to get path for node "
+ + node + " before calling navigateTo(path)", e);
+ }
+ }
+ });
+ }
+
+ // MANAGE ERROR DECORATION
+
+ public static ControlDecoration addDecoration(final Text text) {
+ final ControlDecoration dynDecoration = new ControlDecoration(text,
+ SWT.LEFT);
+ Image icon = getDecorationImage(FieldDecorationRegistry.DEC_ERROR);
+ dynDecoration.setImage(icon);
+ dynDecoration.setMarginWidth(3);
+ dynDecoration.hide();
+ return dynDecoration;
+ }
+
+ public static void refreshDecoration(Text text, ControlDecoration deco,
+ boolean isValid, boolean clean) {
+ if (isValid || clean) {
+ text.setBackground(null);
+ deco.hide();
+ } else {
+ text.setBackground(new Color(text.getDisplay(), 250, 200, 150));
+ deco.show();
+ }
+ }
+
+ public static Image getDecorationImage(String image) {
+ FieldDecorationRegistry registry = FieldDecorationRegistry.getDefault();
+ return registry.getFieldDecoration(image).getImage();
+ }
+
+ public static void addCompulsoryDecoration(Label label) {
+ final ControlDecoration dynDecoration = new ControlDecoration(label,
+ SWT.RIGHT | SWT.TOP);
+ Image icon = getDecorationImage(FieldDecorationRegistry.DEC_REQUIRED);
+ dynDecoration.setImage(icon);
+ dynDecoration.setMarginWidth(3);
+ }
+
+ // TODO the read only generation of read only links for various contact type
+ // should be factorised in the cms Utils.
+ /**
+ * Creates the read-only HTML snippet to display in a label with styling
+ * enabled in order to provide a click-able phone number
+ */
+ public static String getPhoneLink(String value) {
+ return getPhoneLink(value, value);
+ }
+
+ /**
+ * Creates the read-only HTML snippet to display in a label with styling
+ * enabled in order to provide a click-able phone number
+ *
+ * @param value
+ * @param label
+ * a potentially distinct label
+ * @return the link
+ */
+ public static String getPhoneLink(String value, String label) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("<a href=\"tel:");
+ builder.append(value).append("\" target=\"_blank\" >").append(label)
+ .append("</a>");
+ return builder.toString();
+ }
+
+ /**
+ * Creates the read-only HTML snippet to display in a label with styling
+ * enabled in order to provide a click-able mail
+ */
+ public static String getMailLink(String value) {
+ return getMailLink(value, value);
+ }
+
+ /**
+ * Creates the read-only HTML snippet to display in a label with styling
+ * enabled in order to provide a click-able mail
+ *
+ * @param value
+ * @param label
+ * a potentially distinct label
+ * @return the link
+ */
+ public static String getMailLink(String value, String label) {
+ StringBuilder builder = new StringBuilder();
+ value = replaceAmpersand(value);
+ builder.append("<a href=\"mailto:");
+ builder.append(value).append("\" >").append(label).append("</a>");
+ return builder.toString();
+ }
+
+ /**
+ * Creates the read-only HTML snippet to display in a label with styling
+ * enabled in order to provide a click-able link
+ */
+ public static String getUrlLink(String value) {
+ return getUrlLink(value, value);
+ }
+
+ /**
+ * Creates the read-only HTML snippet to display in a label with styling
+ * enabled in order to provide a click-able link
+ */
+ public static String getUrlLink(String value, String label) {
+ StringBuilder builder = new StringBuilder();
+ value = replaceAmpersand(value);
+ label = replaceAmpersand(label);
+ if (!(value.startsWith("http://") || value.startsWith("https://")))
+ value = "http://" + value;
+ builder.append("<a href=\"");
+ builder.append(value + "\" target=\"_blank\" >" + label + "</a>");
+ return builder.toString();
+ }
+
+ private static String AMPERSAND = "&";
+
+ /**
+ * Cleans a String by replacing any '&' by its HTML encoding '&#38;' to
+ * avoid <code>SAXParseException</code> while rendering HTML with RWT
+ */
+ public static String replaceAmpersand(String value) {
+ value = value.replaceAll("&(?![#a-zA-Z0-9]+;)", AMPERSAND);
+ return value;
+ }
+
+ // Prevents instantiation
+ private FormUtils() {
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.forms;
+
+import java.io.StringReader;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.eclipse.rap.rwt.SingletonUtil;
+import org.eclipse.swt.widgets.Widget;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Copy of RAP v2.3 since it is in an internal package.
+ */
+class MarkupValidatorCopy {
+
+ // Used by Eclipse Scout project
+ public static final String MARKUP_VALIDATION_DISABLED = "org.eclipse.rap.rwt.markupValidationDisabled";
+
+ private static final String DTD = createDTD();
+ private static final Map<String, String[]> SUPPORTED_ELEMENTS = createSupportedElementsMap();
+ private final SAXParser saxParser;
+
+ public static MarkupValidatorCopy getInstance() {
+ return SingletonUtil.getSessionInstance(MarkupValidatorCopy.class);
+ }
+
+ public MarkupValidatorCopy() {
+ saxParser = createSAXParser();
+ }
+
+ public void validate(String text) {
+ StringBuilder markup = new StringBuilder();
+ markup.append(DTD);
+ markup.append("<html>");
+ markup.append(text);
+ markup.append("</html>");
+ InputSource inputSource = new InputSource(new StringReader(markup.toString()));
+ try {
+ saxParser.parse(inputSource, new MarkupHandler());
+ } catch (RuntimeException exception) {
+ throw exception;
+ } catch (Exception exception) {
+ throw new IllegalArgumentException("Failed to parse markup text", exception);
+ }
+ }
+
+ public static boolean isValidationDisabledFor(Widget widget) {
+ return Boolean.TRUE.equals(widget.getData(MARKUP_VALIDATION_DISABLED));
+ }
+
+ private static SAXParser createSAXParser() {
+ SAXParser result = null;
+ SAXParserFactory parserFactory = SAXParserFactory.newInstance();
+ try {
+ result = parserFactory.newSAXParser();
+ } catch (Exception exception) {
+ throw new RuntimeException("Failed to create SAX parser", exception);
+ }
+ return result;
+ }
+
+ private static String createDTD() {
+ StringBuilder result = new StringBuilder();
+ result.append("<!DOCTYPE html [");
+ result.append("<!ENTITY quot \""\">");
+ result.append("<!ENTITY amp \"&\">");
+ result.append("<!ENTITY apos \"'\">");
+ result.append("<!ENTITY lt \"<\">");
+ result.append("<!ENTITY gt \">\">");
+ result.append("<!ENTITY nbsp \" \">");
+ result.append("<!ENTITY ensp \" \">");
+ result.append("<!ENTITY emsp \" \">");
+ result.append("<!ENTITY ndash \"–\">");
+ result.append("<!ENTITY mdash \"—\">");
+ result.append("]>");
+ return result.toString();
+ }
+
+ private static Map<String, String[]> createSupportedElementsMap() {
+ Map<String, String[]> result = new HashMap<String, String[]>();
+ result.put("html", new String[0]);
+ result.put("br", new String[0]);
+ result.put("b", new String[] { "style" });
+ result.put("strong", new String[] { "style" });
+ result.put("i", new String[] { "style" });
+ result.put("em", new String[] { "style" });
+ result.put("sub", new String[] { "style" });
+ result.put("sup", new String[] { "style" });
+ result.put("big", new String[] { "style" });
+ result.put("small", new String[] { "style" });
+ result.put("del", new String[] { "style" });
+ result.put("ins", new String[] { "style" });
+ result.put("code", new String[] { "style" });
+ result.put("samp", new String[] { "style" });
+ result.put("kbd", new String[] { "style" });
+ result.put("var", new String[] { "style" });
+ result.put("cite", new String[] { "style" });
+ result.put("dfn", new String[] { "style" });
+ result.put("q", new String[] { "style" });
+ result.put("abbr", new String[] { "style", "title" });
+ result.put("span", new String[] { "style" });
+ result.put("img", new String[] { "style", "src", "width", "height", "title", "alt" });
+ result.put("a", new String[] { "style", "href", "target", "title" });
+ return result;
+ }
+
+ private static class MarkupHandler extends DefaultHandler {
+
+ @Override
+ public void startElement(String uri, String localName, String name, Attributes attributes) {
+ checkSupportedElements(name, attributes);
+ checkSupportedAttributes(name, attributes);
+ checkMandatoryAttributes(name, attributes);
+ }
+
+ private static void checkSupportedElements(String elementName, Attributes attributes) {
+ if (!SUPPORTED_ELEMENTS.containsKey(elementName)) {
+ throw new IllegalArgumentException("Unsupported element in markup text: " + elementName);
+ }
+ }
+
+ private static void checkSupportedAttributes(String elementName, Attributes attributes) {
+ if (attributes.getLength() > 0) {
+ List<String> supportedAttributes = Arrays.asList(SUPPORTED_ELEMENTS.get(elementName));
+ int index = 0;
+ String attributeName = attributes.getQName(index);
+ while (attributeName != null) {
+ if (!supportedAttributes.contains(attributeName)) {
+ String message = "Unsupported attribute \"{0}\" for element \"{1}\" in markup text";
+ message = MessageFormat.format(message, new Object[] { attributeName, elementName });
+ throw new IllegalArgumentException(message);
+ }
+ index++;
+ attributeName = attributes.getQName(index);
+ }
+ }
+ }
+
+ private static void checkMandatoryAttributes(String elementName, Attributes attributes) {
+ checkIntAttribute(elementName, attributes, "img", "width");
+ checkIntAttribute(elementName, attributes, "img", "height");
+ }
+
+ private static void checkIntAttribute(String elementName, Attributes attributes, String checkedElementName,
+ String checkedAttributeName) {
+ if (checkedElementName.equals(elementName)) {
+ String attribute = attributes.getValue(checkedAttributeName);
+ try {
+ Integer.parseInt(attribute);
+ } catch (NumberFormatException exception) {
+ String message = "Mandatory attribute \"{0}\" for element \"{1}\" is missing or not a valid integer";
+ Object[] arguments = new Object[] { checkedAttributeName, checkedElementName };
+ message = MessageFormat.format(message, arguments);
+ throw new IllegalArgumentException(message);
+ }
+ }
+ }
+
+ }
+
+}
--- /dev/null
+/** Argeo CMS forms, based on SWT/JFace. */
+package org.argeo.cms.ui.forms;
\ No newline at end of file
--- /dev/null
+package org.argeo.cms.ui.fs;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.spi.FileSystemProvider;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.Session;
+
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.jcr.CmsJcrUtils;
+import org.argeo.cms.swt.CmsException;
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.fs.FileIconNameLabelProvider;
+import org.argeo.eclipse.ui.fs.FsTableViewer;
+import org.argeo.eclipse.ui.fs.FsUiConstants;
+import org.argeo.eclipse.ui.fs.FsUiUtils;
+import org.argeo.eclipse.ui.fs.NioFileLabelProvider;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+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.layout.RowLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * Default CMS browser composite: a sashForm layout with bookmarks at the left
+ * hand side, a simple table in the middle and an overview at right hand side.
+ */
+public class CmsFsBrowser extends Composite {
+ // private final static Log log = LogFactory.getLog(CmsFsBrowser.class);
+ private static final long serialVersionUID = -40347919096946585L;
+
+ private final FileSystemProvider nodeFileSystemProvider;
+ private final Node currentBaseContext;
+
+ // UI Parts for the browser
+ private Composite leftPannelCmp;
+ private Composite filterCmp;
+ private Text filterTxt;
+ private FsTableViewer directoryDisplayViewer;
+ private Composite rightPannelCmp;
+
+ private FsContextMenu contextMenu;
+
+ // Local context (this composite is state full)
+ private Path initialPath;
+ private Path currDisplayedFolder;
+ private Path currSelected;
+
+ // local variables (to be cleaned)
+ private int bookmarkColWith = 500;
+
+ /*
+ * WARNING: unfinalised implementation of the mechanism to retrieve base
+ * paths
+ */
+
+ private final static String NODE_PREFIX = "node://";
+
+ private String getCurrentHomePath() {
+ Session session = null;
+ try {
+ Repository repo = currentBaseContext.getSession().getRepository();
+ session = CurrentUser.tryAs(() -> repo.login());
+ String homepath = CmsJcrUtils.getUserHome(session).getPath();
+ return homepath;
+ } catch (Exception e) {
+ throw new CmsException("Cannot retrieve Current User Home Path", e);
+ } finally {
+ JcrUtils.logoutQuietly(session);
+ }
+ }
+
+ protected Path[] getMyFilesPath() {
+ // return Paths.get(System.getProperty("user.dir"));
+ String currHomeUriStr = NODE_PREFIX + getCurrentHomePath();
+ try {
+ URI uri = new URI(currHomeUriStr);
+ FileSystem fileSystem = nodeFileSystemProvider.getFileSystem(uri);
+ if (fileSystem == null) {
+ PrivilegedExceptionAction<FileSystem> pea = new PrivilegedExceptionAction<FileSystem>() {
+ @Override
+ public FileSystem run() throws Exception {
+ return nodeFileSystemProvider.newFileSystem(uri, null);
+ }
+
+ };
+ fileSystem = CurrentUser.tryAs(pea);
+ }
+ Path[] paths = { fileSystem.getPath(getCurrentHomePath()), fileSystem.getPath("/") };
+ return paths;
+ } catch (URISyntaxException | PrivilegedActionException e) {
+ throw new RuntimeException("unable to initialise home file system for " + currHomeUriStr, e);
+ }
+ }
+
+ private Path[] getMyGroupsFilesPath() {
+ // TODO
+ Path[] paths = { Paths.get(System.getProperty("user.dir")), Paths.get("/tmp") };
+ return paths;
+ }
+
+ private Path[] getMyBookmarks() {
+ // TODO
+ Path[] paths = { Paths.get(System.getProperty("user.dir")), Paths.get("/tmp"), Paths.get("/opt") };
+ return paths;
+ }
+
+ /* End of warning */
+
+ public CmsFsBrowser(Composite parent, int style, Node context, FileSystemProvider fileSystemProvider) {
+ super(parent, style);
+ this.nodeFileSystemProvider = fileSystemProvider;
+ this.currentBaseContext = context;
+
+ this.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+ SashForm form = new SashForm(this, SWT.HORIZONTAL);
+
+ leftPannelCmp = new Composite(form, SWT.NO_FOCUS);
+ // Bookmarks are still static
+ populateBookmarks(leftPannelCmp);
+
+ Composite centerCmp = new Composite(form, SWT.BORDER | SWT.NO_FOCUS);
+ createDisplay(centerCmp);
+
+ rightPannelCmp = new Composite(form, SWT.NO_FOCUS);
+
+ form.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+ form.setWeights(new int[] { 15, 40, 20 });
+ }
+
+ void refresh() {
+ modifyFilter(false);
+ // also refresh bookmarks and groups
+ }
+
+ private void createDisplay(final Composite parent) {
+ parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+ // top filter
+ filterCmp = new Composite(parent, SWT.NO_FOCUS);
+ filterCmp.setLayoutData(EclipseUiUtils.fillWidth());
+ addFilterPanel(filterCmp);
+
+ // Main display
+ directoryDisplayViewer = new FsTableViewer(parent, SWT.MULTI);
+ List<ColumnDefinition> colDefs = new ArrayList<>();
+ colDefs.add(new ColumnDefinition(new FileIconNameLabelProvider(), "Name", 250));
+ colDefs.add(new ColumnDefinition(new NioFileLabelProvider(FsUiConstants.PROPERTY_SIZE), "Size", 100));
+ colDefs.add(new ColumnDefinition(new NioFileLabelProvider(FsUiConstants.PROPERTY_TYPE), "Type", 150));
+ colDefs.add(new ColumnDefinition(new NioFileLabelProvider(FsUiConstants.PROPERTY_LAST_MODIFIED),
+ "Last modified", 400));
+ final Table table = directoryDisplayViewer.configureDefaultTable(colDefs);
+ table.setLayoutData(EclipseUiUtils.fillAll());
+
+ // table.addKeyListener(new KeyListener() {
+ // private static final long serialVersionUID = -8083424284436715709L;
+ //
+ // @Override
+ // public void keyReleased(KeyEvent e) {
+ // }
+ //
+ // @Override
+ // public void keyPressed(KeyEvent e) {
+ // if (log.isDebugEnabled())
+ // log.debug("Key event received: " + e.keyCode);
+ // IStructuredSelection selection = (IStructuredSelection)
+ // directoryDisplayViewer.getSelection();
+ // Path selected = null;
+ // if (!selection.isEmpty())
+ // selected = ((Path) selection.getFirstElement());
+ // if (e.keyCode == SWT.CR) {
+ // if (!Files.isDirectory(selected))
+ // return;
+ // if (selected != null) {
+ // currDisplayedFolder = selected;
+ // directoryDisplayViewer.setInput(currDisplayedFolder, "*");
+ // }
+ // } else if (e.keyCode == SWT.BS) {
+ // currDisplayedFolder = currDisplayedFolder.getParent();
+ // directoryDisplayViewer.setInput(currDisplayedFolder, "*");
+ // directoryDisplayViewer.getTable().setFocus();
+ // }
+ // }
+ // });
+
+ directoryDisplayViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ IStructuredSelection selection = (IStructuredSelection) directoryDisplayViewer.getSelection();
+ Path selected = null;
+ if (selection.isEmpty())
+ setSelected(null);
+ else
+ selected = ((Path) selection.getFirstElement());
+ if (selected != null) {
+ // TODO manage multiple selection
+ setSelected(selected);
+ }
+ }
+ });
+
+ directoryDisplayViewer.addDoubleClickListener(new IDoubleClickListener() {
+ @Override
+ public void doubleClick(DoubleClickEvent event) {
+ IStructuredSelection selection = (IStructuredSelection) directoryDisplayViewer.getSelection();
+ Path selected = null;
+ if (!selection.isEmpty())
+ selected = ((Path) selection.getFirstElement());
+ if (selected != null) {
+ if (!Files.isDirectory(selected))
+ return;
+ setInput(selected);
+ }
+ }
+ });
+
+ // The context menu
+ contextMenu = new FsContextMenu(this);
+
+ table.addMouseListener(new MouseAdapter() {
+ private static final long serialVersionUID = 6737579410648595940L;
+
+ @Override
+ public void mouseDown(MouseEvent e) {
+ if (e.button == 3) {
+ // contextMenu.setCurrFolderPath(currDisplayedFolder);
+ contextMenu.show(table, new Point(e.x, e.y), currDisplayedFolder);
+ }
+ }
+ });
+ }
+
+ private void addPathElementBtn(Path path) {
+ Button elemBtn = new Button(filterCmp, SWT.PUSH);
+ String nameStr;
+ if (path.toString().equals("/"))
+ nameStr = "[jcr:root]";
+ else
+ nameStr = path.getFileName().toString();
+ elemBtn.setText(nameStr + " >> ");
+ CmsSwtUtils.style(elemBtn, FsStyles.BREAD_CRUMB_BTN);
+ elemBtn.addSelectionListener(new SelectionAdapter() {
+ private static final long serialVersionUID = -4103695476023480651L;
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ setInput(path);
+ }
+ });
+ }
+
+ public void setInput(Path path) {
+ if (path.equals(currDisplayedFolder))
+ return;
+ currDisplayedFolder = path;
+
+ Path diff = initialPath.relativize(currDisplayedFolder);
+
+ for (Control child : filterCmp.getChildren())
+ if (!child.equals(filterTxt))
+ child.dispose();
+
+ addPathElementBtn(initialPath);
+ Path currTarget = initialPath;
+ if (!diff.toString().equals(""))
+ for (Path pathElem : diff) {
+ currTarget = currTarget.resolve(pathElem);
+ addPathElementBtn(currTarget);
+ }
+
+ filterTxt.setText("");
+ filterTxt.moveBelow(null);
+ setSelected(null);
+ filterCmp.getParent().layout(true, true);
+ }
+
+ private void setSelected(Path path) {
+ currSelected = path;
+ setOverviewInput(path);
+ }
+
+ public Viewer getViewer() {
+ return directoryDisplayViewer;
+ }
+
+ private void populateBookmarks(Composite parent) {
+ CmsSwtUtils.clear(parent);
+ parent.setLayout(new GridLayout());
+ ISelectionChangedListener selList = new BookmarksSelChangeListener();
+
+ FsTableViewer homeViewer = new FsTableViewer(parent, SWT.SINGLE | SWT.NO_SCROLL);
+ Table table = homeViewer.configureDefaultSingleColumnTable(bookmarkColWith);
+ GridData gd = EclipseUiUtils.fillWidth();
+ gd.horizontalIndent = 10;
+ table.setLayoutData(gd);
+ homeViewer.addSelectionChangedListener(selList);
+ homeViewer.setPathsInput(getMyFilesPath());
+
+ appendTitle(parent, "Shared files");
+ FsTableViewer groupsViewer = new FsTableViewer(parent, SWT.SINGLE | SWT.NO_SCROLL);
+ table = groupsViewer.configureDefaultSingleColumnTable(bookmarkColWith);
+ gd = EclipseUiUtils.fillWidth();
+ gd.horizontalIndent = 10;
+ table.setLayoutData(gd);
+ groupsViewer.addSelectionChangedListener(selList);
+ groupsViewer.setPathsInput(getMyGroupsFilesPath());
+
+ appendTitle(parent, "My bookmarks");
+ FsTableViewer bookmarksViewer = new FsTableViewer(parent, SWT.SINGLE | SWT.NO_SCROLL);
+ table = bookmarksViewer.configureDefaultSingleColumnTable(bookmarkColWith);
+ gd = EclipseUiUtils.fillWidth();
+ gd.horizontalIndent = 10;
+ table.setLayoutData(gd);
+ bookmarksViewer.addSelectionChangedListener(selList);
+ bookmarksViewer.setPathsInput(getMyBookmarks());
+ }
+
+ /**
+ * Recreates the content of the box that displays information about the
+ * current selected Path.
+ */
+ private void setOverviewInput(Path path) {
+ try {
+ EclipseUiUtils.clear(rightPannelCmp);
+ rightPannelCmp.setLayout(new GridLayout());
+ if (path != null) {
+ // if (isImg(context)) {
+ // EditableImage image = new Img(parent, RIGHT, context,
+ // imageWidth);
+ // image.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER,
+ // true, false,
+ // 2, 1));
+ // }
+
+ Label contextL = new Label(rightPannelCmp, SWT.NONE);
+ contextL.setText(path.getFileName().toString());
+ contextL.setFont(EclipseUiUtils.getBoldFont(rightPannelCmp));
+ addProperty(rightPannelCmp, "Last modified", Files.getLastModifiedTime(path).toString());
+ // addProperty(rightPannelCmp, "Owner",
+ // Files.getOwner(path).getName());
+ if (Files.isDirectory(path)) {
+ addProperty(rightPannelCmp, "Type", "Folder");
+ } else {
+ String mimeType = Files.probeContentType(path);
+ if (EclipseUiUtils.isEmpty(mimeType))
+ mimeType = "<i>Unknown</i>";
+ addProperty(rightPannelCmp, "Type", mimeType);
+ addProperty(rightPannelCmp, "Size", FsUiUtils.humanReadableByteCount(Files.size(path), false));
+ }
+ }
+ rightPannelCmp.layout(true, true);
+ } catch (IOException e) {
+ throw new CmsException("Cannot display details for " + path.toString(), e);
+ }
+ }
+
+ private void addFilterPanel(Composite parent) {
+ RowLayout rl = new RowLayout(SWT.HORIZONTAL);
+ rl.wrap = true;
+ parent.setLayout(rl);
+ // parent.setLayout(EclipseUiUtils.noSpaceGridLayout(new GridLayout(2,
+ // false)));
+
+ filterTxt = new Text(parent, SWT.SEARCH | SWT.ICON_CANCEL);
+ filterTxt.setMessage("Search current folder");
+ filterTxt.setLayoutData(new RowData(250, SWT.DEFAULT));
+ filterTxt.addModifyListener(new ModifyListener() {
+ private static final long serialVersionUID = 1L;
+
+ public void modifyText(ModifyEvent event) {
+ modifyFilter(false);
+ }
+ });
+ filterTxt.addKeyListener(new KeyListener() {
+ private static final long serialVersionUID = 2533535233583035527L;
+
+ @Override
+ public void keyReleased(KeyEvent e) {
+ }
+
+ @Override
+ public void keyPressed(KeyEvent e) {
+ // boolean shiftPressed = (e.stateMask & SWT.SHIFT) != 0;
+ // // boolean altPressed = (e.stateMask & SWT.ALT) != 0;
+ // FilterEntitiesVirtualTable currTable = null;
+ // if (currEdited != null) {
+ // FilterEntitiesVirtualTable table =
+ // browserCols.get(currEdited);
+ // if (table != null && !table.isDisposed())
+ // currTable = table;
+ // }
+ //
+ // if (e.keyCode == SWT.ARROW_DOWN)
+ // currTable.setFocus();
+ // else if (e.keyCode == SWT.BS) {
+ // if (filterTxt.getText().equals("")
+ // && !(currEdited.getNameCount() == 1 ||
+ // currEdited.equals(initialPath))) {
+ // Path oldEdited = currEdited;
+ // Path parentPath = currEdited.getParent();
+ // setEdited(parentPath);
+ // if (browserCols.containsKey(parentPath))
+ // browserCols.get(parentPath).setSelected(oldEdited);
+ // filterTxt.setFocus();
+ // e.doit = false;
+ // }
+ // } else if (e.keyCode == SWT.TAB && !shiftPressed) {
+ // Path uniqueChild = getOnlyChild(currEdited,
+ // filterTxt.getText());
+ // if (uniqueChild != null) {
+ // // Highlight the unique chosen child
+ // currTable.setSelected(uniqueChild);
+ // setEdited(uniqueChild);
+ // }
+ // filterTxt.setFocus();
+ // e.doit = false;
+ // }
+ }
+ });
+ }
+
+ private Path getOnlyChild(Path parent, String filter) {
+ try (DirectoryStream<Path> stream = Files.newDirectoryStream(currDisplayedFolder, filter + "*")) {
+ Path uniqueChild = null;
+ boolean moreThanOne = false;
+ loop: for (Path entry : stream) {
+ if (uniqueChild == null) {
+ uniqueChild = entry;
+ } else {
+ moreThanOne = true;
+ break loop;
+ }
+ }
+ if (!moreThanOne)
+ return uniqueChild;
+ return null;
+ } catch (IOException ioe) {
+ throw new CmsException(
+ "Unable to determine unique child existence and get it under " + parent + " with filter " + filter,
+ ioe);
+ }
+ }
+
+ private void modifyFilter(boolean fromOutside) {
+ if (!fromOutside)
+ if (currDisplayedFolder != null) {
+ String filter = filterTxt.getText() + "*";
+ directoryDisplayViewer.setInput(currDisplayedFolder, filter);
+ }
+ }
+
+ private class BookmarksSelChangeListener implements ISelectionChangedListener {
+
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ IStructuredSelection selection = (IStructuredSelection) event.getSelection();
+ if (selection.isEmpty())
+ return;
+ else {
+ Path newSelected = (Path) selection.getFirstElement();
+ if (newSelected.equals(currDisplayedFolder) && newSelected.equals(initialPath))
+ return;
+ initialPath = newSelected;
+ setInput(newSelected);
+ }
+ }
+ }
+
+ // Simplify UI implementation
+ private void addProperty(Composite parent, String propName, String value) {
+ Label contextL = new Label(parent, SWT.NONE);
+ contextL.setText(propName + ": " + value);
+ }
+
+ private Label appendTitle(Composite parent, String value) {
+ Label titleLbl = new Label(parent, SWT.NONE);
+ titleLbl.setText(value);
+ titleLbl.setFont(EclipseUiUtils.getBoldFont(parent));
+ GridData gd = EclipseUiUtils.fillWidth();
+ gd.horizontalIndent = 5;
+ gd.verticalIndent = 5;
+ titleLbl.setLayoutData(gd);
+ return titleLbl;
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.fs;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.eclipse.ui.specific.FileDropAdapter;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DropTarget;
+import org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.widgets.Control;
+
+/** Allows a control to receive file drops. */
+public class FileDrop {
+ private final static CmsLog log = CmsLog.getLog(FileDrop.class);
+
+ public void createDropTarget(Control control) {
+ FileDropAdapter fileDropAdapter = new FileDropAdapter() {
+ @Override
+ protected void processUpload(InputStream in, String fileName, String contentType) throws IOException {
+ if (log.isDebugEnabled())
+ log.debug("Process upload of " + fileName + " (" + contentType + ")");
+ processFileUpload(in, fileName, contentType);
+ }
+ };
+ DropTarget dropTarget = new DropTarget(control, DND.DROP_MOVE | DND.DROP_COPY);
+ fileDropAdapter.prepareDropTarget(control, dropTarget);
+ }
+
+ public void handleFileDrop(Control control, DropTargetEvent event) {
+ }
+
+ /** Executed in UI thread */
+ protected void processFileUpload(InputStream in, String fileName, String contentType) throws IOException {
+
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.fs;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.swt.CmsException;
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.dialogs.SingleValue;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+/** Generic popup context menu to manage NIO Path in a Viewer. */
+public class FsContextMenu extends Shell {
+ private static final long serialVersionUID = -9120261153509855795L;
+
+ private final static CmsLog log = CmsLog.getLog(FsContextMenu.class);
+
+ // Default known actions
+ public final static String ACTION_ID_CREATE_FOLDER = "createFolder";
+ public final static String ACTION_ID_BOOKMARK_FOLDER = "bookmarkFolder";
+ public final static String ACTION_ID_SHARE_FOLDER = "shareFolder";
+ public final static String ACTION_ID_DOWNLOAD_FOLDER = "downloadFolder";
+ public final static String ACTION_ID_DELETE = "delete";
+ public final static String ACTION_ID_UPLOAD_FILE = "uploadFiles";
+ public final static String ACTION_ID_OPEN = "open";
+
+ // Local context
+ private final CmsFsBrowser browser;
+ // private final Viewer viewer;
+ private final static String KEY_ACTION_ID = "actionId";
+ private final static String[] DEFAULT_ACTIONS = { ACTION_ID_CREATE_FOLDER, ACTION_ID_BOOKMARK_FOLDER,
+ ACTION_ID_SHARE_FOLDER, ACTION_ID_DOWNLOAD_FOLDER, ACTION_ID_DELETE, ACTION_ID_UPLOAD_FILE,
+ ACTION_ID_OPEN };
+ private Map<String, Button> actionButtons = new HashMap<String, Button>();
+
+ private Path currFolderPath;
+
+ public FsContextMenu(CmsFsBrowser browser) { // Viewer viewer, Display
+ // display) {
+ super(browser.getDisplay(), SWT.NO_TRIM | SWT.BORDER | SWT.ON_TOP);
+ this.browser = browser;
+ setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+ Composite boxCmp = new Composite(this, SWT.NO_FOCUS | SWT.BORDER);
+ boxCmp.setLayout(EclipseUiUtils.noSpaceGridLayout());
+ CmsSwtUtils.style(boxCmp, FsStyles.CONTEXT_MENU_BOX);
+ createContextMenu(boxCmp);
+
+ addShellListener(new ActionsShellListener());
+ }
+
+ protected void createContextMenu(Composite boxCmp) {
+ ActionsSelListener asl = new ActionsSelListener();
+ for (String actionId : DEFAULT_ACTIONS) {
+ Button btn = new Button(boxCmp, SWT.FLAT | SWT.PUSH | SWT.LEAD);
+ btn.setText(getLabel(actionId));
+ btn.setLayoutData(EclipseUiUtils.fillWidth());
+ CmsSwtUtils.markup(btn);
+ CmsSwtUtils.style(btn, actionId + FsStyles.BUTTON_SUFFIX);
+ btn.setData(KEY_ACTION_ID, actionId);
+ btn.addSelectionListener(asl);
+ actionButtons.put(actionId, btn);
+ }
+ }
+
+ protected String getLabel(String actionId) {
+ switch (actionId) {
+ case ACTION_ID_CREATE_FOLDER:
+ return "Create Folder";
+ case ACTION_ID_BOOKMARK_FOLDER:
+ return "Bookmark Folder";
+ case ACTION_ID_SHARE_FOLDER:
+ return "Share Folder";
+ case ACTION_ID_DOWNLOAD_FOLDER:
+ return "Download as zip archive";
+ case ACTION_ID_DELETE:
+ return "Delete";
+ case ACTION_ID_UPLOAD_FILE:
+ return "Upload Files";
+ case ACTION_ID_OPEN:
+ return "Open";
+ default:
+ throw new IllegalArgumentException("Unknown action ID " + actionId);
+ }
+ }
+
+ protected void aboutToShow(Control source, Point location) {
+ IStructuredSelection selection = ((IStructuredSelection) browser.getViewer().getSelection());
+ boolean emptySel = true;
+ boolean multiSel = false;
+ boolean isFolder = true;
+ if (selection != null && !selection.isEmpty()) {
+ emptySel = false;
+ multiSel = selection.size() > 1;
+ if (!multiSel && selection.getFirstElement() instanceof Path) {
+ isFolder = Files.isDirectory((Path) selection.getFirstElement());
+ }
+ }
+ if (emptySel) {
+ setVisible(true, ACTION_ID_CREATE_FOLDER, ACTION_ID_UPLOAD_FILE);
+ setVisible(false, ACTION_ID_SHARE_FOLDER, ACTION_ID_DOWNLOAD_FOLDER, ACTION_ID_DELETE, ACTION_ID_OPEN,
+ // to be implemented
+ ACTION_ID_BOOKMARK_FOLDER);
+ } else if (multiSel) {
+ setVisible(true, ACTION_ID_CREATE_FOLDER, ACTION_ID_UPLOAD_FILE, ACTION_ID_DELETE);
+ setVisible(false, ACTION_ID_SHARE_FOLDER, ACTION_ID_DOWNLOAD_FOLDER, ACTION_ID_OPEN,
+ // to be implemented
+ ACTION_ID_BOOKMARK_FOLDER);
+ } else if (isFolder) {
+ setVisible(true, ACTION_ID_CREATE_FOLDER, ACTION_ID_UPLOAD_FILE, ACTION_ID_DELETE);
+ setVisible(false, ACTION_ID_OPEN,
+ // to be implemented
+ ACTION_ID_SHARE_FOLDER, ACTION_ID_DOWNLOAD_FOLDER, ACTION_ID_BOOKMARK_FOLDER);
+ } else {
+ setVisible(true, ACTION_ID_CREATE_FOLDER, ACTION_ID_UPLOAD_FILE, ACTION_ID_OPEN, ACTION_ID_DELETE);
+ setVisible(false, ACTION_ID_SHARE_FOLDER, ACTION_ID_DOWNLOAD_FOLDER,
+ // to be implemented
+ ACTION_ID_BOOKMARK_FOLDER);
+ }
+ }
+
+ private void setVisible(boolean visible, String... buttonIds) {
+ for (String id : buttonIds) {
+ Button button = actionButtons.get(id);
+ button.setVisible(visible);
+ GridData gd = (GridData) button.getLayoutData();
+ gd.heightHint = visible ? SWT.DEFAULT : 0;
+ }
+ }
+
+ public void show(Control source, Point location, Path currFolderPath) {
+ if (isVisible())
+ setVisible(false);
+ // TODO find a better way to retrieve the parent path (cannot be deduced
+ // from table content because it will fail on an empty folder)
+ this.currFolderPath = currFolderPath;
+ aboutToShow(source, location);
+ 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 ActionsMouseListener extends MouseAdapter {
+ // private static final long serialVersionUID = -1041871937815812149L;
+ //
+ // @Override
+ // public void mouseDown(MouseEvent e) {
+ // Object eventSource = e.getSource();
+ // if (e.button == 1) {
+ // if (eventSource instanceof Button) {
+ // Button pressedBtn = (Button) eventSource;
+ // String actionId = (String) pressedBtn.getData(KEY_ACTION_ID);
+ // switch (actionId) {
+ // case ACTION_ID_CREATE_FOLDER:
+ // createFolder();
+ // break;
+ // case ACTION_ID_DELETE:
+ // deleteItems();
+ // break;
+ // default:
+ // throw new IllegalArgumentException("Unimplemented action " + actionId);
+ // // case ACTION_ID_SHARE_FOLDER:
+ // // return "Share Folder";
+ // // case ACTION_ID_DOWNLOAD_FOLDER:
+ // // return "Download as zip archive";
+ // // case ACTION_ID_UPLOAD_FILE:
+ // // return "Upload Files";
+ // // case ACTION_ID_OPEN:
+ // // return "Open";
+ // }
+ // }
+ // }
+ // viewer.getControl().setFocus();
+ // // setVisible(false);
+ // }
+ // }
+
+ class ActionsSelListener extends SelectionAdapter {
+ private static final long serialVersionUID = -1041871937815812149L;
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ Object eventSource = e.getSource();
+ if (eventSource instanceof Button) {
+ Button pressedBtn = (Button) eventSource;
+ String actionId = (String) pressedBtn.getData(KEY_ACTION_ID);
+ switch (actionId) {
+ case ACTION_ID_CREATE_FOLDER:
+ createFolder();
+ break;
+ case ACTION_ID_DELETE:
+ deleteItems();
+ break;
+ case ACTION_ID_OPEN:
+ openFile();
+ break;
+ case ACTION_ID_UPLOAD_FILE:
+ uploadFiles();
+ break;
+ default:
+ throw new IllegalArgumentException("Unimplemented action " + actionId);
+ // case ACTION_ID_SHARE_FOLDER:
+ // return "Share Folder";
+ // case ACTION_ID_DOWNLOAD_FOLDER:
+ // return "Download as zip archive";
+ // case ACTION_ID_OPEN:
+ // return "Open";
+ }
+ }
+ browser.setFocus();
+ // viewer.getControl().setFocus();
+ // setVisible(false);
+
+ }
+ }
+
+ class ActionsShellListener extends org.eclipse.swt.events.ShellAdapter {
+ private static final long serialVersionUID = -5092341449523150827L;
+
+ @Override
+ public void shellDeactivated(ShellEvent e) {
+ setVisible(false);
+ }
+ }
+
+ private void openFile() {
+ log.warn("Implement single sourced, workbench independant \"Open File\" action");
+ }
+
+ private void deleteItems() {
+ IStructuredSelection selection = ((IStructuredSelection) browser.getViewer().getSelection());
+ if (selection.isEmpty())
+ return;
+
+ StringBuilder builder = new StringBuilder();
+ @SuppressWarnings("unchecked")
+ Iterator<Object> iterator = selection.iterator();
+ List<Path> paths = new ArrayList<>();
+
+ while (iterator.hasNext()) {
+ Path path = (Path) iterator.next();
+ builder.append(path.getFileName() + ", ");
+ paths.add(path);
+ }
+ String msg = "You are about to delete following elements: " + builder.substring(0, builder.length() - 2)
+ + ". Are you sure?";
+ if (MessageDialog.openConfirm(this, "Confirm deletion", msg)) {
+ for (Path path : paths) {
+ try {
+ // Might have already been deleted if we are in a tree
+ Files.deleteIfExists(path);
+ } catch (IOException e) {
+ throw new CmsException("Cannot delete path " + path, e);
+ }
+ }
+ browser.refresh();
+ }
+ }
+
+ private void createFolder() {
+ String msg = "Please provide a name.";
+ String name = SingleValue.ask("Create folder", msg);
+ // TODO enhance check of name validity
+ if (EclipseUiUtils.notEmpty(name)) {
+ try {
+ Path child = currFolderPath.resolve(name);
+ if (Files.exists(child))
+ throw new CmsException("An item with name " + name + " already exists at "
+ + currFolderPath.toString() + ", cannot create");
+ else
+ Files.createDirectories(child);
+ browser.refresh();
+ } catch (IOException e) {
+ throw new CmsException("Cannot create folder " + name + " at " + currFolderPath.toString(), e);
+ }
+ }
+ }
+
+ private void uploadFiles() {
+ try {
+ FileDialog dialog = new FileDialog(browser.getShell(), SWT.MULTI);
+ dialog.setText("Choose one or more files to upload");
+
+ if (EclipseUiUtils.notEmpty(dialog.open())) {
+ String[] names = dialog.getFileNames();
+ // Workaround small differences between RAP and RCP
+ // 1. returned names are absolute path on RAP and
+ // relative in RCP
+ // 2. in RCP we must use getFilterPath that does not
+ // exists on RAP
+ Method filterMethod = null;
+ Path parPath = null;
+ try {
+ filterMethod = dialog.getClass().getDeclaredMethod("getFilterPath");
+ String filterPath = (String) filterMethod.invoke(dialog);
+ parPath = Paths.get(filterPath);
+ } catch (NoSuchMethodException nsme) { // RAP
+ }
+ if (names.length == 0)
+ return;
+ else {
+ loop: for (String name : names) {
+ Path tmpPath = Paths.get(name);
+ if (parPath != null)
+ tmpPath = parPath.resolve(tmpPath);
+ if (Files.exists(tmpPath)) {
+ URI uri = tmpPath.toUri();
+ String uriStr = uri.toString();
+
+ if (Files.isDirectory(tmpPath)) {
+ MessageDialog.openError(browser.getShell(), "Unimplemented directory import",
+ "Upload of directories in the system is not yet implemented");
+ continue loop;
+ }
+ Path targetPath = currFolderPath.resolve(tmpPath.getFileName().toString());
+ InputStream in = null;
+ try {
+ in = new ByteArrayInputStream(Files.readAllBytes(tmpPath));
+ Files.copy(in, targetPath);
+ Files.delete(tmpPath);
+ } finally {
+ IOUtils.closeQuietly(in);
+ }
+ if (log.isDebugEnabled())
+ log.debug("copied uploaded file " + uriStr + " to " + targetPath.toString());
+ } else {
+ String msg = "Cannot copy tmp file from " + tmpPath.toString();
+ if (parPath != null)
+ msg += "\nPlease remember that file upload fails when choosing files from the \"Recently Used\" bookmarks on some OS";
+ MessageDialog.openError(browser.getShell(), "Missing file", msg);
+ continue loop;
+ }
+ }
+ browser.refresh();
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ MessageDialog.openError(getShell(), "Upload has failed", "Cannot import files to " + currFolderPath);
+ }
+ }
+
+ public void setCurrFolderPath(Path currFolderPath) {
+ this.currFolderPath = currFolderPath;
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.fs;
+
+/** FS Ui specific CSS styles */
+public interface FsStyles {
+ String BREAD_CRUMB_BTN = "breadCrumb_btn";
+ String CONTEXT_MENU_BOX = "contextMenu_box";
+ String BUTTON_SUFFIX = "_btn";
+}
--- /dev/null
+/** SWT/JFace file system components. */
+package org.argeo.cms.ui.fs;
\ No newline at end of file
--- /dev/null
+package org.argeo.cms.ui.internal;
+
+import org.argeo.api.cms.CmsState;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.util.tracker.ServiceTracker;
+
+public class Activator implements BundleActivator {
+
+ // avoid dependency to RWT OSGi
+ private final static String CONTEXT_NAME_PROP = "contextName";
+
+ private static ServiceTracker<CmsState, CmsState> nodeState;
+
+ // @Override
+ public void start(BundleContext bc) throws Exception {
+ // UI
+// bc.registerService(ApplicationConfiguration.class, new MaintenanceUi(),
+// LangUtils.dico(CONTEXT_NAME_PROP, "system"));
+// bc.registerService(ApplicationConfiguration.class, new UserUi(), LangUtils.dico(CONTEXT_NAME_PROP, "user"));
+
+ nodeState = new ServiceTracker<>(bc, CmsState.class, null);
+ nodeState.open();
+ }
+
+ @Override
+ public void stop(BundleContext context) throws Exception {
+ if (nodeState != null) {
+ nodeState.close();
+ nodeState = null;
+ }
+ }
+
+ public static CmsState getNodeState() {
+ return nodeState.getService();
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.internal;
+
+import java.util.ArrayList;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.swt.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);
+ }
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.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.api.cms.ux.CmsImageManager;
+import org.argeo.cms.ui.widgets.Img;
+import org.argeo.jcr.JcrException;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.rap.fileupload.FileDetails;
+import org.eclipse.rap.fileupload.FileUploadReceiver;
+
+public class JcrFileUploadReceiver extends FileUploadReceiver {
+ private Img img;
+ private final Node parentNode;
+ private final String nodeName;
+ private final CmsImageManager imageManager;
+
+ /** If nodeName is null, use the uploaded file name */
+ public JcrFileUploadReceiver(Img img, Node parentNode, String nodeName, CmsImageManager imageManager) {
+ super();
+ this.img = img;
+ 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(img.getNode(),parentNode, fileName, stream, contentType);
+ return;
+ }
+
+ 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 JcrException("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) {
+
+ }
+
+}
--- /dev/null
+package org.argeo.cms.ui.internal;
+
+import javax.jcr.RepositoryException;
+
+import org.argeo.api.cms.ux.Cms2DSize;
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.cms.ui.util.CmsUiUtils;
+import org.argeo.cms.ui.widgets.EditableImage;
+import org.argeo.cms.ux.AbstractImageManager;
+import org.argeo.cms.ux.CmsUxUtils;
+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 Cms2DSize imageSize;
+
+ public SimpleEditableImage(Composite parent, int swtStyle) {
+ super(parent, swtStyle);
+ // load(getControl());
+ getParent().layout();
+ }
+
+ public SimpleEditableImage(Composite parent, int swtStyle, String src, Cms2DSize 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 = CmsUxUtils.img(src, imageSize);
+ else
+ imgTag = CmsUiUtils.noImg(imageSize != null ? imageSize : AbstractImageManager.NO_IMAGE_SIZE);
+ return imgTag;
+ }
+
+ protected Text createText(Composite box, String style) {
+ Text text = new Text(box, getStyle());
+ CmsSwtUtils.style(text, style);
+ return text;
+ }
+
+ public String getSrc() {
+ return src;
+ }
+
+ public void setSrc(String src) {
+ this.src = src;
+ }
+
+ public Cms2DSize getImageSize() {
+ return imageSize;
+ }
+
+ public void setImageSize(Cms2DSize imageSize) {
+ this.imageSize = imageSize;
+ }
+
+}
--- /dev/null
+package org.argeo.cms.ui.jcr;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Observable;
+import java.util.TreeMap;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.api.cms.CmsLog;
+
+public class DefaultRepositoryRegister extends Observable implements RepositoryRegister {
+ /** Key for a JCR repository alias */
+ private final static String CN = CmsConstants.CN;
+ /** Key for a JCR repository URI */
+ // public final static String JCR_REPOSITORY_URI = "argeo.jcr.repository.uri";
+ private final static CmsLog log = CmsLog.getLog(DefaultRepositoryRegister.class);
+
+ /** Read only map which will be directly exposed. */
+ private Map<String, Repository> repositories = Collections.unmodifiableMap(new TreeMap<String, Repository>());
+
+ @SuppressWarnings("rawtypes")
+ public synchronized Repository getRepository(Map parameters) throws RepositoryException {
+ if (!parameters.containsKey(CN))
+ throw new RepositoryException("Parameter " + CN + " has to be defined.");
+ String alias = parameters.get(CN).toString();
+ if (!repositories.containsKey(alias))
+ throw new RepositoryException("No repository registered with alias " + alias);
+
+ return repositories.get(alias);
+ }
+
+ /** Access to the read-only map */
+ public synchronized Map<String, Repository> getRepositories() {
+ return repositories;
+ }
+
+ /** Registers a service, typically called when OSGi services are bound. */
+ @SuppressWarnings("rawtypes")
+ public synchronized void register(Repository repository, Map properties) {
+ String alias;
+ if (properties == null || !properties.containsKey(CN)) {
+ log.warn("Cannot register a repository if no " + CN + " property is specified.");
+ return;
+ }
+ alias = properties.get(CN).toString();
+ Map<String, Repository> map = new TreeMap<String, Repository>(repositories);
+ map.put(alias, repository);
+ repositories = Collections.unmodifiableMap(map);
+ setChanged();
+ notifyObservers(alias);
+ }
+
+ /** Unregisters a service, typically called when OSGi services are unbound. */
+ @SuppressWarnings("rawtypes")
+ public synchronized void unregister(Repository repository, Map properties) {
+ // TODO: also check bean name?
+ if (properties == null || !properties.containsKey(CN)) {
+ log.warn("Cannot unregister a repository without property " + CN);
+ return;
+ }
+
+ String alias = properties.get(CN).toString();
+ Map<String, Repository> map = new TreeMap<String, Repository>(repositories);
+ if (map.remove(alias) == null) {
+ log.warn("No repository was registered with alias " + alias);
+ return;
+ }
+ repositories = Collections.unmodifiableMap(map);
+ setChanged();
+ notifyObservers(alias);
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.jcr;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.version.Version;
+import javax.jcr.version.VersionHistory;
+import javax.jcr.version.VersionIterator;
+import javax.jcr.version.VersionManager;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+/**
+ * Display some version information of a JCR full versionable node in a tree
+ * like structure
+ */
+public class FullVersioningTreeContentProvider implements ITreeContentProvider {
+ private static final long serialVersionUID = 8691772509491211112L;
+
+ /**
+ * Sends back the first level of the Tree. input element must be a single
+ * node object
+ */
+ public Object[] getElements(Object inputElement) {
+ try {
+ Node rootNode = (Node) inputElement;
+ String curPath = rootNode.getPath();
+ VersionManager vm = rootNode.getSession().getWorkspace()
+ .getVersionManager();
+
+ VersionHistory vh = vm.getVersionHistory(curPath);
+ List<Version> result = new ArrayList<Version>();
+ VersionIterator vi = vh.getAllLinearVersions();
+
+ while (vi.hasNext()) {
+ result.add(vi.nextVersion());
+ }
+ return result.toArray();
+ } catch (RepositoryException re) {
+ throw new EclipseUiException(
+ "Unexpected error while getting version elements", re);
+ }
+ }
+
+ public Object[] getChildren(Object parentElement) {
+ try {
+ if (parentElement instanceof Version) {
+ List<Node> tmp = new ArrayList<Node>();
+ tmp.add(((Version) parentElement).getFrozenNode());
+ return tmp.toArray();
+ }
+ } catch (RepositoryException re) {
+ throw new EclipseUiException("Unexpected error while getting child "
+ + "node for version element", re);
+ }
+ return null;
+ }
+
+ public Object getParent(Object element) {
+ try {
+ // this will not work in a simpleVersionning environment, parent is
+ // not a node.
+ if (element instanceof Node
+ && ((Node) element).isNodeType(NodeType.NT_FROZEN_NODE)) {
+ Node node = (Node) element;
+ return node.getParent();
+ } else
+ return null;
+ } catch (RepositoryException e) {
+ return null;
+ }
+ }
+
+ public boolean hasChildren(Object element) {
+ try {
+ if (element instanceof Version)
+ return true;
+ else if (element instanceof Node)
+ return ((Node) element).hasNodes();
+ else
+ return false;
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot check children of " + element, e);
+ }
+ }
+
+ public void dispose() {
+ }
+
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ }
+
+}
--- /dev/null
+package org.argeo.cms.ui.jcr;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.ui.jcr.model.RepositoriesElem;
+import org.argeo.cms.ui.jcr.model.RepositoryElem;
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ui.jcr.model.WorkspaceElem;
+import org.argeo.cms.ux.widgets.TreeParent;
+import org.argeo.eclipse.ui.EclipseUiException;
+
+/** Useful methods to manage the JCR Browser */
+public class JcrBrowserUtils {
+
+ public static String getPropertyTypeAsString(Property prop) {
+ try {
+ return PropertyType.nameFromValue(prop.getType());
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot check type for " + prop, e);
+ }
+ }
+
+ /** Insure that the UI component is not stale, refresh if needed */
+ public static void forceRefreshIfNeeded(TreeParent element) {
+ Node curNode = null;
+
+ boolean doRefresh = false;
+
+ try {
+ if (element instanceof SingleJcrNodeElem) {
+ curNode = ((SingleJcrNodeElem) element).getNode();
+ } else if (element instanceof WorkspaceElem) {
+ curNode = ((WorkspaceElem) element).getRootNode();
+ }
+
+ if (curNode != null && element.getChildren().length != curNode.getNodes().getSize())
+ doRefresh = true;
+ else if (element instanceof RepositoryElem) {
+ RepositoryElem rn = (RepositoryElem) element;
+ if (rn.isConnected()) {
+ String[] wkpNames = rn.getAccessibleWorkspaceNames();
+ if (element.getChildren().length != wkpNames.length)
+ doRefresh = true;
+ }
+ } else if (element instanceof RepositoriesElem) {
+ doRefresh = true;
+ // Always force refresh for RepositoriesElem : the condition
+ // below does not take remote repository into account and it is
+ // not trivial to do so.
+
+ // RepositoriesElem rn = (RepositoriesElem) element;
+ // if (element.getChildren().length !=
+ // rn.getRepositoryRegister()
+ // .getRepositories().size())
+ // doRefresh = true;
+ }
+ if (doRefresh) {
+ element.clearChildren();
+ element.getChildren();
+ }
+ } catch (RepositoryException re) {
+ throw new EclipseUiException("Unexpected error while synchronising the UI with the JCR repository", re);
+ }
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.jcr;
+
+import javax.jcr.Node;
+
+import org.argeo.cms.ui.jcr.model.RepositoryElem;
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ui.jcr.model.WorkspaceElem;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.TreeViewer;
+
+/** Centralizes the management of double click on a NodeTreeViewer */
+public class JcrDClickListener implements IDoubleClickListener {
+ // private final static Log log = LogFactory
+ // .getLog(GenericNodeDoubleClickListener.class);
+
+ private TreeViewer nodeViewer;
+
+ // private JcrFileProvider jfp;
+ // private FileHandler fileHandler;
+
+ public JcrDClickListener(TreeViewer nodeViewer) {
+ this.nodeViewer = nodeViewer;
+ // jfp = new JcrFileProvider();
+ // Commented out. see https://www.argeo.org/bugzilla/show_bug.cgi?id=188
+ // fileHandler = null;
+ // fileHandler = new FileHandler(jfp);
+ }
+
+ public void doubleClick(DoubleClickEvent event) {
+ if (event.getSelection() == null || event.getSelection().isEmpty())
+ return;
+ Object obj = ((IStructuredSelection) event.getSelection()).getFirstElement();
+ if (obj instanceof RepositoryElem) {
+ RepositoryElem rpNode = (RepositoryElem) obj;
+ if (rpNode.isConnected()) {
+ rpNode.logout();
+ } else {
+ rpNode.login();
+ }
+ nodeViewer.refresh(obj);
+ } else if (obj instanceof WorkspaceElem) {
+ WorkspaceElem wn = (WorkspaceElem) obj;
+ if (wn.isConnected())
+ wn.logout();
+ else
+ wn.login();
+ nodeViewer.refresh(obj);
+ } else if (obj instanceof SingleJcrNodeElem) {
+ SingleJcrNodeElem sjn = (SingleJcrNodeElem) obj;
+ Node node = sjn.getNode();
+ openNode(node);
+ }
+ }
+
+ protected void openNode(Node node) {
+ // TODO implement generic behaviour
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.jcr;
+
+import org.argeo.cms.ui.theme.CmsImages;
+import org.eclipse.swt.graphics.Image;
+
+/** Shared icons. */
+public class JcrImages {
+ public final static Image NODE = CmsImages.createIcon("node.gif");
+ public final static Image FOLDER = CmsImages.createIcon("folder.gif");
+ public final static Image FILE = CmsImages.createIcon("file.gif");
+ public final static Image BINARY = CmsImages.createIcon("binary.png");
+ public final static Image HOME = CmsImages.createIcon("person-logged-in.png");
+ public final static Image SORT = CmsImages.createIcon("sort.gif");
+ public final static Image REMOVE = CmsImages.createIcon("remove.gif");
+
+ public final static Image REPOSITORIES = CmsImages.createIcon("repositories.gif");
+ public final static Image REPOSITORY_DISCONNECTED = CmsImages.createIcon("repository_disconnected.gif");
+ public final static Image REPOSITORY_CONNECTED = CmsImages.createIcon("repository_connected.gif");
+ public final static Image REMOTE_DISCONNECTED = CmsImages.createIcon("remote_disconnected.gif");
+ public final static Image REMOTE_CONNECTED = CmsImages.createIcon("remote_connected.gif");
+ public final static Image WORKSPACE_DISCONNECTED = CmsImages.createIcon("workspace_disconnected.png");
+ public final static Image WORKSPACE_CONNECTED = CmsImages.createIcon("workspace_connected.png");
+
+}
--- /dev/null
+package org.argeo.cms.ui.jcr;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.jcr.util.JcrItemsComparator;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+/**
+ * Implementation of the {@code ITreeContentProvider} in order to display a
+ * single JCR node and its children in a tree like structure
+ */
+public class JcrTreeContentProvider implements ITreeContentProvider {
+ private static final long serialVersionUID = -2128326504754297297L;
+ // private Node rootNode;
+ private JcrItemsComparator itemComparator = new JcrItemsComparator();
+
+ /**
+ * Sends back the first level of the Tree. input element must be a single node
+ * object
+ */
+ public Object[] getElements(Object inputElement) {
+ Node rootNode = (Node) inputElement;
+ return childrenNodes(rootNode);
+ }
+
+ public Object[] getChildren(Object parentElement) {
+ return childrenNodes((Node) parentElement);
+ }
+
+ public Object getParent(Object element) {
+ try {
+ Node node = (Node) element;
+ if (!node.getPath().equals("/"))
+ return node.getParent();
+ else
+ return null;
+ } catch (RepositoryException e) {
+ return null;
+ }
+ }
+
+ public boolean hasChildren(Object element) {
+ try {
+ return ((Node) element).hasNodes();
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot check children existence on " + element, e);
+ }
+ }
+
+ protected Object[] childrenNodes(Node parentNode) {
+ try {
+ List<Node> children = new ArrayList<Node>();
+ NodeIterator nit = parentNode.getNodes();
+ while (nit.hasNext()) {
+ Node node = nit.nextNode();
+// if (node.getName().startsWith("rep:") || node.getName().startsWith("jcr:")
+// || node.getName().startsWith("nt:"))
+// continue nodes;
+ children.add(node);
+ }
+ Node[] arr = children.toArray(new Node[0]);
+ Arrays.sort(arr, itemComparator);
+ return arr;
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot list children of " + parentNode, e);
+ }
+ }
+
+ public void dispose() {
+ }
+
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.jcr;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.cms.jcr.CmsJcrUtils;
+import org.argeo.cms.security.Keyring;
+import org.argeo.cms.ui.jcr.model.RepositoriesElem;
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ux.widgets.TreeParent;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+/**
+ * Implementation of the {@code ITreeContentProvider} to display multiple
+ * repository environment in a tree like structure
+ */
+public class NodeContentProvider implements ITreeContentProvider {
+ private static final long serialVersionUID = -4083809398848374403L;
+ final private RepositoryRegister repositoryRegister;
+ final private RepositoryFactory repositoryFactory;
+
+ // Current user session on the default workspace of the argeo Node
+ final private Session userSession;
+ final private Keyring keyring;
+ private boolean sortChildren;
+
+ // Reference for cleaning
+ private SingleJcrNodeElem homeNode = null;
+ private RepositoriesElem repositoriesNode = null;
+
+ // Utils
+ private TreeBrowserComparator itemComparator = new TreeBrowserComparator();
+
+ public NodeContentProvider(Session userSession, Keyring keyring,
+ RepositoryRegister repositoryRegister,
+ RepositoryFactory repositoryFactory, Boolean sortChildren) {
+ this.userSession = userSession;
+ this.keyring = keyring;
+ this.repositoryRegister = repositoryRegister;
+ this.repositoryFactory = repositoryFactory;
+ this.sortChildren = sortChildren;
+ }
+
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ if (newInput == null)// dispose
+ return;
+
+ if (userSession != null) {
+ Node userHome = CmsJcrUtils.getUserHome(userSession);
+ if (userHome != null) {
+ // TODO : find a way to dynamically get alias for the node
+ if (homeNode != null)
+ homeNode.dispose();
+ homeNode = new SingleJcrNodeElem(null, userHome,
+ userSession.getUserID(), CmsConstants.EGO_REPOSITORY);
+ }
+ }
+ if (repositoryRegister != null) {
+ if (repositoriesNode != null)
+ repositoriesNode.dispose();
+ repositoriesNode = new RepositoriesElem("Repositories",
+ repositoryRegister, repositoryFactory, null, userSession,
+ keyring);
+ }
+ }
+
+ /**
+ * Sends back the first level of the Tree. Independent from inputElement
+ * that can be null
+ */
+ public Object[] getElements(Object inputElement) {
+ List<Object> objs = new ArrayList<Object>();
+ if (homeNode != null)
+ objs.add(homeNode);
+ if (repositoriesNode != null)
+ objs.add(repositoriesNode);
+ return objs.toArray();
+ }
+
+ public Object[] getChildren(Object parentElement) {
+ if (parentElement instanceof TreeParent) {
+ if (sortChildren) {
+ Object[] tmpArr = ((TreeParent) parentElement).getChildren();
+ if (tmpArr == null)
+ return new Object[0];
+ TreeParent[] arr = new TreeParent[tmpArr.length];
+ for (int i = 0; i < tmpArr.length; i++)
+ arr[i] = (TreeParent) tmpArr[i];
+ Arrays.sort(arr, itemComparator);
+ return arr;
+ } else
+ return ((TreeParent) parentElement).getChildren();
+ } else
+ return new Object[0];
+ }
+
+ /**
+ * Sets whether the content provider should order the children nodes or not.
+ * It is user duty to call a full refresh of the tree after changing this
+ * parameter.
+ */
+ public void setSortChildren(boolean sortChildren) {
+ this.sortChildren = sortChildren;
+ }
+
+ public Object getParent(Object element) {
+ if (element instanceof TreeParent) {
+ return ((TreeParent) element).getParent();
+ } else
+ return null;
+ }
+
+ public boolean hasChildren(Object element) {
+ if (element instanceof RepositoriesElem) {
+ RepositoryRegister rr = ((RepositoriesElem) element)
+ .getRepositoryRegister();
+ return rr.getRepositories().size() > 0;
+ } else if (element instanceof TreeParent) {
+ TreeParent tp = (TreeParent) element;
+ return tp.hasChildren();
+ }
+ return false;
+ }
+
+ public void dispose() {
+ if (homeNode != null)
+ homeNode.dispose();
+ if (repositoriesNode != null) {
+ // logs out open sessions
+ // see https://bugzilla.argeo.org/show_bug.cgi?id=23
+ repositoriesNode.dispose();
+ }
+ }
+
+ /**
+ * Specific comparator for this view. See specification here:
+ * https://www.argeo.org/bugzilla/show_bug.cgi?id=139
+ */
+ private class TreeBrowserComparator implements Comparator<TreeParent> {
+
+ public int category(TreeParent element) {
+ if (element instanceof SingleJcrNodeElem) {
+ Node node = ((SingleJcrNodeElem) element).getNode();
+ try {
+ if (node.isNodeType(NodeType.NT_FOLDER))
+ return 5;
+ } catch (RepositoryException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ return 10;
+ }
+
+ public int compare(TreeParent o1, TreeParent o2) {
+ int cat1 = category(o1);
+ int cat2 = category(o2);
+
+ if (cat1 != cat2) {
+ return cat1 - cat2;
+ }
+ return o1.getName().compareTo(o2.getName());
+ }
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.jcr;
+
+import javax.jcr.NamespaceException;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.ui.jcr.model.RemoteRepositoryElem;
+import org.argeo.cms.ui.jcr.model.RepositoriesElem;
+import org.argeo.cms.ui.jcr.model.RepositoryElem;
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ui.jcr.model.WorkspaceElem;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.swt.graphics.Image;
+
+/** Provides reasonable defaults for know JCR types. */
+public class NodeLabelProvider extends ColumnLabelProvider {
+ private static final long serialVersionUID = -3662051696443321843L;
+
+ private final static CmsLog log = CmsLog.getLog(NodeLabelProvider.class);
+
+ public String getText(Object element) {
+ try {
+ if (element instanceof SingleJcrNodeElem) {
+ SingleJcrNodeElem sjn = (SingleJcrNodeElem) element;
+ return getText(sjn.getNode());
+ } else if (element instanceof Node) {
+ return getText((Node) element);
+ } else
+ return super.getText(element);
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Unexpected JCR error while getting node name.");
+ }
+ }
+
+ protected String getText(Node node) throws RepositoryException {
+ String label = node.getName();
+ StringBuffer mixins = new StringBuffer("");
+ for (NodeType type : node.getMixinNodeTypes())
+ mixins.append(' ').append(type.getName());
+
+ return label + " [" + node.getPrimaryNodeType().getName() + mixins + "]";
+ }
+
+ @Override
+ public Image getImage(Object element) {
+ if (element instanceof RemoteRepositoryElem) {
+ if (((RemoteRepositoryElem) element).isConnected())
+ return JcrImages.REMOTE_CONNECTED;
+ else
+ return JcrImages.REMOTE_DISCONNECTED;
+ } else if (element instanceof RepositoryElem) {
+ if (((RepositoryElem) element).isConnected())
+ return JcrImages.REPOSITORY_CONNECTED;
+ else
+ return JcrImages.REPOSITORY_DISCONNECTED;
+ } else if (element instanceof WorkspaceElem) {
+ if (((WorkspaceElem) element).isConnected())
+ return JcrImages.WORKSPACE_CONNECTED;
+ else
+ return JcrImages.WORKSPACE_DISCONNECTED;
+ } else if (element instanceof RepositoriesElem) {
+ return JcrImages.REPOSITORIES;
+ } else if (element instanceof SingleJcrNodeElem) {
+ Node nodeElem = ((SingleJcrNodeElem) element).getNode();
+ return getImage(nodeElem);
+
+ // if (element instanceof Node) {
+ // return getImage((Node) element);
+ // } else if (element instanceof WrappedNode) {
+ // return getImage(((WrappedNode) element).getNode());
+ // } else if (element instanceof NodesWrapper) {
+ // return getImage(((NodesWrapper) element).getNode());
+ // }
+ }
+ // try {
+ // return super.getImage();
+ // } catch (RepositoryException e) {
+ // return null;
+ // }
+ return super.getImage(element);
+ }
+
+ protected Image getImage(Node node) {
+ try {
+ if (node.getPrimaryNodeType().isNodeType(NodeType.NT_FILE))
+ return JcrImages.FILE;
+ else if (node.getPrimaryNodeType().isNodeType(NodeType.NT_FOLDER))
+ return JcrImages.FOLDER;
+ else if (node.getPrimaryNodeType().isNodeType(NodeType.NT_RESOURCE))
+ return JcrImages.BINARY;
+ try {
+ // TODO check workspace type?
+ if (node.getDepth() == 1 && node.hasProperty(Property.JCR_ID))
+ return JcrImages.HOME;
+
+ // optimizes
+// if (node.hasProperty(LdapAttrs.uid.property()) && node.isNodeType(NodeTypes.NODE_USER_HOME))
+// return JcrImages.HOME;
+ } catch (NamespaceException e) {
+ // node namespace is not registered in this repo
+ }
+ return JcrImages.NODE;
+ } catch (RepositoryException e) {
+ log.warn("Error while retrieving type for " + node + " in order to display corresponding image");
+ e.printStackTrace();
+ return null;
+ }
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.jcr;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Repository;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+
+public class OsgiRepositoryRegister extends DefaultRepositoryRegister {
+ private final static BundleContext bc = FrameworkUtil.getBundle(OsgiRepositoryRegister.class).getBundleContext();
+ private final ServiceTracker<Repository, Repository> repositoryTracker;
+
+ public OsgiRepositoryRegister() {
+ repositoryTracker = new ServiceTracker<Repository, Repository>(bc, Repository.class, null) {
+
+ @Override
+ public Repository addingService(ServiceReference<Repository> reference) {
+
+ Repository repository = super.addingService(reference);
+ Map<String, Object> props = new HashMap<>();
+ for (String key : reference.getPropertyKeys()) {
+ props.put(key, reference.getProperty(key));
+ }
+ register(repository, props);
+ return repository;
+ }
+
+ @Override
+ public void removedService(ServiceReference<Repository> reference, Repository service) {
+ Map<String, Object> props = new HashMap<>();
+ for (String key : reference.getPropertyKeys()) {
+ props.put(key, reference.getProperty(key));
+ }
+ unregister(service, props);
+ super.removedService(reference, service);
+ }
+
+ };
+ }
+
+ public void init() {
+ repositoryTracker.open();
+ }
+
+ public void destroy() {
+ repositoryTracker.close();
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.jcr;
+
+import java.util.Set;
+import java.util.TreeSet;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.RepositoryException;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.jcr.util.JcrItemsComparator;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+/** Simple content provider that displays all properties of a given Node */
+public class PropertiesContentProvider implements IStructuredContentProvider {
+ private static final long serialVersionUID = 5227554668841613078L;
+ private JcrItemsComparator itemComparator = new JcrItemsComparator();
+
+ public void dispose() {
+ }
+
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ }
+
+ public Object[] getElements(Object inputElement) {
+ try {
+ if (inputElement instanceof Node) {
+ Set<Property> props = new TreeSet<Property>(itemComparator);
+ PropertyIterator pit = ((Node) inputElement).getProperties();
+ while (pit.hasNext())
+ props.add(pit.nextProperty());
+ return props.toArray();
+ }
+ return new Object[] {};
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot get element for "
+ + inputElement, e);
+ }
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.jcr;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+
+import org.argeo.cms.ui.CmsUiConstants;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.ViewerCell;
+
+/** Default basic label provider for a given JCR Node's properties */
+public class PropertyLabelProvider extends ColumnLabelProvider {
+ private static final long serialVersionUID = -5405794508731390147L;
+
+ // To be able to change column order easily
+ public static final int COLUMN_PROPERTY = 0;
+ public static final int COLUMN_VALUE = 1;
+ public static final int COLUMN_TYPE = 2;
+ public static final int COLUMN_ATTRIBUTES = 3;
+
+ // Utils
+ protected DateFormat timeFormatter = new SimpleDateFormat(CmsUiConstants.DATE_TIME_FORMAT);
+
+ public void update(ViewerCell cell) {
+ Object element = cell.getElement();
+ cell.setText(getColumnText(element, cell.getColumnIndex()));
+ }
+
+ public String getColumnText(Object element, int columnIndex) {
+ try {
+ if (element instanceof Property) {
+ Property prop = (Property) element;
+ if (prop.isMultiple()) {
+ switch (columnIndex) {
+ case COLUMN_PROPERTY:
+ return prop.getName();
+ case COLUMN_VALUE:
+ // Corresponding values are listed on children
+ return "";
+ case COLUMN_TYPE:
+ return JcrBrowserUtils.getPropertyTypeAsString(prop);
+ case COLUMN_ATTRIBUTES:
+ return JcrUtils.getPropertyDefinitionAsString(prop);
+ }
+ } else {
+ switch (columnIndex) {
+ case COLUMN_PROPERTY:
+ return prop.getName();
+ case COLUMN_VALUE:
+ return formatValueAsString(prop.getValue());
+ case COLUMN_TYPE:
+ return JcrBrowserUtils.getPropertyTypeAsString(prop);
+ case COLUMN_ATTRIBUTES:
+ return JcrUtils.getPropertyDefinitionAsString(prop);
+ }
+ }
+ } else if (element instanceof Value) {
+ Value val = (Value) element;
+ switch (columnIndex) {
+ case COLUMN_PROPERTY:
+ // Nothing to show
+ return "";
+ case COLUMN_VALUE:
+ return formatValueAsString(val);
+ case COLUMN_TYPE:
+ // listed on the parent
+ return "";
+ case COLUMN_ATTRIBUTES:
+ // Corresponding attributes are listed on the parent
+ return "";
+ }
+ }
+ } catch (RepositoryException re) {
+ throw new EclipseUiException("Cannot retrieve prop value on " + element, re);
+ }
+ return null;
+ }
+
+ private String formatValueAsString(Value value) {
+ // TODO enhance this method
+ try {
+ String strValue;
+
+ if (value.getType() == PropertyType.BINARY)
+ strValue = "<binary>";
+ else if (value.getType() == PropertyType.DATE)
+ strValue = timeFormatter.format(value.getDate().getTime());
+ else
+ strValue = value.getString();
+ return strValue;
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("unexpected error while formatting value", e);
+ }
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.jcr;
+
+import java.util.Map;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryFactory;
+
+/** Allows to register repositories by name. */
+public interface RepositoryRegister extends RepositoryFactory {
+ /**
+ * The registered {@link Repository} as a read-only map. Note that this
+ * method should be called for each access in order to be sure to be up to
+ * date in case repositories have registered/unregistered
+ */
+ public Map<String, Repository> getRepositories();
+}
--- /dev/null
+package org.argeo.cms.ui.jcr;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.version.Version;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+
+/**
+ * Simple wrapping of the ColumnLabelProvider class to provide text display in
+ * order to build a tree for version. The getText() method does not assume that
+ * {@link Version} extends {@link Node} class to respect JCR 2.0 specification
+ *
+ */
+public class VersionLabelProvider extends ColumnLabelProvider {
+ private static final long serialVersionUID = 5270739851193688238L;
+
+ public String getText(Object element) {
+ try {
+ if (element instanceof Version) {
+ Version version = (Version) element;
+ return version.getName();
+ } else if (element instanceof Node) {
+ return ((Node) element).getName();
+ }
+ } catch (RepositoryException re) {
+ throw new EclipseUiException(
+ "Unexpected error while getting element name", re);
+ }
+ return super.getText(element);
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.jcr.model;
+
+import javax.jcr.Repository;
+
+import org.argeo.cms.ux.widgets.TreeParent;
+
+/** Wrap a MaintainedRepository */
+public class MaintainedRepositoryElem extends RepositoryElem {
+
+ public MaintainedRepositoryElem(String alias, Repository repository, TreeParent parent) {
+ super(alias, repository, parent);
+ // if (!(repository instanceof MaintainedRepository)) {
+ // throw new ArgeoException("Repository " + alias
+ // + " is not a maintained repository");
+ // }
+ }
+
+ // protected MaintainedRepository getMaintainedRepository() {
+ // return (MaintainedRepository) getRepository();
+ // }
+}
--- /dev/null
+package org.argeo.cms.ui.jcr.model;
+
+import java.util.Arrays;
+
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.jcr.CmsJcrUtils;
+import org.argeo.cms.security.Keyring;
+import org.argeo.cms.ux.widgets.TreeParent;
+import org.argeo.eclipse.ui.EclipseUiException;
+
+/** Root of a remote repository */
+public class RemoteRepositoryElem extends RepositoryElem {
+ private final Keyring keyring;
+ /**
+ * A session of the logged in user on the default workspace of the node
+ * repository.
+ */
+ private final Session userSession;
+ private final String remoteNodePath;
+
+ private final RepositoryFactory repositoryFactory;
+ private final String uri;
+
+ public RemoteRepositoryElem(String alias, RepositoryFactory repositoryFactory, String uri, TreeParent parent,
+ Session userSession, Keyring keyring, String remoteNodePath) {
+ super(alias, null, parent);
+ this.repositoryFactory = repositoryFactory;
+ this.uri = uri;
+ this.keyring = keyring;
+ this.userSession = userSession;
+ this.remoteNodePath = remoteNodePath;
+ }
+
+ @Override
+ protected Session repositoryLogin(String workspaceName) throws RepositoryException {
+ Node remoteRepository = userSession.getNode(remoteNodePath);
+ String userID = remoteRepository.getProperty(ArgeoNames.ARGEO_USER_ID).getString();
+ if (userID.trim().equals("")) {
+ return getRepository().login(workspaceName);
+ } else {
+ String pwdPath = remoteRepository.getPath() + '/' + ArgeoNames.ARGEO_PASSWORD;
+ char[] password = keyring.getAsChars(pwdPath);
+ try {
+ SimpleCredentials credentials = new SimpleCredentials(userID, password);
+ return getRepository().login(credentials, workspaceName);
+ } finally {
+ Arrays.fill(password, 0, password.length, ' ');
+ }
+ }
+ }
+
+ @Override
+ public Repository getRepository() {
+ if (repository == null)
+ repository = CmsJcrUtils.getRepositoryByUri(repositoryFactory, uri);
+ return super.getRepository();
+ }
+
+ public void remove() {
+ try {
+ Node remoteNode = userSession.getNode(remoteNodePath);
+ remoteNode.remove();
+ remoteNode.getSession().save();
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot remove " + remoteNodePath, e);
+ }
+ }
+
+}
--- /dev/null
+package org.argeo.cms.ui.jcr.model;
+
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.jcr.CmsJcrUtils;
+import org.argeo.cms.security.Keyring;
+import org.argeo.cms.ui.jcr.RepositoryRegister;
+import org.argeo.cms.ux.widgets.TreeParent;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+
+/**
+ * UI Tree component that implements the Argeo abstraction of a
+ * {@link RepositoryFactory} that enable a user to "mount" various repositories
+ * in a single Tree like View. It is usually meant to be at the root of the UI
+ * Tree and thus {@link #getParent()} method will return null.
+ *
+ * The {@link RepositoryFactory} is injected at instantiation time and must be
+ * use get or register new {@link Repository} objects upon which a reference is
+ * kept here.
+ */
+
+public class RepositoriesElem extends TreeParent implements ArgeoNames {
+ private final RepositoryRegister repositoryRegister;
+ private final RepositoryFactory repositoryFactory;
+
+ /**
+ * A session of the logged in user on the default workspace of the node
+ * repository.
+ */
+ private final Session userSession;
+ private final Keyring keyring;
+
+ public RepositoriesElem(String name, RepositoryRegister repositoryRegister, RepositoryFactory repositoryFactory,
+ TreeParent parent, Session userSession, Keyring keyring) {
+ super(name);
+ this.repositoryRegister = repositoryRegister;
+ this.repositoryFactory = repositoryFactory;
+ this.userSession = userSession;
+ this.keyring = keyring;
+ }
+
+ /**
+ * Override normal behavior to initialize the various repositories only at
+ * request time
+ */
+ @Override
+ public synchronized Object[] getChildren() {
+ if (isLoaded()) {
+ return super.getChildren();
+ } else {
+ // initialize current object
+ Map<String, Repository> refRepos = repositoryRegister.getRepositories();
+ for (String name : refRepos.keySet()) {
+ Repository repository = refRepos.get(name);
+ // if (repository instanceof MaintainedRepository)
+ // super.addChild(new MaintainedRepositoryElem(name,
+ // repository, this));
+ // else
+ super.addChild(new RepositoryElem(name, repository, this));
+ }
+
+ // remote
+ if (keyring != null) {
+ try {
+ addRemoteRepositories(keyring);
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot browse remote repositories", e);
+ }
+ }
+ return super.getChildren();
+ }
+ }
+
+ protected void addRemoteRepositories(Keyring jcrKeyring) throws RepositoryException {
+ Node userHome = CmsJcrUtils.getUserHome(userSession);
+ if (userHome != null && userHome.hasNode(ARGEO_REMOTE)) {
+ NodeIterator it = userHome.getNode(ARGEO_REMOTE).getNodes();
+ while (it.hasNext()) {
+ Node remoteNode = it.nextNode();
+ String uri = remoteNode.getProperty(ARGEO_URI).getString();
+ try {
+ RemoteRepositoryElem remoteRepositoryNode = new RemoteRepositoryElem(remoteNode.getName(),
+ repositoryFactory, uri, this, userSession, jcrKeyring, remoteNode.getPath());
+ super.addChild(remoteRepositoryNode);
+ } catch (Exception e) {
+ ErrorFeedback.show("Cannot add remote repository " + remoteNode, e);
+ }
+ }
+ }
+ }
+
+ public void registerNewRepository(String alias, Repository repository) {
+ // TODO: implement this
+ // Create a new RepositoryNode Object
+ // add it
+ // super.addChild(new RepositoriesNode(...));
+ }
+
+ /** Returns the {@link RepositoryRegister} wrapped by this object. */
+ public RepositoryRegister getRepositoryRegister() {
+ return repositoryRegister;
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.jcr.model;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.cms.ux.widgets.TreeParent;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.jcr.JcrUtils;
+
+/**
+ * UI Tree component that wraps a JCR {@link Repository}. It also keeps a
+ * reference to its parent Tree Ui component; typically the unique
+ * {@link RepositoriesElem} object of the current view to enable bi-directionnal
+ * browsing in the tree.
+ */
+
+public class RepositoryElem extends TreeParent {
+ private String alias;
+ protected Repository repository;
+ private Session defaultSession = null;
+
+ /** Create a new repository with distinct name and alias */
+ public RepositoryElem(String alias, Repository repository, TreeParent parent) {
+ super(alias);
+ this.repository = repository;
+ setParent(parent);
+ this.alias = alias;
+ }
+
+ public void login() {
+ try {
+ defaultSession = repositoryLogin(CmsConstants.SYS_WORKSPACE);
+ String[] wkpNames = defaultSession.getWorkspace().getAccessibleWorkspaceNames();
+ for (String wkpName : wkpNames) {
+ if (wkpName.equals(defaultSession.getWorkspace().getName()))
+ addChild(new WorkspaceElem(this, wkpName, defaultSession));
+ else
+ addChild(new WorkspaceElem(this, wkpName));
+ }
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot connect to repository " + alias, e);
+ }
+ }
+
+ public synchronized void logout() {
+ for (Object child : getChildren()) {
+ if (child instanceof WorkspaceElem)
+ ((WorkspaceElem) child).logout();
+ }
+ clearChildren();
+ JcrUtils.logoutQuietly(defaultSession);
+ defaultSession = null;
+ }
+
+ /**
+ * Actual call to the {@link Repository#login(javax.jcr.Credentials, String)}
+ * method. To be overridden.
+ */
+ protected Session repositoryLogin(String workspaceName) throws RepositoryException {
+ return repository.login(workspaceName);
+ }
+
+ public String[] getAccessibleWorkspaceNames() {
+ try {
+ return defaultSession.getWorkspace().getAccessibleWorkspaceNames();
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot retrieve workspace names", e);
+ }
+ }
+
+ public void createWorkspace(String workspaceName) {
+ if (!isConnected())
+ login();
+ try {
+ defaultSession.getWorkspace().createWorkspace(workspaceName);
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot create workspace", e);
+ }
+ }
+
+ /** returns the {@link Repository} referenced by the current UI Node */
+ public Repository getRepository() {
+ return repository;
+ }
+
+ public String getAlias() {
+ return alias;
+ }
+
+ public Boolean isConnected() {
+ if (defaultSession != null && defaultSession.isLive())
+ return true;
+ else
+ return false;
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.jcr.model;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.Workspace;
+
+import org.argeo.cms.ux.widgets.TreeParent;
+import org.argeo.eclipse.ui.EclipseUiException;
+
+/**
+ * UI Tree component. Wraps a node of a JCR {@link Workspace}. It also keeps a
+ * reference to its parent node that can either be a {@link WorkspaceElem}, a
+ * {@link SingleJcrNodeElem} or null if the node is "mounted" as the root of the
+ * UI tree.
+ */
+public class SingleJcrNodeElem extends TreeParent {
+
+ private final Node node;
+ private String alias = null;
+
+ /** Creates a new UiNode in the UI Tree */
+ public SingleJcrNodeElem(TreeParent parent, Node node, String name) {
+ super(name);
+ setParent(parent);
+ this.node = node;
+ }
+
+ /**
+ * Creates a new UiNode in the UI Tree, keeping a reference to the alias of
+ * the corresponding repository in the current UI environment. It is useful
+ * to be able to mount nodes as roots of the UI tree.
+ */
+ public SingleJcrNodeElem(TreeParent parent, Node node, String name, String alias) {
+ super(name);
+ setParent(parent);
+ this.node = node;
+ this.alias = alias;
+ }
+
+ /** Returns the node wrapped by the current UI object */
+ public Node getNode() {
+ return node;
+ }
+
+ protected String getRepositoryAlias() {
+ return alias;
+ }
+
+ /**
+ * Overrides normal behaviour to initialise children only when first
+ * requested
+ */
+ @Override
+ public synchronized Object[] getChildren() {
+ if (isLoaded()) {
+ return super.getChildren();
+ } else {
+ // initialize current object
+ try {
+ NodeIterator ni = node.getNodes();
+ while (ni.hasNext()) {
+ Node curNode = ni.nextNode();
+ addChild(new SingleJcrNodeElem(this, curNode, curNode.getName()));
+ }
+ return super.getChildren();
+ } catch (RepositoryException re) {
+ throw new EclipseUiException("Cannot initialize SingleJcrNode children", re);
+ }
+ }
+ }
+
+ @Override
+ public boolean hasChildren() {
+ try {
+ if (node.getSession().isLive())
+ return node.hasNodes();
+ else
+ return false;
+ } catch (RepositoryException re) {
+ throw new EclipseUiException("Cannot check children node existence", re);
+ }
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.jcr.model;
+
+import javax.jcr.AccessDeniedException;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+// import javax.jcr.Workspace;
+import javax.jcr.Workspace;
+
+import org.argeo.cms.ux.widgets.TreeParent;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.jcr.JcrUtils;
+
+/**
+ * UI Tree component. Wraps the root node of a JCR {@link Workspace}. It also
+ * keeps a reference to its parent {@link RepositoryElem}, to be able to
+ * retrieve alias of the current used repository
+ */
+public class WorkspaceElem extends TreeParent {
+ private Session session = null;
+
+ public WorkspaceElem(RepositoryElem parent, String name) {
+ this(parent, name, null);
+ }
+
+ public WorkspaceElem(RepositoryElem parent, String name, Session session) {
+ super(name);
+ this.session = session;
+ setParent(parent);
+ }
+
+ public synchronized Session getSession() {
+ return session;
+ }
+
+ public synchronized Node getRootNode() {
+ try {
+ if (session != null)
+ return session.getRootNode();
+ else
+ return null;
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot get root node of workspace " + getName(), e);
+ }
+ }
+
+ public synchronized void login() {
+ try {
+ session = ((RepositoryElem) getParent()).repositoryLogin(getName());
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot connect to repository " + getName(), e);
+ }
+ }
+
+ public Boolean isConnected() {
+ if (session != null && session.isLive())
+ return true;
+ else
+ return false;
+ }
+
+ @Override
+ public synchronized void dispose() {
+ logout();
+ super.dispose();
+ }
+
+ /** Logouts the session, does not nothing if there is no live session. */
+ public synchronized void logout() {
+ clearChildren();
+ JcrUtils.logoutQuietly(session);
+ session = null;
+ }
+
+ @Override
+ public synchronized boolean hasChildren() {
+ try {
+ if (isConnected())
+ try {
+ return session.getRootNode().hasNodes();
+ } catch (AccessDeniedException e) {
+ // current user may not have access to the root node
+ return false;
+ }
+ else
+ return false;
+ } catch (RepositoryException re) {
+ throw new EclipseUiException("Unexpected error while checking children node existence", re);
+ }
+ }
+
+ /** Override normal behaviour to initialize display of the workspace */
+ @Override
+ public synchronized Object[] getChildren() {
+ if (isLoaded()) {
+ return super.getChildren();
+ } else {
+ // initialize current object
+ try {
+ Node rootNode;
+ if (session == null)
+ return null;
+ else
+ rootNode = session.getRootNode();
+ NodeIterator ni = rootNode.getNodes();
+ while (ni.hasNext()) {
+ Node node = ni.nextNode();
+ addChild(new SingleJcrNodeElem(this, node, node.getName()));
+ }
+ return super.getChildren();
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot initialize WorkspaceNode UI object." + getName(), e);
+ }
+ }
+ }
+}
--- /dev/null
+/** Model for SWT/JFace JCR components. */
+package org.argeo.cms.ui.jcr.model;
\ No newline at end of file
--- /dev/null
+/** SWT/JFace JCR components. */
+package org.argeo.cms.ui.jcr;
\ No newline at end of file
--- /dev/null
+/** SWT/JFace components for Argeo CMS. */
+package org.argeo.cms.ui;
\ No newline at end of file
--- /dev/null
+package org.argeo.cms.ui.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.api.cms.ux.CmsStyle;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.jcr.CmsJcrUtils;
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.cms.ui.CmsUiProvider;
+import org.argeo.jcr.JcrException;
+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;
+
+/** A link to an internal or external location. */
+public class CmsLink implements CmsUiProvider {
+ private final static CmsLog log = CmsLog.getLog(CmsLink.class);
+ private BundleContext bundleContext;
+
+ private String label;
+ private String style;
+ private String target;
+ private String image;
+ private boolean openNew = false;
+ private MouseListener mouseListener;
+
+ private int horizontalAlignment = SWT.CENTER;
+ private int verticalAlignment = SWT.CENTER;
+
+ private String loggedInLabel = null;
+ private String loggedInTarget = null;
+
+ // internal
+ // private Boolean isUrl = false;
+ private Integer imageWidth, imageHeight;
+
+ public CmsLink() {
+ super();
+ }
+
+ public CmsLink(String label, String target) {
+ this(label, target, (String) null);
+ }
+
+ public CmsLink(String label, String target, CmsStyle style) {
+ this(label, target, style != null ? style.style() : null);
+ }
+
+ public CmsLink(String label, String target, String style) {
+ super();
+ this.label = label;
+ this.target = target;
+ this.style = style;
+ init();
+ }
+
+ public void init() {
+ if (image != null) {
+ ImageData image = loadImage();
+ if (imageHeight == null && imageWidth == null) {
+ imageWidth = image.width;
+ imageHeight = image.height;
+ } else if (imageHeight == null) {
+ imageHeight = (imageWidth * image.height) / image.width;
+ } else if (imageWidth == null) {
+ imageWidth = (imageHeight * image.width) / image.height;
+ }
+ }
+ }
+
+ /** @return {@link Composite} with a single {@link Label} child. */
+ @Override
+ public Control createUi(final Composite parent, Node context) {
+// if (image != null && (imageWidth == null || imageHeight == null)) {
+// throw new CmsException("Image is not properly configured."
+// + " Make sure bundleContext property is set and init() method has been called.");
+// }
+
+ Composite comp = new Composite(parent, SWT.NONE);
+ comp.setLayout(CmsSwtUtils.noSpaceGridLayout());
+
+ Label link = new Label(comp, SWT.NONE);
+ CmsSwtUtils.markup(link);
+ GridData layoutData = new GridData(horizontalAlignment, verticalAlignment, false, false);
+ if (image != null) {
+ if (imageHeight != null)
+ layoutData.heightHint = imageHeight;
+ if (label == null)
+ if (imageWidth != null)
+ layoutData.widthHint = imageWidth;
+ }
+
+ link.setLayoutData(layoutData);
+ CmsSwtUtils.style(comp, style != null ? style : getDefaultStyle());
+ CmsSwtUtils.style(link, style != null ? style : getDefaultStyle());
+
+ // label
+ StringBuilder labelText = new StringBuilder();
+ if (loggedInTarget != null && isLoggedIn()) {
+ labelText.append("<a style='color:inherit;text-decoration:inherit;' href=\"");
+ if (loggedInTarget.equals("")) {
+ try {
+ Node homeNode = CmsJcrUtils.getUserHome(context.getSession());
+ String homePath = homeNode.getPath();
+ labelText.append("/#" + homePath);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get home path", e);
+ }
+ } else {
+ labelText.append(loggedInTarget);
+ }
+ labelText.append("\">");
+ } else if (target != null) {
+ labelText.append("<a style='color:inherit;text-decoration:inherit;' href='");
+ labelText.append(target).append("'");
+ if (openNew) {
+ labelText.append(" target='_blank'");
+ }
+ labelText.append(">");
+ }
+ if (image != null) {
+ registerImageIfNeeded();
+ String imageLocation = RWT.getResourceManager().getLocation(image);
+ labelText.append("<img");
+ if (imageWidth != null)
+ labelText.append(" width='").append(imageWidth).append('\'');
+ if (imageHeight != null)
+ labelText.append(" height='").append(imageHeight).append('\'');
+ labelText.append(" src=\"").append(imageLocation).append("\"/>");
+
+ }
+
+ if (loggedInLabel != null && isLoggedIn()) {
+ labelText.append(' ').append(loggedInLabel);
+ } else if (label != null) {
+ labelText.append(' ').append(label);
+ }
+
+ if ((loggedInTarget != null && isLoggedIn()) || target != null)
+ labelText.append("</a>");
+
+ link.setText(labelText.toString());
+
+ if (mouseListener != null)
+ link.addMouseListener(mouseListener);
+
+ return comp;
+ }
+
+ private void registerImageIfNeeded() {
+ ResourceManager resourceManager = RWT.getResourceManager();
+ if (!resourceManager.isRegistered(image)) {
+ URL res = getImageUrl();
+ try (InputStream inputStream = res.openStream()) {
+ resourceManager.register(image, inputStream);
+ if (log.isTraceEnabled())
+ log.trace("Registered image " + image);
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot load image " + image, e);
+ }
+ }
+ }
+
+ private ImageData loadImage() {
+ URL url = getImageUrl();
+ ImageData result = null;
+ try (InputStream inputStream = url.openStream()) {
+ result = new ImageData(inputStream);
+ if (log.isTraceEnabled())
+ log.trace("Loaded image " + image);
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot load image " + image, e);
+ }
+ return result;
+ }
+
+ private URL getImageUrl() {
+ URL url;
+ try {
+ // pure URL
+ url = new URL(image);
+ } catch (MalformedURLException e1) {
+ url = bundleContext.getBundle().getResource(image);
+ }
+
+ if (url == null)
+ throw new IllegalStateException("No image " + image + " available.");
+
+ return url;
+ }
+
+ public void setBundleContext(BundleContext bundleContext) {
+ this.bundleContext = bundleContext;
+ }
+
+ public void setLabel(String label) {
+ this.label = label;
+ }
+
+ public void setStyle(String style) {
+ this.style = style;
+ }
+
+ /** @deprecated Use {@link #setStyle(String)} instead. */
+ @Deprecated
+ public void setCustom(String custom) {
+ this.style = 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;
+ }
+
+ public void setLoggedInLabel(String loggedInLabel) {
+ this.loggedInLabel = loggedInLabel;
+ }
+
+ public void setLoggedInTarget(String loggedInTarget) {
+ this.loggedInTarget = loggedInTarget;
+ }
+
+ 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 IllegalArgumentException(
+ "Unsupported vertical alignment " + vAlign + " (must be: top, bottom or center)");
+ }
+ }
+
+ protected boolean isLoggedIn() {
+ return !CurrentUser.isAnonymous();
+ }
+
+ public void setImageWidth(Integer imageWidth) {
+ this.imageWidth = imageWidth;
+ }
+
+ public void setImageHeight(Integer imageHeight) {
+ this.imageHeight = imageHeight;
+ }
+
+ public void setOpenNew(boolean openNew) {
+ this.openNew = openNew;
+ }
+
+ protected String getDefaultStyle() {
+ return SimpleStyle.link.name();
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.util;
+
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Composite;
+
+/** The main pane of a CMS display, with QA and support areas. */
+public class CmsPane {
+
+ private Composite mainArea;
+ private Composite qaArea;
+ private Composite supportArea;
+
+ public CmsPane(Composite parent, int style) {
+ parent.setLayout(CmsSwtUtils.noSpaceGridLayout());
+
+// qaArea = new Composite(parent, SWT.NONE);
+// qaArea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+// RowLayout qaLayout = new RowLayout();
+// qaLayout.spacing = 0;
+// qaArea.setLayout(qaLayout);
+
+ mainArea = new Composite(parent, SWT.NONE);
+ mainArea.setLayout(new GridLayout());
+ mainArea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+// supportArea = new Composite(parent, SWT.NONE);
+// supportArea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+// RowLayout supportLayout = new RowLayout();
+// supportLayout.spacing = 0;
+// supportArea.setLayout(supportLayout);
+ }
+
+ public Composite getMainArea() {
+ return mainArea;
+ }
+
+ public Composite getQaArea() {
+ return qaArea;
+ }
+
+ public Composite getSupportArea() {
+ return supportArea;
+ }
+
+}
--- /dev/null
+package org.argeo.cms.ui.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.servlet.http.HttpServletRequest;
+
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.api.cms.ux.Cms2DSize;
+import org.argeo.api.cms.ux.CmsView;
+import org.argeo.cms.jcr.CmsJcrUtils;
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.cms.ui.CmsUiConstants;
+import org.argeo.cms.ux.AbstractImageManager;
+import org.argeo.cms.ux.CmsUxUtils;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.service.ResourceManager;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.layout.RowData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Table;
+
+/** Static utilities for the CMS framework. */
+public class CmsUiUtils {
+ // private final static Log log = LogFactory.getLog(CmsUiUtils.class);
+
+ /*
+ * CMS VIEW
+ */
+
+ /**
+ * The CMS view related to this display, or null if none is available from this
+ * call.
+ *
+ * @deprecated Use {@link CmsSwtUtils#getCmsView(Composite)} instead.
+ */
+ @Deprecated
+ public static CmsView getCmsView() {
+// return UiContext.getData(CmsView.class.getName());
+ return CmsSwtUtils.getCmsView(Display.getCurrent().getActiveShell());
+ }
+
+ public static StringBuilder getServerBaseUrl(HttpServletRequest request) {
+ try {
+ URL url = new URL(request.getRequestURL().toString());
+ StringBuilder buf = new StringBuilder();
+ buf.append(url.getProtocol()).append("://").append(url.getHost());
+ if (url.getPort() != -1)
+ buf.append(':').append(url.getPort());
+ return buf;
+ } catch (MalformedURLException e) {
+ throw new IllegalArgumentException("Cannot extract server base URL from " + request.getRequestURL(), e);
+ }
+ }
+
+ //
+ public static String getDataUrl(Node node, HttpServletRequest request) {
+ try {
+ StringBuilder buf = getServerBaseUrl(request);
+ buf.append(getDataPath(node));
+ return new URL(buf.toString()).toString();
+ } catch (MalformedURLException e) {
+ throw new IllegalArgumentException("Cannot build data URL for " + node, e);
+ }
+ }
+
+ /** A path in the node repository */
+ public static String getDataPath(Node node) {
+ return getDataPath(CmsConstants.EGO_REPOSITORY, node);
+ }
+
+ public static String getDataPath(String cn, Node node) {
+ return CmsJcrUtils.getDataPath(cn, node);
+ }
+
+ /** Clean reserved URL characters for use in HTTP links. */
+ public static String getDataPathForUrl(Node node) {
+ return CmsSwtUtils.cleanPathForUrl(getDataPath(node));
+ }
+
+ /** @deprecated Use rowData16px() instead. GridData should not be reused. */
+ @Deprecated
+ public static RowData ROW_DATA_16px = new RowData(16, 16);
+
+
+
+ /*
+ * FORM LAYOUT
+ */
+
+
+
+ @Deprecated
+ public static void setItemHeight(Table table, int height) {
+ table.setData(CmsUiConstants.ITEM_HEIGHT, height);
+ }
+
+ //
+ // 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(Node fileNode, String width, String height) {
+ return img(null, fileNode, width, height);
+ }
+
+ public static String img(String serverBase, Node fileNode, String width, String height) {
+// String src = (serverBase != null ? serverBase : "") + NodeUtils.getDataPath(fileNode);
+ String src;
+ src = (serverBase != null ? serverBase : "") + getDataPathForUrl(fileNode);
+ return CmsUxUtils.imgBuilder(src, width, height).append("/>").toString();
+ }
+
+ public static String noImg(Cms2DSize size) {
+ ResourceManager rm = RWT.getResourceManager();
+ return CmsUxUtils.img(rm.getLocation(AbstractImageManager.NO_IMAGE), size);
+ }
+
+ public static String noImg() {
+ return noImg(AbstractImageManager.NO_IMAGE_SIZE);
+ }
+
+// public static Image noImage(Cms2DSize size) {
+// ResourceManager rm = RWT.getResourceManager();
+// InputStream in = null;
+// try {
+// in = rm.getRegisteredContent(AbstractImageManager.NO_IMAGE);
+// ImageData id = new ImageData(in);
+// ImageData scaled = id.scaledTo(size.getWidth(), size.getHeight());
+// Image image = new Image(Display.getCurrent(), scaled);
+// return image;
+// } finally {
+// try {
+// in.close();
+// } catch (IOException e) {
+// // silent
+// }
+// }
+// }
+
+ /** Lorem ipsum text to be used during development. */
+ public final static String LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
+ + " Etiam eleifend hendrerit sem, ac ultricies massa ornare ac."
+ + " Cras aliquam sodales risus, vitae varius lacus molestie quis."
+ + " Vivamus consequat, leo id lacinia volutpat, eros diam efficitur urna, finibus interdum risus turpis at nisi."
+ + " Curabitur vulputate nulla quis scelerisque fringilla. Integer consectetur turpis id lobortis accumsan."
+ + " Pellentesque commodo turpis ac diam ultricies dignissim."
+ + " Curabitur sit amet dolor volutpat lacus aliquam ornare quis sed velit."
+ + " Integer varius quis est et tristique."
+ + " Suspendisse pharetra porttitor purus, eget condimentum magna."
+ + " Duis vitae turpis eros. Sed tincidunt lacinia rutrum."
+ + " Aliquam velit velit, rutrum ut augue sed, condimentum lacinia augue.";
+
+ /** Singleton. */
+ private CmsUiUtils() {
+ }
+
+}
--- /dev/null
+package org.argeo.cms.ui.util;
+
+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 java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.api.cms.ux.Cms2DSize;
+import org.argeo.cms.swt.AbstractSwtImageManager;
+import org.argeo.jcr.JcrException;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.service.ResourceManager;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.widgets.Display;
+
+/** Manages only public images so far. */
+public class DefaultImageManager extends AbstractSwtImageManager<Node> {
+ private final static CmsLog log = CmsLog.getLog(DefaultImageManager.class);
+
+ /** @return null if not available */
+ @Override
+ public String getImageUrl(Node node) {
+ return CmsUiUtils.getDataPathForUrl(node);
+ }
+
+ protected String getResourceName(Node node) {
+ try {
+ String workspace = node.getSession().getWorkspace().getName();
+ if (node.hasNode(JCR_CONTENT))
+ return workspace + '_' + node.getNode(JCR_CONTENT).getIdentifier();
+ else
+ return workspace + '_' + node.getIdentifier();
+ } catch (RepositoryException e) {
+ throw new JcrException(e);
+ }
+ }
+
+ public Binary getImageBinary(Node node) {
+ try {
+ if (node.isNodeType(NT_FILE)) {
+ return node.getNode(JCR_CONTENT).getProperty(JCR_DATA).getBinary();
+ } else {
+ return null;
+ }
+ } catch (RepositoryException e) {
+ throw new JcrException(e);
+ }
+ }
+
+ public Image getSwtImage(Node node) {
+ InputStream inputStream = null;
+ Binary binary = getImageBinary(node);
+ if (binary == null)
+ return null;
+ try {
+ inputStream = binary.getStream();
+ return new Image(Display.getCurrent(), inputStream);
+ } catch (RepositoryException e) {
+ throw new JcrException(e);
+ } finally {
+ IOUtils.closeQuietly(inputStream);
+ JcrUtils.closeQuietly(binary);
+ }
+ }
+
+ @Override
+ public String uploadImage(Node context, Node parentNode, String fileName, InputStream in, String contentType) {
+ 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);
+ inputStream = new ByteArrayInputStream(arr);
+ ImageData id = new ImageData(inputStream);
+ processNewImageFile(context, fileNode, id);
+
+ String mime = contentType != null ? contentType : Files.probeContentType(Paths.get(fileName));
+ if (mime != null) {
+ fileNode.getNode(JCR_CONTENT).setProperty(Property.JCR_MIMETYPE, mime);
+ }
+ fileNode.getSession().save();
+
+ // reset resource manager
+ ResourceManager resourceManager = RWT.getResourceManager();
+ if (previousResourceName != null && resourceManager.isRegistered(previousResourceName)) {
+ resourceManager.unregister(previousResourceName);
+ if (log.isDebugEnabled())
+ log.debug("Unregistered image " + previousResourceName);
+ }
+ return CmsUiUtils.getDataPath(fileNode);
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot upload image " + fileName + " in " + parentNode, e);
+ } catch (RepositoryException e) {
+ throw new JcrException(e);
+ } finally {
+ IOUtils.closeQuietly(inputStream);
+ }
+ }
+
+ /** Does nothing by default. */
+ protected void processNewImageFile(Node context, Node fileNode, ImageData id)
+ throws RepositoryException, IOException {
+ }
+
+ @Override
+ protected String noImg(Cms2DSize size) {
+ return CmsUiUtils.noImg(size);
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.util;
+
+import org.argeo.cms.swt.CmsStyles;
+
+/**
+ * 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);
+ }
+
+ public MenuLink(String label, String target, String custom) {
+ super(label, target, custom);
+ }
+
+ public MenuLink(String label, String target) {
+ super(label, target, CmsStyles.CMS_MENU_LINK);
+ }
+
+}
--- /dev/null
+package org.argeo.cms.ui.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.swt.CmsException;
+import org.argeo.cms.swt.CmsStyles;
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.cms.ui.CmsUiProvider;
+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(CmsSwtUtils.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(CmsSwtUtils.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;
+ }
+
+ public List<CmsUiProvider> getLead() {
+ return lead;
+ }
+
+ public List<CmsUiProvider> getCenter() {
+ return center;
+ }
+
+ public List<CmsUiProvider> getEnd() {
+ return end;
+ }
+
+}
--- /dev/null
+package org.argeo.cms.ui.util;
+
+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.cms.swt.CmsException;
+import org.argeo.cms.ui.CmsUiProvider;
+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
--- /dev/null
+package org.argeo.cms.ui.util;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.swt.CmsStyles;
+import org.argeo.cms.ui.CmsUiProvider;
+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;
+ }
+
+}
--- /dev/null
+package org.argeo.cms.ui.util;
+
+import org.argeo.api.cms.ux.CmsStyle;
+
+/** Simple styles used by the CMS UI utilities. */
+public enum SimpleStyle implements CmsStyle {
+ link;
+}
--- /dev/null
+package org.argeo.cms.ui.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.cms.swt.CmsException;
+import org.eclipse.rap.rwt.service.ResourceLoader;
+import org.osgi.framework.Bundle;
+
+/** {@link ResourceLoader} caching stylesheets. */
+public class StyleSheetResourceLoader implements ResourceLoader {
+ private Bundle themeBundle;
+ private Map<String, StyleSheet> stylesheets = new LinkedHashMap<String, StyleSheet>();
+
+ public StyleSheetResourceLoader(Bundle themeBundle) {
+ this.themeBundle = themeBundle;
+ }
+
+ @Override
+ public InputStream getResourceAsStream(String resourceName) throws IOException {
+ if (!stylesheets.containsKey(resourceName)) {
+ // TODO deal with other bundles
+ // Bundle bundle = bundleContext.getBundle();
+ // String location =
+ // bundle.getLocation().substring("initial@reference:".length());
+ // if (location.startsWith("file:")) {
+ // Path path = null;
+ // try {
+ // path = Paths.get(new URI(location));
+ // } catch (URISyntaxException e) {
+ // e.printStackTrace();
+ // }
+ // if (path != null) {
+ // Path resourcePath = path.resolve(resourceName);
+ // if (Files.exists(resourcePath))
+ // return Files.newInputStream(resourcePath);
+ // }
+ // }
+
+ URL res = themeBundle.getEntry(resourceName);
+ if (res == null)
+ throw new CmsException(
+ "Entry " + resourceName + " not found in bundle " + themeBundle.getSymbolicName());
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ IOUtils.copy(res.openStream(), out);
+ stylesheets.put(resourceName, new StyleSheet(out.toByteArray()));
+ }
+ return new ByteArrayInputStream(stylesheets.get(resourceName).getData());
+ // return res.openStream();
+ }
+
+ private class StyleSheet {
+ private byte[] data;
+
+ public StyleSheet(byte[] data) {
+ super();
+ this.data = data;
+ }
+
+ public byte[] getData() {
+ return data;
+ }
+
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.util;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.cms.swt.CmsException;
+import org.argeo.cms.swt.CmsStyles;
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.ShellAdapter;
+import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+/** Shell displaying system notifications such as exceptions */
+public class SystemNotifications extends Shell implements CmsStyles,
+ MouseListener {
+ private static final long serialVersionUID = -8129377525216022683L;
+
+ private Control source;
+
+ public SystemNotifications(Control source) {
+ super(source.getDisplay(), SWT.NO_TRIM | SWT.BORDER | SWT.ON_TOP);
+ setData(RWT.CUSTOM_VARIANT, CMS_USER_MENU);
+
+ this.source = source;
+
+ // TODO UI
+ // setLocation(source.toDisplay(source.getSize().x - getSize().x,
+ // source.getSize().y));
+ setLayout(new GridLayout());
+ addMouseListener(this);
+
+ addShellListener(new ShellAdapter() {
+ private static final long serialVersionUID = 5178980294808435833L;
+
+ @Override
+ public void shellDeactivated(ShellEvent e) {
+ close();
+ dispose();
+ }
+ });
+
+ }
+
+ public void notifyException(Throwable exception) {
+ Composite pane = this;
+
+ Label lbl = new Label(pane, SWT.NONE);
+ lbl.setText(exception.getLocalizedMessage()
+ + (exception instanceof CmsException ? "" : "("
+ + exception.getClass().getName() + ")") + "\n");
+ lbl.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+ lbl.addMouseListener(this);
+ if (exception.getCause() != null)
+ appendCause(pane, exception.getCause());
+
+ StringBuilder mailToUrl = new StringBuilder("mailto:?");
+ try {
+ mailToUrl.append("subject=").append(
+ URLEncoder.encode(
+ "Exception "
+ + new SimpleDateFormat("yyyy-MM-dd hh:mm")
+ .format(new Date()), "UTF-8")
+ .replace("+", "%20"));
+
+ StringWriter sw = new StringWriter();
+ exception.printStackTrace(new PrintWriter(sw));
+ IOUtils.closeQuietly(sw);
+
+ // see
+ // http://stackoverflow.com/questions/4737841/urlencoder-not-able-to-translate-space-character
+ String encoded = URLEncoder.encode(sw.toString(), "UTF-8").replace(
+ "+", "%20");
+ mailToUrl.append("&body=").append(encoded);
+ } catch (UnsupportedEncodingException e) {
+ mailToUrl.append("&body=").append("Could not encode: ")
+ .append(e.getMessage());
+ }
+ Label mailTo = new Label(pane, SWT.NONE);
+ CmsSwtUtils.markup(mailTo);
+ mailTo.setText("<a href=\"" + mailToUrl + "\">Send details</a>");
+ mailTo.setLayoutData(new GridData(SWT.END, SWT.FILL, true, false));
+
+ pack();
+ layout();
+
+ setLocation(source.toDisplay(source.getSize().x - getSize().x,
+ source.getSize().y - getSize().y));
+ open();
+ }
+
+ private void appendCause(Composite parent, Throwable e) {
+ Label lbl = new Label(parent, SWT.NONE);
+ lbl.setText(" caused by: " + e.getLocalizedMessage() + " ("
+ + e.getClass().getName() + ")" + "\n");
+ lbl.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+ lbl.addMouseListener(this);
+ if (e.getCause() != null)
+ appendCause(parent, e.getCause());
+ }
+
+ @Override
+ public void mouseDoubleClick(MouseEvent e) {
+ }
+
+ @Override
+ public void mouseDown(MouseEvent e) {
+ close();
+ dispose();
+ }
+
+ @Override
+ public void mouseUp(MouseEvent e) {
+ }
+
+}
--- /dev/null
+package org.argeo.cms.ui.util;
+
+import javax.jcr.Node;
+
+import org.argeo.cms.swt.CmsException;
+import org.argeo.cms.swt.auth.CmsLoginShell;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ShellAdapter;
+import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+/** The site-related user menu */
+public class UserMenu extends CmsLoginShell {
+ private final Control source;
+ private final Node context;
+
+ public UserMenu(Control source, Node context) {
+ // FIXME pass CMS context
+ super(CmsUiUtils.getCmsView(), null);
+ this.context = context;
+ createUi();
+ if (source == null)
+ throw new CmsException("Source control cannot be null.");
+ this.source = source;
+ open();
+ }
+
+ @Override
+ protected Shell createShell() {
+ return new Shell(Display.getCurrent(), SWT.NO_TRIM | SWT.BORDER | SWT.ON_TOP);
+ }
+
+ @Override
+ public void open() {
+ Shell shell = getShell();
+ shell.pack();
+ shell.layout();
+ shell.setLocation(source.toDisplay(source.getSize().x - shell.getSize().x, source.getSize().y));
+ shell.addShellListener(new ShellAdapter() {
+ private static final long serialVersionUID = 5178980294808435833L;
+
+ @Override
+ public void shellDeactivated(ShellEvent e) {
+ closeShell();
+ }
+ });
+ super.open();
+ }
+
+ protected Node getContext() {
+ return context;
+ }
+
+}
--- /dev/null
+package org.argeo.cms.ui.util;
+
+import javax.jcr.Node;
+
+import org.argeo.cms.CmsMsg;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.swt.CmsStyles;
+import org.argeo.cms.swt.auth.CmsLoginShell;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+
+/** Open the user menu when clicked */
+public class UserMenuLink extends MenuLink {
+
+ public UserMenuLink() {
+ setCustom(CmsStyles.CMS_USER_MENU_LINK);
+ }
+
+ @Override
+ public Control createUi(Composite parent, Node context) {
+ if (CurrentUser.isAnonymous())
+ setLabel(CmsMsg.login.lead());
+ else {
+ setLabel(CurrentUser.getDisplayName());
+ }
+ Label link = (Label) ((Composite) super.createUi(parent, context)).getChildren()[0];
+ link.addMouseListener(new UserMenuLinkController(context));
+ return link.getParent();
+ }
+
+ protected CmsLoginShell createUserMenu(Control source, Node context) {
+ return new UserMenu(source.getParent(), context);
+ }
+
+ private class UserMenuLinkController implements MouseListener, DisposeListener {
+ private static final long serialVersionUID = 3634864186295639792L;
+
+ private CmsLoginShell userMenu = null;
+ private long lastDisposeTS = 0l;
+
+ private final Node context;
+
+ public UserMenuLinkController(Node context) {
+ this.context = context;
+ }
+
+ //
+ // MOUSE LISTENER
+ //
+ @Override
+ public void mouseDown(MouseEvent e) {
+ if (e.button == 1) {
+ Control source = (Control) e.getSource();
+ if (userMenu == null) {
+ long durationSinceLastDispose = System.currentTimeMillis() - lastDisposeTS;
+ // avoid to reopen the menu, if one has clicked gain
+ if (durationSinceLastDispose > 200) {
+ userMenu = createUserMenu(source, context);
+ userMenu.getShell().addDisposeListener(this);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void mouseDoubleClick(MouseEvent e) {
+ }
+
+ @Override
+ public void mouseUp(MouseEvent e) {
+ }
+
+ @Override
+ public void widgetDisposed(DisposeEvent event) {
+ userMenu = null;
+ lastDisposeTS = System.currentTimeMillis();
+ }
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.cms.ui.CmsUiProvider;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+public class VerticalMenu implements CmsUiProvider {
+ private List<CmsUiProvider> items = new ArrayList<CmsUiProvider>();
+
+ @Override
+ public Control createUi(Composite parent, Node context) throws RepositoryException {
+ Composite part = new Composite(parent, SWT.NONE);
+ part.setLayoutData(new GridData(SWT.LEAD, SWT.TOP, false, false));
+// part.setData(RWT.CUSTOM_VARIANT, custom);
+ part.setLayout(CmsSwtUtils.noSpaceGridLayout());
+ for (CmsUiProvider uiProvider : items) {
+ Control subPart = uiProvider.createUi(part, context);
+ subPart.setLayoutData(new GridData(SWT.LEAD, SWT.TOP, false, false));
+ }
+ return part;
+ }
+
+ public void add(CmsUiProvider uiProvider) {
+ items.add(uiProvider);
+ }
+
+ public List<CmsUiProvider> getItems() {
+ return items;
+ }
+
+ public void setItems(List<CmsUiProvider> items) {
+ this.items = items;
+ }
+
+}
--- /dev/null
+/** Argeo CMS UI utilities. */
+package org.argeo.cms.ui.util;
\ No newline at end of file
--- /dev/null
+package org.argeo.cms.ui.viewers;
+
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.Observable;
+import java.util.Observer;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.security.auth.Subject;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.api.cms.ux.CmsEditable;
+import org.argeo.cms.swt.SwtEditablePart;
+import org.argeo.cms.swt.widgets.ScrolledPage;
+import org.argeo.jcr.JcrException;
+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.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+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;
+import org.xml.sax.SAXParseException;
+
+/** 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 CmsLog log = CmsLog.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 FocusListener focusListener;
+
+ private SwtEditablePart edited;
+ private ISelection selection = StructuredSelection.EMPTY;
+
+ private AccessControlContext accessControlContext;
+
+ 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();
+ focusListener = createFocusListener();
+ }
+ page = findPage(parent);
+ accessControlContext = AccessController.getContext();
+ }
+
+ /**
+ * 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 (RepositoryException e) {
+ throw new JcrException("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;
+ };
+ }
+
+ /** Create (retrieve) the FocusListener to use. */
+ protected FocusListener createFocusListener() {
+ return new FocusListener() {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void focusLost(FocusEvent event) {
+ }
+
+ @Override
+ public void focusGained(FocusEvent event) {
+ }
+ };
+ }
+
+ protected Composite findPage(Composite composite) {
+ if (composite instanceof ScrolledPage) {
+ return (ScrolledPage) composite;
+ } else {
+ if (composite.getParent() == null)
+ return composite;
+ return findPage(composite.getParent());
+ }
+ }
+
+ public void layoutPage() {
+ if (page != null)
+ page.layout(true, true);
+ }
+
+ protected void showControl(Control control) {
+ if (page != null && (page instanceof ScrolledPage))
+ ((ScrolledPage) page).showControl(control);
+ }
+
+ @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(SwtEditablePart part) throws RepositoryException {
+ }
+
+ /** Prepare the edited part */
+ protected void prepare(SwtEditablePart part, Object caretPosition) {
+ }
+
+ /** Notified when the editing state changed. Does nothing, to be overridden */
+ protected void editingStateChanged(CmsEditable cmsEditable) {
+ }
+
+ @Override
+ public void refresh() {
+ // TODO check actual context in order to notice a discrepancy
+ Subject viewerSubject = getViewerSubject();
+ Subject.doAs(viewerSubject, (PrivilegedAction<Void>) () -> {
+ try {
+ if (cmsEditable.canEdit() && !readOnly)
+ mouseListener = createMouseListener();
+ else
+ mouseListener = null;
+ refresh(getControl());
+ // layout(getControl());
+ if (!getControl().isDisposed())
+ layoutPage();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot refresh", e);
+ }
+ return null;
+ });
+ }
+
+ @Override
+ public void setSelection(ISelection selection, boolean reveal) {
+ this.selection = selection;
+ }
+
+ protected void updateContent(SwtEditablePart part) throws RepositoryException {
+ }
+
+ // LOW LEVEL EDITION
+ protected void edit(SwtEditablePart part, Object caretPosition) {
+ try {
+ if (edited == part)
+ return;
+
+ if (edited != null && edited != part) {
+ SwtEditablePart previouslyEdited = edited;
+ try {
+ stopEditing(true);
+ } catch (Exception e) {
+ notifyEditionException(e);
+ edit(previouslyEdited, caretPosition);
+ return;
+ }
+ }
+
+ part.startEditing();
+ edited = part;
+ updateContent(part);
+ prepare(part, caretPosition);
+ edited.getControl().addFocusListener(new FocusListener() {
+ private static final long serialVersionUID = 6883521812717097017L;
+
+ @Override
+ public void focusLost(FocusEvent event) {
+ stopEditing(true);
+ }
+
+ @Override
+ public void focusGained(FocusEvent event) {
+ }
+ });
+
+ layout(part.getControl());
+ showControl(part.getControl());
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot edit " + part, e);
+ }
+ }
+
+ protected void stopEditing(Boolean save) {
+ 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;
+ }
+
+ try {
+ if (save)
+ save(edited);
+
+ edited.stopEditing();
+ SwtEditablePart editablePart = edited;
+ Control control = ((SwtEditablePart) edited).getControl();
+ edited = null;
+ // TODO make edited state management more robust
+ updateContent(editablePart);
+ layout(control);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot stop editing", e);
+ } finally {
+ edited = null;
+ }
+ }
+
+ // METHODS AVAILABLE TO EXTENDING CLASSES
+ protected void saveEdit() {
+ if (edited != null)
+ stopEditing(true);
+ }
+
+ protected void cancelEdit() {
+ if (edited != null)
+ stopEditing(false);
+ }
+
+ /** Layout this controls from the related base page. */
+ public void layout(Control... controls) {
+ page.layout(controls);
+ }
+
+ /**
+ * Find the first {@link SwtEditablePart} in the parents hierarchy of this control
+ */
+ protected SwtEditablePart findDataParent(Control parent) {
+ if (parent instanceof SwtEditablePart) {
+ return (SwtEditablePart) parent;
+ }
+ if (parent.getParent() != null)
+ return findDataParent(parent.getParent());
+ else
+ throw new IllegalStateException("No data parent found");
+ }
+
+ // 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 IllegalStateException("Edited should not be null or disposed at this stage");
+ }
+
+ /** Persist all changes. */
+ protected void persistChanges(Session session) throws RepositoryException {
+ session.save();
+ session.refresh(false);
+ // TODO notify that changes have been persisted
+ }
+
+ /** Convenience method using a Node in order to save the underlying session. */
+ protected void persistChanges(Node anyNode) throws RepositoryException {
+ persistChanges(anyNode.getSession());
+ }
+
+ /** Notify edition exception */
+ protected void notifyEditionException(Throwable e) {
+ Throwable eToLog = e;
+ if (e instanceof IllegalArgumentException)
+ if (e.getCause() instanceof SAXParseException)
+ eToLog = e.getCause();
+ log.error(eToLog.getMessage(), eToLog);
+// if (log.isTraceEnabled())
+// log.trace("Full stack of " + eToLog.getMessage(), e);
+ // TODO Light error notification popup
+ }
+
+ protected Subject getViewerSubject() {
+ Subject res = null;
+ if (accessControlContext != null) {
+ res = Subject.getSubject(accessControlContext);
+ }
+ if (res == null)
+ throw new IllegalStateException("No subject associated with this viewer");
+ return res;
+ }
+
+ // GETTERS / SETTERS
+ public boolean isReadOnly() {
+ return readOnly;
+ }
+
+ protected SwtEditablePart getEdited() {
+ return edited;
+ }
+
+ public MouseListener getMouseListener() {
+ return mouseListener;
+ }
+
+ public FocusListener getFocusListener() {
+ return focusListener;
+ }
+
+ public CmsEditable getCmsEditable() {
+ return cmsEditable;
+ }
+
+ @Override
+ public ISelection getSelection() {
+ return selection;
+ }
+}
\ No newline at end of file
--- /dev/null
+package org.argeo.cms.ui.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;
+}
--- /dev/null
+package org.argeo.cms.ui.viewers;
+
+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.api.cms.ux.CmsEditionEvent;
+import org.argeo.cms.ux.AbstractCmsEditable;
+import org.argeo.jcr.JcrException;
+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 AbstractCmsEditable {
+ 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_SET_PROPERTY)) {
+ // was 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 JcrException("Cannot check whether " + nodePath + " is editing", e);
+ }
+ }
+
+ @Override
+ public void startEditing() {
+ try {
+ versionManager.checkout(nodePath);
+// setChanged();
+ } catch (RepositoryException e1) {
+ throw new JcrException("Cannot publish " + nodePath, e1);
+ }
+ notifyListeners(new CmsEditionEvent(nodePath, CmsEditionEvent.START_EDITING, this));
+ }
+
+ @Override
+ public void stopEditing() {
+ try {
+ versionManager.checkin(nodePath);
+// setChanged();
+ } catch (RepositoryException e1) {
+ throw new JcrException("Cannot publish " + nodePath, e1);
+ }
+ notifyListeners(new CmsEditionEvent(nodePath, CmsEditionEvent.STOP_EDITING, this));
+ }
+
+}
--- /dev/null
+package org.argeo.cms.ui.viewers;
+
+import javax.jcr.Node;
+
+/** An editable part related to a node */
+public interface NodePart extends ItemPart<Node> {
+ public Node getNode();
+}
--- /dev/null
+package org.argeo.cms.ui.viewers;
+
+import javax.jcr.Property;
+
+/** An editable part related to a JCR Property */
+public interface PropertyPart extends ItemPart<Property> {
+ public Property getProperty();
+}
--- /dev/null
+package org.argeo.cms.ui.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.swt.CmsSwtUtils;
+import org.argeo.cms.swt.SwtEditablePart;
+import org.argeo.cms.ui.widgets.JcrComposite;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+/** A structured UI related to a JCR context. */
+public class Section extends JcrComposite {
+ 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) {
+ this(parent, findSection(parent), style, node);
+ }
+
+ public Section(Section section, int style, Node node) {
+ this(section, section, style, node);
+ }
+
+ protected Section(Composite parent, Section parentSection, int style, Node node) {
+ super(parent, style, node);
+ try {
+ this.parentSection = parentSection;
+ if (parentSection != null) {
+ relativeDepth = getNode().getDepth() - parentSection.getNode().getDepth();
+ } else {
+ relativeDepth = 0;
+ }
+ setLayout(CmsSwtUtils.noSpaceGridLayout());
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Cannot create section from " + node, e);
+ }
+ }
+
+ 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 SwtEditablePart)
+ 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 Composite createHeader() {
+ return createHeader(this);
+ }
+
+ public Composite createHeader(Composite parent) {
+ if (sectionHeader != null)
+ sectionHeader.dispose();
+
+ sectionHeader = new Composite(parent, SWT.NONE);
+ sectionHeader.setLayoutData(CmsSwtUtils.fillWidth());
+ sectionHeader.setLayout(CmsSwtUtils.noSpaceGridLayout());
+ // sectionHeader.moveAbove(null);
+ // layout();
+ return sectionHeader;
+ }
+
+ 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 sectionPart = (SectionPart) child;
+ if (sectionPart.getPartId().equals(partId))
+ return sectionPart;
+ }
+ }
+ return null;
+ }
+
+ public SectionPart nextSectionPart(SectionPart sectionPart) {
+ Control[] children = getChildren();
+ for (int i = 0; i < children.length; i++) {
+ if (sectionPart == children[i]) {
+ for (int j = i + 1; j < children.length; j++) {
+ if (children[i + 1] instanceof SectionPart) {
+ return (SectionPart) children[i + 1];
+ }
+ }
+
+// 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());
+ }
+}
--- /dev/null
+package org.argeo.cms.ui.viewers;
+
+import org.argeo.cms.swt.SwtEditablePart;
+
+/** An editable part dynamically related to a Section */
+public interface SectionPart extends SwtEditablePart, NodePart {
+ public String getPartId();
+
+ public Section getSection();
+}
--- /dev/null
+/** Argeo CMS generic viewers, based on JFace. */
+package org.argeo.cms.ui.viewers;
\ No newline at end of file
--- /dev/null
+package org.argeo.cms.ui.widgets;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.api.cms.ux.Cms2DSize;
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.cms.ui.util.CmsUiUtils;
+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 CmsLog log = CmsLog.getLog(EditableImage.class);
+
+ private Cms2DSize preferredImageSize;
+ private Boolean loaded = false;
+
+ public EditableImage(Composite parent, int swtStyle) {
+ super(parent, swtStyle);
+ }
+
+ public EditableImage(Composite parent, int swtStyle, Cms2DSize preferredImageSize) {
+ super(parent, swtStyle);
+ this.preferredImageSize = preferredImageSize;
+ }
+
+ public EditableImage(Composite parent, int style, Node node, boolean cacheImmediately, Cms2DSize 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 CmsUiUtils
+ .noImg(preferredImageSize != null ? preferredImageSize : new Cms2DSize(getSize().x, getSize().y));
+ }
+
+ protected Label createLabel(Composite box, String style) {
+ Label lbl = new Label(box, getStyle());
+ // lbl.setLayoutData(CmsUiUtils.fillWidth());
+ CmsSwtUtils.markup(lbl);
+ CmsSwtUtils.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 = CmsUiUtils.noImg(preferredImageSize);
+ loaded = false;
+ }
+
+ if (imgTag == null) {
+ loaded = false;
+ imgTag = CmsUiUtils.noImg(preferredImageSize);
+ } else
+ loaded = true;
+ if (control != null) {
+ ((Label) control).setText(imgTag);
+ control.setSize(preferredImageSize != null
+ ? new Point(preferredImageSize.getWidth(), preferredImageSize.getHeight())
+ : getSize());
+ } else {
+ loaded = false;
+ }
+ getParent().layout();
+ return loaded;
+ }
+
+ public void setPreferredSize(Cms2DSize size) {
+ this.preferredImageSize = size;
+ if (!loaded) {
+ load((Label) getControl());
+ }
+ }
+
+ protected Text createText(Composite box, String style) {
+ Text text = new Text(box, getStyle());
+ CmsSwtUtils.style(text, style);
+ return text;
+ }
+
+ public Cms2DSize getPreferredImageSize() {
+ return preferredImageSize;
+ }
+
+}
--- /dev/null
+package org.argeo.cms.ui.widgets;
+
+import javax.jcr.Item;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+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;
+
+ private boolean editable = true;
+
+ private Color highlightColor;
+ private Composite highlight;
+
+ private boolean useTextAsLabel = false;
+
+ public EditableText(Composite parent, int style) {
+ super(parent, style);
+ editable = !(SWT.READ_ONLY == (style & SWT.READ_ONLY));
+ highlightColor = parent.getDisplay().getSystemColor(SWT.COLOR_GRAY);
+ }
+
+ 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);
+ editable = !(SWT.READ_ONLY == (style & SWT.READ_ONLY));
+ highlightColor = parent.getDisplay().getSystemColor(SWT.COLOR_GRAY);
+ }
+
+ @Override
+ protected Control createControl(Composite box, String style) {
+ if (isEditing() && getEditable()) {
+ return createText(box, style, true);
+ } else {
+ if (useTextAsLabel) {
+ return createTextLabel(box, style);
+ } else {
+ return createLabel(box, style);
+ }
+ }
+ }
+
+ protected Label createLabel(Composite box, String style) {
+ Label lbl = new Label(box, getStyle() | SWT.WRAP);
+ lbl.setLayoutData(CmsSwtUtils.fillWidth());
+ if (style != null)
+ CmsSwtUtils.style(lbl, style);
+ CmsSwtUtils.markup(lbl);
+ if (mouseListener != null)
+ lbl.addMouseListener(mouseListener);
+ return lbl;
+ }
+
+ protected Text createTextLabel(Composite box, String style) {
+ Text lbl = new Text(box, getStyle() | SWT.MULTI);
+ lbl.setEditable(false);
+ lbl.setLayoutData(CmsSwtUtils.fillWidth());
+ if (style != null)
+ CmsSwtUtils.style(lbl, style);
+ CmsSwtUtils.markup(lbl);
+ if (mouseListener != null)
+ lbl.addMouseListener(mouseListener);
+ return lbl;
+ }
+
+ protected Text createText(Composite box, String style, boolean editable) {
+ highlight = new Composite(box, SWT.NONE);
+ highlight.setBackground(highlightColor);
+ GridData highlightGd = new GridData(SWT.FILL, SWT.FILL, false, false);
+ highlightGd.widthHint = 5;
+ highlightGd.heightHint = 3;
+ highlight.setLayoutData(highlightGd);
+
+ final Text text = new Text(box, getStyle() | SWT.MULTI | SWT.WRAP);
+ text.setEditable(editable);
+ GridData textLayoutData = CmsSwtUtils.fillWidth();
+ // textLayoutData.heightHint = preferredHeight;
+ text.setLayoutData(textLayoutData);
+ if (style != null)
+ CmsSwtUtils.style(text, style);
+ text.setFocus();
+ return text;
+ }
+
+ @Override
+ protected void clear(boolean deep) {
+ if (highlight != null)
+ highlight.dispose();
+ super.clear(deep);
+ }
+
+ 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();
+ }
+
+ public String getText() {
+ Control child = getControl();
+
+ if (child instanceof Label)
+ return ((Label) child).getText();
+ else if (child instanceof Text)
+ return ((Text) child).getText();
+ else
+ throw new IllegalStateException("Unsupported control " + child.getClass());
+ }
+
+ /** @deprecated Use {@link #isEditable()} instead. */
+ @Deprecated
+ public boolean getEditable() {
+ return isEditable();
+ }
+
+ public boolean isEditable() {
+ return editable;
+ }
+
+ public void setUseTextAsLabel(boolean useTextAsLabel) {
+ this.useTextAsLabel = useTextAsLabel;
+ }
+
+}
--- /dev/null
+package org.argeo.cms.ui.widgets;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.api.cms.ux.Cms2DSize;
+import org.argeo.api.cms.ux.CmsImageManager;
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.cms.ui.internal.JcrFileUploadReceiver;
+import org.argeo.cms.ui.viewers.NodePart;
+import org.argeo.cms.ui.viewers.Section;
+import org.argeo.cms.ui.viewers.SectionPart;
+import org.argeo.jcr.Jcr;
+import org.argeo.jcr.JcrException;
+import org.eclipse.rap.fileupload.FileUploadHandler;
+import org.eclipse.rap.fileupload.FileUploadListener;
+import org.eclipse.rap.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.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<Control, Node> imageManager;
+ private FileUploadHandler currentUploadHandler = null;
+ private FileUploadListener fileUploadListener;
+
+ public Img(Composite parent, int swtStyle, Node imgNode, Cms2DSize preferredImageSize) throws RepositoryException {
+ this(Section.findSection(parent), parent, swtStyle, imgNode, preferredImageSize, null);
+ setStyle(TextStyles.TEXT_IMAGE);
+ }
+
+ public Img(Composite parent, int swtStyle, Node imgNode) throws RepositoryException {
+ this(Section.findSection(parent), parent, swtStyle, imgNode, null, null);
+ setStyle(TextStyles.TEXT_IMAGE);
+ }
+
+ public Img(Composite parent, int swtStyle, Node imgNode, CmsImageManager<Control, Node> imageManager)
+ throws RepositoryException {
+ this(Section.findSection(parent), parent, swtStyle, imgNode, null, imageManager);
+ setStyle(TextStyles.TEXT_IMAGE);
+ }
+
+ Img(Section section, Composite parent, int swtStyle, Node imgNode, Cms2DSize preferredImageSize,
+ CmsImageManager<Control, Node> imageManager) throws RepositoryException {
+ super(parent, swtStyle, imgNode, false, preferredImageSize);
+ this.section = section;
+ this.imageManager = imageManager != null ? imageManager
+ : (CmsImageManager<Control, Node>) CmsSwtUtils.getCmsView(section).getImageManager();
+ CmsSwtUtils.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 JcrException("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) {
+ Node imgNode = getNode();
+ boolean loaded = imageManager.load(imgNode, lbl, getPreferredImageSize());
+ // getParent().layout();
+ return loaded;
+ }
+
+ protected Node getUploadFolder() {
+ return Jcr.getParent(getNode());
+ }
+
+ protected String getUploadName() {
+ Node node = getNode();
+ return Jcr.getName(node) + '[' + Jcr.getIndex(node) + ']';
+ }
+
+ protected CmsImageManager<Control, Node> getImageManager() {
+ return imageManager;
+ }
+
+ protected Control createImageChooser(Composite box, String style) throws RepositoryException {
+ JcrFileUploadReceiver receiver = new JcrFileUploadReceiver(this, getUploadFolder(), getUploadName(),
+ imageManager);
+ if (currentUploadHandler != null)
+ currentUploadHandler.dispose();
+ currentUploadHandler = prepareUpload(receiver);
+ final ServerPushSession pushSession = new ServerPushSession();
+ final FileUpload fileUpload = new FileUpload(box, SWT.NONE);
+ CmsSwtUtils.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();
+ }
+
+}
--- /dev/null
+package org.argeo.cms.ui.widgets;
+
+import javax.jcr.Item;
+import javax.jcr.ItemNotFoundException;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.jcr.JcrException;
+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 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) {
+ this(parent, style, item, false);
+ }
+
+ public JcrComposite(Composite parent, int style, Item item, boolean cacheImmediately) {
+ super(parent, style);
+ if (item != null)
+ try {
+ 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 UnsupportedOperationException("Multiple properties not supported yet.");
+ this.property = property.getName();
+ node = property.getParent();
+ }
+ this.nodeId = node.getIdentifier();
+ if (cacheImmediately)
+ this.cache = node;
+// }
+ setLayout(CmsSwtUtils.noSpaceGridLayout());
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Cannot create composite from " + item, e);
+ }
+ }
+
+ public synchronized Node getNode() {
+ try {
+ if (!itemIsNode())
+ throw new IllegalStateException("Item is not a Node");
+ return getNodeInternal();
+ } catch (RepositoryException e) {
+ throw new JcrException("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 String getPropertyName() {
+ try {
+ return getProperty().getName();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get property name", e);
+ }
+ }
+
+ public synchronized Node getPropertyNode() {
+ try {
+ return getProperty().getNode();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get property name", e);
+ }
+ }
+
+ public synchronized Property getProperty() {
+ try {
+ if (itemIsNode())
+ throw new IllegalStateException("Item is not a Property");
+ Node node = getNodeInternal();
+ if (!node.hasProperty(property))
+ throw new IllegalStateException("Property " + property + " is not set on " + node);
+ return node.getProperty(property);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get property " + property + " from node " + nodeId, e);
+ }
+ }
+
+ public synchronized boolean itemIsNode() {
+ return property == null;
+ }
+
+ public synchronized boolean itemExists() {
+ if (session == null)
+ return false;
+ try {
+ Node n = session.getNodeByIdentifier(nodeId);
+ if (!itemIsNode())
+ return n.hasProperty(property);
+ else
+ return true;
+ } catch (ItemNotFoundException e) {
+ return false;
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot check whether node exists", e);
+ }
+ }
+
+ /** Set/update the cache or change the node */
+ public synchronized void setNode(Node node) {
+ if (!itemIsNode())
+ throw new IllegalArgumentException("Cannot set a Node on a Property");
+
+ if (node == null) {// clear cache
+ this.cache = null;
+ return;
+ }
+
+ try {
+// if (session != null || session != node.getSession())// check session
+// throw new IllegalArgumentException("Uncompatible session");
+// if (session == null)
+ session = node.getSession();
+ if (nodeId == null || !nodeId.equals(node.getIdentifier())) {
+ nodeId = node.getIdentifier();
+ cache = node;
+ itemUpdated();
+ } else {
+ cache = node;// set/update cache
+ }
+ } catch (RepositoryException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /** Set/update the cache or change the property */
+ public synchronized void setProperty(Property prop) {
+ if (itemIsNode())
+ throw new IllegalArgumentException("Cannot set a Property on a Node");
+
+ if (prop == null) {// clear cache
+ this.cache = null;
+ return;
+ }
+
+ try {
+ if (session == null || session != prop.getSession())// check session
+ throw new IllegalArgumentException("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
+ }
+ } catch (RepositoryException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ 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;
+// }
+}
--- /dev/null
+package org.argeo.cms.ui.widgets;
+
+import javax.jcr.Item;
+
+import org.argeo.cms.swt.CmsSwtUtils;
+import org.argeo.cms.ui.CmsUiConstants;
+import org.argeo.eclipse.ui.specific.EclipseUiSpecificUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.FocusListener;
+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 CmsUiConstants {
+ private static final long serialVersionUID = -6372283442330912755L;
+ private Control control;
+
+ private Composite container;
+ private Composite box;
+
+ protected MouseListener mouseListener;
+ protected FocusListener focusListener;
+
+ private Boolean editing = Boolean.FALSE;
+
+ private Composite ancestorToLayout;
+
+ public StyledControl(Composite parent, int swtStyle) {
+ super(parent, swtStyle);
+ setLayout(CmsSwtUtils.noSpaceGridLayout());
+ }
+
+ public StyledControl(Composite parent, int style, Item item) {
+ super(parent, style, item);
+ }
+
+ public StyledControl(Composite parent, int style, Item item, boolean cacheImmediately) {
+ super(parent, style, item, cacheImmediately);
+ }
+
+ protected abstract Control createControl(Composite box, String style);
+
+ protected Composite createBox() {
+ Composite box = new Composite(container, SWT.INHERIT_DEFAULT);
+ setContainerLayoutData(box);
+ box.setLayout(CmsSwtUtils.noSpaceGridLayout(3));
+ return box;
+ }
+
+ protected Composite createContainer() {
+ Composite container = new Composite(this, SWT.INHERIT_DEFAULT);
+ setContainerLayoutData(container);
+ container.setLayout(CmsSwtUtils.noSpaceGridLayout());
+ return container;
+ }
+
+ 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) EclipseUiSpecificUtils.getStyleData(control);
+ clear(false);
+ refreshControl(style);
+
+ // add the focus listener to the newly created edition control
+ if (focusListener != null)
+ control.addFocusListener(focusListener);
+ }
+
+ public synchronized void stopEditing() {
+ assert isEditing();
+ editing = false;
+ String style = (String) EclipseUiSpecificUtils.getStyleData(control);
+ clear(false);
+ refreshControl(style);
+ }
+
+ protected void refreshControl(String style) {
+ control = createControl(box, style);
+ setControlLayoutData(control);
+ if (ancestorToLayout != null)
+ ancestorToLayout.layout(true, true);
+ else
+ getParent().layout(true, true);
+ }
+
+ public void setStyle(String style) {
+ Object currentStyle = null;
+ if (control != null)
+ currentStyle = EclipseUiSpecificUtils.getStyleData(control);
+ if (currentStyle != null && currentStyle.equals(style))
+ return;
+
+ clear(true);
+ refreshControl(style);
+
+ if (style != null) {
+ CmsSwtUtils.style(box, style + "_box");
+ CmsSwtUtils.style(container, style + "_container");
+ }
+ }
+
+ /** To be overridden */
+ protected void setControlLayoutData(Control control) {
+ control.setLayoutData(CmsSwtUtils.fillWidth());
+ }
+
+ /** To be overridden */
+ protected void setContainerLayoutData(Composite composite) {
+ composite.setLayoutData(CmsSwtUtils.fillWidth());
+ }
+
+ protected void clear(boolean deep) {
+ if (deep) {
+ for (Control control : getChildren())
+ control.dispose();
+ container = createContainer();
+ box = createBox();
+ } 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);
+ }
+
+ public void setFocusListener(FocusListener focusListener) {
+ if (this.focusListener != null && control != null)
+ control.removeFocusListener(this.focusListener);
+ this.focusListener = focusListener;
+ if (control != null && this.focusListener != null)
+ control.addFocusListener(focusListener);
+ }
+
+ public void setAncestorToLayout(Composite ancestorToLayout) {
+ this.ancestorToLayout = ancestorToLayout;
+ }
+
+}
--- /dev/null
+package org.argeo.cms.ui.widgets;
+
+/** 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";
+
+}
--- /dev/null
+/** Argeo CMS generic widgets, based on SWT. */
+package org.argeo.cms.ui.widgets;
\ No newline at end of file
--- /dev/null
+package org.argeo.eclipse.ui.jcr;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.eclipse.ui.AbstractTreeContentProvider;
+import org.argeo.eclipse.ui.EclipseUiException;
+
+/** Canonical implementation of tree content provider manipulating JCR nodes. */
+public abstract class AbstractNodeContentProvider extends
+ AbstractTreeContentProvider {
+ private static final long serialVersionUID = -4905836490027272569L;
+
+ private final static CmsLog log = CmsLog
+ .getLog(AbstractNodeContentProvider.class);
+
+ private Session session;
+
+ public AbstractNodeContentProvider(Session session) {
+ this.session = session;
+ }
+
+ /**
+ * Whether this path is a base path (and thus has no parent). By default it
+ * returns true if path is '/' (root node)
+ */
+ protected Boolean isBasePath(String path) {
+ // root node
+ return path.equals("/");
+ }
+
+ @Override
+ public Object[] getChildren(Object element) {
+ Object[] children;
+ if (element instanceof Node) {
+ try {
+ Node node = (Node) element;
+ children = getChildren(node);
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot get children of " + element, e);
+ }
+ } else if (element instanceof WrappedNode) {
+ WrappedNode wrappedNode = (WrappedNode) element;
+ try {
+ children = getChildren(wrappedNode.getNode());
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot get children of "
+ + wrappedNode, e);
+ }
+ } else if (element instanceof NodesWrapper) {
+ NodesWrapper node = (NodesWrapper) element;
+ children = node.getChildren();
+ } else {
+ children = super.getChildren(element);
+ }
+
+ children = sort(element, children);
+ return children;
+ }
+
+ /** Do not sort by default. To be overidden to provide custom sort. */
+ protected Object[] sort(Object parent, Object[] children) {
+ return children;
+ }
+
+ /**
+ * To be overridden in order to filter out some nodes. Does nothing by
+ * default. The provided list is a temporary one and can thus be modified
+ * directly . (e.g. via an iterator)
+ */
+ protected List<Node> filterChildren(List<Node> children)
+ throws RepositoryException {
+ return children;
+ }
+
+ protected Object[] getChildren(Node node) throws RepositoryException {
+ List<Node> nodes = new ArrayList<Node>();
+ for (NodeIterator nit = node.getNodes(); nit.hasNext();)
+ nodes.add(nit.nextNode());
+ nodes = filterChildren(nodes);
+ return nodes.toArray();
+ }
+
+ @Override
+ public Object getParent(Object element) {
+ if (element instanceof Node) {
+ Node node = (Node) element;
+ try {
+ String path = node.getPath();
+ if (isBasePath(path))
+ return null;
+ else
+ return node.getParent();
+ } catch (RepositoryException e) {
+ log.warn("Cannot get parent of " + element + ": " + e);
+ return null;
+ }
+ } else if (element instanceof WrappedNode) {
+ WrappedNode wrappedNode = (WrappedNode) element;
+ return wrappedNode.getParent();
+ } else if (element instanceof NodesWrapper) {
+ NodesWrapper nodesWrapper = (NodesWrapper) element;
+ return this.getParent(nodesWrapper.getNode());
+ }
+ return super.getParent(element);
+ }
+
+ @Override
+ public boolean hasChildren(Object element) {
+ try {
+ if (element instanceof Node) {
+ Node node = (Node) element;
+ return node.hasNodes();
+ } else if (element instanceof WrappedNode) {
+ WrappedNode wrappedNode = (WrappedNode) element;
+ return wrappedNode.getNode().hasNodes();
+ } else if (element instanceof NodesWrapper) {
+ NodesWrapper nodesWrapper = (NodesWrapper) element;
+ return nodesWrapper.hasChildren();
+ }
+
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot check whether " + element
+ + " has children", e);
+ }
+ return super.hasChildren(element);
+ }
+
+ public Session getSession() {
+ return session;
+ }
+}
\ No newline at end of file
--- /dev/null
+package org.argeo.eclipse.ui.jcr;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.observation.Event;
+import javax.jcr.observation.EventIterator;
+import javax.jcr.observation.EventListener;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.swt.widgets.Display;
+
+/**
+ * {@link EventListener} which simplifies running actions within the UI thread.
+ */
+public abstract class AsyncUiEventListener implements EventListener {
+ // private final static Log logSuper = LogFactory
+ // .getLog(AsyncUiEventListener.class);
+ private final CmsLog logThis = CmsLog.getLog(getClass());
+
+ private final Display display;
+
+ public AsyncUiEventListener(Display display) {
+ super();
+ this.display = display;
+ }
+
+ /** Called asynchronously in the UI thread. */
+ protected abstract void onEventInUiThread(List<Event> events) throws RepositoryException;
+
+ /**
+ * Whether these events should be processed in the UI or skipped with no UI
+ * job created.
+ */
+ protected Boolean willProcessInUiThread(List<Event> events) throws RepositoryException {
+ return true;
+ }
+
+ protected CmsLog getLog() {
+ return logThis;
+ }
+
+ public final void onEvent(final EventIterator eventIterator) {
+ final List<Event> events = new ArrayList<Event>();
+ while (eventIterator.hasNext())
+ events.add(eventIterator.nextEvent());
+
+ if (logThis.isTraceEnabled())
+ logThis.trace("Received " + events.size() + " events");
+
+ try {
+ if (!willProcessInUiThread(events))
+ return;
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot test skip events " + events, e);
+ }
+
+ // Job job = new Job("JCR Events") {
+ // protected IStatus run(IProgressMonitor monitor) {
+ // if (display.isDisposed()) {
+ // logSuper.warn("Display is disposed cannot update UI");
+ // return Status.CANCEL_STATUS;
+ // }
+
+ if (!display.isDisposed())
+ display.asyncExec(new Runnable() {
+ public void run() {
+ try {
+ onEventInUiThread(events);
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot process events " + events, e);
+ }
+ }
+ });
+
+ // return Status.OK_STATUS;
+ // }
+ // };
+ // job.schedule();
+ }
+}
--- /dev/null
+package org.argeo.eclipse.ui.jcr;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.swt.graphics.Image;
+
+/**
+ * Default label provider to manage node and corresponding UI objects. It
+ * provides reasonable overwrite-able default for known JCR types.
+ */
+public class DefaultNodeLabelProvider extends ColumnLabelProvider {
+ private static final long serialVersionUID = 1216182332792151235L;
+
+ public String getText(Object element) {
+ try {
+ if (element instanceof Node) {
+ return getText((Node) element);
+ } else if (element instanceof WrappedNode) {
+ return getText(((WrappedNode) element).getNode());
+ } else if (element instanceof NodesWrapper) {
+ return getText(((NodesWrapper) element).getNode());
+ }
+ return super.getText(element);
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot get text for of " + element, e);
+ }
+ }
+
+ protected String getText(Node node) throws RepositoryException {
+ if (node.isNodeType(NodeType.MIX_TITLE)
+ && node.hasProperty(Property.JCR_TITLE))
+ return node.getProperty(Property.JCR_TITLE).getString();
+ else
+ return node.getName();
+ }
+
+ @Override
+ public Image getImage(Object element) {
+ try {
+ if (element instanceof Node) {
+ return getImage((Node) element);
+ } else if (element instanceof WrappedNode) {
+ return getImage(((WrappedNode) element).getNode());
+ } else if (element instanceof NodesWrapper) {
+ return getImage(((NodesWrapper) element).getNode());
+ }
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot retrieve image for " + element, e);
+ }
+ return super.getImage(element);
+ }
+
+ protected Image getImage(Node node) throws RepositoryException {
+ // FIXME who uses that?
+ return null;
+ }
+
+ @Override
+ public String getToolTipText(Object element) {
+ try {
+ if (element instanceof Node) {
+ return getToolTipText((Node) element);
+ } else if (element instanceof WrappedNode) {
+ return getToolTipText(((WrappedNode) element).getNode());
+ } else if (element instanceof NodesWrapper) {
+ return getToolTipText(((NodesWrapper) element).getNode());
+ }
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot get tooltip for " + element, e);
+ }
+ return super.getToolTipText(element);
+ }
+
+ protected String getToolTipText(Node node) throws RepositoryException {
+ return null;
+ }
+}
\ No newline at end of file
--- /dev/null
+package org.argeo.eclipse.ui.jcr;
+
+import java.util.Calendar;
+
+import javax.jcr.Node;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.jcr.lists.NodeViewerComparator;
+import org.argeo.eclipse.ui.jcr.lists.RowViewerComparator;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.widgets.Table;
+
+/** Utility methods to simplify UI development using SWT (or RWT), jface and JCR. */
+public class JcrUiUtils {
+
+ /**
+ * Centralizes management of updating property value. Among other to avoid
+ * infinite loop when the new value is the same as the ones that is already
+ * stored in JCR.
+ *
+ * @return true if the value as changed
+ */
+ public static boolean setJcrProperty(Node node, String propName,
+ int propertyType, Object value) {
+ try {
+ switch (propertyType) {
+ case PropertyType.STRING:
+ if ("".equals((String) value)
+ && (!node.hasProperty(propName) || node
+ .hasProperty(propName)
+ && "".equals(node.getProperty(propName)
+ .getString())))
+ // workaround the fact that the Text widget value cannot be
+ // set to null
+ return false;
+ else if (node.hasProperty(propName)
+ && node.getProperty(propName).getString()
+ .equals((String) value))
+ // nothing changed yet
+ return false;
+ else {
+ node.setProperty(propName, (String) value);
+ return true;
+ }
+ case PropertyType.BOOLEAN:
+ if (node.hasProperty(propName)
+ && node.getProperty(propName).getBoolean() == (Boolean) value)
+ // nothing changed yet
+ return false;
+ else {
+ node.setProperty(propName, (Boolean) value);
+ return true;
+ }
+ case PropertyType.DATE:
+ if (node.hasProperty(propName)
+ && node.getProperty(propName).getDate()
+ .equals((Calendar) value))
+ // nothing changed yet
+ return false;
+ else {
+ node.setProperty(propName, (Calendar) value);
+ return true;
+ }
+ case PropertyType.LONG:
+ Long lgValue = (Long) value;
+
+ if (lgValue == null)
+ lgValue = 0L;
+
+ if (node.hasProperty(propName)
+ && node.getProperty(propName).getLong() == lgValue)
+ // nothing changed yet
+ return false;
+ else {
+ node.setProperty(propName, lgValue);
+ return true;
+ }
+
+ default:
+ throw new EclipseUiException("Unimplemented property save");
+ }
+ } catch (RepositoryException re) {
+ throw new EclipseUiException("Unexpected error while setting property",
+ re);
+ }
+ }
+
+ /**
+ * Creates a new selection adapter in order to provide sorting abitily on a
+ * SWT Table that display a row list
+ **/
+ public static SelectionAdapter getRowSelectionAdapter(final int index,
+ final int propertyType, final String selectorName,
+ final String propertyName, final RowViewerComparator comparator,
+ final TableViewer viewer) {
+ SelectionAdapter selectionAdapter = new SelectionAdapter() {
+ private static final long serialVersionUID = -5738918304901437720L;
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ Table table = viewer.getTable();
+ comparator.setColumn(propertyType, selectorName, propertyName);
+ int dir = table.getSortDirection();
+ if (table.getSortColumn() == table.getColumn(index)) {
+ dir = dir == SWT.UP ? SWT.DOWN : SWT.UP;
+ } else {
+ dir = SWT.DOWN;
+ }
+ table.setSortDirection(dir);
+ table.setSortColumn(table.getColumn(index));
+ viewer.refresh();
+ }
+ };
+ return selectionAdapter;
+ }
+
+ /**
+ * Creates a new selection adapter in order to provide sorting abitily on a
+ * swt table that display a row list
+ **/
+ public static SelectionAdapter getNodeSelectionAdapter(final int index,
+ final int propertyType, final String propertyName,
+ final NodeViewerComparator comparator, final TableViewer viewer) {
+ SelectionAdapter selectionAdapter = new SelectionAdapter() {
+ private static final long serialVersionUID = -1683220869195484625L;
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ Table table = viewer.getTable();
+ comparator.setColumn(propertyType, propertyName);
+ int dir = table.getSortDirection();
+ if (table.getSortColumn() == table.getColumn(index)) {
+ dir = dir == SWT.UP ? SWT.DOWN : SWT.UP;
+ } else {
+ dir = SWT.DOWN;
+ }
+ table.setSortDirection(dir);
+ table.setSortColumn(table.getColumn(index));
+ viewer.refresh();
+ }
+ };
+ return selectionAdapter;
+ }
+}
--- /dev/null
+package org.argeo.eclipse.ui.jcr;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.Image;
+
+/** Simplifies writing JCR-based column label provider. */
+public class NodeColumnLabelProvider extends ColumnLabelProvider {
+ private static final long serialVersionUID = -6586692836928505358L;
+
+ protected String getNodeText(Node node) throws RepositoryException {
+ return super.getText(node);
+ }
+
+ protected String getNodeToolTipText(Node node) throws RepositoryException {
+ return super.getToolTipText(node);
+ }
+
+ protected Image getNodeImage(Node node) throws RepositoryException {
+ return super.getImage(node);
+ }
+
+ protected Font getNodeFont(Node node) throws RepositoryException {
+ return super.getFont(node);
+ }
+
+ public Color getNodeBackground(Node node) throws RepositoryException {
+ return super.getBackground(node);
+ }
+
+ public Color getNodeForeground(Node node) throws RepositoryException {
+ return super.getForeground(node);
+ }
+
+ @Override
+ public String getText(Object element) {
+ try {
+ if (element instanceof Node)
+ return getNodeText((Node) element);
+ else if (element instanceof NodeElement)
+ return getNodeText(((NodeElement) element).getNode());
+ else
+ throw new IllegalArgumentException("Unsupported element type " + element.getClass());
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Repository exception when accessing " + element, e);
+ }
+ }
+
+ @Override
+ public Image getImage(Object element) {
+ try {
+ if (element instanceof Node)
+ return getNodeImage((Node) element);
+ else if (element instanceof NodeElement)
+ return getNodeImage(((NodeElement) element).getNode());
+ else
+ throw new IllegalArgumentException("Unsupported element type " + element.getClass());
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Repository exception when accessing " + element, e);
+ }
+ }
+
+ @Override
+ public String getToolTipText(Object element) {
+ try {
+ if (element instanceof Node)
+ return getNodeToolTipText((Node) element);
+ else if (element instanceof NodeElement)
+ return getNodeToolTipText(((NodeElement) element).getNode());
+ else
+ throw new IllegalArgumentException("Unsupported element type " + element.getClass());
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Repository exception when accessing " + element, e);
+ }
+ }
+
+ @Override
+ public Font getFont(Object element) {
+ try {
+ if (element instanceof Node)
+ return getNodeFont((Node) element);
+ else if (element instanceof NodeElement)
+ return getNodeFont(((NodeElement) element).getNode());
+ else
+ throw new IllegalArgumentException("Unsupported element type " + element.getClass());
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Repository exception when accessing " + element, e);
+ }
+ }
+
+ @Override
+ public Color getBackground(Object element) {
+ try {
+ if (element instanceof Node)
+ return getNodeBackground((Node) element);
+ else if (element instanceof NodeElement)
+ return getNodeBackground(((NodeElement) element).getNode());
+ else
+ throw new IllegalArgumentException("Unsupported element type " + element.getClass());
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Repository exception when accessing " + element, e);
+ }
+ }
+
+ @Override
+ public Color getForeground(Object element) {
+ try {
+ if (element instanceof Node)
+ return getNodeForeground((Node) element);
+ else if (element instanceof NodeElement)
+ return getNodeForeground(((NodeElement) element).getNode());
+ else
+ throw new IllegalArgumentException("Unsupported element type " + element.getClass());
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Repository exception when accessing " + element, e);
+ }
+ }
+
+}
--- /dev/null
+package org.argeo.eclipse.ui.jcr;
+
+import javax.jcr.Node;
+
+/** An element which is related to a JCR {@link Node}. */
+public interface NodeElement {
+ Node getNode();
+}
--- /dev/null
+package org.argeo.eclipse.ui.jcr;\r
+\r
+import javax.jcr.Node;\r
+import javax.jcr.RepositoryException;\r
+\r
+import org.argeo.eclipse.ui.EclipseUiException;\r
+import org.eclipse.jface.viewers.IElementComparer;\r
+\r
+/** Element comparer for JCR node, to be used in JFace viewers. */\r
+public class NodeElementComparer implements IElementComparer {\r
+\r
+ public boolean equals(Object a, Object b) {\r
+ try {\r
+ if ((a instanceof Node) && (b instanceof Node)) {\r
+ Node nodeA = (Node) a;\r
+ Node nodeB = (Node) b;\r
+ return nodeA.getIdentifier().equals(nodeB.getIdentifier());\r
+ } else {\r
+ return a.equals(b);\r
+ }\r
+ } catch (RepositoryException e) {\r
+ throw new EclipseUiException("Cannot compare nodes", e);\r
+ }\r
+ }\r
+\r
+ public int hashCode(Object element) {\r
+ try {\r
+ if (element instanceof Node)\r
+ return ((Node) element).getIdentifier().hashCode();\r
+ return element.hashCode();\r
+ } catch (RepositoryException e) {\r
+ throw new EclipseUiException("Cannot get hash code", e);\r
+ }\r
+ }\r
+\r
+}\r
--- /dev/null
+package org.argeo.eclipse.ui.jcr;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+
+/**
+ * Element of tree which is based on a node, but whose children are not
+ * necessarily this node children.
+ */
+public class NodesWrapper {
+ private final Node node;
+
+ public NodesWrapper(Node node) {
+ super();
+ this.node = node;
+ }
+
+ protected NodeIterator getNodeIterator() throws RepositoryException {
+ return node.getNodes();
+ }
+
+ protected List<WrappedNode> getWrappedNodes() throws RepositoryException {
+ List<WrappedNode> nodes = new ArrayList<WrappedNode>();
+ for (NodeIterator nit = getNodeIterator(); nit.hasNext();)
+ nodes.add(new WrappedNode(this, nit.nextNode()));
+ return nodes;
+ }
+
+ public Object[] getChildren() {
+ try {
+ return getWrappedNodes().toArray();
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot get wrapped children", e);
+ }
+ }
+
+ /**
+ * @return true by default because we don't want to compute the wrapped
+ * nodes twice
+ */
+ public Boolean hasChildren() {
+ return true;
+ }
+
+ public Node getNode() {
+ return node;
+ }
+
+ @Override
+ public int hashCode() {
+ return node.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof NodesWrapper)
+ return node.equals(((NodesWrapper) obj).getNode());
+ else
+ return false;
+ }
+
+ public String toString() {
+ return "nodes wrapper based on " + node;
+ }
+}
--- /dev/null
+package org.argeo.eclipse.ui.jcr;
+
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.query.Query;
+
+import org.argeo.jcr.JcrException;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+/** Content provider based on a JCR {@link Query}. */
+public class QueryTableContentProvider implements IStructuredContentProvider {
+ private static final long serialVersionUID = 760371460907204722L;
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ }
+
+ @Override
+ public Object[] getElements(Object inputElement) {
+ Query query = (Query) inputElement;
+ try {
+ NodeIterator nit = query.execute().getNodes();
+ return JcrUtils.nodeIteratorToList(nit).toArray();
+ } catch (RepositoryException e) {
+ throw new JcrException(e);
+ }
+ }
+
+}
--- /dev/null
+package org.argeo.eclipse.ui.jcr;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.query.Row;
+
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.Image;
+
+/** Simplifies writing JCR-based column label provider. */
+public class RowColumnLabelProvider extends ColumnLabelProvider {
+ private static final long serialVersionUID = -6586692836928505358L;
+
+ protected String getRowText(Row row) throws RepositoryException {
+ return super.getText(row);
+ }
+
+ protected String getRowToolTipText(Row row) throws RepositoryException {
+ return super.getToolTipText(row);
+ }
+
+ protected Image getRowImage(Row row) throws RepositoryException {
+ return super.getImage(row);
+ }
+
+ protected Font getRowFont(Row row) throws RepositoryException {
+ return super.getFont(row);
+ }
+
+ public Color getRowBackground(Row row) throws RepositoryException {
+ return super.getBackground(row);
+ }
+
+ public Color getRowForeground(Row row) throws RepositoryException {
+ return super.getForeground(row);
+ }
+
+ @Override
+ public String getText(Object element) {
+ try {
+ if (element instanceof Row)
+ return getRowText((Row) element);
+ else
+ throw new IllegalArgumentException("Unsupported element type " + element.getClass());
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Repository exception when accessing " + element, e);
+ }
+ }
+
+ @Override
+ public Image getImage(Object element) {
+ try {
+ if (element instanceof Row)
+ return getRowImage((Row) element);
+ else
+ throw new IllegalArgumentException("Unsupported element type " + element.getClass());
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Repository exception when accessing " + element, e);
+ }
+ }
+
+ @Override
+ public String getToolTipText(Object element) {
+ try {
+ if (element instanceof Row)
+ return getRowToolTipText((Row) element);
+ else
+ throw new IllegalArgumentException("Unsupported element type " + element.getClass());
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Repository exception when accessing " + element, e);
+ }
+ }
+
+ @Override
+ public Font getFont(Object element) {
+ try {
+ if (element instanceof Row)
+ return getRowFont((Row) element);
+ else
+ throw new IllegalArgumentException("Unsupported element type " + element.getClass());
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Repository exception when accessing " + element, e);
+ }
+ }
+
+ @Override
+ public Color getBackground(Object element) {
+ try {
+ if (element instanceof Row)
+ return getRowBackground((Row) element);
+ else
+ throw new IllegalArgumentException("Unsupported element type " + element.getClass());
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Repository exception when accessing " + element, e);
+ }
+ }
+
+ @Override
+ public Color getForeground(Object element) {
+ try {
+ if (element instanceof Row)
+ return getRowForeground((Row) element);
+ else
+ throw new IllegalArgumentException("Unsupported element type " + element.getClass());
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Repository exception when accessing " + element, e);
+ }
+ }
+
+}
--- /dev/null
+package org.argeo.eclipse.ui.jcr;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.jcr.JcrUtils;
+
+/** Simple JCR node content provider taking a list of String as base path. */
+public class SimpleNodeContentProvider extends AbstractNodeContentProvider {
+ private static final long serialVersionUID = -8245193308831384269L;
+ private final List<String> basePaths;
+ private Boolean mkdirs = false;
+
+ public SimpleNodeContentProvider(Session session, String... basePaths) {
+ this(session, Arrays.asList(basePaths));
+ }
+
+ public SimpleNodeContentProvider(Session session, List<String> basePaths) {
+ super(session);
+ this.basePaths = basePaths;
+ }
+
+ @Override
+ protected Boolean isBasePath(String path) {
+ if (basePaths.contains(path))
+ return true;
+ return super.isBasePath(path);
+ }
+
+ public Object[] getElements(Object inputElement) {
+ try {
+ List<Node> baseNodes = new ArrayList<Node>();
+ for (String basePath : basePaths)
+ if (mkdirs && !getSession().itemExists(basePath))
+ baseNodes.add(JcrUtils.mkdirs(getSession(), basePath));
+ else
+ baseNodes.add(getSession().getNode(basePath));
+ return baseNodes.toArray();
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot get base nodes for " + basePaths,
+ e);
+ }
+ }
+
+ public List<String> getBasePaths() {
+ return basePaths;
+ }
+
+ public void setMkdirs(Boolean mkdirs) {
+ this.mkdirs = mkdirs;
+ }
+
+}
--- /dev/null
+package org.argeo.eclipse.ui.jcr;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.version.Version;
+
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.swt.graphics.Image;
+
+/** Simplifies writing JCR-based column label provider. */
+public class VersionColumnLabelProvider extends ColumnLabelProvider {
+ private static final long serialVersionUID = -6117690082313161159L;
+
+ protected String getVersionText(Version version) throws RepositoryException {
+ return super.getText(version);
+ }
+
+ protected String getVersionToolTipText(Version version) throws RepositoryException {
+ return super.getToolTipText(version);
+ }
+
+ protected Image getVersionImage(Version version) throws RepositoryException {
+ return super.getImage(version);
+ }
+
+ protected String getUserName(Version version) throws RepositoryException {
+ Node node = version.getFrozenNode();
+ if(node.hasProperty(Property.JCR_LAST_MODIFIED_BY))
+ return node.getProperty(Property.JCR_LAST_MODIFIED_BY).getString();
+ if(node.hasProperty(Property.JCR_CREATED_BY))
+ return node.getProperty(Property.JCR_CREATED_BY).getString();
+ return null;
+ }
+
+// protected String getActivityTitle(Version version) throws RepositoryException {
+// Node activity = getActivity(version);
+// if (activity == null)
+// return null;
+// if (activity.hasProperty("jcr:activityTitle"))
+// return activity.getProperty("jcr:activityTitle").getString();
+// else
+// return activity.getName();
+// }
+//
+// protected Node getActivity(Version version) throws RepositoryException {
+// if (version.hasProperty(Property.JCR_ACTIVITY)) {
+// return version.getProperty(Property.JCR_ACTIVITY).getNode();
+// } else
+// return null;
+// }
+
+ @Override
+ public String getText(Object element) {
+ try {
+ return getVersionText((Version) element);
+ } catch (RepositoryException e) {
+ throw new RuntimeException("Runtime repository exception when accessing " + element, e);
+ }
+ }
+
+ @Override
+ public Image getImage(Object element) {
+ try {
+ return getVersionImage((Version) element);
+ } catch (RepositoryException e) {
+ throw new RuntimeException("Runtime repository exception when accessing " + element, e);
+ }
+ }
+
+ @Override
+ public String getToolTipText(Object element) {
+ try {
+ return getVersionToolTipText((Version) element);
+ } catch (RepositoryException e) {
+ throw new RuntimeException("Runtime repository exception when accessing " + element, e);
+ }
+ }
+
+}
--- /dev/null
+package org.argeo.eclipse.ui.jcr;
+
+import javax.jcr.version.VersionHistory;
+
+import org.argeo.jcr.Jcr;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+/** Content provider based on a {@link VersionHistory}. */
+public class VersionHistoryContentProvider implements IStructuredContentProvider {
+ private static final long serialVersionUID = -4921107883428887012L;
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ }
+
+ @Override
+ public Object[] getElements(Object inputElement) {
+ VersionHistory versionHistory = (VersionHistory) inputElement;
+ return Jcr.getLinearVersions(versionHistory).toArray();
+ }
+
+}
--- /dev/null
+package org.argeo.eclipse.ui.jcr;
+
+import javax.jcr.Node;
+
+/** Wrap a node (created from a {@link NodesWrapper}) */
+public class WrappedNode {
+ private final NodesWrapper parent;
+ private final Node node;
+
+ public WrappedNode(NodesWrapper parent, Node node) {
+ super();
+ this.parent = parent;
+ this.node = node;
+ }
+
+ public NodesWrapper getParent() {
+ return parent;
+ }
+
+ public Node getNode() {
+ return node;
+ }
+
+ public String toString() {
+ return "wrapped " + node;
+ }
+
+ @Override
+ public int hashCode() {
+ return node.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof WrappedNode)
+ return node.equals(((WrappedNode) obj).getNode());
+ else
+ return false;
+ }
+
+}
--- /dev/null
+package org.argeo.eclipse.ui.jcr.lists;
+
+import javax.jcr.Node;
+import javax.jcr.query.Row;
+
+import org.argeo.eclipse.ui.ColumnDefinition;
+
+/**
+ * Utility object to manage column in various tables and extracts displaying
+ * data from JCR
+ */
+public class JcrColumnDefinition extends ColumnDefinition {
+ private final static int DEFAULT_COLUMN_SIZE = 120;
+
+ private String selectorName;
+ private String propertyName;
+ private int propertyType;
+ private int columnSize;
+
+ /**
+ * Use this kind of columns to configure a table that displays JCR
+ * {@link Row}
+ *
+ * @param selectorName
+ * @param propertyName
+ * @param propertyType
+ * @param headerLabel
+ */
+ public JcrColumnDefinition(String selectorName, String propertyName,
+ int propertyType, String headerLabel) {
+ super(new SimpleJcrRowLabelProvider(selectorName, propertyName),
+ headerLabel);
+ this.selectorName = selectorName;
+ this.propertyName = propertyName;
+ this.propertyType = propertyType;
+ this.columnSize = DEFAULT_COLUMN_SIZE;
+ }
+
+ /**
+ * Use this kind of columns to configure a table that displays JCR
+ * {@link Row}
+ *
+ * @param selectorName
+ * @param propertyName
+ * @param propertyType
+ * @param headerLabel
+ * @param columnSize
+ */
+ public JcrColumnDefinition(String selectorName, String propertyName,
+ int propertyType, String headerLabel, int columnSize) {
+ super(new SimpleJcrRowLabelProvider(selectorName, propertyName),
+ headerLabel, columnSize);
+ this.selectorName = selectorName;
+ this.propertyName = propertyName;
+ this.propertyType = propertyType;
+ this.columnSize = columnSize;
+ }
+
+ /**
+ * Use this kind of columns to configure a table that displays JCR
+ * {@link Node}
+ *
+ * @param propertyName
+ * @param propertyType
+ * @param headerLabel
+ * @param columnSize
+ */
+ public JcrColumnDefinition(String propertyName, int propertyType,
+ String headerLabel, int columnSize) {
+ super(new SimpleJcrNodeLabelProvider(propertyName), headerLabel,
+ columnSize);
+ this.propertyName = propertyName;
+ this.propertyType = propertyType;
+ this.columnSize = columnSize;
+ }
+
+ public String getSelectorName() {
+ return selectorName;
+ }
+
+ public void setSelectorName(String selectorName) {
+ this.selectorName = selectorName;
+ }
+
+ public String getPropertyName() {
+ return propertyName;
+ }
+
+ public void setPropertyName(String propertyName) {
+ this.propertyName = propertyName;
+ }
+
+ public int getPropertyType() {
+ return propertyType;
+ }
+
+ public void setPropertyType(int propertyType) {
+ this.propertyType = propertyType;
+ }
+
+ public int getColumnSize() {
+ return columnSize;
+ }
+
+ public void setColumnSize(int columnSize) {
+ this.columnSize = columnSize;
+ }
+
+ public String getHeaderLabel() {
+ return super.getLabel();
+ }
+
+ public void setHeaderLabel(String headerLabel) {
+ super.setLabel(headerLabel);
+ }
+}
--- /dev/null
+package org.argeo.eclipse.ui.jcr.lists;
+
+import java.math.BigDecimal;
+import java.util.Calendar;
+
+import javax.jcr.Node;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+import javax.jcr.ValueFormatException;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerComparator;
+
+/**
+ * Base comparator to enable ordering on Table or Tree viewer that display Jcr
+ * Nodes.
+ *
+ * Note that the following snippet must be added before setting the comparator
+ * to the corresponding control: <code>
+ * // IMPORTANT: initialize comparator before setting it
+ * JcrColumnDefinition firstCol = colDefs.get(0);
+ * comparator.setColumn(firstCol.getPropertyType(),
+ * firstCol.getPropertyName());
+ * viewer.setComparator(comparator); </code>
+ */
+public class NodeViewerComparator extends ViewerComparator {
+ private static final long serialVersionUID = -7782916140737279027L;
+
+ protected String propertyName;
+
+ protected int propertyType;
+ public static final int ASCENDING = 0, DESCENDING = 1;
+ protected int direction = DESCENDING;
+
+ public NodeViewerComparator() {
+ }
+
+ /**
+ * e1 and e2 must both be Jcr nodes.
+ *
+ * @param viewer
+ * @param e1
+ * @param e2
+ * @return the comparison
+ */
+ @Override
+ public int compare(Viewer viewer, Object e1, Object e2) {
+ int rc = 0;
+ long lc = 0;
+
+ try {
+ Node n1 = (Node) e1;
+ Node n2 = (Node) e2;
+
+ Value v1 = null;
+ Value v2 = null;
+ if (n1.hasProperty(propertyName))
+ v1 = n1.getProperty(propertyName).getValue();
+ if (n2.hasProperty(propertyName))
+ v2 = n2.getProperty(propertyName).getValue();
+
+ if (v2 == null && v1 == null)
+ return 0;
+ else if (v2 == null)
+ return -1;
+ else if (v1 == null)
+ return 1;
+
+ switch (propertyType) {
+ case PropertyType.STRING:
+ rc = v1.getString().compareTo(v2.getString());
+ break;
+ case PropertyType.BOOLEAN:
+ boolean b1 = v1.getBoolean();
+ boolean b2 = v2.getBoolean();
+ if (b1 == b2)
+ rc = 0;
+ else
+ // we assume true is greater than false
+ rc = b1 ? 1 : -1;
+ break;
+ case PropertyType.DATE:
+ Calendar c1 = v1.getDate();
+ Calendar c2 = v2.getDate();
+ if (c1 == null || c2 == null)
+ // log.trace("undefined date");
+ ;
+ lc = c1.getTimeInMillis() - c2.getTimeInMillis();
+ if (lc < Integer.MIN_VALUE)
+ rc = -1;
+ else if (lc > Integer.MAX_VALUE)
+ rc = 1;
+ else
+ rc = (int) lc;
+ break;
+ case PropertyType.LONG:
+ long l1;
+ long l2;
+ // TODO Sometimes an empty string is set instead of a long
+ try {
+ l1 = v1.getLong();
+ } catch (ValueFormatException ve) {
+ l1 = 0;
+ }
+ try {
+ l2 = v2.getLong();
+ } catch (ValueFormatException ve) {
+ l2 = 0;
+ }
+
+ lc = l1 - l2;
+ if (lc < Integer.MIN_VALUE)
+ rc = -1;
+ else if (lc > Integer.MAX_VALUE)
+ rc = 1;
+ else
+ rc = (int) lc;
+ break;
+ case PropertyType.DECIMAL:
+ BigDecimal bd1 = v1.getDecimal();
+ BigDecimal bd2 = v2.getDecimal();
+ rc = bd1.compareTo(bd2);
+ break;
+ case PropertyType.DOUBLE:
+ Double d1 = v1.getDouble();
+ Double d2 = v2.getDouble();
+ rc = d1.compareTo(d2);
+ break;
+ default:
+ throw new EclipseUiException(
+ "Unimplemented comparaison for PropertyType "
+ + propertyType);
+ }
+ // If descending order, flip the direction
+ if (direction == DESCENDING) {
+ rc = -rc;
+ }
+
+ } catch (RepositoryException re) {
+ throw new EclipseUiException("Unexpected error "
+ + "while comparing nodes", re);
+ }
+ return rc;
+ }
+
+ /**
+ * @param propertyType
+ * Corresponding JCR type
+ * @param propertyName
+ * name of the property to use.
+ */
+ public void setColumn(int propertyType, String propertyName) {
+ if (this.propertyName != null && this.propertyName.equals(propertyName)) {
+ // Same column as last sort; toggle the direction
+ direction = 1 - direction;
+ } else {
+ // New column; do an ascending sort
+ this.propertyType = propertyType;
+ this.propertyName = propertyName;
+ direction = ASCENDING;
+ }
+ }
+
+ // Getters and setters
+ protected String getPropertyName() {
+ return propertyName;
+ }
+
+ protected void setPropertyName(String propertyName) {
+ this.propertyName = propertyName;
+ }
+
+ protected int getPropertyType() {
+ return propertyType;
+ }
+
+ protected void setPropertyType(int propertyType) {
+ this.propertyType = propertyType;
+ }
+
+ protected int getDirection() {
+ return direction;
+ }
+
+ protected void setDirection(int direction) {
+ this.direction = direction;
+ }
+}
--- /dev/null
+package org.argeo.eclipse.ui.jcr.lists;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.query.Row;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.jface.viewers.Viewer;
+
+/**
+ * Base comparator to enable ordering on Table or Tree viewer that display Jcr
+ * rows
+ */
+public class RowViewerComparator extends NodeViewerComparator {
+ private static final long serialVersionUID = 7020939505172625113L;
+ protected String selectorName;
+
+ public RowViewerComparator() {
+ }
+
+ /**
+ * e1 and e2 must both be Jcr rows.
+ *
+ * @param viewer
+ * @param e1
+ * @param e2
+ * @return the comparison
+ */
+ @Override
+ public int compare(Viewer viewer, Object e1, Object e2) {
+ try {
+ Node n1 = ((Row) e1).getNode(selectorName);
+ Node n2 = ((Row) e2).getNode(selectorName);
+ return super.compare(viewer, n1, n2);
+ } catch (RepositoryException re) {
+ throw new EclipseUiException("Unexpected error "
+ + "while comparing nodes", re);
+ }
+ }
+
+ /**
+ * @param propertyType
+ * Corresponding JCR type
+ * @param propertyName
+ * name of the property to use.
+ */
+ public void setColumn(int propertyType, String selectorName,
+ String propertyName) {
+ if (this.selectorName != null && getPropertyName() != null
+ && this.selectorName.equals(selectorName)
+ && this.getPropertyName().equals(propertyName)) {
+ // Same column as last sort; toggle the direction
+ setDirection(1 - getDirection());
+ } else {
+ // New column; do a descending sort
+ setPropertyType(propertyType);
+ setPropertyName(propertyName);
+ this.selectorName = selectorName;
+ setDirection(NodeViewerComparator.ASCENDING);
+ }
+ }
+}
--- /dev/null
+package org.argeo.eclipse.ui.jcr.lists;
+
+import java.text.DateFormat;
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.text.SimpleDateFormat;
+
+import javax.jcr.Node;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+
+/** Base implementation of a label provider for controls that display JCR Nodes */
+public class SimpleJcrNodeLabelProvider extends ColumnLabelProvider {
+ private static final long serialVersionUID = -5215787695436221993L;
+
+ private final static String DEFAULT_DATE_FORMAT = "EEE, dd MMM yyyy";
+ private final static String DEFAULT_NUMBER_FORMAT = "#,##0.0";
+
+ private DateFormat dateFormat;
+ private NumberFormat numberFormat;
+
+ final private String propertyName;
+
+ /**
+ * Default Label provider for a given property of a node. Using default
+ * pattern for date and number formating
+ */
+ public SimpleJcrNodeLabelProvider(String propertyName) {
+ this.propertyName = propertyName;
+ dateFormat = new SimpleDateFormat(DEFAULT_DATE_FORMAT);
+ numberFormat = DecimalFormat.getInstance();
+ ((DecimalFormat) numberFormat).applyPattern(DEFAULT_NUMBER_FORMAT);
+ }
+
+ /**
+ * Label provider for a given property of a node optionally precising date
+ * and/or number format patterns
+ */
+ public SimpleJcrNodeLabelProvider(String propertyName,
+ String dateFormatPattern, String numberFormatPattern) {
+ this.propertyName = propertyName;
+ dateFormat = new SimpleDateFormat(
+ dateFormatPattern == null ? DEFAULT_DATE_FORMAT
+ : dateFormatPattern);
+ numberFormat = DecimalFormat.getInstance();
+ ((DecimalFormat) numberFormat)
+ .applyPattern(numberFormatPattern == null ? DEFAULT_NUMBER_FORMAT
+ : numberFormatPattern);
+ }
+
+ @Override
+ public String getText(Object element) {
+ try {
+ Node currNode = (Node) element;
+
+ if (currNode.hasProperty(propertyName)) {
+ if (currNode.getProperty(propertyName).isMultiple()) {
+ StringBuilder builder = new StringBuilder();
+ for (Value value : currNode.getProperty(propertyName)
+ .getValues()) {
+ String currStr = getSingleValueAsString(value);
+ if (notEmptyString(currStr))
+ builder.append(currStr).append("; ");
+ }
+ if (builder.length() > 0)
+ builder.deleteCharAt(builder.length() - 2);
+
+ return builder.toString();
+ } else
+ return getSingleValueAsString(currNode.getProperty(
+ propertyName).getValue());
+ } else
+ return "";
+ } catch (RepositoryException re) {
+ throw new EclipseUiException("Unable to get text from row", re);
+ }
+ }
+
+ private String getSingleValueAsString(Value value)
+ throws RepositoryException {
+ switch (value.getType()) {
+ case PropertyType.STRING:
+ return value.getString();
+ case PropertyType.BOOLEAN:
+ return "" + value.getBoolean();
+ case PropertyType.DATE:
+ return dateFormat.format(value.getDate().getTime());
+ case PropertyType.LONG:
+ return "" + value.getLong();
+ case PropertyType.DECIMAL:
+ return numberFormat.format(value.getDecimal());
+ case PropertyType.DOUBLE:
+ return numberFormat.format(value.getDouble());
+ case PropertyType.NAME:
+ return value.getString();
+ default:
+ throw new EclipseUiException("Unimplemented label provider "
+ + "for property type " + value.getType()
+ + " while getting property " + propertyName + " - value: "
+ + value.getString());
+
+ }
+ }
+
+ private boolean notEmptyString(String string) {
+ return string != null && !"".equals(string.trim());
+ }
+
+ public void setDateFormat(String dateFormatPattern) {
+ dateFormat = new SimpleDateFormat(dateFormatPattern);
+ }
+
+ public void setNumberFormat(String numberFormatPattern) {
+ ((DecimalFormat) numberFormat).applyPattern(numberFormatPattern);
+ }
+}
--- /dev/null
+package org.argeo.eclipse.ui.jcr.lists;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.query.Row;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+
+/**
+ * Base implementation of a label provider for widgets that display JCR Rows.
+ */
+public class SimpleJcrRowLabelProvider extends SimpleJcrNodeLabelProvider {
+ private static final long serialVersionUID = -3414654948197181740L;
+
+ final private String selectorName;
+
+ /**
+ * Default Label provider for a given property of a row. Using default
+ * pattern for date and number formating
+ */
+ public SimpleJcrRowLabelProvider(String selectorName, String propertyName) {
+ super(propertyName);
+ this.selectorName = selectorName;
+ }
+
+ /**
+ * Label provider for a given property of a node optionally precising date
+ * and/or number format patterns
+ */
+ public SimpleJcrRowLabelProvider(String selectorName, String propertyName,
+ String dateFormatPattern, String numberFormatPattern) {
+ super(propertyName, dateFormatPattern, numberFormatPattern);
+ this.selectorName = selectorName;
+ }
+
+ @Override
+ public String getText(Object element) {
+ try {
+ Row currRow = (Row) element;
+ Node currNode = currRow.getNode(selectorName);
+ return super.getText(currNode);
+ } catch (RepositoryException re) {
+ throw new EclipseUiException("Unable to get Node " + selectorName
+ + " from row " + element, re);
+ }
+ }
+}
--- /dev/null
+/** Generic SWT/JFace JCR utilities for lists. */
+package org.argeo.eclipse.ui.jcr.lists;
\ No newline at end of file
--- /dev/null
+/** Generic SWT/JFace JCR utilities. */
+package org.argeo.eclipse.ui.jcr;
\ No newline at end of file
--- /dev/null
+package org.argeo.eclipse.ui.jcr.util;
+
+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.IOUtils;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.FileProvider;
+
+/**
+ * Implements a FileProvider for UI purposes. Note that it might not be very
+ * reliable as long as we have not fixed login and multi repository issues that
+ * will be addressed in the next version.
+ *
+ * NOTE: id used here is the real id of the JCR Node, not the JCR Path
+ *
+ * Relies on common approach for JCR file handling implementation.
+ *
+ */
+@SuppressWarnings("deprecation")
+public class JcrFileProvider implements FileProvider {
+
+ // private Object[] rootNodes;
+ private Node refNode;
+
+ /**
+ * Must be set in order for the provider to be able to get current session
+ * and thus have the ability to get the file node corresponding to a given
+ * file ID
+ *
+ * @param refNode
+ */
+ public void setReferenceNode(Node refNode) {
+ // FIXME : this introduces some concurrency ISSUES.
+ this.refNode = refNode;
+ }
+
+ public byte[] getByteArrayFileFromId(String fileId) {
+ InputStream fis = null;
+ byte[] ba = null;
+ Node child = getFileNodeFromId(fileId);
+ try {
+ fis = (InputStream) child.getProperty(Property.JCR_DATA).getBinary().getStream();
+ ba = IOUtils.toByteArray(fis);
+
+ } catch (Exception e) {
+ throw new EclipseUiException("Stream error while opening file", e);
+ } finally {
+ IOUtils.closeQuietly(fis);
+ }
+ return ba;
+ }
+
+ public InputStream getInputStreamFromFileId(String fileId) {
+ try {
+ InputStream fis = null;
+
+ Node child = getFileNodeFromId(fileId);
+ fis = (InputStream) child.getProperty(Property.JCR_DATA).getBinary().getStream();
+ return fis;
+ } catch (RepositoryException re) {
+ throw new EclipseUiException("Cannot get stream from file node for Id " + fileId, re);
+ }
+ }
+
+ /**
+ * Throws an exception if the node is not found in the current repository (a
+ * bit like a FileNotFoundException)
+ *
+ * @param fileId
+ * @return Returns the child node of the nt:file node. It is the child node
+ * that have the jcr:data property where actual file is stored.
+ * never null
+ */
+ private Node getFileNodeFromId(String fileId) {
+ try {
+ Node result = refNode.getSession().getNodeByIdentifier(fileId);
+
+ // rootNodes: for (int j = 0; j < rootNodes.length; j++) {
+ // // in case we have a classic JCR Node
+ // if (rootNodes[j] instanceof Node) {
+ // Node curNode = (Node) rootNodes[j];
+ // if (result != null)
+ // break rootNodes;
+ // } // Case of a repository Node
+ // else if (rootNodes[j] instanceof RepositoryNode) {
+ // Object[] nodes = ((RepositoryNode) rootNodes[j])
+ // .getChildren();
+ // for (int i = 0; i < nodes.length; i++) {
+ // Node node = (Node) nodes[i];
+ // result = node.getSession().getNodeByIdentifier(fileId);
+ // if (result != null)
+ // break rootNodes;
+ // }
+ // }
+ // }
+
+ // Sanity checks
+ if (result == null)
+ throw new EclipseUiException("File node not found for ID" + fileId);
+
+ Node child = null;
+
+ boolean isValid = true;
+ if (!result.isNodeType(NodeType.NT_FILE))
+ // useless: mandatory child node
+ // || !result.hasNode(Property.JCR_CONTENT))
+ isValid = false;
+ else {
+ child = result.getNode(Property.JCR_CONTENT);
+ if (!(child.isNodeType(NodeType.NT_RESOURCE) || child.hasProperty(Property.JCR_DATA)))
+ isValid = false;
+ }
+
+ if (!isValid)
+ throw new EclipseUiException("ERROR: In the current implemented model, '" + NodeType.NT_FILE
+ + "' file node must have a child node named jcr:content "
+ + "that has a BINARY Property named jcr:data " + "where the actual data is stored");
+ return child;
+
+ } catch (RepositoryException re) {
+ throw new EclipseUiException("Erreur while getting file node of ID " + fileId, re);
+ }
+ }
+}
--- /dev/null
+package org.argeo.eclipse.ui.jcr.util;
+
+import java.util.Comparator;
+
+import javax.jcr.Item;
+import javax.jcr.RepositoryException;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+
+/** Compares two JCR items (node or properties) based on their names. */
+public class JcrItemsComparator implements Comparator<Item> {
+ public int compare(Item o1, Item o2) {
+ try {
+ // TODO: put folder before files
+ return o1.getName().toLowerCase().compareTo(o2.getName().toLowerCase());
+ } catch (RepositoryException e) {
+ throw new EclipseUiException("Cannot compare " + o1 + " and " + o2, e);
+ }
+ }
+
+}
--- /dev/null
+package org.argeo.eclipse.ui.jcr.util;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.jface.viewers.IElementComparer;
+
+/** Compare JCR nodes based on their JCR identifiers, for use in JFace viewers. */
+public class NodeViewerComparer implements IElementComparer {
+
+ // force comparison on Node IDs only.
+ public boolean equals(Object elementA, Object elementB) {
+ if (!(elementA instanceof Node) || !(elementB instanceof Node)) {
+ return elementA == null ? elementB == null : elementA
+ .equals(elementB);
+ } else {
+
+ boolean result = false;
+ try {
+ String idA = ((Node) elementA).getIdentifier();
+ String idB = ((Node) elementB).getIdentifier();
+ result = idA == null ? idB == null : idA.equals(idB);
+ } catch (RepositoryException re) {
+ throw new EclipseUiException("cannot compare nodes", re);
+ }
+
+ return result;
+ }
+ }
+
+ public int hashCode(Object element) {
+ // TODO enhanced this method.
+ return element.getClass().toString().hashCode();
+ }
+}
\ No newline at end of file
--- /dev/null
+package org.argeo.eclipse.ui.jcr.util;
+
+import java.io.InputStream;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.FileProvider;
+
+/**
+ * Implements a FileProvider for UI purposes. Unlike the
+ * <code> JcrFileProvider </code>, it relies on a single session and manages
+ * nodes with path only.
+ *
+ * Note that considered id is the JCR path
+ *
+ * Relies on common approach for JCR file handling implementation.
+ */
+@SuppressWarnings("deprecation")
+public class SingleSessionFileProvider implements FileProvider {
+
+ private Session session;
+
+ public SingleSessionFileProvider(Session session) {
+ this.session = session;
+ }
+
+ public byte[] getByteArrayFileFromId(String fileId) {
+ InputStream fis = null;
+ byte[] ba = null;
+ Node child = getFileNodeFromId(fileId);
+ try {
+ fis = (InputStream) child.getProperty(Property.JCR_DATA)
+ .getBinary().getStream();
+ ba = IOUtils.toByteArray(fis);
+
+ } catch (Exception e) {
+ throw new EclipseUiException("Stream error while opening file", e);
+ } finally {
+ IOUtils.closeQuietly(fis);
+ }
+ return ba;
+ }
+
+ public InputStream getInputStreamFromFileId(String fileId) {
+ try {
+ InputStream fis = null;
+
+ Node child = getFileNodeFromId(fileId);
+ fis = (InputStream) child.getProperty(Property.JCR_DATA)
+ .getBinary().getStream();
+ return fis;
+ } catch (RepositoryException re) {
+ throw new EclipseUiException("Cannot get stream from file node for Id "
+ + fileId, re);
+ }
+ }
+
+ /**
+ *
+ * @param fileId
+ * @return Returns the child node of the nt:file node. It is the child node
+ * that have the jcr:data property where actual file is stored.
+ * never null
+ */
+ private Node getFileNodeFromId(String fileId) {
+ try {
+ Node result = null;
+ result = session.getNode(fileId);
+
+ // Sanity checks
+ if (result == null)
+ throw new EclipseUiException("File node not found for ID" + fileId);
+
+ // Ensure that the node have the correct type.
+ if (!result.isNodeType(NodeType.NT_FILE))
+ throw new EclipseUiException(
+ "Cannot open file children Node that are not of "
+ + NodeType.NT_RESOURCE + " type.");
+
+ Node child = result.getNodes().nextNode();
+ if (child == null || !child.isNodeType(NodeType.NT_RESOURCE))
+ throw new EclipseUiException(
+ "ERROR: IN the current implemented model, "
+ + NodeType.NT_FILE
+ + " file node must have one and only one child of the nt:ressource, where actual data is stored");
+ return child;
+ } catch (RepositoryException re) {
+ throw new EclipseUiException("Erreur while getting file node of ID "
+ + fileId, re);
+ }
+ }
+}
--- /dev/null
+/** Generic SWT/JFace JCR helpers. */
+package org.argeo.eclipse.ui.jcr.util;
\ No newline at end of file
--- /dev/null
+<?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-17">
+ <attributes>
+ <attribute name="module" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>org.argeo.cms.jcr</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>
+ <buildCommand>
+ <name>org.eclipse.pde.ds.core.builder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.pde.PluginNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
--- /dev/null
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnull.secondary=
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary=
+org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullable.secondary=
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
+org.eclipse.jdt.core.compiler.problem.APILeak=warning
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning
+org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning
+org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled
+org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=info
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" activate="init" deactivate="destroy" name="org.argeo.cms.dataServletContext">
+ <implementation class="org.argeo.cms.jcr.internal.servlet.DataServletContext"/>
+ <service>
+ <provide interface="org.osgi.service.http.context.ServletContextHelper"/>
+ </service>
+ <property name="osgi.http.whiteboard.context.name" type="String" value="dataServletContext"/>
+ <property name="osgi.http.whiteboard.context.path" type="String" value="/data"/>
+</scr:component>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="org.argeo.cms.filesServlet">
+ <implementation class="org.argeo.cms.jcr.internal.servlet.CmsWebDavServlet"/>
+ <service>
+ <provide interface="javax.servlet.Servlet"/>
+ </service>
+ <property name="osgi.http.whiteboard.servlet.pattern" type="String" value="/*"/>
+ <property name="osgi.http.whiteboard.context.select" type="String" value="(osgi.http.whiteboard.context.name=filesServletContext)"/>
+ <property name="servlet.init.resource-config" type="String" value="/org/argeo/cms/jcr/internal/servlet/webdav-config.xml"/>
+ <property name="servlet.init.resource-path-prefix" type="String" value="/files"/>
+ <reference bind="setRepository" cardinality="1..1" interface="javax.jcr.Repository" policy="static" target="(cn=ego)"/>
+</scr:component>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" activate="init" deactivate="destroy" name="org.argeo.cms.filesServletContext">
+ <implementation class="org.argeo.cms.jcr.internal.servlet.JcrServletContext"/>
+ <service>
+ <provide interface="org.osgi.service.http.context.ServletContextHelper"/>
+ </service>
+ <property name="osgi.http.whiteboard.context.name" type="String" value="filesServletContext"/>
+ <property name="osgi.http.whiteboard.context.path" type="String" value="/files"/>
+</scr:component>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" activate="start" deactivate="stop" name="JCR Content Provider">
+ <implementation class="org.argeo.cms.jcr.acr.JcrContentProvider"/>
+ <reference bind="setJcrRepository" cardinality="1..1" interface="javax.jcr.Repository" name="Repository" policy="static" target="(cn=ego)"/>
+ <property name="acr.mount.path" type="String" value="/sys"/>
+ <service>
+ <provide interface="org.argeo.api.acr.spi.ContentProvider"/>
+ <provide interface="org.argeo.cms.jcr.acr.JcrContentProvider"/>
+ </service>
+</scr:component>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" activate="start" deactivate="stop" name="JCR Deployment">
+ <implementation class="org.argeo.cms.jcr.internal.CmsJcrDeployment"/>
+</scr:component>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="JCR FS Provider">
+ <implementation class="org.argeo.cms.jcr.internal.CmsJcrFsProvider"/>
+ <property name="service.pid" type="String" value="org.argeo.api.fsProvider"/>
+ <service>
+ <provide interface="java.nio.file.spi.FileSystemProvider"/>
+ </service>
+ <reference bind="setRepositoryFactory" cardinality="1..1" interface="javax.jcr.RepositoryFactory" name="RepositoryFactory" policy="static"/>
+ <reference bind="setRepository" cardinality="1..1" interface="javax.jcr.Repository" name="Repository" policy="static"/>
+</scr:component>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="JCR Repository Factory">
+ <implementation class="org.argeo.cms.jcr.internal.JcrRepositoryFactory"/>
+ <service>
+ <provide interface="javax.jcr.RepositoryFactory"/>
+ </service>
+ <reference cardinality="1..1" interface="org.argeo.api.cms.CmsState" name="CmsState" policy="static"/>
+</scr:component>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" activate="init" deactivate="destroy" name="org.argeo.cms.jcrServletContext">
+ <implementation class="org.argeo.cms.jcr.internal.servlet.JcrServletContext"/>
+ <service>
+ <provide interface="org.osgi.service.http.context.ServletContextHelper"/>
+ </service>
+ <property name="osgi.http.whiteboard.context.name" type="String" value="jcrServletContext"/>
+ <property name="osgi.http.whiteboard.context.path" type="String" value="/jcr"/>
+</scr:component>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" activate="init" deactivate="destroy" name="Jackrabbit Repository Contexts Factory">
+ <implementation class="org.argeo.cms.jcr.internal.RepositoryContextsFactory"/>
+ <property name="service.pid" type="String" value="org.argeo.api.repos"/>
+ <reference bind="setCmsState" cardinality="1..1" interface="org.argeo.api.cms.CmsState" name="CmsState" policy="static"/>
+</scr:component>
--- /dev/null
+Bundle-Activator: org.argeo.cms.jcr.internal.osgi.CmsJcrActivator
+
+Provide-Capability:\
+cms.datamodel; name=jcrx; cnd=/org/argeo/jcr/jcrx.cnd; abstract=true,\
+cms.datamodel; name=argeo; cnd=/org/argeo/cms/jcr/argeo.cnd; abstract=true,\
+cms.datamodel;name=ldap; cnd=/org/argeo/cms/jcr/ldap.cnd; abstract=true,\
+osgi.service;objectClass="javax.jcr.Repository"
+
+Import-Package:\
+org.argeo.cms.servlet,\
+javax.jcr.security,\
+org.h2;resolution:=optional;version="[1,3)",\
+org.postgresql;version="[42,43)";resolution:=optional,\
+org.apache.commons.httpclient.cookie;resolution:=optional,\
+org.osgi.framework.namespace;version=0.0.0,\
+org.osgi.*;version=0.0.0,\
+org.osgi.service.http.whiteboard,\
+org.apache.jackrabbit.api.stats;version="[1,4)",\
+org.apache.jackrabbit.api;version="[1,4)",\
+org.apache.jackrabbit.commons;version="[1,4)",\
+org.apache.jackrabbit.spi;version="[1,4)",\
+org.apache.jackrabbit.spi2dav;version="[1,4)",\
+org.apache.jackrabbit.spi2davex;version="[1,4)",\
+org.apache.jackrabbit.webdav.jcr;version="[1,4)",\
+org.apache.jackrabbit.webdav.server;version="[1,4)",\
+org.apache.jackrabbit.webdav.simple;version="[1,4)",\
+org.apache.jackrabbit.*;version="[1,4)",\
+junit.*;resolution:=optional,\
+javax.servlet.*;version="[3,5)",\
+*
+
+Service-Component:\
+OSGI-INF/repositoryContextsFactory.xml,\
+OSGI-INF/jcrRepositoryFactory.xml,\
+OSGI-INF/jcrFsProvider.xml,\
+OSGI-INF/jcrDeployment.xml,\
+OSGI-INF/jcrServletContext.xml,\
+OSGI-INF/dataServletContext.xml,\
+OSGI-INF/filesServletContext.xml,\
+OSGI-INF/filesServlet.xml,\
--- /dev/null
+output.. = bin/
+bin.includes = META-INF/,\
+ .,\
+ OSGI-INF/,\
+ OSGI-INF/jcrContentProvider.xml
+source.. = src/
+additional.bundles = org.apache.jackrabbit.data,\
+ org.apache.jackrabbit.spi.commons
--- /dev/null
+package org.argeo.cms.fs;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.spi.FileSystemProvider;
+
+import javax.jcr.NoSuchWorkspaceException;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.query.Query;
+import javax.jcr.query.QueryManager;
+
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.jcr.Jcr;
+
+/** Utilities around documents. */
+public class CmsFsUtils {
+ // TODO make it more robust and configurable
+ private static String baseWorkspaceName = CmsConstants.SYS_WORKSPACE;
+
+ public static Node getNode(Repository repository, Path path) {
+ String workspaceName = path.getNameCount() == 0 ? baseWorkspaceName : path.getName(0).toString();
+ String jcrPath = '/' + path.subpath(1, path.getNameCount()).toString();
+ try {
+ Session newSession;
+ try {
+ newSession = repository.login(workspaceName);
+ } catch (NoSuchWorkspaceException e) {
+ // base workspace
+ newSession = repository.login(baseWorkspaceName);
+ jcrPath = path.toString();
+ }
+ return newSession.getNode(jcrPath);
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Cannot get node from path " + path, e);
+ }
+ }
+
+ public static NodeIterator getLastUpdatedDocuments(Session session) {
+ try {
+ String qStr = "//element(*, nt:file)";
+ qStr += " order by @jcr:lastModified descending";
+ QueryManager queryManager = session.getWorkspace().getQueryManager();
+ @SuppressWarnings("deprecation")
+ Query xpathQuery = queryManager.createQuery(qStr, Query.XPATH);
+ xpathQuery.setLimit(8);
+ NodeIterator nit = xpathQuery.execute().getNodes();
+ return nit;
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Unable to retrieve last updated documents", e);
+ }
+ }
+
+ public static Path getPath(FileSystemProvider nodeFileSystemProvider, URI uri) {
+ try {
+ FileSystem fileSystem = nodeFileSystemProvider.getFileSystem(uri);
+ if (fileSystem == null)
+ fileSystem = nodeFileSystemProvider.newFileSystem(uri, null);
+ String path = uri.getPath();
+ return fileSystem.getPath(path);
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to initialise file system for " + uri, e);
+ }
+ }
+
+ public static Path getPath(FileSystemProvider nodeFileSystemProvider, Node node) {
+ String workspaceName = Jcr.getWorkspaceName(node);
+ String fullPath = baseWorkspaceName.equals(workspaceName) ? Jcr.getPath(node)
+ : '/' + workspaceName + Jcr.getPath(node);
+ URI uri;
+ try {
+ uri = new URI(CmsConstants.SCHEME_NODE, null, fullPath, null);
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Cannot interpret " + fullPath + " as an URI", e);
+ }
+ return getPath(nodeFileSystemProvider, uri);
+ }
+
+ /** Singleton. */
+ private CmsFsUtils() {
+ }
+}
--- /dev/null
+package org.argeo.cms.internal.jcr;
+
+import java.util.Properties;
+
+import org.apache.jackrabbit.core.config.BeanConfig;
+import org.apache.jackrabbit.core.config.ConfigurationException;
+import org.apache.jackrabbit.core.config.RepositoryConfigurationParser;
+import org.apache.jackrabbit.core.config.WorkspaceSecurityConfig;
+import org.apache.jackrabbit.core.util.db.ConnectionFactory;
+import org.w3c.dom.Element;
+
+/**
+ * A {@link RepositoryConfigurationParser} providing more flexibility with
+ * classloaders.
+ */
+@SuppressWarnings("restriction")
+class CustomRepositoryConfigurationParser extends RepositoryConfigurationParser {
+ private ClassLoader classLoader = null;
+
+ public CustomRepositoryConfigurationParser(Properties variables) {
+ super(variables);
+ }
+
+ public CustomRepositoryConfigurationParser(Properties variables, ConnectionFactory connectionFactory) {
+ super(variables, connectionFactory);
+ }
+
+ @Override
+ protected RepositoryConfigurationParser createSubParser(Properties variables) {
+ Properties props = new Properties(getVariables());
+ props.putAll(variables);
+ CustomRepositoryConfigurationParser subParser = new CustomRepositoryConfigurationParser(props,
+ connectionFactory);
+ subParser.setClassLoader(classLoader);
+ return subParser;
+ }
+
+ @Override
+ public WorkspaceSecurityConfig parseWorkspaceSecurityConfig(Element parent) throws ConfigurationException {
+ WorkspaceSecurityConfig workspaceSecurityConfig = super.parseWorkspaceSecurityConfig(parent);
+ workspaceSecurityConfig.getAccessControlProviderConfig().setClassLoader(classLoader);
+ return workspaceSecurityConfig;
+ }
+
+ @Override
+ protected BeanConfig parseBeanConfig(Element parent, String name) throws ConfigurationException {
+ BeanConfig beanConfig = super.parseBeanConfig(parent, name);
+ if (beanConfig.getClassName().startsWith("org.argeo")) {
+ beanConfig.setClassLoader(classLoader);
+ }
+ return beanConfig;
+ }
+
+ public void setClassLoader(ClassLoader classLoader) {
+ this.classLoader = classLoader;
+ }
+
+}
--- /dev/null
+package org.argeo.cms.internal.jcr;
+
+/** Pre-defined Jackrabbit repository configurations. */
+enum JackrabbitType {
+ /** Local file system */
+ localfs,
+ /** Embedded Java H2 database */
+ h2,
+ /** Embedded Java H2 database in PostgreSQL compatibility mode */
+ h2_postgresql,
+ /** PostgreSQL */
+ postgresql,
+ /** PostgreSQL with datastore */
+ postgresql_ds,
+ /** PostgreSQL with cluster */
+ postgresql_cluster,
+ /** PostgreSQL with cluster and datastore */
+ postgresql_cluster_ds,
+ /** Memory */
+ memory;
+}
--- /dev/null
+package org.argeo.cms.internal.jcr;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+import org.apache.jackrabbit.core.data.DataIdentifier;
+import org.apache.jackrabbit.core.data.DataRecord;
+import org.apache.jackrabbit.core.data.DataStoreException;
+import org.apache.jackrabbit.core.data.FileDataStore;
+
+/**
+ * <b>experimental</b> Duplicate added entries in another directory (typically a
+ * remote mount).
+ */
+@SuppressWarnings("restriction")
+public class LocalFsDataStore extends FileDataStore {
+ String redundantPath;
+ FileDataStore redundantStore;
+
+ @Override
+ public void init(String homeDir) {
+ // init primary first
+ super.init(homeDir);
+
+ if (redundantPath != null) {
+ // redundant directory must be created first
+ // TODO implement some polling?
+ if (Files.exists(Paths.get(redundantPath))) {
+ redundantStore = new FileDataStore();
+ redundantStore.setPath(redundantPath);
+ redundantStore.init(homeDir);
+ }
+ }
+ }
+
+ @Override
+ public DataRecord addRecord(InputStream input) throws DataStoreException {
+ DataRecord dataRecord = super.addRecord(input);
+ syncRedundantRecord(dataRecord);
+ return dataRecord;
+ }
+
+ @Override
+ public DataRecord getRecord(DataIdentifier identifier) throws DataStoreException {
+ DataRecord dataRecord = super.getRecord(identifier);
+ syncRedundantRecord(dataRecord);
+ return dataRecord;
+ }
+
+ protected void syncRedundantRecord(DataRecord dataRecord) throws DataStoreException {
+ if (redundantStore == null)
+ return;
+ if (redundantStore.getRecordIfStored(dataRecord.getIdentifier()) == null) {
+ try (InputStream redundant = dataRecord.getStream()) {
+ redundantStore.addRecord(redundant);
+ } catch (IOException e) {
+ throw new DataStoreException("Cannot add redundant record.", e);
+ }
+ }
+ }
+
+ public void setRedundantPath(String redundantPath) {
+ this.redundantPath = redundantPath;
+ }
+
+}
--- /dev/null
+package org.argeo.cms.internal.jcr;
+
+import org.argeo.api.cms.CmsConstants;
+
+/** JCR repository configuration */
+public enum RepoConf {
+ /** Repository type */
+ type("h2"),
+ /** Default workspace */
+ defaultWorkspace(CmsConstants.SYS_WORKSPACE),
+ /** Database URL */
+ dburl(null),
+ /** Database user */
+ dbuser(null),
+ /** Database password */
+ dbpassword(null),
+
+ /** The identifier (can be an URL locating the repo) */
+ labeledUri(null),
+ //
+ // JACKRABBIT SPECIFIC
+ //
+ /** Maximum database pool size */
+ maxPoolSize(10),
+ /** Maximum cache size in MB */
+ maxCacheMB(null),
+ /** Bundle cache size in MB */
+ bundleCacheMB(8),
+ /** Extractor pool size */
+ extractorPoolSize(0),
+ /** Search cache size */
+ searchCacheSize(1000),
+ /** Max volatile index size */
+ maxVolatileIndexSize(1048576),
+ /** Cluster id (if appropriate configuration) */
+ clusterId("default"),
+ /** Indexes base path */
+ indexesBase(null);
+
+ /** The default value. */
+ private Object def;
+
+ RepoConf(String oid, Object def) {
+ this.def = def;
+ }
+
+ RepoConf(Object def) {
+ this.def = def;
+ }
+
+ public Object getDefault() {
+ return def;
+ }
+
+}
--- /dev/null
+package org.argeo.cms.internal.jcr;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Properties;
+import java.util.UUID;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.jackrabbit.core.RepositoryContext;
+import org.apache.jackrabbit.core.RepositoryImpl;
+import org.apache.jackrabbit.core.cache.CacheManager;
+import org.apache.jackrabbit.core.config.RepositoryConfig;
+import org.apache.jackrabbit.core.config.RepositoryConfigurationParser;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.jcr.internal.CmsPaths;
+import org.xml.sax.InputSource;
+
+/** Can interpret properties in order to create an actual JCR repository. */
+public class RepositoryBuilder {
+ private final static CmsLog log = CmsLog.getLog(RepositoryBuilder.class);
+
+ public RepositoryContext createRepositoryContext(Dictionary<String, ?> properties)
+ throws RepositoryException, IOException {
+ RepositoryConfig repositoryConfig = createRepositoryConfig(properties);
+ RepositoryContext repositoryContext = createJackrabbitRepository(repositoryConfig);
+ RepositoryImpl repository = repositoryContext.getRepository();
+
+ // cache
+ Object maxCacheMbStr = prop(properties, RepoConf.maxCacheMB);
+ if (maxCacheMbStr != null) {
+ Integer maxCacheMB = Integer.parseInt(maxCacheMbStr.toString());
+ CacheManager cacheManager = repository.getCacheManager();
+ cacheManager.setMaxMemory(maxCacheMB * 1024l * 1024l);
+ cacheManager.setMaxMemoryPerCache((maxCacheMB / 4) * 1024l * 1024l);
+ }
+
+ return repositoryContext;
+ }
+
+ RepositoryConfig createRepositoryConfig(Dictionary<String, ?> properties) throws RepositoryException, IOException {
+ JackrabbitType type = JackrabbitType.valueOf(prop(properties, RepoConf.type).toString());
+ ClassLoader cl = getClass().getClassLoader();
+ final String base = "/org/argeo/cms/internal/jcr";
+ try (InputStream in = cl.getResourceAsStream(base + "/repository-" + type.name() + ".xml")) {
+ if (in == null)
+ throw new IllegalArgumentException("Repository configuration not found");
+ InputSource config = new InputSource(in);
+ Properties jackrabbitVars = getConfigurationProperties(type, properties);
+ // RepositoryConfig repositoryConfig = RepositoryConfig.create(config,
+ // jackrabbitVars);
+
+ // custom configuration parser
+ CustomRepositoryConfigurationParser parser = new CustomRepositoryConfigurationParser(jackrabbitVars);
+ parser.setClassLoader(cl);
+ RepositoryConfig repositoryConfig = parser.parseRepositoryConfig(config);
+ repositoryConfig.init();
+
+ // set the proper classloaders
+ repositoryConfig.getSecurityConfig().getSecurityManagerConfig().setClassLoader(cl);
+ repositoryConfig.getSecurityConfig().getAccessManagerConfig().setClassLoader(cl);
+// for (WorkspaceConfig workspaceConfig : repositoryConfig.getWorkspaceConfigs()) {
+// workspaceConfig.getSecurityConfig().getAccessControlProviderConfig().setClassLoader(cl);
+// }
+ return repositoryConfig;
+ }
+ }
+
+ private Properties getConfigurationProperties(JackrabbitType type, Dictionary<String, ?> properties) {
+ Properties props = new Properties();
+ for (Enumeration<String> keys = properties.keys(); keys.hasMoreElements();) {
+ String key = keys.nextElement();
+ props.put(key, properties.get(key));
+ }
+
+ // cluster id
+ // cf. https://wiki.apache.org/jackrabbit/Clustering
+ // TODO deal with multiple repos
+ String clusterId = System.getProperty("org.apache.jackrabbit.core.cluster.node_id");
+ String clusterIdProp = props.getProperty(RepoConf.clusterId.name());
+ if (clusterId != null) {
+ if (clusterIdProp != null)
+ throw new IllegalArgumentException("Cluster id defined as System properties and in deploy config");
+ props.put(RepoConf.clusterId.name(), clusterId);
+ } else {
+ clusterId = clusterIdProp;
+ }
+
+ // home
+ String homeUri = props.getProperty(RepoConf.labeledUri.name());
+ Path homePath;
+ if (homeUri == null) {
+ String cn = props.getProperty(CmsConstants.CN);
+ assert cn != null;
+ if (clusterId != null) {
+ homePath = CmsPaths.getRepoDirPath(cn + '/' + clusterId);
+ } else {
+ homePath = CmsPaths.getRepoDirPath(cn);
+ }
+ } else {
+ try {
+ URI uri = new URI(homeUri);
+ String host = uri.getHost();
+ if (host == null || host.trim().equals("")) {
+ homePath = Paths.get(uri).toAbsolutePath();
+ } else {
+ // TODO remote at this stage?
+ throw new IllegalArgumentException("Cannot manage repository path for host " + host);
+ }
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Invalid repository home URI", e);
+ }
+ }
+ // TODO use Jackrabbit API (?)
+ Path rootUuidPath = homePath.resolve("repository/meta/rootUUID");
+ try {
+ if (!Files.exists(rootUuidPath)) {
+ Files.createDirectories(rootUuidPath.getParent());
+ Files.write(rootUuidPath, UUID.randomUUID().toString().getBytes());
+ }
+ // File homeDir = homePath.toFile();
+ // homeDir.mkdirs();
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot set up repository home " + homePath, e);
+ }
+ // home cannot be overridden
+ props.put(RepositoryConfigurationParser.REPOSITORY_HOME_VARIABLE, homePath.toString());
+
+ setProp(props, RepoConf.indexesBase, CmsPaths.getRepoIndexesBase().toString());
+ // common
+ setProp(props, RepoConf.defaultWorkspace);
+ setProp(props, RepoConf.maxPoolSize);
+ // Jackrabbit defaults
+ setProp(props, RepoConf.bundleCacheMB);
+ // See http://wiki.apache.org/jackrabbit/Search
+ setProp(props, RepoConf.extractorPoolSize);
+ setProp(props, RepoConf.searchCacheSize);
+ setProp(props, RepoConf.maxVolatileIndexSize);
+
+ // specific
+ String dburl;
+ switch (type) {
+ case h2:
+ dburl = "jdbc:h2:" + homePath.toAbsolutePath() + "/h2/repository";
+ setProp(props, RepoConf.dburl, dburl);
+ setProp(props, RepoConf.dbuser, "sa");
+ setProp(props, RepoConf.dbpassword, "");
+ break;
+ case h2_postgresql:
+ dburl = "jdbc:h2:" + homePath.toAbsolutePath() + "/h2/repository;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE";
+ setProp(props, RepoConf.dburl, dburl);
+ setProp(props, RepoConf.dbuser, "sa");
+ setProp(props, RepoConf.dbpassword, "");
+ break;
+ case postgresql:
+ case postgresql_ds:
+ case postgresql_cluster:
+ case postgresql_cluster_ds:
+ dburl = "jdbc:postgresql://localhost/demo";
+ setProp(props, RepoConf.dburl, dburl);
+ setProp(props, RepoConf.dbuser, "argeo");
+ setProp(props, RepoConf.dbpassword, "argeo");
+ break;
+ case memory:
+ break;
+ case localfs:
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported node type " + type);
+ }
+ return props;
+ }
+
+ private void setProp(Properties props, RepoConf key, String def) {
+ Object value = props.get(key.name());
+ if (value == null)
+ value = def;
+ if (value == null)
+ value = key.getDefault();
+ if (value != null)
+ props.put(key.name(), value.toString());
+ }
+
+ private void setProp(Properties props, RepoConf key) {
+ setProp(props, key, null);
+ }
+
+ private String prop(Dictionary<String, ?> properties, RepoConf key) {
+ Object value = properties.get(key.name());
+ if (value == null)
+ return key.getDefault() != null ? key.getDefault().toString() : null;
+ else
+ return value.toString();
+ }
+
+ private RepositoryContext createJackrabbitRepository(RepositoryConfig repositoryConfig) throws RepositoryException {
+ ClassLoader currentContextCl = Thread.currentThread().getContextClassLoader();
+ Thread.currentThread().setContextClassLoader(RepositoryBuilder.class.getClassLoader());
+ try {
+ long begin = System.currentTimeMillis();
+ //
+ // Actual repository creation
+ //
+ RepositoryContext repositoryContext = RepositoryContext.create(repositoryConfig);
+
+ double duration = ((double) (System.currentTimeMillis() - begin)) / 1000;
+ if (log.isDebugEnabled())
+ log.debug(
+ "Created Jackrabbit repository in " + duration + " s, home: " + repositoryConfig.getHomeDir());
+
+ return repositoryContext;
+ } finally {
+ Thread.currentThread().setContextClassLoader(currentContextCl);
+ }
+ }
+
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+ <!-- Shared datasource -->
+ <DataSources>
+ <DataSource name="dataSource">
+ <param name="driver" value="org.h2.Driver" />
+ <param name="url" value="${dburl}" />
+ <param name="user" value="${dbuser}" />
+ <param name="password" value="${dbpassword}" />
+ <param name="databaseType" value="h2" />
+ <param name="maxPoolSize" value="${maxPoolSize}" />
+ </DataSource>
+ </DataSources>
+
+ <!-- File system and datastore -->
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="default" />
+ <param name="schemaObjectPrefix" value="fs_" />
+ </FileSystem>
+ <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
+ <param name="path" value="${rep.home}/datastore" />
+ </DataStore>
+
+ <!-- Workspace templates -->
+ <Workspaces rootPath="${rep.home}/workspaces"
+ defaultWorkspace="${defaultWorkspace}" />
+ <Workspace name="${wsp.name}">
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="default" />
+ <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${indexesBase}/${cn}/${wsp.name}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+<!-- <param name="tikaConfigPath" value="${indexesBase}/${cn}/tika-config.xml" /> -->
+ <param name="supportHighlighting" value="true" />
+ </SearchIndex>
+ <WorkspaceSecurity>
+ <AccessControlProvider
+ class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+ </WorkspaceSecurity>
+ </Workspace>
+
+ <!-- Versioning -->
+ <Versioning rootPath="${rep.home}/version">
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="default" />
+ <param name="schemaObjectPrefix" value="fs_ver_" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schemaObjectPrefix" value="pm_ver_" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ </Versioning>
+
+ <!-- Indexing -->
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${indexesBase}/${cn}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+<!-- <param name="tikaConfigPath" value="${indexesBase}/${cn}/tika-config.xml" /> -->
+ <param name="supportHighlighting" value="true" />
+ </SearchIndex>
+
+ <!-- Security -->
+ <Security appName="Jackrabbit">
+ <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+ workspaceName="security" />
+ <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+ </Security>
+</Repository>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+ <!-- Shared datasource -->
+ <DataSources>
+ <DataSource name="dataSource">
+ <param name="driver" value="org.h2.Driver" />
+ <param name="url" value="${dburl}" />
+ <param name="user" value="${dbuser}" />
+ <param name="password" value="${dbpassword}" />
+ <param name="databaseType" value="postgresql" />
+ <param name="maxPoolSize" value="${maxPoolSize}" />
+ </DataSource>
+ </DataSources>
+
+ <!-- File system and datastore -->
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="default" />
+ <param name="schemaObjectPrefix" value="fs_" />
+ </FileSystem>
+ <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
+ <param name="path" value="${rep.home}/datastore" />
+ </DataStore>
+
+ <!-- Workspace templates -->
+ <Workspaces rootPath="${rep.home}/workspaces"
+ defaultWorkspace="${defaultWorkspace}" />
+ <Workspace name="${wsp.name}">
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="default" />
+ <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${indexesBase}/${cn}/${wsp.name}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+<!-- <param name="tikaConfigPath" value="${indexesBase}/${cn}/tika-config.xml" /> -->
+ <param name="supportHighlighting" value="true" />
+ </SearchIndex>
+ <WorkspaceSecurity>
+ <AccessControlProvider
+ class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+ </WorkspaceSecurity>
+ </Workspace>
+
+ <!-- Versioning -->
+ <Versioning rootPath="${rep.home}/version">
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="default" />
+ <param name="schemaObjectPrefix" value="fs_ver_" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schemaObjectPrefix" value="pm_ver_" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ </Versioning>
+
+ <!-- Indexing -->
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${indexesBase}/${cn}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+<!-- <param name="tikaConfigPath" value="${indexesBase}/${cn}/tika-config.xml" /> -->
+ <param name="supportHighlighting" value="true" />
+ </SearchIndex>
+
+ <!-- Security -->
+ <Security appName="Jackrabbit">
+ <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+ workspaceName="security" />
+ <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+ </Security>
+</Repository>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+ <!-- File system and datastore -->
+ <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+ <param name="path" value="${rep.home}/repository" />
+ </FileSystem>
+ <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
+ <param name="path" value="${rep.home}/datastore" />
+ </DataStore>
+
+ <!-- Workspace templates -->
+ <Workspaces rootPath="${rep.home}/workspaces"
+ defaultWorkspace="${defaultWorkspace}" />
+ <Workspace name="${wsp.name}">
+ <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+ <param name="path" value="${wsp.home}" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${indexesBase}/${cn}/${wsp.name}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+ </SearchIndex>
+ <WorkspaceSecurity>
+ <AccessControlProvider
+ class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+ </WorkspaceSecurity>
+ </Workspace>
+
+ <!-- Versioning -->
+ <Versioning rootPath="${rep.home}/version">
+ <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+ <param name="path" value="${rep.home}/version" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ </Versioning>
+
+ <!-- Indexing -->
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${indexesBase}/${cn}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+ </SearchIndex>
+
+ <!-- Security -->
+ <Security appName="Jackrabbit">
+ <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+ workspaceName="security" />
+ <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+ </Security>
+</Repository>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+ <!-- File system and datastore -->
+ <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+
+ <!-- Workspace templates -->
+ <Workspaces rootPath="${rep.home}/workspaces"
+ defaultWorkspace="${defaultWorkspace}" configRootPath="/workspaces" />
+ <Workspace name="${wsp.name}">
+ <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+ <param name="blobFSBlockSize" value="1" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${indexesBase}/${cn}/${wsp.name}/index" />
+ <param name="directoryManagerClass"
+ value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+ <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+ </SearchIndex>
+ <WorkspaceSecurity>
+ <AccessControlProvider
+ class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+ </WorkspaceSecurity>
+ </Workspace>
+
+ <!-- Versioning -->
+ <Versioning rootPath="${rep.home}/version">
+ <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+ <param name="blobFSBlockSize" value="1" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ </Versioning>
+
+ <!-- Indexing -->
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${indexesBase}/${cn}/index" />
+ <param name="directoryManagerClass"
+ value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+ <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+ </SearchIndex>
+
+ <!-- Security -->
+ <Security appName="Jackrabbit">
+ <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+ workspaceName="security" />
+ <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+ </Security>
+</Repository>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+ <!-- Shared datasource -->
+ <DataSources>
+ <DataSource name="dataSource">
+ <param name="driver" value="org.postgresql.Driver" />
+ <param name="url" value="${dburl}" />
+ <param name="user" value="${dbuser}" />
+ <param name="password" value="${dbpassword}" />
+ <param name="databaseType" value="postgresql" />
+ <param name="maxPoolSize" value="${maxPoolSize}" />
+ </DataSource>
+ </DataSources>
+
+ <!-- File system and datastore -->
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="postgresql" />
+ <param name="schemaObjectPrefix" value="fs_" />
+ </FileSystem>
+
+ <!-- Workspace templates -->
+ <Workspaces rootPath="${rep.home}/workspaces"
+ defaultWorkspace="${defaultWorkspace}" />
+ <Workspace name="${wsp.name}">
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="postgresql" />
+ <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${indexesBase}/${cn}/${wsp.name}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+ </SearchIndex>
+ <WorkspaceSecurity>
+ <AccessControlProvider
+ class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+ </WorkspaceSecurity>
+ </Workspace>
+
+ <!-- Versioning -->
+ <Versioning rootPath="${rep.home}/version">
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="postgresql" />
+ <param name="schemaObjectPrefix" value="fs_ver_" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schemaObjectPrefix" value="pm_ver_" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ </Versioning>
+
+ <!-- Indexing -->
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${indexesBase}/${cn}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+ </SearchIndex>
+
+ <!-- Security -->
+ <Security appName="Jackrabbit">
+ <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+ workspaceName="security" />
+ <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+ </Security>
+</Repository>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+ <!-- Shared datasource -->
+ <DataSources>
+ <DataSource name="dataSource">
+ <param name="driver" value="org.postgresql.Driver" />
+ <param name="url" value="${dburl}" />
+ <param name="user" value="${dbuser}" />
+ <param name="password" value="${dbpassword}" />
+ <param name="databaseType" value="postgresql" />
+ <param name="maxPoolSize" value="${maxPoolSize}" />
+ </DataSource>
+ </DataSources>
+
+ <!-- File system and datastore -->
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="postgresql" />
+ <param name="schemaObjectPrefix" value="fs_" />
+ </FileSystem>
+
+ <!-- Workspace templates -->
+ <Workspaces rootPath="${rep.home}/workspaces"
+ defaultWorkspace="${defaultWorkspace}" />
+ <Workspace name="${wsp.name}">
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="postgresql" />
+ <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${indexesBase}/${cn}/${wsp.name}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+ </SearchIndex>
+ <WorkspaceSecurity>
+ <AccessControlProvider
+ class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+ </WorkspaceSecurity>
+ </Workspace>
+
+ <!-- Versioning -->
+ <Versioning rootPath="${rep.home}/version">
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="postgresql" />
+ <param name="schemaObjectPrefix" value="fs_ver_" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schemaObjectPrefix" value="pm_ver_" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ </Versioning>
+
+ <!-- Indexing -->
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${indexesBase}/${cn}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+ </SearchIndex>
+
+ <!-- Security -->
+ <Security appName="Jackrabbit">
+ <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+ workspaceName="security" />
+ <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+ </Security>
+
+ <!-- Clustering -->
+ <Cluster id="${clusterId}">
+ <Journal class="org.apache.jackrabbit.core.journal.DatabaseJournal">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schemaObjectPrefix" value="journal_" />
+ </Journal>
+ </Cluster>
+</Repository>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+ <!-- Shared datasource -->
+ <DataSources>
+ <DataSource name="dataSource">
+ <param name="driver" value="org.postgresql.Driver" />
+ <param name="url" value="${dburl}" />
+ <param name="user" value="${dbuser}" />
+ <param name="password" value="${dbpassword}" />
+ <param name="databaseType" value="postgresql" />
+ <param name="maxPoolSize" value="${maxPoolSize}" />
+ </DataSource>
+ </DataSources>
+
+ <!-- File system and datastore -->
+ <FileSystem
+ class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="postgresql" />
+ <param name="schemaObjectPrefix" value="fs_" />
+ </FileSystem>
+ <DataStore
+ class="org.argeo.cms.internal.jcr.LocalFsDataStore">
+ <param name="path" value="${rep.home}/../datastore" />
+ <param name="redundantPath" value="${rep.home}/../datastorer" />
+ </DataStore>
+
+ <!-- Workspace templates -->
+ <Workspaces rootPath="${rep.home}/workspaces"
+ defaultWorkspace="${defaultWorkspace}" />
+ <Workspace name="${wsp.name}">
+ <FileSystem
+ class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="postgresql" />
+ <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ <SearchIndex
+ class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path"
+ value="${indexesBase}/${cn}/${wsp.name}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize"
+ value="${maxVolatileIndexSize}" />
+ </SearchIndex>
+ <WorkspaceSecurity>
+ <AccessControlProvider
+ class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+ </WorkspaceSecurity>
+ </Workspace>
+
+ <!-- Versioning -->
+ <Versioning rootPath="${rep.home}/version">
+ <FileSystem
+ class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="postgresql" />
+ <param name="schemaObjectPrefix" value="fs_ver_" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schemaObjectPrefix" value="pm_ver_" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ </Versioning>
+
+ <!-- Indexing -->
+ <SearchIndex
+ class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${indexesBase}/${cn}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize"
+ value="${maxVolatileIndexSize}" />
+ </SearchIndex>
+
+ <!-- Security -->
+ <Security appName="Jackrabbit">
+ <SecurityManager
+ class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+ workspaceName="security" />
+ <AccessManager
+ class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+ </Security>
+
+ <!-- Clustering -->
+ <Cluster id="${clusterId}" syncDelay="100">
+ <Journal
+ class="org.apache.jackrabbit.core.journal.DatabaseJournal">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schemaObjectPrefix" value="journal_" />
+ </Journal>
+ </Cluster>
+</Repository>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+ <!-- Shared datasource -->
+ <DataSources>
+ <DataSource name="dataSource">
+ <param name="driver" value="org.postgresql.Driver" />
+ <param name="url" value="${dburl}" />
+ <param name="user" value="${dbuser}" />
+ <param name="password" value="${dbpassword}" />
+ <param name="databaseType" value="postgresql" />
+ <param name="maxPoolSize" value="${maxPoolSize}" />
+ </DataSource>
+ </DataSources>
+
+ <!-- File system and datastore -->
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="postgresql" />
+ <param name="schemaObjectPrefix" value="fs_" />
+ </FileSystem>
+ <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
+ <param name="path" value="${rep.home}/datastore" />
+ </DataStore>
+
+ <!-- Workspace templates -->
+ <Workspaces rootPath="${rep.home}/workspaces"
+ defaultWorkspace="${defaultWorkspace}" />
+ <Workspace name="${wsp.name}">
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="postgresql" />
+ <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${indexesBase}/${cn}/${wsp.name}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+ </SearchIndex>
+ <WorkspaceSecurity>
+ <AccessControlProvider
+ class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+ </WorkspaceSecurity>
+ </Workspace>
+
+ <!-- Versioning -->
+ <Versioning rootPath="${rep.home}/version">
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="postgresql" />
+ <param name="schemaObjectPrefix" value="fs_ver_" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schemaObjectPrefix" value="pm_ver_" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ </Versioning>
+
+ <!-- Indexing -->
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${indexesBase}/${cn}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+ </SearchIndex>
+
+ <!-- Security -->
+ <Security appName="Jackrabbit">
+ <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+ workspaceName="security" />
+ <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+ </Security>
+</Repository>
\ No newline at end of file
--- /dev/null
+package org.argeo.cms.jcr;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.NoSuchWorkspaceException;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.security.auth.AuthPermission;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.argeo.api.cms.CmsAuth;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.util.CurrentSubject;
+
+/** Utilities related to Argeo model in JCR */
+public class CmsJcrUtils {
+ /**
+ * Wraps the call to the repository factory based on parameter
+ * {@link CmsConstants#CN} in order to simplify it and protect against future
+ * API changes.
+ */
+ public static Repository getRepositoryByAlias(RepositoryFactory repositoryFactory, String alias) {
+ try {
+ Map<String, String> parameters = new HashMap<String, String>();
+ parameters.put(CmsConstants.CN, alias);
+ return repositoryFactory.getRepository(parameters);
+ } catch (RepositoryException e) {
+ throw new RuntimeException("Unexpected exception when trying to retrieve repository with alias " + alias,
+ e);
+ }
+ }
+
+ /**
+ * Wraps the call to the repository factory based on parameter
+ * {@link CmsConstants#LABELED_URI} in order to simplify it and protect against
+ * future API changes.
+ */
+ public static Repository getRepositoryByUri(RepositoryFactory repositoryFactory, String uri) {
+ return getRepositoryByUri(repositoryFactory, uri, null);
+ }
+
+ /**
+ * Wraps the call to the repository factory based on parameter
+ * {@link CmsConstants#LABELED_URI} in order to simplify it and protect against
+ * future API changes.
+ */
+ public static Repository getRepositoryByUri(RepositoryFactory repositoryFactory, String uri, String alias) {
+ try {
+ Map<String, String> parameters = new HashMap<String, String>();
+ parameters.put(CmsConstants.LABELED_URI, uri);
+ if (alias != null)
+ parameters.put(CmsConstants.CN, alias);
+ return repositoryFactory.getRepository(parameters);
+ } catch (RepositoryException e) {
+ throw new RuntimeException("Unexpected exception when trying to retrieve repository with uri " + uri, e);
+ }
+ }
+
+ /**
+ * Returns the home node of the user or null if none was found.
+ *
+ * @param session the session to use in order to perform the search, this can
+ * be a session with a different user ID than the one searched,
+ * typically when a system or admin session is used.
+ * @param username the username of the user
+ */
+ public static Node getUserHome(Session session, String username) {
+// try {
+// QueryObjectModelFactory qomf = session.getWorkspace().getQueryManager().getQOMFactory();
+// Selector sel = qomf.selector(NodeTypes.NODE_USER_HOME, "sel");
+// DynamicOperand dop = qomf.propertyValue(sel.getSelectorName(), NodeNames.LDAP_UID);
+// StaticOperand sop = qomf.literal(session.getValueFactory().createValue(username));
+// Constraint constraint = qomf.comparison(dop, QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, sop);
+// Query query = qomf.createQuery(sel, constraint, null, null);
+// return querySingleNode(query);
+// } catch (RepositoryException e) {
+// throw new RuntimeException("Cannot find home for user " + username, e);
+// }
+
+ try {
+ checkUserWorkspace(session, username);
+ String homePath = getHomePath(username);
+ if (session.itemExists(homePath))
+ return session.getNode(homePath);
+ // legacy
+ homePath = "/home/" + username;
+ if (session.itemExists(homePath))
+ return session.getNode(homePath);
+ return null;
+ } catch (RepositoryException e) {
+ throw new RuntimeException("Cannot find home for user " + username, e);
+ }
+ }
+
+ private static String getHomePath(String username) {
+ LdapName dn;
+ try {
+ dn = new LdapName(username);
+ } catch (InvalidNameException e) {
+ throw new IllegalArgumentException("Invalid name " + username, e);
+ }
+ String userId = dn.getRdn(dn.size() - 1).getValue().toString();
+ return '/' + userId;
+ }
+
+ private static void checkUserWorkspace(Session session, String username) {
+ String workspaceName = session.getWorkspace().getName();
+ if (!CmsConstants.HOME_WORKSPACE.equals(workspaceName))
+ throw new IllegalArgumentException(workspaceName + " is not the home workspace for user " + username);
+ }
+
+ /**
+ * Returns the home node of the user or null if none was found.
+ *
+ * @param session the session to use in order to perform the search, this can
+ * be a session with a different user ID than the one searched,
+ * typically when a system or admin session is used.
+ * @param groupname the name of the group
+ */
+ public static Node getGroupHome(Session session, String groupname) {
+// try {
+// QueryObjectModelFactory qomf = session.getWorkspace().getQueryManager().getQOMFactory();
+// Selector sel = qomf.selector(NodeTypes.NODE_GROUP_HOME, "sel");
+// DynamicOperand dop = qomf.propertyValue(sel.getSelectorName(), NodeNames.LDAP_CN);
+// StaticOperand sop = qomf.literal(session.getValueFactory().createValue(cn));
+// Constraint constraint = qomf.comparison(dop, QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, sop);
+// Query query = qomf.createQuery(sel, constraint, null, null);
+// return querySingleNode(query);
+// } catch (RepositoryException e) {
+// throw new RuntimeException("Cannot find home for group " + cn, e);
+// }
+
+ try {
+ checkGroupWorkspace(session, groupname);
+ String homePath = getGroupPath(groupname);
+ if (session.itemExists(homePath))
+ return session.getNode(homePath);
+ // legacy
+ homePath = "/groups/" + groupname;
+ if (session.itemExists(homePath))
+ return session.getNode(homePath);
+ return null;
+ } catch (RepositoryException e) {
+ throw new RuntimeException("Cannot find home for group " + groupname, e);
+ }
+
+ }
+
+ private static String getGroupPath(String groupname) {
+ String cn;
+ try {
+ LdapName dn = new LdapName(groupname);
+ cn = dn.getRdn(dn.size() - 1).getValue().toString();
+ } catch (InvalidNameException e) {
+ cn = groupname;
+ }
+ return '/' + cn;
+ }
+
+ private static void checkGroupWorkspace(Session session, String groupname) {
+ String workspaceName = session.getWorkspace().getName();
+ if (!CmsConstants.SRV_WORKSPACE.equals(workspaceName))
+ throw new IllegalArgumentException(workspaceName + " is not the group workspace for group " + groupname);
+ }
+
+ /**
+ * Queries one single node.
+ *
+ * @return one single node or null if none was found
+ * @throws ArgeoJcrException if more than one node was found
+ */
+// private static Node querySingleNode(Query query) {
+// NodeIterator nodeIterator;
+// try {
+// QueryResult queryResult = query.execute();
+// nodeIterator = queryResult.getNodes();
+// } catch (RepositoryException e) {
+// throw new RuntimeException("Cannot execute query " + query, e);
+// }
+// Node node;
+// if (nodeIterator.hasNext())
+// node = nodeIterator.nextNode();
+// else
+// return null;
+//
+// if (nodeIterator.hasNext())
+// throw new RuntimeException("Query returned more than one node.");
+// return node;
+// }
+
+ /** Returns the home node of the session user or null if none was found. */
+ public static Node getUserHome(Session session) {
+ String userID = session.getUserID();
+ return getUserHome(session, userID);
+ }
+
+ /** Whether this node is the home of the user of the underlying session. */
+ public static boolean isUserHome(Node node) {
+ try {
+ String userID = node.getSession().getUserID();
+ return node.hasProperty(Property.JCR_ID) && node.getProperty(Property.JCR_ID).getString().equals(userID);
+ } catch (RepositoryException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * Translate the path to this node into a path containing the name of the
+ * repository and the name of the workspace.
+ */
+ public static String getDataPath(String cn, Node node) {
+ assert node != null;
+ StringBuilder buf = new StringBuilder(CmsConstants.PATH_DATA);
+ try {
+ return buf.append('/').append(cn).append('/').append(node.getSession().getWorkspace().getName())
+ .append(node.getPath()).toString();
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Cannot get data path for " + node + " in repository " + cn, e);
+ }
+ }
+
+ /**
+ * Translate the path to this node into a path containing the name of the
+ * repository and the name of the workspace.
+ */
+ public static String getDataPath(Node node) {
+ return getDataPath(CmsConstants.NODE, node);
+ }
+
+ /**
+ * Open a JCR session with full read/write rights on the data, as
+ * {@link CmsConstants#ROLE_USER_ADMIN}, using the
+ * {@link CmsAuth#LOGIN_CONTEXT_DATA_ADMIN} login context. For security hardened
+ * deployement, use {@link AuthPermission} on this login context.
+ */
+ public static Session openDataAdminSession(Repository repository, String workspaceName) {
+ LoginContext loginContext;
+ try {
+ loginContext = CmsAuth.DATA_ADMIN.newLoginContext();
+ loginContext.login();
+ } catch (LoginException e1) {
+ throw new RuntimeException("Could not login as data admin", e1);
+ } finally {
+ }
+
+ ClassLoader currentCl = Thread.currentThread().getContextClassLoader();
+ try {
+ Thread.currentThread().setContextClassLoader(CmsJcrUtils.class.getClassLoader());
+ return CurrentSubject.callAs(loginContext.getSubject(), () -> {
+ try {
+ return JcrUtils.loginOrCreateWorkspace(repository, workspaceName);
+ } catch (NoSuchWorkspaceException e) {// should not happen
+ throw new IllegalArgumentException("No workspace " + workspaceName + " available", e);
+ } catch (RepositoryException e) {
+ throw new RuntimeException("Cannot open data admin session", e);
+ }
+ }
+
+ );
+ } finally {
+ Thread.currentThread().setContextClassLoader(currentCl);
+ }
+ }
+
+ /** Singleton. */
+ private CmsJcrUtils() {
+ }
+
+}
--- /dev/null
+package org.argeo.cms.jcr.acr;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.ForkJoinPool;
+
+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 javax.jcr.nodetype.NodeType;
+import javax.xml.namespace.QName;
+import javax.xml.transform.Source;
+import javax.xml.transform.stream.StreamSource;
+
+import org.argeo.api.acr.Content;
+import org.argeo.api.acr.NamespaceUtils;
+import org.argeo.api.acr.spi.ContentProvider;
+import org.argeo.api.acr.spi.ProvidedSession;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.cms.acr.AbstractContent;
+import org.argeo.cms.acr.ContentUtils;
+import org.argeo.jcr.Jcr;
+import org.argeo.jcr.JcrException;
+import org.argeo.jcr.JcrUtils;
+
+/** A JCR {@link Node} accessed as {@link Content}. */
+public class JcrContent extends AbstractContent {
+// private Node jcrNode;
+
+ private JcrContentProvider provider;
+
+ private String jcrWorkspace;
+ private String jcrPath;
+
+ protected JcrContent(ProvidedSession session, JcrContentProvider provider, String jcrWorkspace, String jcrPath) {
+ super(session);
+ this.provider = provider;
+ this.jcrWorkspace = jcrWorkspace;
+ this.jcrPath = jcrPath;
+ }
+
+ @Override
+ public QName getName() {
+ String name = Jcr.getName(getJcrNode());
+ if (name.equals("")) {// root
+ String mountPath = provider.getMountPath();
+ name = ContentUtils.getParentPath(mountPath)[1];
+ // name = Jcr.getWorkspaceName(getJcrNode());
+ }
+ return NamespaceUtils.parsePrefixedName(provider, name);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public <A> Optional<A> get(QName key, Class<A> clss) {
+ if (isDefaultAttrTypeRequested(clss)) {
+ return Optional.of((A) get(getJcrNode(), key.toString()));
+ }
+ return Optional.of((A) Jcr.get(getJcrNode(), key.toString()));
+ }
+
+ @Override
+ public Iterator<Content> iterator() {
+ try {
+ return new JcrContentIterator(getJcrNode().getNodes());
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot list children of " + getJcrNode(), e);
+ }
+ }
+
+ @Override
+ protected Iterable<QName> keys() {
+ try {
+ Set<QName> keys = new HashSet<>();
+ for (PropertyIterator propertyIterator = getJcrNode().getProperties(); propertyIterator.hasNext();) {
+ Property property = propertyIterator.nextProperty();
+ // TODO convert standard names
+ // TODO skip technical properties
+ QName name = NamespaceUtils.parsePrefixedName(provider, property.getName());
+ keys.add(name);
+ }
+ return keys;
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot list properties of " + getJcrNode(), e);
+ }
+ }
+
+ public Node getJcrNode() {
+ try {
+ // TODO caching?
+ return provider.getJcrSession(getSession(), jcrWorkspace).getNode(jcrPath);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot retrieve " + jcrPath + " from workspace " + jcrWorkspace, e);
+ }
+ }
+
+ /** Cast to a standard Java object. */
+ static Object get(Node node, String property) {
+ try {
+ Property p = node.getProperty(property);
+ if (p.isMultiple()) {
+ Value[] values = p.getValues();
+ List<Object> lst = new ArrayList<>();
+ for (Value value : values) {
+ lst.add(convertSingleValue(value));
+ }
+ return lst;
+ } else {
+ Value value = node.getProperty(property).getValue();
+ return convertSingleValue(value);
+ }
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot cast value from " + property + " of node " + node, e);
+ }
+ }
+
+ static Object convertSingleValue(Value value) throws RepositoryException {
+ switch (value.getType()) {
+ case PropertyType.STRING:
+ return value.getString();
+ case PropertyType.DOUBLE:
+ return (Double) value.getDouble();
+ case PropertyType.LONG:
+ return (Long) value.getLong();
+ case PropertyType.BOOLEAN:
+ return (Boolean) value.getBoolean();
+ case PropertyType.DATE:
+ Calendar calendar = value.getDate();
+ return calendar.toInstant();
+ case PropertyType.BINARY:
+ throw new IllegalArgumentException("Binary is not supported as an attribute");
+ default:
+ return value.getString();
+ }
+ }
+
+ class JcrContentIterator implements Iterator<Content> {
+ private final NodeIterator nodeIterator;
+ // we keep track in order to be able to delete it
+ private JcrContent current = null;
+
+ protected JcrContentIterator(NodeIterator nodeIterator) {
+ this.nodeIterator = nodeIterator;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return nodeIterator.hasNext();
+ }
+
+ @Override
+ public Content next() {
+ current = new JcrContent(getSession(), provider, jcrWorkspace, Jcr.getPath(nodeIterator.nextNode()));
+ return current;
+ }
+
+ @Override
+ public void remove() {
+ if (current != null) {
+ Jcr.remove(current.getJcrNode());
+ }
+ }
+
+ }
+
+ @Override
+ public String getPath() {
+ try {
+ // Note: it is important to to use the default way (recursing through parents),
+ // since the session may not have access to parent nodes
+ return ContentUtils.ROOT_SLASH + jcrWorkspace + getJcrNode().getPath();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get depth of " + getJcrNode(), e);
+ }
+ }
+
+ @Override
+ public int getDepth() {
+ try {
+ return getJcrNode().getDepth() + 1;
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get depth of " + getJcrNode(), e);
+ }
+ }
+
+ @Override
+ public Content getParent() {
+ if (Jcr.isRoot(getJcrNode())) // root
+ return null;
+ return new JcrContent(getSession(), provider, jcrWorkspace, Jcr.getParentPath(getJcrNode()));
+ }
+
+ @Override
+ public Content add(QName name, QName... classes) {
+ if (classes.length > 0) {
+ QName primaryType = classes[0];
+ Node child = Jcr.addNode(getJcrNode(), name.toString(), primaryType.toString());
+ for (int i = 1; i < classes.length; i++) {
+ try {
+ child.addMixin(classes[i].toString());
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot add child to " + getJcrNode(), e);
+ }
+ }
+
+ } else {
+ Jcr.addNode(getJcrNode(), name.toString(), NodeType.NT_UNSTRUCTURED);
+ }
+ return null;
+ }
+
+ @Override
+ public void remove() {
+ Jcr.remove(getJcrNode());
+ }
+
+ @Override
+ protected void removeAttr(QName key) {
+ Property property = Jcr.getProperty(getJcrNode(), key.toString());
+ if (property != null) {
+ try {
+ property.remove();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot remove property " + key + " from " + getJcrNode(), e);
+ }
+ }
+
+ }
+
+ boolean exists() {
+ try {
+ return provider.getJcrSession(getSession(), jcrWorkspace).itemExists(jcrPath);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot check whether " + jcrPath + " exists", e);
+ }
+ }
+
+ /*
+ * ADAPTERS
+ */
+ @SuppressWarnings("unchecked")
+ public <A> A adapt(Class<A> clss) {
+ if (Source.class.isAssignableFrom(clss)) {
+// try {
+ PipedInputStream in = new PipedInputStream();
+
+ ForkJoinPool.commonPool().execute(() -> {
+ try (PipedOutputStream out = new PipedOutputStream(in)) {
+ provider.getJcrSession(getSession(), jcrWorkspace).exportDocumentView(jcrPath, out, true, false);
+ out.flush();
+ } catch (IOException | RepositoryException e) {
+ throw new RuntimeException("Cannot export " + jcrPath + " in workspace " + jcrWorkspace, e);
+ }
+
+ });
+ return (A) new StreamSource(in);
+// } catch (IOException e) {
+// throw new RuntimeException("Cannot adapt " + JcrContent.this + " to " + clss, e);
+// }
+ } else
+
+ return super.adapt(clss);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public <C extends Closeable> C open(Class<C> clss) throws IOException, IllegalArgumentException {
+ if (InputStream.class.isAssignableFrom(clss)) {
+ Node node = getJcrNode();
+ if (Jcr.isNodeType(node, NodeType.NT_FILE)) {
+ try {
+ return (C) JcrUtils.getFileAsStream(node);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot open " + jcrPath + " in workspace " + jcrWorkspace, e);
+ }
+ }
+ }
+ return super.open(clss);
+ }
+
+ @Override
+ public ContentProvider getProvider() {
+ return provider;
+ }
+
+ @Override
+ public String getSessionLocalId() {
+ try {
+ return getJcrNode().getIdentifier();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get identifier for " + getJcrNode(), e);
+ }
+ }
+
+ /*
+ * TYPING
+ */
+ @Override
+ public List<QName> getContentClasses() {
+ try {
+// Node node = getJcrNode();
+// List<QName> res = new ArrayList<>();
+// res.add(nodeTypeToQName(node.getPrimaryNodeType()));
+// for (NodeType mixin : node.getMixinNodeTypes()) {
+// res.add(nodeTypeToQName(mixin));
+// }
+// return res;
+ Node context = getJcrNode();
+
+ List<QName> res = new ArrayList<>();
+ // primary node type
+ NodeType primaryType = context.getPrimaryNodeType();
+ res.add(nodeTypeToQName(primaryType));
+
+ Set<QName> secondaryTypes = new TreeSet<>(NamespaceUtils.QNAME_COMPARATOR);
+ for (NodeType mixinType : context.getMixinNodeTypes()) {
+ secondaryTypes.add(nodeTypeToQName(mixinType));
+ }
+ for (NodeType superType : primaryType.getDeclaredSupertypes()) {
+ secondaryTypes.add(nodeTypeToQName(superType));
+ }
+ // mixins
+ for (NodeType mixinType : context.getMixinNodeTypes()) {
+ for (NodeType superType : mixinType.getDeclaredSupertypes()) {
+ secondaryTypes.add(nodeTypeToQName(superType));
+ }
+ }
+// // entity type
+// if (context.isNodeType(EntityType.entity.get())) {
+// if (context.hasProperty(EntityNames.ENTITY_TYPE)) {
+// String entityTypeName = context.getProperty(EntityNames.ENTITY_TYPE).getString();
+// if (byType.containsKey(entityTypeName)) {
+// types.add(entityTypeName);
+// }
+// }
+// }
+ res.addAll(secondaryTypes);
+ return res;
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot list node types from " + getJcrNode(), e);
+ }
+ }
+
+ private QName nodeTypeToQName(NodeType nodeType) {
+ String name = nodeType.getName();
+ return QName.valueOf(name);
+ }
+
+ @Override
+ public int getSiblingIndex() {
+ return Jcr.getIndex(getJcrNode());
+ }
+
+ /*
+ * STATIC UTLITIES
+ */
+ public static Content nodeToContent(Node node) {
+ if (node == null)
+ return null;
+ try {
+ ProvidedSession contentSession = (ProvidedSession) node.getSession()
+ .getAttribute(ProvidedSession.class.getName());
+ if (contentSession == null)
+ throw new IllegalArgumentException(
+ "Cannot adapt " + node + " to content, because it was not loaded from a content session");
+ return contentSession.get(ContentUtils.SLASH + CmsConstants.SYS_WORKSPACE + node.getPath());
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot adapt " + node + " to a content", e);
+ }
+ }
+
+}
--- /dev/null
+package org.argeo.cms.jcr.acr;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Objects;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.xml.namespace.NamespaceContext;
+
+import org.argeo.api.acr.Content;
+import org.argeo.api.acr.spi.ContentProvider;
+import org.argeo.api.acr.spi.ProvidedContent;
+import org.argeo.api.acr.spi.ProvidedSession;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.cms.acr.ContentUtils;
+import org.argeo.cms.jcr.CmsJcrUtils;
+import org.argeo.jcr.JcrException;
+import org.argeo.jcr.JcrUtils;
+
+/** A JCR workspace accessed as an {@link ContentProvider}. */
+public class JcrContentProvider implements ContentProvider, NamespaceContext {
+
+ private Repository jcrRepository;
+ private Session adminSession;
+
+ private String mountPath;
+
+ // cache
+ private String jcrWorkspace;
+
+ private Map<ProvidedSession, JcrSessionAdapter> sessionAdapters = Collections.synchronizedMap(new HashMap<>());
+
+ public void start(Map<String, String> properties) {
+ mountPath = properties.get(CmsConstants.ACR_MOUNT_PATH);
+ if ("/".equals(mountPath))
+ throw new IllegalArgumentException("JCR content provider cannot be root /");
+ Objects.requireNonNull(mountPath);
+ jcrWorkspace = ContentUtils.getParentPath(mountPath)[1];
+ adminSession = CmsJcrUtils.openDataAdminSession(jcrRepository, jcrWorkspace);
+ }
+
+ public void stop() {
+ if (adminSession.isLive())
+ JcrUtils.logoutQuietly(adminSession);
+ }
+
+ public void setJcrRepository(Repository jcrRepository) {
+ this.jcrRepository = jcrRepository;
+ }
+
+ @Override
+ public ProvidedContent get(ProvidedSession contentSession, String relativePath) {
+ String jcrPath = "/" + relativePath;
+ return new JcrContent(contentSession, this, jcrWorkspace, jcrPath);
+ }
+
+ @Override
+ public boolean exists(ProvidedSession contentSession, String relativePath) {
+ String jcrWorkspace = ContentUtils.getParentPath(mountPath)[1];
+ String jcrPath = "/" + relativePath;
+ return new JcrContent(contentSession, this, jcrWorkspace, jcrPath).exists();
+ }
+
+ public Session getJcrSession(ProvidedSession contentSession, String jcrWorkspace) {
+ JcrSessionAdapter sessionAdapter = sessionAdapters.get(contentSession);
+ if (sessionAdapter == null) {
+ final JcrSessionAdapter newSessionAdapter = new JcrSessionAdapter(jcrRepository, contentSession,
+ contentSession.getSubject());
+ sessionAdapters.put(contentSession, newSessionAdapter);
+ contentSession.onClose().thenAccept((s) -> newSessionAdapter.close());
+ sessionAdapter = newSessionAdapter;
+ }
+
+ Session jcrSession = sessionAdapter.getSession(jcrWorkspace);
+ return jcrSession;
+ }
+
+ public Session getJcrSession(Content content, String jcrWorkspace) {
+ return getJcrSession(((ProvidedContent) content).getSession(), jcrWorkspace);
+ }
+
+ @Override
+ public String getMountPath() {
+ return mountPath;
+ }
+
+ /*
+ * NAMESPACE CONTEXT
+ */
+ @Override
+ public String getNamespaceURI(String prefix) {
+ try {
+ return adminSession.getNamespaceURI(prefix);
+ } catch (RepositoryException e) {
+ throw new JcrException(e);
+ }
+ }
+
+ @Override
+ public String getPrefix(String namespaceURI) {
+ try {
+ return adminSession.getNamespacePrefix(namespaceURI);
+ } catch (RepositoryException e) {
+ throw new JcrException(e);
+ }
+ }
+
+ @Override
+ public Iterator<String> getPrefixes(String namespaceURI) {
+ try {
+ return Arrays.asList(adminSession.getNamespacePrefix(namespaceURI)).iterator();
+ } catch (RepositoryException e) {
+ throw new JcrException(e);
+ }
+ }
+
+}
--- /dev/null
+package org.argeo.cms.jcr.acr;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+import javax.jcr.nodetype.NodeType;
+import javax.xml.XMLConstants;
+import javax.xml.namespace.NamespaceContext;
+import javax.xml.namespace.QName;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.Source;
+import javax.xml.transform.dom.DOMSource;
+
+import org.argeo.api.acr.Content;
+import org.argeo.api.acr.ContentName;
+import org.argeo.api.acr.CrName;
+import org.argeo.api.acr.DName;
+import org.argeo.api.acr.NamespaceUtils;
+import org.argeo.api.acr.spi.ProvidedContent;
+import org.argeo.api.acr.spi.ProvidedSession;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.Jcr;
+import org.argeo.jcr.JcrException;
+import org.argeo.jcr.JcrUtils;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+/** Utilities around integration between JCR and ACR. */
+public class JcrContentUtils {
+ private final static CmsLog log = CmsLog.getLog(JcrContentUtils.class);
+
+ public static void copyFiles(Node folder, Content collection, String... additionalCollectionTypes) {
+ try {
+ log.debug("Copy collection " + collection);
+
+ NamespaceContext jcrNamespaceContext = new JcrSessionNamespaceContext(folder.getSession());
+
+ nodes: for (NodeIterator it = folder.getNodes(); it.hasNext();) {
+ Node node = it.nextNode();
+ String name = node.getName();
+ if (node.isNodeType(NodeType.NT_FILE)) {
+ Content file = collection.anyOrAddChild(new ContentName(name));
+ try (InputStream in = JcrUtils.getFileAsStream(node)) {
+ file.write(InputStream.class).complete(in);
+ }
+ } else if (node.isNodeType(NodeType.NT_FOLDER)) {
+ Content subCol = collection.add(name, DName.collection.qName());
+ copyFiles(node, subCol, additionalCollectionTypes);
+ } else {
+ List<QName> contentClasses = typesAsContentClasses(node, jcrNamespaceContext);
+ for (String collectionType : additionalCollectionTypes) {
+ if (node.isNodeType(collectionType)) {
+ contentClasses.add(DName.collection.qName());
+ Content subCol = collection.add(name,
+ contentClasses.toArray(new QName[contentClasses.size()]));
+ setAttributes(node, subCol, jcrNamespaceContext);
+// setContentClasses(node, subCol, jcrNamespaceContext);
+ copyFiles(node, subCol, additionalCollectionTypes);
+ continue nodes;
+ }
+ }
+
+ QName qName = NamespaceUtils.parsePrefixedName(name);
+// if (NamespaceUtils.hasNamespace(qName)) {
+ if (node.getIndex() > 1) {
+ log.warn("Same name siblings not supported, skipping " + node);
+ continue nodes;
+ }
+ Content content = collection.add(qName, contentClasses.toArray(new QName[contentClasses.size()]));
+ Source source = toSource(node);
+ ((ProvidedContent) content).getSession().edit((s) -> {
+ ((ProvidedSession) s).notifyModification((ProvidedContent) content);
+ content.write(Source.class).complete(source);
+// try {
+// //setContentClasses(node, content, jcrNamespaceContext);
+// } catch (RepositoryException e) {
+// // TODO Auto-generated catch block
+// e.printStackTrace();
+// }
+ }).toCompletableFuture().join();
+ setAttributes(node, content, jcrNamespaceContext);
+
+// } else {
+// // ignore
+// log.debug(() -> "Ignored " + node);
+// continue nodes;
+// }
+ }
+ }
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot copy files from " + folder + " to " + collection, e);
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot copy files from " + folder + " to " + collection, e);
+ }
+ }
+
+ private static Source toSource(Node node) throws RepositoryException {
+
+// try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+// node.getSession().exportDocumentView(node.getPath(), out, true, false);
+// System.out.println(new String(out.toByteArray(), StandardCharsets.UTF_8));
+//// DocumentBuilder documentBuilder = DocumentBuilderFactory.newNSInstance().newDocumentBuilder();
+//// Document document;
+//// try (ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray())) {
+//// document = documentBuilder.parse(in);
+//// }
+//// cleanJcrDom(document);
+//// return new DOMSource(document);
+// } catch (IOException e) {
+// throw new RuntimeException(e);
+// }
+
+ try (PipedInputStream in = new PipedInputStream();) {
+
+ CompletableFuture<Document> toDo = CompletableFuture.supplyAsync(() -> {
+ try {
+ DocumentBuilder documentBuilder = DocumentBuilderFactory.newNSInstance().newDocumentBuilder();
+ return documentBuilder.parse(in);
+ } catch (ParserConfigurationException | SAXException | IOException e) {
+ throw new RuntimeException("Cannot parse", e);
+ }
+ });
+
+ // TODO optimise
+ try (PipedOutputStream out = new PipedOutputStream(in)) {
+ node.getSession().exportDocumentView(node.getPath(), out, true, false);
+ } catch (IOException | RepositoryException e) {
+ throw new RuntimeException("Cannot export " + node + " in workspace " + Jcr.getWorkspaceName(node), e);
+ }
+ Document document = toDo.get();
+ cleanJcrDom(document);
+ return new DOMSource(document);
+ } catch (IOException | InterruptedException | ExecutionException e1) {
+ throw new RuntimeException("Cannot parse", e1);
+ }
+
+ }
+
+ public static void setAttributes(Node source, Content target, NamespaceContext jcrNamespaceContext)
+ throws RepositoryException {
+ properties: for (PropertyIterator pit = source.getProperties(); pit.hasNext();) {
+ Property p = pit.nextProperty();
+ // TODO migrate JCR title, last modified, etc. ?
+ if (p.getName().startsWith("jcr:"))
+ continue properties;
+ if (p.isMultiple()) {
+ List<String> attr = new ArrayList<>();
+ for (Value value : p.getValues()) {
+ attr.add(value.getString());
+ }
+ target.put(NamespaceUtils.parsePrefixedName(jcrNamespaceContext, p.getName()), attr);
+ } else {
+ target.put(NamespaceUtils.parsePrefixedName(jcrNamespaceContext, p.getName()), p.getString());
+ }
+ }
+ }
+
+ public static List<QName> typesAsContentClasses(Node source, NamespaceContext jcrNamespaceContext)
+ throws RepositoryException {
+ // TODO super types?
+ List<QName> contentClasses = new ArrayList<>();
+ contentClasses
+ .add(NamespaceUtils.parsePrefixedName(jcrNamespaceContext, source.getPrimaryNodeType().getName()));
+ for (NodeType nodeType : source.getMixinNodeTypes()) {
+ contentClasses.add(NamespaceUtils.parsePrefixedName(jcrNamespaceContext, nodeType.getName()));
+ }
+ // filter out JCR types
+ for (Iterator<QName> it = contentClasses.iterator(); it.hasNext();) {
+ QName type = it.next();
+ if (type.getNamespaceURI().equals(JCR_NT_NAMESPACE_URI)
+ || type.getNamespaceURI().equals(JCR_MIX_NAMESPACE_URI)) {
+ it.remove();
+ }
+ }
+ // target.addContentClasses(contentClasses.toArray(new
+ // QName[contentClasses.size()]));
+ return contentClasses;
+ }
+
+ static final String JCR_NAMESPACE_URI = "http://www.jcp.org/jcr/1.0";
+ static final String JCR_NT_NAMESPACE_URI = "http://www.jcp.org/jcr/nt/1.0";
+ static final String JCR_MIX_NAMESPACE_URI = "http://www.jcp.org/jcr/mix/1.0";
+
+ public static void cleanJcrDom(Document document) {
+ Element documentElement = document.getDocumentElement();
+ Set<String> namespaceUris = new HashSet<>();
+ cleanJcrDom(documentElement, namespaceUris);
+
+ // remove unused namespaces
+ NamedNodeMap attrs = documentElement.getAttributes();
+ Set<Attr> toRemove = new HashSet<>();
+ for (int i = 0; i < attrs.getLength(); i++) {
+ Attr attr = (Attr) attrs.item(i);
+// log.debug("Check "+i+" " + attr);
+ String prefix = attr.getPrefix();
+ if (prefix != null && prefix.equals(XMLConstants.XMLNS_ATTRIBUTE)) {
+ String namespaceUri = attr.getValue();
+ if (!namespaceUris.contains(namespaceUri)) {
+ toRemove.add(attr);
+ // log.debug("Removing "+i+" " + namespaceUri);
+ }
+ }
+ }
+ for (Attr attr : toRemove)
+ documentElement.removeAttributeNode(attr);
+
+ }
+
+ private static void cleanJcrDom(Element element, Set<String> namespaceUris) {
+ NodeList children = element.getElementsByTagName("*");
+ for (int i = 0; i < children.getLength(); i++) {
+ Element child = (Element) children.item(i);
+ if (!namespaceUris.contains(child.getNamespaceURI()))
+ namespaceUris.add(child.getNamespaceURI());
+ cleanJcrDom(child, namespaceUris);
+ }
+
+ NamedNodeMap attrs = element.getAttributes();
+ attributes: for (int i = 0; i < attrs.getLength(); i++) {
+ Attr attr = (Attr) attrs.item(i);
+ String namespaceUri = attr.getNamespaceURI();
+ if (namespaceUri == null)
+ continue attributes;
+ if (JCR_NAMESPACE_URI.equals(namespaceUri)) {
+ // FIXME probably wrong to change attributes length
+ element.removeAttributeNode(attr);
+ continue attributes;
+ }
+ if (!namespaceUris.contains(namespaceUri))
+ namespaceUris.add(attr.getNamespaceURI());
+
+ }
+
+ }
+
+ /** singleton */
+ private JcrContentUtils() {
+ }
+
+}
--- /dev/null
+package org.argeo.cms.jcr.acr;
+
+import java.security.PrivilegedAction;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.security.auth.Subject;
+
+import org.apache.jackrabbit.core.SessionImpl;
+import org.argeo.api.acr.spi.ProvidedSession;
+import org.argeo.jcr.JcrException;
+import org.argeo.jcr.JcrUtils;
+
+/** Manages JCR {@link Session} in an ACR context. */
+class JcrSessionAdapter {
+ private Repository repository;
+ private Subject subject;
+
+ private ProvidedSession contentSession;
+
+ private Map<Thread, Map<String, Session>> threadSessions = Collections.synchronizedMap(new HashMap<>());
+
+ private boolean closed = false;
+
+ private Thread lastRetrievingThread = null;
+
+ public JcrSessionAdapter(Repository repository, ProvidedSession contentSession, Subject subject) {
+ this.repository = repository;
+ this.contentSession = contentSession;
+ this.subject = subject;
+ }
+
+ public synchronized void close() {
+ for (Map<String, Session> sessions : threadSessions.values()) {
+ for (Session session : sessions.values()) {
+ JcrUtils.logoutQuietly(session);
+ }
+ sessions.clear();
+ }
+ threadSessions.clear();
+ closed = true;
+ }
+
+ public synchronized Session getSession(String workspace) {
+ if (closed)
+ throw new IllegalStateException("JCR session adapter is closed.");
+
+ Thread currentThread = Thread.currentThread();
+ if (lastRetrievingThread == null)
+ lastRetrievingThread = currentThread;
+
+ Map<String, Session> threadSession = threadSessions.get(currentThread);
+ if (threadSession == null) {
+ threadSession = new HashMap<>();
+ threadSessions.put(currentThread, threadSession);
+ }
+
+ Session session = threadSession.get(workspace);
+ if (session == null) {
+ session = Subject.doAs(subject, (PrivilegedAction<Session>) () -> {
+ try {
+// String username = CurrentUser.getUsername(subject);
+// SimpleCredentials credentials = new SimpleCredentials(username, new char[0]);
+// credentials.setAttribute(ProvidedSession.class.getName(), contentSession);
+ Session sess = repository.login(workspace);
+ // Jackrabbit specific:
+ ((SessionImpl)sess).setAttribute(ProvidedSession.class.getName(), contentSession);
+ return sess;
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Cannot log in to " + workspace, e);
+ }
+ });
+ threadSession.put(workspace, session);
+ }
+
+ if (lastRetrievingThread != currentThread) {
+ try {
+ session.refresh(true);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot refresh JCR session " + session, e);
+ }
+ }
+ lastRetrievingThread = currentThread;
+ return session;
+ }
+
+}
--- /dev/null
+package org.argeo.cms.jcr.acr;
+
+import java.util.Arrays;
+import java.util.Iterator;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.xml.namespace.NamespaceContext;
+
+import org.argeo.jcr.JcrException;
+
+/** A {@link NamespaceContext} based on a JCR {@link Session}. */
+public class JcrSessionNamespaceContext implements NamespaceContext {
+ private final Session session;
+
+ public JcrSessionNamespaceContext(Session session) {
+ this.session = session;
+ }
+
+ @Override
+ public String getNamespaceURI(String prefix) {
+ try {
+ return session.getNamespaceURI(prefix);
+ } catch (RepositoryException e) {
+ throw new JcrException(e);
+ }
+ }
+
+ @Override
+ public String getPrefix(String namespaceURI) {
+ try {
+ return session.getNamespacePrefix(namespaceURI);
+ } catch (RepositoryException e) {
+ throw new JcrException(e);
+ }
+ }
+
+ @Override
+ public Iterator<String> getPrefixes(String namespaceURI) {
+ try {
+ return Arrays.asList(session.getNamespacePrefix(namespaceURI)).iterator();
+ } catch (RepositoryException e) {
+ throw new JcrException(e);
+ }
+ }
+}
--- /dev/null
+<argeo = 'http://www.argeo.org/ns/argeo'>
+
+// GENERIC TYPES
+[argeo:remoteRepository] > nt:unstructured
+- argeo:uri (STRING)
+- argeo:userID (STRING)
++ argeo:password (argeo:encrypted)
+
+// TABULAR CONTENT
+[argeo:table] > nt:file
++ * (argeo:column) *
+
+[argeo:column] > mix:title
+- jcr:requiredType (STRING) = 'STRING'
+
+[argeo:csv] > nt:resource
+
+// CRYPTO
+[argeo:encrypted]
+mixin
+// initialization vector used by some algorithms
+- argeo:iv (BINARY)
+
+[argeo:pbeKeySpec]
+mixin
+- argeo:secretKeyFactory (STRING)
+- argeo:salt (BINARY)
+- argeo:iterationCount (LONG)
+- argeo:keyLength (LONG)
+- argeo:secretKeyEncryption (STRING)
+
+[argeo:pbeSpec] > argeo:pbeKeySpec
+mixin
+- argeo:cipher (STRING)
--- /dev/null
+// DN (see https://tools.ietf.org/html/rfc4514)
+<cn = 'http://www.argeo.org/ns/rfc4514/cn'>
+<l = 'http://www.argeo.org/ns/rfc4514/l'>
+<st = 'http://www.argeo.org/ns/rfc4514/st'>
+<o = 'http://www.argeo.org/ns/rfc4514/o'>
+<ou = 'http://www.argeo.org/ns/rfc4514/ou'>
+<c = 'http://www.argeo.org/ns/rfc4514/c'>
+<street = 'http://www.argeo.org/ns/rfc4514/street'>
+<dc = 'http://www.argeo.org/ns/rfc4514/dc'>
+<uid = 'http://www.argeo.org/ns/rfc4514/uid'>
--- /dev/null
+package org.argeo.cms.jcr.internal;
+
+import static org.argeo.cms.osgi.DataModelNamespace.CMS_DATA_MODEL_NAMESPACE;
+import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.servlet.Servlet;
+
+import org.apache.jackrabbit.commons.cnd.CndImporter;
+import org.apache.jackrabbit.core.RepositoryContext;
+import org.apache.jackrabbit.core.RepositoryImpl;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.jcr.CmsJcrUtils;
+import org.argeo.cms.jcr.internal.servlet.CmsRemotingServlet;
+import org.argeo.cms.jcr.internal.servlet.CmsWebDavServlet;
+import org.argeo.cms.jcr.internal.servlet.JcrHttpUtils;
+import org.argeo.cms.osgi.DataModelNamespace;
+import org.argeo.jcr.Jcr;
+import org.argeo.jcr.JcrException;
+import org.argeo.jcr.JcrUtils;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.wiring.BundleCapability;
+import org.osgi.framework.wiring.BundleWire;
+import org.osgi.framework.wiring.BundleWiring;
+import org.osgi.service.http.whiteboard.HttpWhiteboardConstants;
+import org.osgi.util.tracker.ServiceTracker;
+
+/** Implementation of a CMS deployment. */
+public class CmsJcrDeployment {
+ private final CmsLog log = CmsLog.getLog(getClass());
+ private final BundleContext bc = FrameworkUtil.getBundle(getClass()).getBundleContext();
+
+ private DataModels dataModels;
+ private String webDavConfig = JcrHttpUtils.WEBDAV_CONFIG;
+
+ private boolean argeoDataModelExtensionsAvailable = false;
+
+ // Readiness
+ private boolean nodeAvailable = false;
+
+// CmsDeployment cmsDeployment;
+ public void start() {
+ dataModels = new DataModels(bc);
+
+ ServiceTracker<?, ?> repoContextSt = new RepositoryContextStc();
+ repoContextSt.open();
+ // KernelUtils.asyncOpen(repoContextSt);
+
+// nodeDeployment = CmsJcrActivator.getService(NodeDeployment.class);
+
+ //JcrInitUtils.addToDeployment(cmsDeployment);
+
+// contentRepository.registerTypes(NamespaceRegistry.PREFIX_JCR, NamespaceRegistry.NAMESPACE_JCR, null);
+// contentRepository.registerTypes(NamespaceRegistry.PREFIX_MIX, NamespaceRegistry.NAMESPACE_MIX, null);
+// contentRepository.registerTypes(NamespaceRegistry.PREFIX_NT, NamespaceRegistry.NAMESPACE_NT, null);
+// // Jackrabbit
+// // see
+// // https://jackrabbit.apache.org/archive/wiki/JCR/NamespaceRegistry_115513459.html
+// contentRepository.registerTypes("rep", "internal", null);
+
+ }
+
+ public void stop() {
+// if (nodeHttp != null)
+// nodeHttp.destroy();
+
+
+ try {
+ for (ServiceReference<JackrabbitLocalRepository> sr : bc
+ .getServiceReferences(JackrabbitLocalRepository.class, null)) {
+ bc.getService(sr).destroy();
+ }
+ } catch (InvalidSyntaxException e1) {
+ log.error("Cannot clean repositories", e1);
+ }
+
+ }
+
+// public void setCmsDeployment(CmsDeployment cmsDeployment) {
+// this.cmsDeployment = cmsDeployment;
+// }
+
+ /**
+ * Checks whether the deployment is available according to expectations, and
+ * mark it as available.
+ */
+// private synchronized void checkReadiness() {
+// if (isAvailable())
+// return;
+// if (nodeAvailable && userAdminAvailable && (httpExpected ? httpAvailable : true)) {
+// String data = KernelUtils.getFrameworkProp(KernelUtils.OSGI_INSTANCE_AREA);
+// String state = KernelUtils.getFrameworkProp(KernelUtils.OSGI_CONFIGURATION_AREA);
+// availableSince = System.currentTimeMillis();
+// long jvmUptime = ManagementFactory.getRuntimeMXBean().getUptime();
+// String jvmUptimeStr = " in " + (jvmUptime / 1000) + "." + (jvmUptime % 1000) + "s";
+// log.info("## ARGEO NODE AVAILABLE" + (log.isDebugEnabled() ? jvmUptimeStr : "") + " ##");
+// if (log.isDebugEnabled()) {
+// log.debug("## state: " + state);
+// if (data != null)
+// log.debug("## data: " + data);
+// }
+// long begin = bc.getService(bc.getServiceReference(NodeState.class)).getAvailableSince();
+// long initDuration = System.currentTimeMillis() - begin;
+// if (log.isTraceEnabled())
+// log.trace("Kernel initialization took " + initDuration + "ms");
+// tributeToFreeSoftware(initDuration);
+// }
+// }
+
+ private void prepareNodeRepository(Repository deployedNodeRepository, List<String> publishAsLocalRepo) {
+// if (availableSince != null) {
+// throw new IllegalStateException("Deployment is already available");
+// }
+
+ // home
+ prepareDataModel(CmsConstants.NODE_REPOSITORY, deployedNodeRepository, publishAsLocalRepo);
+
+ // init from backup
+// if (deployConfig.isFirstInit()) {
+// Path restorePath = Paths.get(System.getProperty("user.dir"), "restore");
+// if (Files.exists(restorePath)) {
+// if (log.isDebugEnabled())
+// log.debug("Found backup " + restorePath + ", restoring it...");
+// LogicalRestore logicalRestore = new LogicalRestore(bc, deployedNodeRepository, restorePath);
+// KernelUtils.doAsDataAdmin(logicalRestore);
+// log.info("Restored backup from " + restorePath);
+// }
+// }
+
+ // init from repository
+// Collection<ServiceReference<Repository>> initRepositorySr;
+// try {
+// initRepositorySr = bc.getServiceReferences(Repository.class,
+// "(" + CmsConstants.CN + "=" + CmsConstants.NODE_INIT + ")");
+// } catch (InvalidSyntaxException e1) {
+// throw new IllegalArgumentException(e1);
+// }
+// Iterator<ServiceReference<Repository>> it = initRepositorySr.iterator();
+// while (it.hasNext()) {
+// ServiceReference<Repository> sr = it.next();
+// Object labeledUri = sr.getProperties().get(LdapAttrs.labeledURI.name());
+// Repository initRepository = bc.getService(sr);
+// if (log.isDebugEnabled())
+// log.debug("Found init repository " + labeledUri + ", copying it...");
+// initFromRepository(deployedNodeRepository, initRepository);
+// log.info("Node repository initialised from " + labeledUri);
+// }
+ }
+
+ /** Init from a (typically remote) repository. */
+ private void initFromRepository(Repository deployedNodeRepository, Repository initRepository) {
+ Session initSession = null;
+ try {
+ initSession = initRepository.login();
+ workspaces: for (String workspaceName : initSession.getWorkspace().getAccessibleWorkspaceNames()) {
+ if ("security".equals(workspaceName))
+ continue workspaces;
+ if (log.isDebugEnabled())
+ log.debug("Copying workspace " + workspaceName + " from init repository...");
+ long begin = System.currentTimeMillis();
+ Session targetSession = null;
+ Session sourceSession = null;
+ try {
+ try {
+ targetSession = CmsJcrUtils.openDataAdminSession(deployedNodeRepository, workspaceName);
+ } catch (IllegalArgumentException e) {// no such workspace
+ Session adminSession = CmsJcrUtils.openDataAdminSession(deployedNodeRepository, null);
+ try {
+ adminSession.getWorkspace().createWorkspace(workspaceName);
+ } finally {
+ Jcr.logout(adminSession);
+ }
+ targetSession = CmsJcrUtils.openDataAdminSession(deployedNodeRepository, workspaceName);
+ }
+ sourceSession = initRepository.login(workspaceName);
+// JcrUtils.copyWorkspaceXml(sourceSession, targetSession);
+ // TODO deal with referenceable nodes
+ JcrUtils.copy(sourceSession.getRootNode(), targetSession.getRootNode());
+ targetSession.save();
+ long duration = System.currentTimeMillis() - begin;
+ if (log.isDebugEnabled())
+ log.debug("Copied workspace " + workspaceName + " from init repository in " + (duration / 1000)
+ + " s");
+ } catch (Exception e) {
+ log.error("Cannot copy workspace " + workspaceName + " from init repository.", e);
+ } finally {
+ Jcr.logout(sourceSession);
+ Jcr.logout(targetSession);
+ }
+ }
+ } catch (RepositoryException e) {
+ throw new JcrException(e);
+ } finally {
+ Jcr.logout(initSession);
+ }
+ }
+
+ private void prepareHomeRepository(RepositoryImpl deployedRepository) {
+ Session adminSession = KernelUtils.openAdminSession(deployedRepository);
+ try {
+ argeoDataModelExtensionsAvailable = Arrays
+ .asList(adminSession.getWorkspace().getNamespaceRegistry().getURIs())
+ .contains(ArgeoNames.ARGEO_NAMESPACE);
+ } catch (RepositoryException e) {
+ log.warn("Cannot check whether Argeo namespace is registered assuming it isn't.", e);
+ argeoDataModelExtensionsAvailable = false;
+ } finally {
+ JcrUtils.logoutQuietly(adminSession);
+ }
+
+ // Publish home with the highest service ranking
+ Hashtable<String, Object> regProps = new Hashtable<>();
+ regProps.put(CmsConstants.CN, CmsConstants.EGO_REPOSITORY);
+ regProps.put(Constants.SERVICE_RANKING, Integer.MAX_VALUE);
+ Repository egoRepository = new EgoRepository(deployedRepository, false);
+ bc.registerService(Repository.class, egoRepository, regProps);
+ registerRepositoryServlets(CmsConstants.EGO_REPOSITORY, egoRepository);
+
+ // Keyring only if Argeo extensions are available
+// if (argeoDataModelExtensionsAvailable) {
+// new ServiceTracker<CallbackHandler, CallbackHandler>(bc, CallbackHandler.class, null) {
+//
+// @Override
+// public CallbackHandler addingService(ServiceReference<CallbackHandler> reference) {
+// NodeKeyRing nodeKeyring = new NodeKeyRing(egoRepository);
+// CallbackHandler callbackHandler = bc.getService(reference);
+// nodeKeyring.setDefaultCallbackHandler(callbackHandler);
+// bc.registerService(LangUtils.names(Keyring.class, CryptoKeyring.class, ManagedService.class),
+// nodeKeyring, LangUtils.dict(Constants.SERVICE_PID, CmsConstants.NODE_KEYRING_PID));
+// return callbackHandler;
+// }
+//
+// }.open();
+// }
+ }
+
+ /** Session is logged out. */
+ private void prepareDataModel(String cn, Repository repository, List<String> publishAsLocalRepo) {
+ Session adminSession = KernelUtils.openAdminSession(repository);
+ try {
+ Set<String> processed = new HashSet<String>();
+ bundles: for (Bundle bundle : bc.getBundles()) {
+ BundleWiring wiring = bundle.adapt(BundleWiring.class);
+ if (wiring == null)
+ continue bundles;
+ if (CmsConstants.NODE_REPOSITORY.equals(cn))// process all data models
+ processWiring(cn, adminSession, wiring, processed, false, publishAsLocalRepo);
+ else {
+ List<BundleCapability> capabilities = wiring.getCapabilities(CMS_DATA_MODEL_NAMESPACE);
+ for (BundleCapability capability : capabilities) {
+ String dataModelName = (String) capability.getAttributes().get(DataModelNamespace.NAME);
+ if (dataModelName.equals(cn))// process only own data model
+ processWiring(cn, adminSession, wiring, processed, false, publishAsLocalRepo);
+ }
+ }
+ }
+ } finally {
+ JcrUtils.logoutQuietly(adminSession);
+ }
+ }
+
+ private void processWiring(String cn, Session adminSession, BundleWiring wiring, Set<String> processed,
+ boolean importListedAbstractModels, List<String> publishAsLocalRepo) {
+ // recursively process requirements first
+ List<BundleWire> requiredWires = wiring.getRequiredWires(CMS_DATA_MODEL_NAMESPACE);
+ for (BundleWire wire : requiredWires) {
+ processWiring(cn, adminSession, wire.getProviderWiring(), processed, true, publishAsLocalRepo);
+ }
+
+ List<BundleCapability> capabilities = wiring.getCapabilities(CMS_DATA_MODEL_NAMESPACE);
+ capabilities: for (BundleCapability capability : capabilities) {
+ if (!importListedAbstractModels
+ && KernelUtils.asBoolean((String) capability.getAttributes().get(DataModelNamespace.ABSTRACT))) {
+ continue capabilities;
+ }
+ boolean publish = registerDataModelCapability(cn, adminSession, capability, processed);
+ if (publish)
+ publishAsLocalRepo.add((String) capability.getAttributes().get(DataModelNamespace.NAME));
+ }
+ }
+
+ private boolean registerDataModelCapability(String cn, Session adminSession, BundleCapability capability,
+ Set<String> processed) {
+ Map<String, Object> attrs = capability.getAttributes();
+ String name = (String) attrs.get(DataModelNamespace.NAME);
+ if (processed.contains(name)) {
+ if (log.isTraceEnabled())
+ log.trace("Data model " + name + " has already been processed");
+ return false;
+ }
+
+ // CND
+ String path = (String) attrs.get(DataModelNamespace.CND);
+ if (path != null) {
+ File dataModel = bc.getBundle().getDataFile("dataModels/" + path);
+ if (!dataModel.exists()) {
+ URL url = capability.getRevision().getBundle().getResource(path);
+ if (url == null)
+ throw new IllegalArgumentException("No data model '" + name + "' found under path " + path);
+ try (Reader reader = new InputStreamReader(url.openStream())) {
+ CndImporter.registerNodeTypes(reader, adminSession, true);
+ processed.add(name);
+ dataModel.getParentFile().mkdirs();
+ dataModel.createNewFile();
+ if (log.isDebugEnabled())
+ log.debug("Registered CND " + url);
+ } catch (Exception e) {
+ log.error("Cannot import CND " + url, e);
+ }
+ }
+ }
+
+ if (KernelUtils.asBoolean((String) attrs.get(DataModelNamespace.ABSTRACT)))
+ return false;
+ // Non abstract
+ boolean isStandalone = isStandalone(name);
+ boolean publishLocalRepo;
+ if (isStandalone && name.equals(cn))// includes the node itself
+ publishLocalRepo = true;
+ else if (!isStandalone && cn.equals(CmsConstants.NODE_REPOSITORY))
+ publishLocalRepo = true;
+ else
+ publishLocalRepo = false;
+
+ return publishLocalRepo;
+ }
+
+ boolean isStandalone(String dataModelName) {
+ return true;
+ //return cmsDeployment.getProps(CmsConstants.NODE_REPOS_FACTORY_PID, dataModelName) != null;
+ }
+
+ private void publishLocalRepo(String dataModelName, Repository repository) {
+ Hashtable<String, Object> properties = new Hashtable<>();
+ properties.put(CmsConstants.CN, dataModelName);
+ LocalRepository localRepository;
+ String[] classes;
+ if (repository instanceof RepositoryImpl) {
+ localRepository = new JackrabbitLocalRepository((RepositoryImpl) repository, dataModelName);
+ classes = new String[] { Repository.class.getName(), LocalRepository.class.getName(),
+ JackrabbitLocalRepository.class.getName() };
+ } else {
+ localRepository = new LocalRepository(repository, dataModelName);
+ classes = new String[] { Repository.class.getName(), LocalRepository.class.getName() };
+ }
+ bc.registerService(classes, localRepository, properties);
+
+ // TODO make it configurable
+ registerRepositoryServlets(dataModelName, localRepository);
+ if (log.isTraceEnabled())
+ log.trace("Published data model " + dataModelName);
+ }
+
+// @Override
+// public synchronized Long getAvailableSince() {
+// return availableSince;
+// }
+//
+// public synchronized boolean isAvailable() {
+// return availableSince != null;
+// }
+
+ protected void registerRepositoryServlets(String alias, Repository repository) {
+ registerRemotingServlet(alias, repository);
+ registerWebdavServlet(alias, repository);
+ }
+
+ protected void registerWebdavServlet(String alias, Repository repository) {
+ CmsWebDavServlet webdavServlet = new CmsWebDavServlet(alias, repository);
+ Hashtable<String, String> ip = new Hashtable<>();
+ ip.put(HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX + CmsWebDavServlet.INIT_PARAM_RESOURCE_CONFIG, webDavConfig);
+ ip.put(HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX + CmsWebDavServlet.INIT_PARAM_RESOURCE_PATH_PREFIX,
+ "/" + alias);
+
+ ip.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, "/" + alias + "/*");
+ ip.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT,
+ "(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_PATH + "=" + CmsConstants.PATH_DATA + ")");
+ bc.registerService(Servlet.class, webdavServlet, ip);
+ }
+
+ protected void registerRemotingServlet(String alias, Repository repository) {
+ CmsRemotingServlet remotingServlet = new CmsRemotingServlet(alias, repository);
+ Hashtable<String, String> ip = new Hashtable<>();
+ ip.put(CmsConstants.CN, alias);
+ // Properties ip = new Properties();
+ ip.put(HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX + CmsRemotingServlet.INIT_PARAM_RESOURCE_PATH_PREFIX,
+ "/" + alias);
+ ip.put(HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX + CmsRemotingServlet.INIT_PARAM_AUTHENTICATE_HEADER,
+ "Negotiate");
+
+ // Looks like a bug in Jackrabbit remoting init
+ Path tmpDir;
+ try {
+ tmpDir = Files.createTempDirectory("remoting_" + alias);
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot create temp directory for remoting servlet", e);
+ }
+ ip.put(HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX + CmsRemotingServlet.INIT_PARAM_HOME, tmpDir.toString());
+ ip.put(HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX + CmsRemotingServlet.INIT_PARAM_TMP_DIRECTORY,
+ "remoting_" + alias);
+ ip.put(HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX + CmsRemotingServlet.INIT_PARAM_PROTECTED_HANDLERS_CONFIG,
+ JcrHttpUtils.DEFAULT_PROTECTED_HANDLERS);
+ ip.put(HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX + CmsRemotingServlet.INIT_PARAM_CREATE_ABSOLUTE_URI, "false");
+
+ ip.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, "/" + alias + "/*");
+ ip.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT,
+ "(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_PATH + "=" + CmsConstants.PATH_JCR + ")");
+ bc.registerService(Servlet.class, remotingServlet, ip);
+ }
+
+ private class RepositoryContextStc extends ServiceTracker<RepositoryContext, RepositoryContext> {
+
+ public RepositoryContextStc() {
+ super(bc, RepositoryContext.class, null);
+ }
+
+ @Override
+ public RepositoryContext addingService(ServiceReference<RepositoryContext> reference) {
+ RepositoryContext repoContext = bc.getService(reference);
+ String cn = (String) reference.getProperty(CmsConstants.CN);
+ if (cn != null) {
+ List<String> publishAsLocalRepo = new ArrayList<>();
+ if (cn.equals(CmsConstants.NODE_REPOSITORY)) {
+// JackrabbitDataModelMigration.clearRepositoryCaches(repoContext.getRepositoryConfig());
+ prepareNodeRepository(repoContext.getRepository(), publishAsLocalRepo);
+ // TODO separate home repository
+ prepareHomeRepository(repoContext.getRepository());
+ registerRepositoryServlets(cn, repoContext.getRepository());
+ nodeAvailable = true;
+// checkReadiness();
+ } else {
+ prepareDataModel(cn, repoContext.getRepository(), publishAsLocalRepo);
+ }
+ // Publish all at once, so that bundles with multiple CNDs are consistent
+ for (String dataModelName : publishAsLocalRepo)
+ publishLocalRepo(dataModelName, repoContext.getRepository());
+ }
+ return repoContext;
+ }
+
+ @Override
+ public void modifiedService(ServiceReference<RepositoryContext> reference, RepositoryContext service) {
+ }
+
+ @Override
+ public void removedService(ServiceReference<RepositoryContext> reference, RepositoryContext service) {
+ }
+
+ }
+
+}
--- /dev/null
+package org.argeo.cms.jcr.internal;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystemAlreadyExistsException;
+import java.nio.file.Path;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.jcr.CmsJcrUtils;
+import org.argeo.jackrabbit.fs.AbstractJackrabbitFsProvider;
+import org.argeo.jcr.fs.JcrFileSystem;
+import org.argeo.jcr.fs.JcrFileSystemProvider;
+import org.argeo.jcr.fs.JcrFsException;
+
+/** Implementation of an {@link FileSystemProvider} based on Jackrabbit. */
+public class CmsJcrFsProvider extends AbstractJackrabbitFsProvider {
+ private Map<String, CmsFileSystem> fileSystems = new HashMap<>();
+
+ private RepositoryFactory repositoryFactory;
+ private Repository repository;
+
+ @Override
+ public String getScheme() {
+ return CmsConstants.SCHEME_NODE;
+ }
+
+ @Override
+ public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
+// BundleContext bc = FrameworkUtil.getBundle(CmsJcrFsProvider.class).getBundleContext();
+ String username = CurrentUser.getUsername();
+ if (username == null) {
+ // TODO deal with anonymous
+ return null;
+ }
+ if (fileSystems.containsKey(username))
+ throw new FileSystemAlreadyExistsException("CMS file system already exists for user " + username);
+
+ try {
+ String host = uri.getHost();
+ if (host != null && !host.trim().equals("")) {
+ URI repoUri = new URI("http", uri.getUserInfo(), uri.getHost(), uri.getPort(), "/jcr/node", null, null);
+// RepositoryFactory repositoryFactory = bc.getService(bc.getServiceReference(RepositoryFactory.class));
+ Repository repository = CmsJcrUtils.getRepositoryByUri(repositoryFactory, repoUri.toString());
+ CmsFileSystem fileSystem = new CmsFileSystem(this, repository);
+ fileSystems.put(username, fileSystem);
+ return fileSystem;
+ } else {
+// Repository repository = bc.getService(
+// bc.getServiceReferences(Repository.class, "(cn=" + CmsConstants.EGO_REPOSITORY + ")")
+// .iterator().next());
+
+ // Session session = repository.login();
+ CmsFileSystem fileSystem = new CmsFileSystem(this, repository);
+ fileSystems.put(username, fileSystem);
+ return fileSystem;
+ }
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Cannot open file system " + uri + " for user " + username, e);
+ }
+ }
+
+ @Override
+ public FileSystem getFileSystem(URI uri) {
+ return currentUserFileSystem();
+ }
+
+ @Override
+ public Path getPath(URI uri) {
+ JcrFileSystem fileSystem = currentUserFileSystem();
+ String path = uri.getPath();
+ if (fileSystem == null)
+ try {
+ fileSystem = (JcrFileSystem) newFileSystem(uri, new HashMap<String, Object>());
+ } catch (IOException e) {
+ throw new JcrFsException("Could not autocreate file system", e);
+ }
+ return fileSystem.getPath(path);
+ }
+
+ protected JcrFileSystem currentUserFileSystem() {
+ String username = CurrentUser.getUsername();
+ return fileSystems.get(username);
+ }
+
+ public Node getUserHome(Repository repository) {
+ try {
+ Session session = repository.login(CmsConstants.HOME_WORKSPACE);
+ return CmsJcrUtils.getUserHome(session);
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Cannot get user home", e);
+ }
+ }
+
+ public void setRepositoryFactory(RepositoryFactory repositoryFactory) {
+ this.repositoryFactory = repositoryFactory;
+ }
+
+ public void setRepository(Repository repository) {
+ this.repository = repository;
+ }
+
+ static class CmsFileSystem extends JcrFileSystem {
+ public CmsFileSystem(JcrFileSystemProvider provider, Repository repository) throws IOException {
+ super(provider, repository);
+ }
+
+ public boolean skipNode(Node node) throws RepositoryException {
+// if (node.isNodeType(NodeType.NT_HIERARCHY_NODE) || node.isNodeType(NodeTypes.NODE_USER_HOME)
+// || node.isNodeType(NodeTypes.NODE_GROUP_HOME))
+ if (node.isNodeType(NodeType.NT_HIERARCHY_NODE))
+ return false;
+ // FIXME Better identifies home
+ if (node.hasProperty(Property.JCR_ID))
+ return false;
+ return true;
+ }
+
+ }
+}
--- /dev/null
+package org.argeo.cms.jcr.internal;
+
+import java.nio.file.Path;
+
+/** Centralises access to the default node deployment directories. */
+@Deprecated
+public class CmsPaths {
+ public static Path getRepoDirPath(String cn) {
+ return KernelUtils.getOsgiInstancePath(KernelConstants.DIR_REPOS + '/' + cn);
+ }
+
+ public static Path getRepoIndexesBase() {
+ return KernelUtils.getOsgiInstancePath(KernelConstants.DIR_INDEXES);
+ }
+
+ /** Singleton. */
+ private CmsPaths() {
+ }
+}
--- /dev/null
+package org.argeo.cms.jcr.internal;
+
+import java.util.GregorianCalendar;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.observation.Event;
+import javax.jcr.observation.EventIterator;
+import javax.jcr.observation.EventListener;
+import javax.jcr.version.VersionManager;
+
+import org.apache.jackrabbit.api.JackrabbitValue;
+import org.apache.jackrabbit.core.RepositoryImpl;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+
+/** Ensure consistency of files, folder and last modified nodes. */
+class CmsWorkspaceIndexer implements EventListener {
+ private final static CmsLog log = CmsLog.getLog(CmsWorkspaceIndexer.class);
+
+// private final static String MIX_ETAG = "mix:etag";
+ private final static String JCR_ETAG = "jcr:etag";
+// private final static String JCR_LAST_MODIFIED = "jcr:lastModified";
+// private final static String JCR_LAST_MODIFIED_BY = "jcr:lastModifiedBy";
+// private final static String JCR_MIXIN_TYPES = "jcr:mixinTypes";
+ private final static String JCR_DATA = "jcr:data";
+ private final static String JCR_CONTENT = "jcr:data";
+
+ private String cn;
+ private String workspaceName;
+ private RepositoryImpl repositoryImpl;
+ private Session session;
+ private VersionManager versionManager;
+
+ private LinkedBlockingDeque<Event> toProcess = new LinkedBlockingDeque<>();
+ private IndexingThread indexingThread;
+ private AtomicBoolean stopping = new AtomicBoolean(false);
+
+ public CmsWorkspaceIndexer(RepositoryImpl repositoryImpl, String cn, String workspaceName)
+ throws RepositoryException {
+ this.cn = cn;
+ this.workspaceName = workspaceName;
+ this.repositoryImpl = repositoryImpl;
+ }
+
+ public void init() {
+ session = KernelUtils.openAdminSession(repositoryImpl, workspaceName);
+ try {
+ String[] nodeTypes = { NodeType.NT_FILE, NodeType.MIX_LAST_MODIFIED };
+ session.getWorkspace().getObservationManager().addEventListener(this,
+ Event.NODE_ADDED | Event.PROPERTY_CHANGED, "/", true, null, nodeTypes, true);
+ versionManager = session.getWorkspace().getVersionManager();
+
+ indexingThread = new IndexingThread();
+ indexingThread.start();
+ } catch (RepositoryException e1) {
+ throw new IllegalStateException(e1);
+ }
+ }
+
+ public void destroy() {
+ stopping.set(true);
+ indexingThread.interrupt();
+ // TODO make it configurable
+ try {
+ indexingThread.join(10 * 60 * 1000);
+ } catch (InterruptedException e1) {
+ log.warn("Indexing thread interrupted. Will log out session.");
+ }
+
+ try {
+ session.getWorkspace().getObservationManager().removeEventListener(this);
+ } catch (RepositoryException e) {
+ if (log.isTraceEnabled())
+ log.warn("Cannot unregistered JCR event listener", e);
+ } finally {
+ JcrUtils.logoutQuietly(session);
+ }
+ }
+
+ private synchronized void processEvents(EventIterator events) {
+ long begin = System.currentTimeMillis();
+ long count = 0;
+ while (events.hasNext()) {
+ Event event = events.nextEvent();
+ try {
+ toProcess.put(event);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+// processEvent(event);
+ count++;
+ }
+ long duration = System.currentTimeMillis() - begin;
+ if (log.isTraceEnabled())
+ log.trace("Processed " + count + " events in " + duration + " ms");
+ notifyAll();
+ }
+
+ protected synchronized void processEvent(Event event) {
+ try {
+ String eventPath = event.getPath();
+ if (event.getType() == Event.NODE_ADDED) {
+ if (!versionManager.isCheckedOut(eventPath))
+ return;// ignore checked-in nodes
+ if (log.isTraceEnabled())
+ log.trace("NODE_ADDED " + eventPath);
+// session.refresh(true);
+ session.refresh(false);
+ Node node = session.getNode(eventPath);
+ Node parentNode = node.getParent();
+ if (parentNode.isNodeType(NodeType.NT_FILE)) {
+ if (node.isNodeType(NodeType.NT_UNSTRUCTURED)) {
+ if (!node.isNodeType(NodeType.MIX_LAST_MODIFIED))
+ node.addMixin(NodeType.MIX_LAST_MODIFIED);
+ Property property = node.getProperty(Property.JCR_DATA);
+ String etag = toEtag(property.getValue());
+ session.save();
+ node.setProperty(JCR_ETAG, etag);
+ if (log.isTraceEnabled())
+ log.trace("ETag and last modified added to new " + node);
+ } else if (node.isNodeType(NodeType.NT_RESOURCE)) {
+// if (!node.isNodeType(MIX_ETAG))
+// node.addMixin(MIX_ETAG);
+// session.save();
+// Property property = node.getProperty(Property.JCR_DATA);
+// String etag = toEtag(property.getValue());
+// node.setProperty(JCR_ETAG, etag);
+// session.save();
+ }
+// setLastModifiedRecursive(parentNode, event);
+// session.save();
+// if (log.isTraceEnabled())
+// log.trace("ETag and last modified added to new " + node);
+ }
+
+// if (node.isNodeType(NodeType.NT_FOLDER)) {
+// setLastModifiedRecursive(node, event);
+// session.save();
+// if (log.isTraceEnabled())
+// log.trace("Last modified added to new " + node);
+// }
+ } else if (event.getType() == Event.PROPERTY_CHANGED) {
+ String propertyName = extractItemName(eventPath);
+ // skip if last modified properties are explicitly set
+ if (!propertyName.equals(JCR_DATA))
+ return;
+// if (propertyName.equals(JCR_LAST_MODIFIED))
+// return;
+// if (propertyName.equals(JCR_LAST_MODIFIED_BY))
+// return;
+// if (propertyName.equals(JCR_MIXIN_TYPES))
+// return;
+// if (propertyName.equals(JCR_ETAG))
+// return;
+
+ if (log.isTraceEnabled())
+ log.trace("PROPERTY_CHANGED " + eventPath);
+
+ if (!session.propertyExists(eventPath))
+ return;
+ session.refresh(false);
+ Property property = session.getProperty(eventPath);
+ Node node = property.getParent();
+ if (property.getType() == PropertyType.BINARY && propertyName.equals(JCR_DATA)
+ && node.isNodeType(NodeType.NT_UNSTRUCTURED)) {
+ String etag = toEtag(property.getValue());
+ node.setProperty(JCR_ETAG, etag);
+ Node parentNode = node.getParent();
+ if (parentNode.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
+ setLastModified(parentNode, event);
+ }
+ if (log.isTraceEnabled())
+ log.trace("ETag and last modified updated for " + node);
+ }
+// setLastModified(node, event);
+// session.save();
+// if (log.isTraceEnabled())
+// log.trace("ETag and last modified updated for " + node);
+ } else if (event.getType() == Event.NODE_REMOVED) {
+ String removeNodePath = eventPath;
+ String nodeName = extractItemName(eventPath);
+ if (JCR_CONTENT.equals(nodeName)) // parent is a file, deleted anyhow
+ return;
+ if (log.isTraceEnabled())
+ log.trace("NODE_REMOVED " + eventPath);
+// String parentPath = JcrUtils.parentPath(removeNodePath);
+// session.refresh(true);
+// setLastModified(parentPath, event);
+// session.save();
+ if (log.isTraceEnabled())
+ log.trace("Last modified updated for parents of removed " + removeNodePath);
+ }
+ } catch (Exception e) {
+ if (log.isTraceEnabled())
+ log.warn("Cannot process event " + event, e);
+ } finally {
+// try {
+// session.refresh(true);
+// if (session.hasPendingChanges())
+// session.save();
+//// session.refresh(false);
+// } catch (RepositoryException e) {
+// if (log.isTraceEnabled())
+// log.warn("Cannot refresh JCR session", e);
+// }
+ }
+
+ }
+
+ private String extractItemName(String path) {
+ if (path == null || path.length() <= 1)
+ return null;
+ int lastIndex = path.lastIndexOf('/');
+ if (lastIndex >= 0) {
+ return path.substring(lastIndex + 1);
+ } else {
+ return path;
+ }
+ }
+
+ @Override
+ public void onEvent(EventIterator events) {
+ processEvents(events);
+// Runnable toRun = new Runnable() {
+//
+// @Override
+// public void run() {
+// processEvents(events);
+// }
+// };
+// Future<?> future = Activator.getInternalExecutorService().submit(toRun);
+// try {
+// // make the call synchronous
+// future.get(60, TimeUnit.SECONDS);
+// } catch (TimeoutException | ExecutionException | InterruptedException e) {
+// // silent
+// }
+ }
+
+ static String toEtag(Value v) {
+ if (v instanceof JackrabbitValue) {
+ JackrabbitValue value = (JackrabbitValue) v;
+ return '\"' + value.getContentIdentity() + '\"';
+ } else {
+ return null;
+ }
+
+ }
+
+ protected synchronized void setLastModified(Node node, Event event) throws RepositoryException {
+ GregorianCalendar calendar = new GregorianCalendar();
+ calendar.setTimeInMillis(event.getDate());
+ node.setProperty(Property.JCR_LAST_MODIFIED, calendar);
+ node.setProperty(Property.JCR_LAST_MODIFIED_BY, event.getUserID());
+ if (log.isTraceEnabled())
+ log.trace("Last modified set on " + node);
+ }
+
+ /** Recursively set the last updated time on parents. */
+ protected synchronized void setLastModifiedRecursive(Node node, Event event) throws RepositoryException {
+ if (versionManager.isCheckedOut(node.getPath())) {
+ if (node.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
+ setLastModified(node, event);
+ }
+ if (node.isNodeType(NodeType.NT_FOLDER) && !node.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
+ node.addMixin(NodeType.MIX_LAST_MODIFIED);
+ if (log.isTraceEnabled())
+ log.trace("Last modified mix-in added to " + node);
+ }
+
+ }
+
+ // end condition
+ if (node.getDepth() == 0) {
+// try {
+// node.getSession().save();
+// } catch (RepositoryException e) {
+// log.warn("Cannot index workspace", e);
+// }
+ return;
+ } else {
+ Node parent = node.getParent();
+ setLastModifiedRecursive(parent, event);
+ }
+ }
+
+ /**
+ * Recursively set the last updated time on parents. Useful to use paths when
+ * dealing with deletions.
+ */
+ protected synchronized void setLastModifiedRecursive(String path, Event event) throws RepositoryException {
+ // root node will always exist, so end condition is delegated to the other
+ // recursive setLastModified method
+ if (session.nodeExists(path)) {
+ setLastModifiedRecursive(session.getNode(path), event);
+ } else {
+ setLastModifiedRecursive(JcrUtils.parentPath(path), event);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Indexer for workspace " + workspaceName + " of repository " + cn;
+ }
+
+ class IndexingThread extends Thread {
+
+ public IndexingThread() {
+ super(CmsWorkspaceIndexer.this.toString());
+ // TODO Auto-generated constructor stub
+ }
+
+ @Override
+ public void run() {
+ life: while (session != null && session.isLive()) {
+ try {
+ Event nextEvent = toProcess.take();
+ processEvent(nextEvent);
+ } catch (InterruptedException e) {
+ // silent
+ interrupted();
+ }
+
+ if (stopping.get() && toProcess.isEmpty()) {
+ break life;
+ }
+ }
+ if (log.isDebugEnabled())
+ log.debug(CmsWorkspaceIndexer.this.toString() + " has shut down.");
+ }
+
+ }
+
+}
\ No newline at end of file
--- /dev/null
+package org.argeo.cms.jcr.internal;
+
+import static org.argeo.cms.osgi.DataModelNamespace.CMS_DATA_MODEL_NAMESPACE;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.osgi.DataModelNamespace;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.BundleListener;
+import org.osgi.framework.wiring.BundleCapability;
+import org.osgi.framework.wiring.BundleWire;
+import org.osgi.framework.wiring.BundleWiring;
+
+class DataModels implements BundleListener {
+ private final static CmsLog log = CmsLog.getLog(DataModels.class);
+
+ private Map<String, DataModel> dataModels = new TreeMap<>();
+
+ public DataModels(BundleContext bc) {
+ for (Bundle bundle : bc.getBundles())
+ processBundle(bundle, null);
+ bc.addBundleListener(this);
+ }
+
+ public List<DataModel> getNonAbstractDataModels() {
+ List<DataModel> res = new ArrayList<>();
+ for (String name : dataModels.keySet()) {
+ DataModel dataModel = dataModels.get(name);
+ if (!dataModel.isAbstract())
+ res.add(dataModel);
+ }
+ // TODO reorder?
+ return res;
+ }
+
+ @Override
+ public void bundleChanged(BundleEvent event) {
+ if (event.getType() == Bundle.RESOLVED) {
+ processBundle(event.getBundle(), null);
+ } else if (event.getType() == Bundle.UNINSTALLED) {
+ BundleWiring wiring = event.getBundle().adapt(BundleWiring.class);
+ List<BundleCapability> providedDataModels = wiring.getCapabilities(CMS_DATA_MODEL_NAMESPACE);
+ if (providedDataModels.size() == 0)
+ return;
+ for (BundleCapability bundleCapability : providedDataModels) {
+ dataModels.remove(bundleCapability.getAttributes().get(DataModelNamespace.NAME));
+ }
+ }
+
+ }
+
+ protected void processBundle(Bundle bundle, List<Bundle> scannedBundles) {
+ if (scannedBundles != null && scannedBundles.contains(bundle))
+ throw new IllegalStateException("Cycle in CMS data model requirements for " + bundle);
+ BundleWiring wiring = bundle.adapt(BundleWiring.class);
+ if (wiring == null) {
+ int bundleState = bundle.getState();
+ if (bundleState != Bundle.INSTALLED && bundleState != Bundle.UNINSTALLED) {// ignore unresolved bundles
+ log.warn("Bundle " + bundle.getSymbolicName() + " #" + bundle.getBundleId() + " ("
+ + bundle.getLocation() + ") cannot be adapted to a wiring");
+ } else {
+ if (log.isTraceEnabled())
+ log.warn("Bundle " + bundle.getSymbolicName() + " is not resolved.");
+ }
+ return;
+ }
+ List<BundleCapability> providedDataModels = wiring.getCapabilities(CMS_DATA_MODEL_NAMESPACE);
+ if (providedDataModels.size() == 0)
+ return;
+ List<BundleWire> requiredDataModels = wiring.getRequiredWires(CMS_DATA_MODEL_NAMESPACE);
+ // process requirements first
+ for (BundleWire bundleWire : requiredDataModels) {
+ List<Bundle> nextScannedBundles = new ArrayList<>();
+ if (scannedBundles != null)
+ nextScannedBundles.addAll(scannedBundles);
+ nextScannedBundles.add(bundle);
+ Bundle providerBundle = bundleWire.getProvider().getBundle();
+ processBundle(providerBundle, nextScannedBundles);
+ }
+ for (BundleCapability bundleCapability : providedDataModels) {
+ String name = (String) bundleCapability.getAttributes().get(DataModelNamespace.NAME);
+ assert name != null;
+ if (!dataModels.containsKey(name)) {
+ DataModel dataModel = new DataModel(name, bundleCapability, requiredDataModels);
+ dataModels.put(dataModel.getName(), dataModel);
+ }
+ }
+ }
+
+ /** Return a negative depth if dataModel is required by ref, 0 otherwise. */
+ static int required(DataModel ref, DataModel dataModel, int depth) {
+ for (DataModel dm : ref.getRequired()) {
+ if (dm.equals(dataModel))// found here
+ return depth - 1;
+ int d = required(dm, dataModel, depth - 1);
+ if (d != 0)// found deeper
+ return d;
+ }
+ return 0;// not found
+ }
+
+ class DataModel {
+ private final String name;
+ private final boolean abstrct;
+ // private final boolean standalone;
+ private final String cnd;
+ private final List<DataModel> required;
+
+ private DataModel(String name, BundleCapability bundleCapability, List<BundleWire> requiredDataModels) {
+ assert CMS_DATA_MODEL_NAMESPACE.equals(bundleCapability.getNamespace());
+ this.name = name;
+ Map<String, Object> attrs = bundleCapability.getAttributes();
+ abstrct = KernelUtils.asBoolean((String) attrs.get(DataModelNamespace.ABSTRACT));
+ // standalone = KernelUtils.asBoolean((String)
+ // attrs.get(DataModelNamespace.CAPABILITY_STANDALONE_ATTRIBUTE));
+ cnd = (String) attrs.get(DataModelNamespace.CND);
+ List<DataModel> req = new ArrayList<>();
+ for (BundleWire wire : requiredDataModels) {
+ String requiredDataModelName = (String) wire.getCapability().getAttributes()
+ .get(DataModelNamespace.NAME);
+ assert requiredDataModelName != null;
+ DataModel requiredDataModel = dataModels.get(requiredDataModelName);
+ if (requiredDataModel == null)
+ throw new IllegalStateException("No required data model " + requiredDataModelName);
+ req.add(requiredDataModel);
+ }
+ required = Collections.unmodifiableList(req);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public boolean isAbstract() {
+ return abstrct;
+ }
+
+ // public boolean isStandalone() {
+ // return !isAbstract();
+ // }
+
+ public String getCnd() {
+ return cnd;
+ }
+
+ public List<DataModel> getRequired() {
+ return required;
+ }
+
+ // @Override
+ // public int compareTo(DataModel o) {
+ // if (equals(o))
+ // return 0;
+ // int res = required(this, o, 0);
+ // if (res != 0)
+ // return res;
+ // // the other way round
+ // res = required(o, this, 0);
+ // if (res != 0)
+ // return -res;
+ // return 0;
+ // }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof DataModel)
+ return ((DataModel) obj).name.equals(name);
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "Data model " + name;
+ }
+
+ }
+
+}
--- /dev/null
+package org.argeo.cms.jcr.internal;
+
+import java.security.PrivilegedAction;
+import java.text.SimpleDateFormat;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.security.Privilege;
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+
+import org.argeo.api.cms.CmsAuth;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.cms.jcr.CmsJcrUtils;
+import org.argeo.jcr.JcrException;
+import org.argeo.jcr.JcrRepositoryWrapper;
+import org.argeo.jcr.JcrUtils;
+
+/**
+ * Make sure each user has a home directory available.
+ */
+class EgoRepository extends JcrRepositoryWrapper {
+
+ /** The home base path. */
+// private String homeBasePath = KernelConstants.DEFAULT_HOME_BASE_PATH;
+// private String usersBasePath = KernelConstants.DEFAULT_USERS_BASE_PATH;
+// private String groupsBasePath = KernelConstants.DEFAULT_GROUPS_BASE_PATH;
+
+ private Set<String> checkedUsers = new HashSet<String>();
+
+ private SimpleDateFormat usersDatePath = new SimpleDateFormat("YYYY/MM");
+
+ private String defaultHomeWorkspace = CmsConstants.HOME_WORKSPACE;
+ private String defaultGroupsWorkspace = CmsConstants.SRV_WORKSPACE;
+// private String defaultGuestsWorkspace = NodeConstants.GUESTS_WORKSPACE;
+ private final boolean remote;
+
+ public EgoRepository(Repository repository, boolean remote) {
+ super(repository);
+ this.remote = remote;
+ putDescriptor(CmsConstants.CN, CmsConstants.EGO_REPOSITORY);
+ if (!remote) {
+ LoginContext lc;
+ try {
+ lc = new LoginContext(CmsAuth.LOGIN_CONTEXT_DATA_ADMIN);
+ lc.login();
+ } catch (javax.security.auth.login.LoginException e1) {
+ throw new IllegalStateException("Cannot login as system", e1);
+ }
+ Subject.doAs(lc.getSubject(), new PrivilegedAction<Void>() {
+
+ @Override
+ public Void run() {
+ loginOrCreateWorkspace(defaultHomeWorkspace);
+ loginOrCreateWorkspace(defaultGroupsWorkspace);
+ return null;
+ }
+
+ });
+ }
+ }
+
+ private void loginOrCreateWorkspace(String workspace) {
+ Session adminSession = null;
+ try {
+ adminSession = JcrUtils.loginOrCreateWorkspace(getRepository(workspace), workspace);
+// JcrUtils.addPrivilege(adminSession, "/", NodeConstants.ROLE_USER, Privilege.JCR_READ);
+
+// initJcr(adminSession);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot init JCR home", e);
+ } finally {
+ JcrUtils.logoutQuietly(adminSession);
+ }
+ }
+
+// @Override
+// public Session login(Credentials credentials, String workspaceName)
+// throws LoginException, NoSuchWorkspaceException, RepositoryException {
+// if (workspaceName == null) {
+// return super.login(credentials, getUserHomeWorkspace());
+// } else {
+// return super.login(credentials, workspaceName);
+// }
+// }
+
+ protected String getUserHomeWorkspace() {
+ // TODO base on JAAS Subject metadata
+ return defaultHomeWorkspace;
+ }
+
+ protected String getGroupsWorkspace() {
+ // TODO base on JAAS Subject metadata
+ return defaultGroupsWorkspace;
+ }
+
+// protected String getGuestsWorkspace() {
+// // TODO base on JAAS Subject metadata
+// return defaultGuestsWorkspace;
+// }
+
+ @Override
+ protected void processNewSession(Session session, String workspaceName) {
+ String username = session.getUserID();
+ if (username == null || username.toString().equals(""))
+ return;
+ if (session.getUserID().equals(CmsConstants.ROLE_ANONYMOUS))
+ return;
+
+ String userHomeWorkspace = getUserHomeWorkspace();
+ if (workspaceName == null || !workspaceName.equals(userHomeWorkspace))
+ return;
+
+ if (checkedUsers.contains(username))
+ return;
+ Session adminSession = KernelUtils.openAdminSession(getRepository(workspaceName), workspaceName);
+ try {
+ syncJcr(adminSession, username);
+ checkedUsers.add(username);
+ } finally {
+ JcrUtils.logoutQuietly(adminSession);
+ }
+ }
+
+ /*
+ * JCR
+ */
+ /** Session is logged out. */
+ private void initJcr(Session adminSession) {
+ try {
+// JcrUtils.mkdirs(adminSession, homeBasePath);
+// JcrUtils.mkdirs(adminSession, groupsBasePath);
+ adminSession.save();
+
+// JcrUtils.addPrivilege(adminSession, homeBasePath, NodeConstants.ROLE_USER_ADMIN, Privilege.JCR_READ);
+// JcrUtils.addPrivilege(adminSession, groupsBasePath, NodeConstants.ROLE_USER_ADMIN, Privilege.JCR_READ);
+ adminSession.save();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot initialize home repository", e);
+ } finally {
+ JcrUtils.logoutQuietly(adminSession);
+ }
+ }
+
+ protected synchronized void syncJcr(Session adminSession, String username) {
+ // only in the default workspace
+// if (workspaceName != null)
+// return;
+ // skip system users
+ if (username.endsWith(CmsConstants.SYSTEM_ROLES_BASEDN))
+ return;
+
+ try {
+ Node userHome = CmsJcrUtils.getUserHome(adminSession, username);
+ if (userHome == null) {
+// String homePath = generateUserPath(username);
+ String userId = extractUserId(username);
+// if (adminSession.itemExists(homePath))// duplicate user id
+// userHome = adminSession.getNode(homePath).getParent().addNode(JcrUtils.lastPathElement(homePath));
+// else
+// userHome = JcrUtils.mkdirs(adminSession, homePath);
+ userHome = adminSession.getRootNode().addNode(userId);
+// userHome.addMixin(NodeTypes.NODE_USER_HOME);
+ userHome.addMixin(NodeType.MIX_CREATED);
+ userHome.addMixin(NodeType.MIX_TITLE);
+ userHome.setProperty(Property.JCR_ID, username);
+ // TODO use display name
+ userHome.setProperty(Property.JCR_TITLE, userId);
+// userHome.setProperty(NodeNames.LDAP_UID, username);
+ adminSession.save();
+
+ JcrUtils.clearAccessControList(adminSession, userHome.getPath(), username);
+ JcrUtils.addPrivilege(adminSession, userHome.getPath(), username, Privilege.JCR_ALL);
+// JackrabbitSecurityUtils.denyPrivilege(adminSession, userHome.getPath(), NodeConstants.ROLE_USER,
+// Privilege.JCR_READ);
+ }
+ if (adminSession.hasPendingChanges())
+ adminSession.save();
+ } catch (RepositoryException e) {
+ JcrUtils.discardQuietly(adminSession);
+ throw new JcrException("Cannot sync node security model for " + username, e);
+ }
+ }
+
+ /** Generate path for a new user home */
+ private String generateUserPath(String username) {
+ LdapName dn;
+ try {
+ dn = new LdapName(username);
+ } catch (InvalidNameException e) {
+ throw new IllegalArgumentException("Invalid name " + username, e);
+ }
+ String userId = dn.getRdn(dn.size() - 1).getValue().toString();
+ return '/' + userId;
+// int atIndex = userId.indexOf('@');
+// if (atIndex < 0) {
+// return homeBasePath+'/' + userId;
+// } else {
+// return usersBasePath + '/' + usersDatePath.format(new Date()) + '/' + userId;
+// }
+ }
+
+ private String extractUserId(String username) {
+ LdapName dn;
+ try {
+ dn = new LdapName(username);
+ } catch (InvalidNameException e) {
+ throw new IllegalArgumentException("Invalid name " + username, e);
+ }
+ String userId = dn.getRdn(dn.size() - 1).getValue().toString();
+ return userId;
+// int atIndex = userId.indexOf('@');
+// if (atIndex < 0) {
+// return homeBasePath+'/' + userId;
+// } else {
+// return usersBasePath + '/' + usersDatePath.format(new Date()) + '/' + userId;
+// }
+ }
+
+ public void createWorkgroup(LdapName dn) {
+ String groupsWorkspace = getGroupsWorkspace();
+ Session adminSession = KernelUtils.openAdminSession(getRepository(groupsWorkspace), groupsWorkspace);
+ String cn = dn.getRdn(dn.size() - 1).getValue().toString();
+ Node newWorkgroup = CmsJcrUtils.getGroupHome(adminSession, cn);
+ if (newWorkgroup != null) {
+ JcrUtils.logoutQuietly(adminSession);
+ throw new IllegalStateException("Workgroup " + newWorkgroup + " already exists for " + dn);
+ }
+ try {
+ // TODO enhance transformation of cn to a valid node name
+ // String relPath = cn.replaceAll("[^a-zA-Z0-9]", "_");
+ String relPath = JcrUtils.replaceInvalidChars(cn);
+ newWorkgroup = adminSession.getRootNode().addNode(relPath, NodeType.NT_UNSTRUCTURED);
+// newWorkgroup = JcrUtils.mkdirs(adminSession.getNode(groupsBasePath), relPath, NodeType.NT_UNSTRUCTURED);
+// newWorkgroup.addMixin(NodeTypes.NODE_GROUP_HOME);
+ newWorkgroup.addMixin(NodeType.MIX_CREATED);
+ newWorkgroup.addMixin(NodeType.MIX_TITLE);
+ newWorkgroup.setProperty(Property.JCR_ID, dn.toString());
+ newWorkgroup.setProperty(Property.JCR_TITLE, cn);
+// newWorkgroup.setProperty(NodeNames.LDAP_CN, cn);
+ adminSession.save();
+ JcrUtils.addPrivilege(adminSession, newWorkgroup.getPath(), dn.toString(), Privilege.JCR_ALL);
+ adminSession.save();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot create workgroup", e);
+ } finally {
+ JcrUtils.logoutQuietly(adminSession);
+ }
+
+ }
+
+ public boolean isRemote() {
+ return remote;
+ }
+
+}
--- /dev/null
+package org.argeo.cms.jcr.internal;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.jackrabbit.core.RepositoryImpl;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.api.cms.CmsLog;
+
+class JackrabbitLocalRepository extends LocalRepository {
+ private final static CmsLog log = CmsLog.getLog(JackrabbitLocalRepository.class);
+ final String SECURITY_WORKSPACE = "security";
+
+ private Map<String, CmsWorkspaceIndexer> workspaceMonitors = new TreeMap<>();
+
+ public JackrabbitLocalRepository(RepositoryImpl repository, String cn) {
+ super(repository, cn);
+// Session session = KernelUtils.openAdminSession(repository);
+// try {
+// if (NodeConstants.NODE.equals(cn))
+// for (String workspaceName : session.getWorkspace().getAccessibleWorkspaceNames()) {
+// addMonitor(workspaceName);
+// }
+// } catch (RepositoryException e) {
+// throw new IllegalStateException(e);
+// } finally {
+// JcrUtils.logoutQuietly(session);
+// }
+ }
+
+ protected RepositoryImpl getJackrabbitrepository(String workspaceName) {
+ return (RepositoryImpl) getRepository(workspaceName);
+ }
+
+ @Override
+ protected synchronized void processNewSession(Session session, String workspaceName) {
+// String realWorkspaceName = session.getWorkspace().getName();
+// addMonitor(realWorkspaceName);
+ }
+
+ private void addMonitor(String realWorkspaceName) {
+ if (realWorkspaceName.equals(SECURITY_WORKSPACE))
+ return;
+ if (!CmsConstants.NODE_REPOSITORY.equals(getCn()))
+ return;
+
+ if (!workspaceMonitors.containsKey(realWorkspaceName)) {
+ try {
+ CmsWorkspaceIndexer workspaceMonitor = new CmsWorkspaceIndexer(
+ getJackrabbitrepository(realWorkspaceName), getCn(), realWorkspaceName);
+ workspaceMonitors.put(realWorkspaceName, workspaceMonitor);
+ workspaceMonitor.init();
+ if (log.isDebugEnabled())
+ log.debug("Registered " + workspaceMonitor);
+ } catch (RepositoryException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public void destroy() {
+ for (String workspaceName : workspaceMonitors.keySet()) {
+ workspaceMonitors.get(workspaceName).destroy();
+ }
+ }
+
+}
--- /dev/null
+package org.argeo.cms.jcr.internal;
+
+import java.io.ByteArrayInputStream;
+import java.io.CharArrayReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
+import java.security.SecureRandom;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.query.Query;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.ArgeoTypes;
+import org.argeo.cms.jcr.CmsJcrUtils;
+import org.argeo.cms.security.AbstractKeyring;
+import org.argeo.cms.security.PBEKeySpecCallback;
+import org.argeo.jcr.JcrException;
+import org.argeo.jcr.JcrUtils;
+
+/** JCR based implementation of a keyring */
+public class JcrKeyring extends AbstractKeyring implements ArgeoNames {
+ private final static CmsLog log = CmsLog.getLog(JcrKeyring.class);
+ /**
+ * Stronger with 256, but causes problem with Oracle JVM, force 128 in this case
+ */
+ public final static Long DEFAULT_SECRETE_KEY_LENGTH = 256l;
+ public final static String DEFAULT_SECRETE_KEY_FACTORY = "PBKDF2WithHmacSHA1";
+ public final static String DEFAULT_SECRETE_KEY_ENCRYPTION = "AES";
+ public final static String DEFAULT_CIPHER_NAME = "AES/CBC/PKCS5Padding";
+
+ private Integer iterationCountFactor = 200;
+ private Long secretKeyLength = DEFAULT_SECRETE_KEY_LENGTH;
+ private String secretKeyFactoryName = DEFAULT_SECRETE_KEY_FACTORY;
+ private String secretKeyEncryption = DEFAULT_SECRETE_KEY_ENCRYPTION;
+ private String cipherName = DEFAULT_CIPHER_NAME;
+
+ private final Repository repository;
+ // TODO remove thread local session ; open a session each time
+ private ThreadLocal<Session> sessionThreadLocal = new ThreadLocal<Session>() {
+
+ @Override
+ protected Session initialValue() {
+ return login();
+ }
+
+ };
+
+ // FIXME is it really still needed?
+ /**
+ * When setup is called the session has not yet been saved and we don't want to
+ * save it since there maybe other data which would be inconsistent. So we keep
+ * a reference to this node which will then be used (an reset to null) when
+ * handling the PBE callback. We keep one per thread in case multiple users are
+ * accessing the same instance of a keyring.
+ */
+ // private ThreadLocal<Node> notYetSavedKeyring = new ThreadLocal<Node>() {
+ //
+ // @Override
+ // protected Node initialValue() {
+ // return null;
+ // }
+ // };
+
+ public JcrKeyring(Repository repository) {
+ this.repository = repository;
+ }
+
+ private Session session() {
+ Session session = this.sessionThreadLocal.get();
+ if (!session.isLive()) {
+ session = login();
+ sessionThreadLocal.set(session);
+ }
+ return session;
+ }
+
+ private Session login() {
+ try {
+ return repository.login(CmsConstants.HOME_WORKSPACE);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot login key ring session", e);
+ }
+ }
+
+ @Override
+ protected synchronized Boolean isSetup() {
+ Session session = null;
+ try {
+ // if (notYetSavedKeyring.get() != null)
+ // return true;
+ session = session();
+ session.refresh(true);
+ Node userHome = CmsJcrUtils.getUserHome(session);
+ return userHome.hasNode(ARGEO_KEYRING);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot check whether keyring is setup", e);
+ } finally {
+ JcrUtils.logoutQuietly(session);
+ }
+ }
+
+ @Override
+ protected synchronized void setup(char[] password) {
+ Binary binary = null;
+ // InputStream in = null;
+ try {
+ session().refresh(true);
+ Node userHome = CmsJcrUtils.getUserHome(session());
+ Node keyring;
+ if (userHome.hasNode(ARGEO_KEYRING)) {
+ throw new IllegalArgumentException("Keyring already set up");
+ } else {
+ keyring = userHome.addNode(ARGEO_KEYRING);
+ }
+ keyring.addMixin(ArgeoTypes.ARGEO_PBE_SPEC);
+
+ // deterministic salt and iteration count based on username
+ String username = session().getUserID();
+ byte[] salt = new byte[8];
+ byte[] usernameBytes = username.getBytes(StandardCharsets.UTF_8);
+ for (int i = 0; i < salt.length; i++) {
+ if (i < usernameBytes.length)
+ salt[i] = usernameBytes[i];
+ else
+ salt[i] = 0;
+ }
+ try (InputStream in = new ByteArrayInputStream(salt);) {
+ binary = session().getValueFactory().createBinary(in);
+ keyring.setProperty(ARGEO_SALT, binary);
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot set keyring salt", e);
+ }
+
+ Integer iterationCount = username.length() * iterationCountFactor;
+ keyring.setProperty(ARGEO_ITERATION_COUNT, iterationCount);
+
+ // default algo
+ // TODO check if algo and key length are available, use DES if not
+ keyring.setProperty(ARGEO_SECRET_KEY_FACTORY, secretKeyFactoryName);
+ keyring.setProperty(ARGEO_KEY_LENGTH, secretKeyLength);
+ keyring.setProperty(ARGEO_SECRET_KEY_ENCRYPTION, secretKeyEncryption);
+ keyring.setProperty(ARGEO_CIPHER, cipherName);
+
+ keyring.getSession().save();
+
+ // encrypted password hash
+ // IOUtils.closeQuietly(in);
+ // JcrUtils.closeQuietly(binary);
+ // byte[] btPass = hash(password, salt, iterationCount);
+ // in = new ByteArrayInputStream(btPass);
+ // binary = session().getValueFactory().createBinary(in);
+ // keyring.setProperty(ARGEO_PASSWORD, binary);
+
+ // notYetSavedKeyring.set(keyring);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot setup keyring", e);
+ } finally {
+ JcrUtils.closeQuietly(binary);
+ // IOUtils.closeQuietly(in);
+ // JcrUtils.discardQuietly(session());
+ }
+ }
+
+ @Override
+ protected synchronized void handleKeySpecCallback(PBEKeySpecCallback pbeCallback) {
+ Session session = null;
+ try {
+ session = session();
+ session.refresh(true);
+ Node userHome = CmsJcrUtils.getUserHome(session);
+ Node keyring;
+ if (userHome.hasNode(ARGEO_KEYRING))
+ keyring = userHome.getNode(ARGEO_KEYRING);
+ // else if (notYetSavedKeyring.get() != null)
+ // keyring = notYetSavedKeyring.get();
+ else
+ throw new IllegalStateException("Keyring not setup");
+
+ pbeCallback.set(keyring.getProperty(ARGEO_SECRET_KEY_FACTORY).getString(),
+ JcrUtils.getBinaryAsBytes(keyring.getProperty(ARGEO_SALT)),
+ (int) keyring.getProperty(ARGEO_ITERATION_COUNT).getLong(),
+ (int) keyring.getProperty(ARGEO_KEY_LENGTH).getLong(),
+ keyring.getProperty(ARGEO_SECRET_KEY_ENCRYPTION).getString());
+
+ // if (notYetSavedKeyring.get() != null)
+ // notYetSavedKeyring.remove();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot handle key spec callback", e);
+ } finally {
+ JcrUtils.logoutQuietly(session);
+ }
+ }
+
+ /** The parent node must already exist at this path. */
+ @Override
+ protected synchronized void encrypt(String path, InputStream unencrypted) {
+ // should be called first for lazy initialization
+ SecretKey secretKey = getSecretKey(null);
+ Cipher cipher = createCipher();
+
+ // Binary binary = null;
+ // InputStream in = null;
+ try {
+ session().refresh(true);
+ Node node;
+ if (!session().nodeExists(path)) {
+ String parentPath = JcrUtils.parentPath(path);
+ if (!session().nodeExists(parentPath))
+ throw new IllegalStateException("No parent node of " + path);
+ Node parentNode = session().getNode(parentPath);
+ node = parentNode.addNode(JcrUtils.nodeNameFromPath(path));
+ } else {
+ node = session().getNode(path);
+ }
+ encrypt(secretKey, cipher, node, unencrypted);
+ // node.addMixin(ArgeoTypes.ARGEO_ENCRYPTED);
+ // SecureRandom random = new SecureRandom();
+ // byte[] iv = new byte[16];
+ // random.nextBytes(iv);
+ // cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
+ // JcrUtils.setBinaryAsBytes(node, ARGEO_IV, iv);
+ //
+ // try (InputStream in = new CipherInputStream(unencrypted, cipher);) {
+ // binary = session().getValueFactory().createBinary(in);
+ // node.setProperty(Property.JCR_DATA, binary);
+ // session().save();
+ // }
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot encrypt", e);
+ } finally {
+ try {
+ unencrypted.close();
+ } catch (IOException e) {
+ // silent
+ }
+ // IOUtils.closeQuietly(unencrypted);
+ // IOUtils.closeQuietly(in);
+ // JcrUtils.closeQuietly(binary);
+ JcrUtils.logoutQuietly(session());
+ }
+ }
+
+ protected synchronized void encrypt(SecretKey secretKey, Cipher cipher, Node node, InputStream unencrypted) {
+ try {
+ node.addMixin(ArgeoTypes.ARGEO_ENCRYPTED);
+ SecureRandom random = new SecureRandom();
+ byte[] iv = new byte[16];
+ random.nextBytes(iv);
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
+ JcrUtils.setBinaryAsBytes(node, ARGEO_IV, iv);
+
+ Binary binary = null;
+ try (InputStream in = new CipherInputStream(unencrypted, cipher);) {
+ binary = session().getValueFactory().createBinary(in);
+ node.setProperty(Property.JCR_DATA, binary);
+ session().save();
+ } finally {
+ JcrUtils.closeQuietly(binary);
+ }
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot encrypt", e);
+ } catch (GeneralSecurityException | IOException e) {
+ throw new RuntimeException("Cannot encrypt", e);
+ }
+ }
+
+ @Override
+ protected synchronized InputStream decrypt(String path) {
+ Binary binary = null;
+ try {
+ session().refresh(true);
+ if (!session().nodeExists(path)) {
+ char[] password = ask();
+ Reader reader = new CharArrayReader(password);
+ return new ByteArrayInputStream(IOUtils.toByteArray(reader, StandardCharsets.UTF_8));
+ } else {
+ // should be called first for lazy initialisation
+ SecretKey secretKey = getSecretKey(null);
+ Cipher cipher = createCipher();
+ Node node = session().getNode(path);
+ return decrypt(secretKey, cipher, node);
+ }
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot decrypt", e);
+ } catch (GeneralSecurityException | IOException e) {
+ throw new RuntimeException("Cannot decrypt", e);
+ } finally {
+ JcrUtils.closeQuietly(binary);
+ JcrUtils.logoutQuietly(session());
+ }
+ }
+
+ protected synchronized InputStream decrypt(SecretKey secretKey, Cipher cipher, Node node)
+ throws RepositoryException, GeneralSecurityException {
+ if (node.hasProperty(ARGEO_IV)) {
+ byte[] iv = JcrUtils.getBinaryAsBytes(node.getProperty(ARGEO_IV));
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
+ } else {
+ cipher.init(Cipher.DECRYPT_MODE, secretKey);
+ }
+
+ Binary binary = node.getProperty(Property.JCR_DATA).getBinary();
+ InputStream encrypted = binary.getStream();
+ return new CipherInputStream(encrypted, cipher);
+ }
+
+ protected Cipher createCipher() {
+ try {
+ Node userHome = CmsJcrUtils.getUserHome(session());
+ if (!userHome.hasNode(ARGEO_KEYRING))
+ throw new IllegalArgumentException("Keyring not setup");
+ Node keyring = userHome.getNode(ARGEO_KEYRING);
+ String cipherName = keyring.getProperty(ARGEO_CIPHER).getString();
+ Provider securityProvider = getSecurityProvider();
+ Cipher cipher;
+ if (securityProvider == null)// TODO use BC?
+ cipher = Cipher.getInstance(cipherName);
+ else
+ cipher = Cipher.getInstance(cipherName, securityProvider);
+ return cipher;
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ throw new IllegalArgumentException("Cannot get cipher", e);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get cipher", e);
+ } finally {
+
+ }
+ }
+
+ public synchronized void changePassword(char[] oldPassword, char[] newPassword) {
+ // TODO make it XA compatible
+ SecretKey oldSecretKey = getSecretKey(oldPassword);
+ SecretKey newSecretKey = getSecretKey(newPassword);
+ Session session = session();
+ try {
+ NodeIterator encryptedNodes = session.getWorkspace().getQueryManager()
+ .createQuery("select * from [argeo:encrypted]", Query.JCR_SQL2).execute().getNodes();
+ while (encryptedNodes.hasNext()) {
+ Node node = encryptedNodes.nextNode();
+ InputStream in = decrypt(oldSecretKey, createCipher(), node);
+ encrypt(newSecretKey, createCipher(), node, in);
+ if (log.isDebugEnabled())
+ log.debug("Converted keyring encrypted value of " + node.getPath());
+ }
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException("Cannot change JCR keyring password", e);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot change JCR keyring password", e);
+ } finally {
+ JcrUtils.logoutQuietly(session);
+ }
+ }
+
+ // public synchronized void setSession(Session session) {
+ // this.session = session;
+ // }
+
+ public void setIterationCountFactor(Integer iterationCountFactor) {
+ this.iterationCountFactor = iterationCountFactor;
+ }
+
+ public void setSecretKeyLength(Long keyLength) {
+ this.secretKeyLength = keyLength;
+ }
+
+ public void setSecretKeyFactoryName(String secreteKeyFactoryName) {
+ this.secretKeyFactoryName = secreteKeyFactoryName;
+ }
+
+ public void setSecretKeyEncryption(String secreteKeyEncryption) {
+ this.secretKeyEncryption = secreteKeyEncryption;
+ }
+
+ public void setCipherName(String cipherName) {
+ this.cipherName = cipherName;
+ }
+
+}
\ No newline at end of file
--- /dev/null
+package org.argeo.cms.jcr.internal;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+
+import org.apache.jackrabbit.jcr2dav.Jcr2davRepositoryFactory;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.internal.jcr.RepoConf;
+import org.argeo.cms.jcr.internal.osgi.CmsJcrActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+
+/**
+ * OSGi-aware Jackrabbit repository factory which can retrieve/publish
+ * {@link Repository} as OSGi services.
+ */
+public class JcrRepositoryFactory implements RepositoryFactory {
+ private final CmsLog log = CmsLog.getLog(getClass());
+// private final BundleContext bundleContext = FrameworkUtil.getBundle(getClass()).getBundleContext();
+
+ // private Resource fileRepositoryConfiguration = new ClassPathResource(
+ // "/org/argeo/cms/internal/kernel/repository-localfs.xml");
+
+ protected Repository getRepositoryByAlias(String alias) {
+ BundleContext bundleContext = CmsJcrActivator.getBundleContext();
+ if (bundleContext != null) {
+ try {
+ Collection<ServiceReference<Repository>> srs = bundleContext.getServiceReferences(Repository.class,
+ "(" + CmsConstants.CN + "=" + alias + ")");
+ if (srs.size() == 0)
+ throw new IllegalArgumentException("No repository with alias " + alias + " found in OSGi registry");
+ else if (srs.size() > 1)
+ throw new IllegalArgumentException(
+ srs.size() + " repositories with alias " + alias + " found in OSGi registry");
+ return bundleContext.getService(srs.iterator().next());
+ } catch (InvalidSyntaxException e) {
+ throw new IllegalArgumentException("Cannot find repository with alias " + alias, e);
+ }
+ } else {
+ // TODO ability to filter static services
+ return null;
+ }
+ }
+
+ // private void publish(String alias, Repository repository, Properties
+ // properties) {
+ // if (bundleContext != null) {
+ // // do not modify reference
+ // Hashtable<String, String> props = new Hashtable<String, String>();
+ // props.putAll(props);
+ // props.put(JCR_REPOSITORY_ALIAS, alias);
+ // bundleContext.registerService(Repository.class.getName(), repository,
+ // props);
+ // }
+ // }
+
+ @SuppressWarnings({ "rawtypes" })
+ public Repository getRepository(Map parameters) throws RepositoryException {
+ // // check if can be found by alias
+ // Repository repository = super.getRepository(parameters);
+ // if (repository != null)
+ // return repository;
+
+ // check if remote
+ Repository repository;
+ String uri = null;
+ if (parameters.containsKey(RepoConf.labeledUri.name()))
+ uri = parameters.get(CmsConstants.LABELED_URI).toString();
+ else if (parameters.containsKey(KernelConstants.JACKRABBIT_REPOSITORY_URI))
+ uri = parameters.get(KernelConstants.JACKRABBIT_REPOSITORY_URI).toString();
+
+ if (uri != null) {
+ if (uri.startsWith("http")) {// http, https
+ Object defaultWorkspace = parameters.get(RepoConf.defaultWorkspace.name());
+ repository = createRemoteRepository(uri, defaultWorkspace != null ? defaultWorkspace.toString() : null);
+ } else if (uri.startsWith("file"))// http, https
+ repository = createFileRepository(uri, parameters);
+ else if (uri.startsWith("vm")) {
+ // log.warn("URI " + uri + " should have been managed by generic
+ // JCR repository factory");
+ repository = getRepositoryByAlias(getAliasFromURI(uri));
+ } else
+ throw new IllegalArgumentException("Unrecognized URI format " + uri);
+
+ }
+
+ else if (parameters.containsKey(CmsConstants.CN)) {
+ // Properties properties = new Properties();
+ // properties.putAll(parameters);
+ String alias = parameters.get(CmsConstants.CN).toString();
+ // publish(alias, repository, properties);
+ // log.info("Registered JCR repository under alias '" + alias + "'
+ // with properties " + properties);
+ repository = getRepositoryByAlias(alias);
+ } else
+ throw new IllegalArgumentException("Not enough information in " + parameters);
+
+ if (repository == null)
+ throw new IllegalArgumentException("Repository not found " + parameters);
+
+ return repository;
+ }
+
+ protected Repository createRemoteRepository(String uri, String defaultWorkspace) throws RepositoryException {
+ Map<String, String> params = new HashMap<String, String>();
+ params.put(KernelConstants.JACKRABBIT_REPOSITORY_URI, uri);
+ if (defaultWorkspace != null)
+ params.put(KernelConstants.JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, defaultWorkspace);
+ Repository repository = new Jcr2davRepositoryFactory().getRepository(params);
+ if (repository == null)
+ throw new IllegalArgumentException("Remote Davex repository " + uri + " not found");
+ log.info("Initialized remote Jackrabbit repository from uri " + uri);
+ return repository;
+ }
+
+ @SuppressWarnings({ "rawtypes" })
+ protected Repository createFileRepository(final String uri, Map parameters) throws RepositoryException {
+ throw new UnsupportedOperationException();
+ // InputStream configurationIn = null;
+ // try {
+ // Properties vars = new Properties();
+ // vars.putAll(parameters);
+ // String dirPath = uri.substring("file:".length());
+ // File homeDir = new File(dirPath);
+ // if (homeDir.exists() && !homeDir.isDirectory())
+ // throw new ArgeoJcrException("Repository home " + dirPath + " is not a
+ // directory");
+ // if (!homeDir.exists())
+ // homeDir.mkdirs();
+ // configurationIn = fileRepositoryConfiguration.getInputStream();
+ // vars.put(RepositoryConfigurationParser.REPOSITORY_HOME_VARIABLE,
+ // homeDir.getCanonicalPath());
+ // RepositoryConfig repositoryConfig = RepositoryConfig.create(new
+ // InputSource(configurationIn), vars);
+ //
+ // // TransientRepository repository = new
+ // // TransientRepository(repositoryConfig);
+ // final RepositoryImpl repository =
+ // RepositoryImpl.create(repositoryConfig);
+ // Session session = repository.login();
+ // // FIXME make it generic
+ // org.argeo.jcr.JcrUtils.addPrivilege(session, "/", "ROLE_ADMIN",
+ // "jcr:all");
+ // org.argeo.jcr.JcrUtils.logoutQuietly(session);
+ // Runtime.getRuntime().addShutdownHook(new Thread("Clean JCR repository
+ // " + uri) {
+ // public void run() {
+ // repository.shutdown();
+ // log.info("Destroyed repository " + uri);
+ // }
+ // });
+ // log.info("Initialized file Jackrabbit repository from uri " + uri);
+ // return repository;
+ // } catch (Exception e) {
+ // throw new ArgeoJcrException("Cannot create repository " + uri, e);
+ // } finally {
+ // IOUtils.closeQuietly(configurationIn);
+ // }
+ }
+
+ protected String getAliasFromURI(String uri) {
+ try {
+ URI uriObj = new URI(uri);
+ String alias = uriObj.getPath();
+ if (alias.charAt(0) == '/')
+ alias = alias.substring(1);
+ if (alias.charAt(alias.length() - 1) == '/')
+ alias = alias.substring(0, alias.length() - 1);
+ return alias;
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Cannot interpret URI " + uri, e);
+ }
+ }
+
+ /**
+ * Called after the repository has been initialised. Does nothing by default.
+ */
+ @SuppressWarnings("rawtypes")
+ protected void postInitialization(Repository repository, Map parameters) {
+
+ }
+}
--- /dev/null
+package org.argeo.cms.jcr.internal;
+
+import org.argeo.api.cms.CmsConstants;
+
+/** Internal CMS constants. */
+@Deprecated
+public interface KernelConstants {
+ // Directories
+ String DIR_NODE = "node";
+ String DIR_REPOS = "repos";
+ String DIR_INDEXES = "indexes";
+ String DIR_TRANSACTIONS = "transactions";
+
+ // Files
+ String DEPLOY_CONFIG_PATH = DIR_NODE + '/' + CmsConstants.DEPLOY_BASEDN + ".ldif";
+ String DEFAULT_KEYSTORE_PATH = DIR_NODE + '/' + CmsConstants.NODE + ".p12";
+ String DEFAULT_PEM_KEY_PATH = DIR_NODE + '/' + CmsConstants.NODE + ".key";
+ String DEFAULT_PEM_CERT_PATH = DIR_NODE + '/' + CmsConstants.NODE + ".crt";
+ String NODE_KEY_TAB_PATH = DIR_NODE + "/krb5.keytab";
+
+ // Security
+ String JAAS_CONFIG = "/org/argeo/cms/internal/kernel/jaas.cfg";
+ String JAAS_CONFIG_IPA = "/org/argeo/cms/internal/kernel/jaas-ipa.cfg";
+
+ // Java
+ String JAAS_CONFIG_PROP = "java.security.auth.login.config";
+
+ // DEFAULTS JCR PATH
+ String DEFAULT_HOME_BASE_PATH = "/home";
+ String DEFAULT_USERS_BASE_PATH = "/users";
+ String DEFAULT_GROUPS_BASE_PATH = "/groups";
+
+ // KERBEROS
+ String DEFAULT_KERBEROS_SERVICE = "HTTP";
+
+ // HTTP client
+ String COOKIE_POLICY_BROWSER_COMPATIBILITY = "compatibility";
+
+ // RWT / RAP
+ // String PATH_WORKBENCH = "/ui";
+ // String PATH_WORKBENCH_PUBLIC = PATH_WORKBENCH + "/public";
+
+ String JETTY_FACTORY_PID = "org.eclipse.equinox.http.jetty.config";
+ String WHITEBOARD_PATTERN_PROP = "osgi.http.whiteboard.servlet.pattern";
+ // default Jetty server configured via JettyConfigurator
+ String DEFAULT_JETTY_SERVER = "default";
+ String CMS_JETTY_CUSTOMIZER_CLASS = "org.argeo.equinox.jetty.CmsJettyCustomizer";
+
+ // avoid dependencies
+ String CONTEXT_NAME_PROP = "contextName";
+ String JACKRABBIT_REPOSITORY_URI = "org.apache.jackrabbit.repository.uri";
+ String JACKRABBIT_REMOTE_DEFAULT_WORKSPACE = "org.apache.jackrabbit.spi2davex.WorkspaceNameDefault";
+}
--- /dev/null
+package org.argeo.cms.jcr.internal;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.PrivilegedAction;
+import java.security.URIParameter;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.Properties;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.argeo.api.cms.CmsAuth;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.jcr.internal.osgi.CmsJcrActivator;
+import org.argeo.cms.osgi.DataModelNamespace;
+import org.osgi.framework.BundleContext;
+import org.osgi.util.tracker.ServiceTracker;
+
+/** Package utilities */
+class KernelUtils implements KernelConstants {
+ final static String OSGI_INSTANCE_AREA = "osgi.instance.area";
+ final static String OSGI_CONFIGURATION_AREA = "osgi.configuration.area";
+
+ static void setJaasConfiguration(URL jaasConfigurationUrl) {
+ try {
+ URIParameter uriParameter = new URIParameter(jaasConfigurationUrl.toURI());
+ javax.security.auth.login.Configuration jaasConfiguration = javax.security.auth.login.Configuration
+ .getInstance("JavaLoginConfig", uriParameter);
+ javax.security.auth.login.Configuration.setConfiguration(jaasConfiguration);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Cannot set configuration " + jaasConfigurationUrl, e);
+ }
+ }
+
+ static Dictionary<String, ?> asDictionary(Properties props) {
+ Hashtable<String, Object> hashtable = new Hashtable<String, Object>();
+ for (Object key : props.keySet()) {
+ hashtable.put(key.toString(), props.get(key));
+ }
+ return hashtable;
+ }
+
+ static Dictionary<String, ?> asDictionary(ClassLoader cl, String resource) {
+ Properties props = new Properties();
+ try {
+ props.load(cl.getResourceAsStream(resource));
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Cannot load " + resource + " from classpath", e);
+ }
+ return asDictionary(props);
+ }
+
+ static File getExecutionDir(String relativePath) {
+ File executionDir = new File(getFrameworkProp("user.dir"));
+ if (relativePath == null)
+ return executionDir;
+ try {
+ return new File(executionDir, relativePath).getCanonicalFile();
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Cannot get canonical file", e);
+ }
+ }
+
+ static File getOsgiInstanceDir() {
+ return new File(getBundleContext().getProperty(OSGI_INSTANCE_AREA).substring("file:".length()))
+ .getAbsoluteFile();
+ }
+
+ static Path getOsgiInstancePath(String relativePath) {
+ return Paths.get(getOsgiInstanceUri(relativePath));
+ }
+
+ static URI getOsgiInstanceUri(String relativePath) {
+ String osgiInstanceBaseUri = getFrameworkProp(OSGI_INSTANCE_AREA);
+ if (osgiInstanceBaseUri != null)
+ return safeUri(osgiInstanceBaseUri + (relativePath != null ? relativePath : ""));
+ else
+ return Paths.get(System.getProperty("user.dir")).toUri();
+ }
+
+ static File getOsgiConfigurationFile(String relativePath) {
+ try {
+ return new File(new URI(getBundleContext().getProperty(OSGI_CONFIGURATION_AREA) + relativePath))
+ .getCanonicalFile();
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Cannot get configuration file for " + relativePath, e);
+ }
+ }
+
+ static String getFrameworkProp(String key, String def) {
+ BundleContext bundleContext = CmsJcrActivator.getBundleContext();
+ String value;
+ if (bundleContext != null)
+ value = bundleContext.getProperty(key);
+ else
+ value = System.getProperty(key);
+ if (value == null)
+ return def;
+ return value;
+ }
+
+ static String getFrameworkProp(String key) {
+ return getFrameworkProp(key, null);
+ }
+
+ // Security
+ // static Subject anonymousLogin() {
+ // Subject subject = new Subject();
+ // LoginContext lc;
+ // try {
+ // lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, subject);
+ // lc.login();
+ // return subject;
+ // } catch (LoginException e) {
+ // throw new CmsException("Cannot login as anonymous", e);
+ // }
+ // }
+
+ static void logFrameworkProperties(CmsLog log) {
+ BundleContext bc = getBundleContext();
+ for (Object sysProp : new TreeSet<Object>(System.getProperties().keySet())) {
+ log.debug(sysProp + "=" + bc.getProperty(sysProp.toString()));
+ }
+ // String[] keys = { Constants.FRAMEWORK_STORAGE,
+ // Constants.FRAMEWORK_OS_NAME, Constants.FRAMEWORK_OS_VERSION,
+ // Constants.FRAMEWORK_PROCESSOR, Constants.FRAMEWORK_SECURITY,
+ // Constants.FRAMEWORK_TRUST_REPOSITORIES,
+ // Constants.FRAMEWORK_WINDOWSYSTEM, Constants.FRAMEWORK_VENDOR,
+ // Constants.FRAMEWORK_VERSION, Constants.FRAMEWORK_STORAGE_CLEAN,
+ // Constants.FRAMEWORK_LANGUAGE, Constants.FRAMEWORK_UUID };
+ // for (String key : keys)
+ // log.debug(key + "=" + bc.getProperty(key));
+ }
+
+ static void printSystemProperties(PrintStream out) {
+ TreeMap<String, String> display = new TreeMap<>();
+ for (Object key : System.getProperties().keySet())
+ display.put(key.toString(), System.getProperty(key.toString()));
+ for (String key : display.keySet())
+ out.println(key + "=" + display.get(key));
+ }
+
+ static Session openAdminSession(Repository repository) {
+ return openAdminSession(repository, null);
+ }
+
+ static Session openAdminSession(final Repository repository, final String workspaceName) {
+ LoginContext loginContext = loginAsDataAdmin();
+ return Subject.doAs(loginContext.getSubject(), new PrivilegedAction<Session>() {
+
+ @Override
+ public Session run() {
+ try {
+ return repository.login(workspaceName);
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Cannot open admin session", e);
+ } finally {
+ try {
+ loginContext.logout();
+ } catch (LoginException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+
+ });
+ }
+
+ static LoginContext loginAsDataAdmin() {
+ ClassLoader currentCl = Thread.currentThread().getContextClassLoader();
+ Thread.currentThread().setContextClassLoader(KernelUtils.class.getClassLoader());
+ LoginContext loginContext;
+ try {
+ loginContext = new LoginContext(CmsAuth.LOGIN_CONTEXT_DATA_ADMIN);
+ loginContext.login();
+ } catch (LoginException e1) {
+ throw new IllegalStateException("Could not login as data admin", e1);
+ } finally {
+ Thread.currentThread().setContextClassLoader(currentCl);
+ }
+ return loginContext;
+ }
+
+ static void doAsDataAdmin(Runnable action) {
+ LoginContext loginContext = loginAsDataAdmin();
+ Subject.doAs(loginContext.getSubject(), new PrivilegedAction<Void>() {
+
+ @Override
+ public Void run() {
+ try {
+ action.run();
+ return null;
+ } finally {
+ try {
+ loginContext.logout();
+ } catch (LoginException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+
+ });
+ }
+
+ static void asyncOpen(ServiceTracker<?, ?> st) {
+ Runnable run = new Runnable() {
+
+ @Override
+ public void run() {
+ st.open();
+ }
+ };
+// Activator.getInternalExecutorService().execute(run);
+ new Thread(run, "Open service tracker " + st).start();
+ }
+
+ static BundleContext getBundleContext() {
+ return CmsJcrActivator.getBundleContext();
+ }
+
+ static boolean asBoolean(String value) {
+ if (value == null)
+ return false;
+ switch (value) {
+ case "true":
+ return true;
+ case "false":
+ return false;
+ default:
+ throw new IllegalArgumentException(
+ "Unsupported value for attribute " + DataModelNamespace.ABSTRACT + ": " + value);
+ }
+ }
+
+ private static URI safeUri(String uri) {
+ if (uri == null)
+ throw new IllegalArgumentException("URI cannot be null");
+ try {
+ return new URI(uri);
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Badly formatted URI " + uri, e);
+ }
+ }
+
+ private KernelUtils() {
+
+ }
+}
--- /dev/null
+package org.argeo.cms.jcr.internal;
+
+import javax.jcr.Repository;
+
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.jcr.JcrRepositoryWrapper;
+
+class LocalRepository extends JcrRepositoryWrapper {
+ private final String cn;
+
+ public LocalRepository(Repository repository, String cn) {
+ super(repository);
+ this.cn = cn;
+ // Map<String, Object> attrs = dataModelCapability.getAttributes();
+ // cn = (String) attrs.get(DataModelNamespace.NAME);
+ putDescriptor(CmsConstants.CN, cn);
+ }
+
+ String getCn() {
+ return cn;
+ }
+
+}
--- /dev/null
+package org.argeo.cms.jcr.internal;
+
+import java.util.Dictionary;
+
+import javax.jcr.Repository;
+
+import org.osgi.service.cm.ConfigurationException;
+import org.osgi.service.cm.ManagedService;
+
+class NodeKeyRing extends JcrKeyring implements ManagedService{
+
+ public NodeKeyRing(Repository repository) {
+ super(repository);
+ }
+
+ @Override
+ public void updated(Dictionary<String, ?> properties) throws ConfigurationException {
+ }
+}
--- /dev/null
+package org.argeo.cms.jcr.internal;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+
+import org.apache.jackrabbit.core.RepositoryContext;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.api.cms.CmsState;
+import org.argeo.cms.CmsDeployProperty;
+import org.argeo.cms.internal.jcr.RepoConf;
+import org.argeo.cms.internal.jcr.RepositoryBuilder;
+import org.argeo.cms.jcr.internal.osgi.CmsJcrActivator;
+import org.argeo.util.LangUtils;
+import org.osgi.service.cm.ManagedServiceFactory;
+
+/** A {@link ManagedServiceFactory} creating or referencing JCR repositories. */
+public class RepositoryContextsFactory {
+ private final static CmsLog log = CmsLog.getLog(RepositoryContextsFactory.class);
+ private final static String NODE_REPO_PROP_PREFIX = "argeo.node.repo.";
+// private final BundleContext bc = FrameworkUtil.getBundle(RepositoryServiceFactory.class).getBundleContext();
+
+// private Map<String, RepositoryContext> repositories = new HashMap<String, RepositoryContext>();
+// private Map<String, Object> pidToCn = new HashMap<String, Object>();
+
+ private RepositoryContext repositoryContext;
+
+ private CmsState cmsState;
+
+ public void init() {
+ Dictionary<String, Object> config = getNodeRepositoryConfig();
+ deployRepository(config);
+ }
+
+ public void destroy() {
+ if (this.repositoryContext != null) {
+ this.repositoryContext.getRepository().shutdown();
+ }
+// for (String pid : repositories.keySet()) {
+// try {
+// RepositoryContext repositoryContext = repositories.get(pid);
+// // Must start in another thread otherwise shutdown is interrupted
+// // TODO use an executor?
+// new Thread(() -> {
+// repositoryContext.getRepository().shutdown();
+// if (log.isDebugEnabled())
+// log.debug("Shut down repository " + pid
+// + (pidToCn.containsKey(pid) ? " (" + pidToCn.get(pid) + ")" : ""));
+// }, "Shutdown JCR repository " + pid).start();
+// } catch (Exception e) {
+// log.error("Error when shutting down Jackrabbit repository " + pid, e);
+// }
+// }
+ }
+
+// @Override
+// public String getName() {
+// return "Jackrabbit repository service factory";
+// }
+
+ /** Override the provided config with the framework properties */
+ private Dictionary<String, Object> getNodeRepositoryConfig() {
+ Dictionary<String, Object> props = new Hashtable<String, Object>();
+ addDeployProperty(CmsDeployProperty.DB_URL, RepoConf.dburl, props);
+ addDeployProperty(CmsDeployProperty.DB_USER, RepoConf.dbuser, props);
+ addDeployProperty(CmsDeployProperty.DB_PASSWORD, RepoConf.dbpassword, props);
+ for (RepoConf repoConf : RepoConf.values()) {
+ Object value = getFrameworkProp(NODE_REPO_PROP_PREFIX + repoConf.name());
+ if (value != null) {
+ props.put(repoConf.name(), value);
+ if (log.isDebugEnabled())
+ log.debug("Set node repo configuration " + repoConf.name() + " to " + value);
+ }
+ }
+ props.put(CmsConstants.CN, CmsConstants.NODE_REPOSITORY);
+ return props;
+ }
+
+ private void addDeployProperty(CmsDeployProperty deployProperty, RepoConf repoConf,
+ Dictionary<String, Object> props) {
+ String value = getFrameworkProp(deployProperty.getProperty());
+ if (value != null) {
+ props.put(repoConf.name(), value);
+ }
+
+ }
+
+// @Override
+// public void updated(String pid, Dictionary<String, ?> properties) throws ConfigurationException {
+ protected void deployRepository(Dictionary<String, Object> properties) {
+// if (repositories.containsKey(pid))
+// throw new IllegalArgumentException("Already a repository registered for " + pid);
+
+ if (properties == null)
+ return;
+
+ Object cn = properties.get(CmsConstants.CN);
+// if (cn != null)
+// for (String otherPid : pidToCn.keySet()) {
+// Object o = pidToCn.get(otherPid);
+// if (cn.equals(o)) {
+// RepositoryContext repositoryContext = repositories.remove(otherPid);
+// repositories.put(pid, repositoryContext);
+// if (log.isDebugEnabled())
+// log.debug("Ignoring update of Jackrabbit repository " + cn);
+// // FIXME perform a proper update (also of the OSGi service)
+// return;
+// }
+// }
+
+ try {
+ Object labeledUri = properties.get(RepoConf.labeledUri.name());
+ if (labeledUri == null) {
+ RepositoryBuilder repositoryBuilder = new RepositoryBuilder();
+ RepositoryContext repositoryContext = repositoryBuilder.createRepositoryContext(properties);
+// repositories.put(pid, repositoryContext);
+// Dictionary<String, Object> props = LangUtils.dict(Constants.SERVICE_PID, pid);
+ Dictionary<String, Object> props = new Hashtable<>();
+ // props.put(ArgeoJcrConstants.JCR_REPOSITORY_URI,
+ // properties.get(RepoConf.labeledUri.name()));
+ if (cn != null) {
+ props.put(CmsConstants.CN, cn);
+ // props.put(NodeConstants.JCR_REPOSITORY_ALIAS, cn);
+// pidToCn.put(pid, cn);
+ }
+ CmsJcrActivator.registerService(RepositoryContext.class, repositoryContext, props);
+ this.repositoryContext = repositoryContext;
+ } else {
+ Object defaultWorkspace = properties.get(RepoConf.defaultWorkspace.name());
+ if (defaultWorkspace == null)
+ defaultWorkspace = RepoConf.defaultWorkspace.getDefault();
+ URI uri = new URI(labeledUri.toString());
+// RepositoryFactory repositoryFactory = bc
+// .getService(bc.getServiceReference(RepositoryFactory.class));
+ RepositoryFactory repositoryFactory = CmsJcrActivator.getService(RepositoryFactory.class);
+ Map<String, String> parameters = new HashMap<String, String>();
+ parameters.put(RepoConf.labeledUri.name(), uri.toString());
+ parameters.put(RepoConf.defaultWorkspace.name(), defaultWorkspace.toString());
+ Repository repository = repositoryFactory.getRepository(parameters);
+ // Repository repository = NodeUtils.getRepositoryByUri(repositoryFactory,
+ // uri.toString());
+// Dictionary<String, Object> props = LangUtils.dict(Constants.SERVICE_PID, pid);
+ Dictionary<String, Object> props = new Hashtable<>();
+ props.put(RepoConf.labeledUri.name(),
+ new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), null, null)
+ .toString());
+ if (cn != null) {
+ props.put(CmsConstants.CN, cn);
+// pidToCn.put(pid, cn);
+ }
+ CmsJcrActivator.registerService(Repository.class, repository, props);
+
+ // home
+ if (cn.equals(CmsConstants.NODE_REPOSITORY)) {
+ Dictionary<String, Object> homeProps = LangUtils.dict(CmsConstants.CN, CmsConstants.EGO_REPOSITORY);
+ EgoRepository homeRepository = new EgoRepository(repository, true);
+ CmsJcrActivator.registerService(Repository.class, homeRepository, homeProps);
+ }
+ }
+ } catch (RepositoryException | URISyntaxException | IOException e) {
+ throw new IllegalStateException("Cannot create Jackrabbit repository " + properties, e);
+ }
+
+ }
+
+// @Override
+// public void deleted(String pid) {
+// RepositoryContext repositoryContext = repositories.remove(pid);
+// repositoryContext.getRepository().shutdown();
+// if (log.isDebugEnabled())
+// log.debug("Deleted repository " + pid);
+// }
+
+ private String getFrameworkProp(String key) {
+ return cmsState.getDeployProperty(key);
+ }
+
+ public void setCmsState(CmsState cmsState) {
+ this.cmsState = cmsState;
+ }
+
+}
--- /dev/null
+package org.argeo.cms.jcr.internal;
+
+import java.io.File;
+import java.lang.management.ManagementFactory;
+
+import org.apache.jackrabbit.api.stats.RepositoryStatistics;
+import org.apache.jackrabbit.stats.RepositoryStatisticsImpl;
+import org.argeo.api.cms.CmsLog;
+
+/**
+ * Background thread started by the kernel, which gather statistics and
+ * monitor/control other processes.
+ */
+public class StatisticsThread extends Thread {
+ private final static CmsLog log = CmsLog.getLog(StatisticsThread.class);
+
+ private RepositoryStatisticsImpl repoStats;
+
+ /** The smallest period of operation, in ms */
+ private final long PERIOD = 60 * 1000l;
+ /** One ms in ns */
+ private final static long m = 1000l * 1000l;
+ private final static long M = 1024l * 1024l;
+
+ private boolean running = true;
+
+ private CmsLog kernelStatsLog = CmsLog.getLog("argeo.stats.kernel");
+ private CmsLog nodeStatsLog = CmsLog.getLog("argeo.stats.node");
+
+ @SuppressWarnings("unused")
+ private long cycle = 0l;
+
+ public StatisticsThread(String name) {
+ super(name);
+ }
+
+ private void doSmallestPeriod() {
+ // Clean expired sessions
+ // FIXME re-enable it in CMS
+ //CmsSessionImpl.closeInvalidSessions();
+
+ if (kernelStatsLog.isDebugEnabled()) {
+ StringBuilder line = new StringBuilder(64);
+ line.append("§\t");
+ long freeMem = Runtime.getRuntime().freeMemory() / M;
+ long totalMem = Runtime.getRuntime().totalMemory() / M;
+ long maxMem = Runtime.getRuntime().maxMemory() / M;
+ double loadAvg = ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage();
+ // in min
+ boolean min = true;
+ long uptime = ManagementFactory.getRuntimeMXBean().getUptime() / (1000 * 60);
+ if (uptime > 24 * 60) {
+ min = false;
+ uptime = uptime / 60;
+ }
+ line.append(uptime).append(min ? " min" : " h").append('\t');
+ line.append(loadAvg).append('\t').append(maxMem).append('\t').append(totalMem).append('\t').append(freeMem)
+ .append('\t');
+ kernelStatsLog.debug(line);
+ }
+
+ if (nodeStatsLog.isDebugEnabled()) {
+ File dataDir = KernelUtils.getOsgiInstanceDir();
+ long freeSpace = dataDir.getUsableSpace() / M;
+ // File currentRoot = null;
+ // for (File root : File.listRoots()) {
+ // String rootPath = root.getAbsolutePath();
+ // if (dataDir.getAbsolutePath().startsWith(rootPath)) {
+ // if (currentRoot == null
+ // || (rootPath.length() > currentRoot.getPath()
+ // .length())) {
+ // currentRoot = root;
+ // }
+ // }
+ // }
+ // long totalSpace = currentRoot.getTotalSpace();
+ StringBuilder line = new StringBuilder(128);
+ line.append("§\t").append(freeSpace).append(" MB left in " + dataDir);
+ line.append('\n');
+ if (repoStats != null)
+ for (RepositoryStatistics.Type type : RepositoryStatistics.Type.values()) {
+ long[] vals = repoStats.getTimeSeries(type).getValuePerMinute();
+ long val = vals[vals.length - 1];
+ line.append(type.name()).append('\t').append(val).append('\n');
+ }
+ nodeStatsLog.debug(line);
+ }
+ }
+
+ @Override
+ public void run() {
+ if (log.isTraceEnabled())
+ log.trace("Kernel thread started.");
+ final long periodNs = PERIOD * m;
+ while (running) {
+ long beginNs = System.nanoTime();
+ doSmallestPeriod();
+
+ long waitNs = periodNs - (System.nanoTime() - beginNs);
+ if (waitNs < 0)
+ continue;
+ // wait
+ try {
+ sleep(waitNs / m, (int) (waitNs % m));
+ } catch (InterruptedException e) {
+ // silent
+ }
+ cycle++;
+ }
+ }
+
+ public synchronized void destroyAndJoin() {
+ running = false;
+ notifyAll();
+// interrupt();
+// try {
+// join(PERIOD * 2);
+// } catch (InterruptedException e) {
+// // throw new CmsException("Kernel thread destruction was interrupted");
+// log.error("Kernel thread destruction was interrupted", e);
+// }
+ }
+}
--- /dev/null
+package org.argeo.cms.jcr.internal.osgi;
+
+import java.util.Dictionary;
+
+import org.argeo.cms.jcr.internal.StatisticsThread;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+
+public class CmsJcrActivator implements BundleActivator {
+ private static BundleContext bundleContext;
+
+// private List<Runnable> stopHooks = new ArrayList<>();
+ private StatisticsThread kernelThread;
+
+// private JackrabbitRepositoryContextsFactory repositoryServiceFactory;
+// private CmsJcrDeployment jcrDeployment;
+
+ @Override
+ public void start(BundleContext context) throws Exception {
+ bundleContext = context;
+
+ // kernel thread
+ kernelThread = new StatisticsThread("Kernel Thread");
+ kernelThread.setContextClassLoader(getClass().getClassLoader());
+ kernelThread.start();
+
+ // JCR
+// repositoryServiceFactory = new JackrabbitRepositoryContextsFactory();
+//// stopHooks.add(() -> repositoryServiceFactory.shutdown());
+// registerService(ManagedServiceFactory.class, repositoryServiceFactory,
+// LangUtils.dict(Constants.SERVICE_PID, CmsConstants.NODE_REPOS_FACTORY_PID));
+
+// JcrRepositoryFactory repositoryFactory = new JcrRepositoryFactory();
+// registerService(RepositoryFactory.class, repositoryFactory, null);
+
+ // File System
+// CmsJcrFsProvider cmsFsProvider = new CmsJcrFsProvider();
+// ServiceLoader<FileSystemProvider> fspSl = ServiceLoader.load(FileSystemProvider.class);
+// for (FileSystemProvider fsp : fspSl) {
+// log.debug("FileSystemProvider " + fsp);
+// if (fsp instanceof CmsFsProvider) {
+// cmsFsProvider = (CmsFsProvider) fsp;
+// }
+// }
+// for (FileSystemProvider fsp : FileSystemProvider.installedProviders()) {
+// log.debug("Installed FileSystemProvider " + fsp);
+// }
+// registerService(FileSystemProvider.class, cmsFsProvider,
+// LangUtils.dict(Constants.SERVICE_PID, CmsConstants.NODE_FS_PROVIDER_PID));
+
+// jcrDeployment = new CmsJcrDeployment();
+// jcrDeployment.init();
+ }
+
+ @Override
+ public void stop(BundleContext context) throws Exception {
+// if (jcrDeployment != null)
+// jcrDeployment.destroy();
+
+// if (repositoryServiceFactory != null)
+// repositoryServiceFactory.shutdown();
+
+ if (kernelThread != null)
+ kernelThread.destroyAndJoin();
+
+ bundleContext = null;
+ }
+
+ @Deprecated
+ public static <T> void registerService(Class<T> clss, T service, Dictionary<String, ?> properties) {
+ if (bundleContext != null) {
+ bundleContext.registerService(clss, service, properties);
+ }
+
+ }
+
+ @Deprecated
+ public static BundleContext getBundleContext() {
+ return bundleContext;
+ }
+
+ @Deprecated
+ public static <T> T getService(Class<T> clss) {
+ if (bundleContext != null) {
+ return bundleContext.getService(bundleContext.getServiceReference(clss));
+ } else {
+ return null;
+ }
+ }
+
+}
--- /dev/null
+package org.argeo.cms.jcr.internal.servlet;
+
+import java.util.Map;
+
+import javax.jcr.Repository;
+
+import org.apache.jackrabbit.server.SessionProvider;
+import org.apache.jackrabbit.server.remoting.davex.JcrRemotingServlet;
+import org.argeo.api.cms.CmsConstants;
+
+/** A {@link JcrRemotingServlet} based on {@link CmsSessionProvider}. */
+public class CmsRemotingServlet extends JcrRemotingServlet {
+ private static final long serialVersionUID = 6459455509684213633L;
+ private Repository repository;
+ private SessionProvider sessionProvider;
+
+ public CmsRemotingServlet() {
+ }
+
+ public CmsRemotingServlet(String alias, Repository repository) {
+ this.repository = repository;
+ this.sessionProvider = new CmsSessionProvider(alias);
+ }
+
+ @Override
+ public Repository getRepository() {
+ return repository;
+ }
+
+ public void setRepository(Repository repository, Map<String, String> properties) {
+ this.repository = repository;
+ String alias = properties.get(CmsConstants.CN);
+ if (alias != null)
+ sessionProvider = new CmsSessionProvider(alias);
+ else
+ throw new IllegalArgumentException("Only aliased repositories are supported");
+ }
+
+ @Override
+ protected SessionProvider getSessionProvider() {
+ return sessionProvider;
+ }
+
+}
--- /dev/null
+package org.argeo.cms.jcr.internal.servlet;
+
+import java.io.Serializable;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.security.auth.Subject;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.jackrabbit.server.SessionProvider;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.api.cms.CmsSession;
+import org.argeo.cms.auth.RemoteAuthUtils;
+import org.argeo.cms.servlet.ServletHttpRequest;
+import org.argeo.jcr.JcrUtils;
+
+/**
+ * Implements an open session in view patter: a new JCR session is created for
+ * each request
+ */
+public class CmsSessionProvider implements SessionProvider, Serializable {
+ private static final long serialVersionUID = -1358136599534938466L;
+
+ private final static CmsLog log = CmsLog.getLog(CmsSessionProvider.class);
+
+ private final String alias;
+
+ private LinkedHashMap<Session, CmsDataSession> cmsSessions = new LinkedHashMap<>();
+
+ public CmsSessionProvider(String alias) {
+ this.alias = alias;
+ }
+
+ public Session getSession(HttpServletRequest request, Repository rep, String workspace)
+ throws javax.jcr.LoginException, ServletException, RepositoryException {
+
+ // a client is scanning parent URLs.
+// if (workspace == null)
+// return null;
+
+ CmsSession cmsSession = RemoteAuthUtils.getCmsSession(new ServletHttpRequest(request));
+ // CmsSessionImpl cmsSession = WebCmsSessionImpl.getCmsSession(request);
+ if (log.isTraceEnabled()) {
+ log.trace("Get JCR session from " + cmsSession);
+ }
+ if (cmsSession == null)
+ throw new IllegalStateException("Cannot find a session for request " + request.getRequestURI());
+ CmsDataSession cmsDataSession = new CmsDataSession(cmsSession);
+ Session session = cmsDataSession.getDataSession(alias, workspace, rep);
+ cmsSessions.put(session, cmsDataSession);
+ return session;
+ }
+
+ public void releaseSession(Session session) {
+// JcrUtils.logoutQuietly(session);
+ if (cmsSessions.containsKey(session)) {
+ CmsDataSession cmsDataSession = cmsSessions.get(session);
+ cmsDataSession.releaseDataSession(alias, session);
+ } else {
+ log.warn("JCR session " + session + " not found in CMS session list. Logging it out...");
+ JcrUtils.logoutQuietly(session);
+ }
+ }
+
+ static class CmsDataSession {
+ private CmsSession cmsSession;
+
+ private Map<String, Session> dataSessions = new HashMap<>();
+ private Set<String> dataSessionsInUse = new HashSet<>();
+ private Set<Session> additionalDataSessions = new HashSet<>();
+
+ private CmsDataSession(CmsSession cmsSession) {
+ this.cmsSession = cmsSession;
+ cmsSession.addOnCloseCallback((sess) -> close());
+ }
+
+ public Session newDataSession(String cn, String workspace, Repository repository) {
+ checkValid();
+ return login(repository, workspace);
+ }
+
+ public synchronized Session getDataSession(String cn, String workspace, Repository repository) {
+ checkValid();
+ // FIXME make it more robust
+ if (workspace == null)
+ workspace = CmsConstants.SYS_WORKSPACE;
+ String path = cn + '/' + workspace;
+ if (dataSessionsInUse.contains(path)) {
+ try {
+ wait(1000);
+ if (dataSessionsInUse.contains(path)) {
+ Session session = login(repository, workspace);
+ additionalDataSessions.add(session);
+ if (log.isTraceEnabled())
+ log.trace("Additional data session " + path + " for " + cmsSession.getUserDn());
+ return session;
+ }
+ } catch (InterruptedException e) {
+ // silent
+ }
+ }
+
+ Session session = null;
+ if (dataSessions.containsKey(path)) {
+ session = dataSessions.get(path);
+ } else {
+ session = login(repository, workspace);
+ dataSessions.put(path, session);
+ if (log.isTraceEnabled())
+ log.trace("New data session " + path + " for " + cmsSession.getUserDn());
+ }
+ dataSessionsInUse.add(path);
+ return session;
+ }
+
+ private Session login(Repository repository, String workspace) {
+ try {
+ return Subject.doAs(cmsSession.getSubject(), new PrivilegedExceptionAction<Session>() {
+ @Override
+ public Session run() throws Exception {
+ return repository.login(workspace);
+ }
+ });
+ } catch (PrivilegedActionException e) {
+ throw new IllegalStateException("Cannot log in " + cmsSession.getUserDn() + " to JCR", e);
+ }
+ }
+
+ public synchronized void releaseDataSession(String cn, Session session) {
+ if (additionalDataSessions.contains(session)) {
+ JcrUtils.logoutQuietly(session);
+ additionalDataSessions.remove(session);
+ if (log.isTraceEnabled())
+ log.trace("Remove additional data session " + session);
+ return;
+ }
+ String path = cn + '/' + session.getWorkspace().getName();
+ if (!dataSessionsInUse.contains(path))
+ log.warn("Data session " + path + " was not in use for " + cmsSession.getUserDn());
+ dataSessionsInUse.remove(path);
+ Session registeredSession = dataSessions.get(path);
+ if (session != registeredSession)
+ log.warn("Data session " + path + " not consistent for " + cmsSession.getUserDn());
+ if (log.isTraceEnabled())
+ log.trace("Released data session " + session + " for " + path);
+ notifyAll();
+ }
+
+ private void checkValid() {
+ if (!cmsSession.isValid())
+ throw new IllegalStateException(
+ "CMS session " + cmsSession.getUuid() + " is not valid since " + cmsSession.getEnd());
+ }
+
+ protected void close() {
+ synchronized (this) {
+ // TODO check data session in use ?
+ for (String path : dataSessions.keySet())
+ JcrUtils.logoutQuietly(dataSessions.get(path));
+ for (Session session : additionalDataSessions)
+ JcrUtils.logoutQuietly(session);
+ }
+ }
+ }
+}
--- /dev/null
+package org.argeo.cms.jcr.internal.servlet;
+
+import java.util.Map;
+
+import javax.jcr.Repository;
+
+import org.apache.jackrabbit.webdav.simple.SimpleWebdavServlet;
+import org.argeo.api.cms.CmsConstants;
+
+/** A {@link SimpleWebdavServlet} based on {@link CmsSessionProvider}. */
+public class CmsWebDavServlet extends SimpleWebdavServlet {
+ private static final long serialVersionUID = 7485800288686328063L;
+ private Repository repository;
+
+ public CmsWebDavServlet() {
+ }
+
+ public CmsWebDavServlet(String alias, Repository repository) {
+ this.repository = repository;
+ setSessionProvider(new CmsSessionProvider(alias));
+ }
+
+ @Override
+ public Repository getRepository() {
+ return repository;
+ }
+
+ public void setRepository(Repository repository, Map<String, String> properties) {
+ this.repository = repository;
+ String alias = properties.get(CmsConstants.CN);
+ if (alias != null)
+ setSessionProvider(new CmsSessionProvider(alias));
+ else
+ throw new IllegalArgumentException("Only aliased repositories are supported");
+ }
+
+}
--- /dev/null
+package org.argeo.cms.jcr.internal.servlet;
+
+import org.argeo.cms.servlet.CmsServletContext;
+
+/** Internal subclass, so that config resources can be loaded from our bundle. */
+public class DataServletContext extends CmsServletContext {
+
+}
--- /dev/null
+package org.argeo.cms.jcr.internal.servlet;
+
+import java.util.Enumeration;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.argeo.api.cms.CmsLog;
+
+public class JcrHttpUtils {
+ public final static String HEADER_AUTHORIZATION = "Authorization";
+ public final static String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate";
+
+ public final static String DEFAULT_PROTECTED_HANDLERS = "/org/argeo/cms/jcr/internal/servlet/protectedHandlers.xml";
+ public final static String WEBDAV_CONFIG = "/org/argeo/cms/jcr/internal/servlet/webdav-config.xml";
+
+ static boolean isBrowser(String userAgent) {
+ return userAgent.contains("webkit") || userAgent.contains("gecko") || userAgent.contains("firefox")
+ || userAgent.contains("msie") || userAgent.contains("chrome") || userAgent.contains("chromium")
+ || userAgent.contains("opera") || userAgent.contains("browser");
+ }
+
+ public static void logResponseHeaders(CmsLog log, HttpServletResponse response) {
+ if (!log.isDebugEnabled())
+ return;
+ for (String headerName : response.getHeaderNames()) {
+ Object headerValue = response.getHeader(headerName);
+ log.debug(headerName + ": " + headerValue);
+ }
+ }
+
+ public static void logRequestHeaders(CmsLog log, HttpServletRequest request) {
+ if (!log.isDebugEnabled())
+ return;
+ for (Enumeration<String> headerNames = request.getHeaderNames(); headerNames.hasMoreElements();) {
+ String headerName = headerNames.nextElement();
+ Object headerValue = request.getHeader(headerName);
+ log.debug(headerName + ": " + headerValue);
+ }
+ log.debug(request.getRequestURI() + "\n");
+ }
+
+ public static void logRequest(CmsLog log, HttpServletRequest request) {
+ log.debug("contextPath=" + request.getContextPath());
+ log.debug("servletPath=" + request.getServletPath());
+ log.debug("requestURI=" + request.getRequestURI());
+ log.debug("queryString=" + request.getQueryString());
+ StringBuilder buf = new StringBuilder();
+ // headers
+ Enumeration<String> en = request.getHeaderNames();
+ while (en.hasMoreElements()) {
+ String header = en.nextElement();
+ Enumeration<String> values = request.getHeaders(header);
+ while (values.hasMoreElements())
+ buf.append(" " + header + ": " + values.nextElement());
+ buf.append('\n');
+ }
+
+ // attributed
+ Enumeration<String> an = request.getAttributeNames();
+ while (an.hasMoreElements()) {
+ String attr = an.nextElement();
+ Object value = request.getAttribute(attr);
+ buf.append(" " + attr + ": " + value);
+ buf.append('\n');
+ }
+ log.debug("\n" + buf);
+ }
+
+ private JcrHttpUtils() {
+
+ }
+}
--- /dev/null
+package org.argeo.cms.jcr.internal.servlet;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.security.AccessControlContext;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.PropertyType;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.nodetype.NodeType;
+import javax.security.auth.Subject;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.jackrabbit.api.JackrabbitNode;
+import org.apache.jackrabbit.api.JackrabbitValue;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.integration.CmsExceptionsChain;
+import org.argeo.jcr.JcrUtils;
+import org.osgi.service.http.context.ServletContextHelper;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/** Access a JCR repository via web services. */
+public class JcrReadServlet extends HttpServlet {
+ private static final long serialVersionUID = 6536175260540484539L;
+ private final static CmsLog log = CmsLog.getLog(JcrReadServlet.class);
+
+ protected final static String ACCEPT_HTTP_HEADER = "Accept";
+ protected final static String CONTENT_DISPOSITION_HTTP_HEADER = "Content-Disposition";
+
+ protected final static String OCTET_STREAM_CONTENT_TYPE = "application/octet-stream";
+ protected final static String XML_CONTENT_TYPE = "application/xml";
+ protected final static String JSON_CONTENT_TYPE = "application/json";
+
+ private final static String PARAM_VERBOSE = "verbose";
+ private final static String PARAM_DEPTH = "depth";
+
+ protected final static String JCR_NODES = "jcr:nodes";
+ // cf. javax.jcr.Property
+ protected final static String JCR_PATH = "path";
+ protected final static String JCR_NAME = "name";
+
+ protected final static String _JCR = "_jcr";
+ protected final static String JCR_PREFIX = "jcr:";
+ protected final static String REP_PREFIX = "rep:";
+
+ private Repository repository;
+ private Integer maxDepth = 8;
+
+ private ObjectMapper objectMapper = new ObjectMapper();
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ if (log.isTraceEnabled())
+ log.trace("Data service: " + req.getPathInfo());
+
+ String dataWorkspace = getWorkspace(req);
+ String jcrPath = getJcrPath(req);
+
+ boolean verbose = req.getParameter(PARAM_VERBOSE) != null && !req.getParameter(PARAM_VERBOSE).equals("false");
+ int depth = 1;
+ if (req.getParameter(PARAM_DEPTH) != null) {
+ depth = Integer.parseInt(req.getParameter(PARAM_DEPTH));
+ if (depth > maxDepth)
+ throw new RuntimeException("Depth " + depth + " is higher than maximum " + maxDepth);
+ }
+
+ Session session = null;
+ try {
+ // authentication
+ session = openJcrSession(req, resp, getRepository(), dataWorkspace);
+ if (!session.itemExists(jcrPath))
+ throw new RuntimeException("JCR node " + jcrPath + " does not exist");
+ Node node = session.getNode(jcrPath);
+
+ List<String> acceptHeader = readAcceptHeader(req);
+ if (!acceptHeader.isEmpty() && node.isNodeType(NodeType.NT_FILE)) {
+ resp.setContentType(OCTET_STREAM_CONTENT_TYPE);
+ resp.addHeader(CONTENT_DISPOSITION_HTTP_HEADER, "attachment; filename='" + node.getName() + "'");
+ IOUtils.copy(JcrUtils.getFileAsStream(node), resp.getOutputStream());
+ resp.flushBuffer();
+ } else {
+ if (!acceptHeader.isEmpty() && acceptHeader.get(0).equals(XML_CONTENT_TYPE)) {
+ // TODO Use req.startAsync(); ?
+ resp.setContentType(XML_CONTENT_TYPE);
+ session.exportSystemView(node.getPath(), resp.getOutputStream(), false, depth <= 1);
+ return;
+ }
+ if (!acceptHeader.isEmpty() && !acceptHeader.contains(JSON_CONTENT_TYPE)) {
+ if (log.isTraceEnabled())
+ log.warn("Content type " + acceptHeader + " in Accept header is not supported. Supported: "
+ + JSON_CONTENT_TYPE + " (default), " + XML_CONTENT_TYPE);
+ }
+ resp.setContentType(JSON_CONTENT_TYPE);
+ JsonGenerator jsonGenerator = getObjectMapper().getFactory().createGenerator(resp.getWriter());
+ jsonGenerator.writeStartObject();
+ writeNodeChildren(node, jsonGenerator, depth, verbose);
+ writeNodeProperties(node, jsonGenerator, verbose);
+ jsonGenerator.writeEndObject();
+ jsonGenerator.flush();
+ }
+ } catch (Exception e) {
+ new CmsExceptionsChain(e).writeAsJson(getObjectMapper(), resp);
+ } finally {
+ JcrUtils.logoutQuietly(session);
+ }
+ }
+
+ protected Session openJcrSession(HttpServletRequest req, HttpServletResponse resp, Repository repository,
+ String workspace) throws RepositoryException {
+ AccessControlContext acc = (AccessControlContext) req.getAttribute(ServletContextHelper.REMOTE_USER);
+ Subject subject = Subject.getSubject(acc);
+ try {
+ return Subject.doAs(subject, new PrivilegedExceptionAction<Session>() {
+
+ @Override
+ public Session run() throws RepositoryException {
+ return repository.login(workspace);
+ }
+
+ });
+ } catch (PrivilegedActionException e) {
+ if (e.getException() instanceof RepositoryException)
+ throw (RepositoryException) e.getException();
+ else
+ throw new RuntimeException(e.getException());
+ }
+// return workspace != null ? repository.login(workspace) : repository.login();
+ }
+
+ protected String getWorkspace(HttpServletRequest req) {
+ String path = req.getPathInfo();
+ try {
+ path = URLDecoder.decode(path, StandardCharsets.UTF_8.name());
+ } catch (UnsupportedEncodingException e) {
+ throw new IllegalArgumentException(e);
+ }
+ String[] pathTokens = path.split("/");
+ return pathTokens[1];
+ }
+
+ protected String getJcrPath(HttpServletRequest req) {
+ String path = req.getPathInfo();
+ try {
+ path = URLDecoder.decode(path, StandardCharsets.UTF_8.name());
+ } catch (UnsupportedEncodingException e) {
+ throw new IllegalArgumentException(e);
+ }
+ String[] pathTokens = path.split("/");
+ String domain = pathTokens[1];
+ String jcrPath = path.substring(domain.length() + 1);
+ return jcrPath;
+ }
+
+ protected List<String> readAcceptHeader(HttpServletRequest req) {
+ List<String> lst = new ArrayList<>();
+ String acceptHeader = req.getHeader(ACCEPT_HTTP_HEADER);
+ if (acceptHeader == null)
+ return lst;
+// Enumeration<String> acceptHeader = req.getHeaders(ACCEPT_HTTP_HEADER);
+// while (acceptHeader.hasMoreElements()) {
+ String[] arr = acceptHeader.split("\\.");
+ for (int i = 0; i < arr.length; i++) {
+ String str = arr[i].trim();
+ if (!"".equals(str))
+ lst.add(str);
+ }
+// }
+ return lst;
+ }
+
+ protected void writeNodeProperties(Node node, JsonGenerator jsonGenerator, boolean verbose)
+ throws RepositoryException, IOException {
+ String jcrPath = node.getPath();
+ Map<String, Map<String, Property>> namespaces = new TreeMap<>();
+
+ PropertyIterator pit = node.getProperties();
+ properties: while (pit.hasNext()) {
+ Property property = pit.nextProperty();
+
+ final String propertyName = property.getName();
+ int columnIndex = propertyName.indexOf(':');
+ if (columnIndex > 0) {
+ // mark prefix with a '_' before the name of the object, according to JSON
+ // conventions to indicate a special value
+ String prefix = "_" + propertyName.substring(0, columnIndex);
+ String unqualifiedName = propertyName.substring(columnIndex + 1);
+ if (!namespaces.containsKey(prefix))
+ namespaces.put(prefix, new LinkedHashMap<String, Property>());
+ Map<String, Property> map = namespaces.get(prefix);
+ assert !map.containsKey(unqualifiedName);
+ map.put(unqualifiedName, property);
+ continue properties;
+ }
+
+ if (property.getType() == PropertyType.BINARY) {
+ if (!(node instanceof JackrabbitNode)) {
+ continue properties;// skip
+ }
+ }
+
+ writeProperty(propertyName, property, jsonGenerator);
+ }
+
+ for (String prefix : namespaces.keySet()) {
+ Map<String, Property> map = namespaces.get(prefix);
+ jsonGenerator.writeFieldName(prefix);
+ jsonGenerator.writeStartObject();
+ if (_JCR.equals(prefix)) {
+ jsonGenerator.writeStringField(JCR_NAME, node.getName());
+ jsonGenerator.writeStringField(JCR_PATH, jcrPath);
+ }
+ properties: for (String unqualifiedName : map.keySet()) {
+ Property property = map.get(unqualifiedName);
+ if (property.getType() == PropertyType.BINARY) {
+ if (!(node instanceof JackrabbitNode)) {
+ continue properties;// skip
+ }
+ }
+ writeProperty(unqualifiedName, property, jsonGenerator);
+ }
+ jsonGenerator.writeEndObject();
+ }
+ }
+
+ protected void writeProperty(String fieldName, Property property, JsonGenerator jsonGenerator)
+ throws RepositoryException, IOException {
+ if (!property.isMultiple()) {
+ jsonGenerator.writeFieldName(fieldName);
+ writePropertyValue(property.getType(), property.getValue(), jsonGenerator);
+ } else {
+ jsonGenerator.writeFieldName(fieldName);
+ jsonGenerator.writeStartArray();
+ Value[] values = property.getValues();
+ for (Value value : values) {
+ writePropertyValue(property.getType(), value, jsonGenerator);
+ }
+ jsonGenerator.writeEndArray();
+ }
+ }
+
+ protected void writePropertyValue(int type, Value value, JsonGenerator jsonGenerator)
+ throws RepositoryException, IOException {
+ if (type == PropertyType.DOUBLE)
+ jsonGenerator.writeNumber(value.getDouble());
+ else if (type == PropertyType.LONG)
+ jsonGenerator.writeNumber(value.getLong());
+ else if (type == PropertyType.BINARY) {
+ if (value instanceof JackrabbitValue) {
+ String contentIdentity = ((JackrabbitValue) value).getContentIdentity();
+ jsonGenerator.writeString("SHA256:" + contentIdentity);
+ } else {
+ // TODO write Base64 ?
+ jsonGenerator.writeNull();
+ }
+ } else
+ jsonGenerator.writeString(value.getString());
+ }
+
+ protected void writeNodeChildren(Node node, JsonGenerator jsonGenerator, int depth, boolean verbose)
+ throws RepositoryException, IOException {
+ if (!node.hasNodes())
+ return;
+ if (depth <= 0)
+ return;
+ NodeIterator nit;
+
+ nit = node.getNodes();
+ children: while (nit.hasNext()) {
+ Node child = nit.nextNode();
+ if (!verbose && child.getName().startsWith(REP_PREFIX)) {
+ continue children;// skip Jackrabbit auth metadata
+ }
+
+ jsonGenerator.writeFieldName(child.getName());
+ jsonGenerator.writeStartObject();
+ writeNodeChildren(child, jsonGenerator, depth - 1, verbose);
+ writeNodeProperties(child, jsonGenerator, verbose);
+ jsonGenerator.writeEndObject();
+ }
+ }
+
+ public void setRepository(Repository repository) {
+ this.repository = repository;
+ }
+
+ public void setMaxDepth(Integer maxDepth) {
+ this.maxDepth = maxDepth;
+ }
+
+ protected Repository getRepository() {
+ return repository;
+ }
+
+ protected ObjectMapper getObjectMapper() {
+ return objectMapper;
+ }
+
+}
--- /dev/null
+package org.argeo.cms.jcr.internal.servlet;
+
+import org.argeo.cms.servlet.PrivateWwwAuthServletContext;
+
+/** Internal subclass, so that config resources can be loaded from our bundle. */
+public class JcrServletContext extends PrivateWwwAuthServletContext {
+
+}
--- /dev/null
+package org.argeo.cms.jcr.internal.servlet;
+
+import java.io.IOException;
+
+import javax.jcr.ImportUUIDBehavior;
+import javax.jcr.Node;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.integration.CmsExceptionsChain;
+import org.argeo.jcr.JcrUtils;
+
+/** Access a JCR repository via web services. */
+public class JcrWriteServlet extends JcrReadServlet {
+ private static final long serialVersionUID = 17272653843085492L;
+ private final static CmsLog log = CmsLog.getLog(JcrWriteServlet.class);
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ if (log.isDebugEnabled())
+ log.debug("Data service POST: " + req.getPathInfo());
+
+ String dataWorkspace = getWorkspace(req);
+ String jcrPath = getJcrPath(req);
+
+ Session session = null;
+ try {
+ // authentication
+ session = openJcrSession(req, resp, getRepository(), dataWorkspace);
+
+ if (req.getContentType() != null && req.getContentType().equals(XML_CONTENT_TYPE)) {
+// resp.setContentType(XML_CONTENT_TYPE);
+ session.getWorkspace().importXML(jcrPath, req.getInputStream(),
+ ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING);
+ return;
+ }
+
+ if (!session.itemExists(jcrPath)) {
+ String parentPath = FilenameUtils.getFullPathNoEndSeparator(jcrPath);
+ String fileName = FilenameUtils.getName(jcrPath);
+ Node folderNode = JcrUtils.mkfolders(session, parentPath);
+ byte[] bytes = IOUtils.toByteArray(req.getInputStream());
+ JcrUtils.copyBytesAsFile(folderNode, fileName, bytes);
+ } else {
+ Node node = session.getNode(jcrPath);
+ if (!node.isNodeType(NodeType.NT_FILE))
+ throw new IllegalArgumentException("Node " + jcrPath + " exists but is not a file");
+ byte[] bytes = IOUtils.toByteArray(req.getInputStream());
+ JcrUtils.copyBytesAsFile(node.getParent(), node.getName(), bytes);
+ }
+ session.save();
+ } catch (Exception e) {
+ new CmsExceptionsChain(e).writeAsJson(getObjectMapper(), resp);
+ } finally {
+ JcrUtils.logoutQuietly(session);
+ }
+ }
+
+ @Override
+ protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ if (log.isDebugEnabled())
+ log.debug("Data service DELETE: " + req.getPathInfo());
+
+ String dataWorkspace = getWorkspace(req);
+ String jcrPath = getJcrPath(req);
+
+ Session session = null;
+ try {
+ // authentication
+ session = openJcrSession(req, resp, getRepository(), dataWorkspace);
+ if (!session.itemExists(jcrPath)) {
+ // ignore
+ return;
+ } else {
+ Node node = session.getNode(jcrPath);
+ node.remove();
+ }
+ session.save();
+ } catch (Exception e) {
+ new CmsExceptionsChain(e).writeAsJson(getObjectMapper(), resp);
+ } finally {
+ JcrUtils.logoutQuietly(session);
+ }
+ }
+
+}
--- /dev/null
+package org.argeo.cms.jcr.internal.servlet;
+
+import static javax.jcr.Property.JCR_DESCRIPTION;
+import static javax.jcr.Property.JCR_LAST_MODIFIED;
+import static javax.jcr.Property.JCR_TITLE;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.PrivilegedExceptionAction;
+import java.util.Calendar;
+import java.util.Collection;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.argeo.api.cms.CmsAuth;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.cms.jcr.CmsJcrUtils;
+import org.argeo.jcr.JcrUtils;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+
+public class LinkServlet extends HttpServlet {
+ private final BundleContext bc = FrameworkUtil.getBundle(getClass()).getBundleContext();
+
+ private static final long serialVersionUID = 3749990143146845708L;
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String path = request.getPathInfo();
+ String userAgent = request.getHeader("User-Agent").toLowerCase();
+ boolean isBot = false;
+ // boolean isCompatibleBrowser = false;
+ if (userAgent.contains("bot") || userAgent.contains("facebook") || userAgent.contains("twitter")) {
+ isBot = true;
+ }
+ // else if (userAgent.contains("webkit") ||
+ // userAgent.contains("gecko") || userAgent.contains("firefox")
+ // || userAgent.contains("msie") || userAgent.contains("chrome") ||
+ // userAgent.contains("chromium")
+ // || userAgent.contains("opera") || userAgent.contains("browser"))
+ // {
+ // isCompatibleBrowser = true;
+ // }
+
+ if (isBot) {
+ // log.warn("# BOT " + request.getHeader("User-Agent"));
+ canonicalAnswer(request, response, path);
+ return;
+ }
+
+ // if (isCompatibleBrowser && log.isTraceEnabled())
+ // log.trace("# BWS " + request.getHeader("User-Agent"));
+ redirectTo(response, "/#" + path);
+ }
+
+ private void redirectTo(HttpServletResponse response, String location) {
+ response.setHeader("Location", location);
+ response.setStatus(HttpServletResponse.SC_FOUND);
+ }
+
+ // private boolean canonicalAnswerNeededBy(HttpServletRequest request) {
+ // String userAgent = request.getHeader("User-Agent").toLowerCase();
+ // return userAgent.startsWith("facebookexternalhit/");
+ // }
+
+ /** For bots which don't understand RWT. */
+ private void canonicalAnswer(HttpServletRequest request, HttpServletResponse response, String path) {
+ Session session = null;
+ try {
+ PrintWriter writer = response.getWriter();
+ session = Subject.doAs(anonymousLogin(), new PrivilegedExceptionAction<Session>() {
+
+ @Override
+ public Session run() throws Exception {
+ Collection<ServiceReference<Repository>> srs = bc.getServiceReferences(Repository.class,
+ "(" + CmsConstants.CN + "=" + CmsConstants.EGO_REPOSITORY + ")");
+ Repository repository = bc.getService(srs.iterator().next());
+ return repository.login();
+ }
+
+ });
+ Node node = session.getNode(path);
+ String title = node.hasProperty(JCR_TITLE) ? node.getProperty(JCR_TITLE).getString() : node.getName();
+ String desc = node.hasProperty(JCR_DESCRIPTION) ? node.getProperty(JCR_DESCRIPTION).getString() : null;
+ Calendar lastUpdate = node.hasProperty(JCR_LAST_MODIFIED) ? node.getProperty(JCR_LAST_MODIFIED).getDate()
+ : null;
+ String url = getCanonicalUrl(node, request);
+ String imgUrl = null;
+ // TODO support images
+// loop: for (NodeIterator it = node.getNodes(); it.hasNext();) {
+// // Takes the first found cms:image
+// Node child = it.nextNode();
+// if (child.isNodeType(CMS_IMAGE)) {
+// imgUrl = getDataUrl(child, request);
+// break loop;
+// }
+// }
+ StringBuilder buf = new StringBuilder();
+ buf.append("<html>");
+ buf.append("<head>");
+ writeMeta(buf, "og:title", escapeHTML(title));
+ writeMeta(buf, "og:type", "website");
+ buf.append("<meta name='twitter:card' content='summary' />");
+ buf.append("<meta name='twitter:site' content='@argeo_org' />");
+ writeMeta(buf, "og:url", url);
+ if (desc != null)
+ writeMeta(buf, "og:description", escapeHTML(desc));
+ if (imgUrl != null)
+ writeMeta(buf, "og:image", imgUrl);
+ if (lastUpdate != null)
+ writeMeta(buf, "og:updated_time", Long.toString(lastUpdate.getTime().getTime()));
+ buf.append("</head>");
+ buf.append("<body>");
+ buf.append("<p><b>!! This page is meant for indexing robots, not for real people," + " visit <a href='/#")
+ .append(path).append("'>").append(escapeHTML(title)).append("</a> instead.</b></p>");
+ writeCanonical(buf, node);
+ buf.append("</body>");
+ buf.append("</html>");
+ writer.print(buf.toString());
+
+ response.setHeader("Content-Type", "text/html");
+ writer.flush();
+ } catch (Exception e) {
+ throw new IllegalStateException("Cannot write canonical answer", e);
+ } finally {
+ JcrUtils.logoutQuietly(session);
+ }
+ }
+
+ /**
+ * From http://stackoverflow.com/questions/1265282/recommended-method-for-
+ * escaping-html-in-java (+ escaping '). TODO Use
+ * org.apache.commons.lang.StringEscapeUtils
+ */
+ private String escapeHTML(String s) {
+ StringBuilder out = new StringBuilder(Math.max(16, s.length()));
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ if (c > 127 || c == '\'' || c == '"' || c == '<' || c == '>' || c == '&') {
+ out.append("&#");
+ out.append((int) c);
+ out.append(';');
+ } else {
+ out.append(c);
+ }
+ }
+ return out.toString();
+ }
+
+ private void writeMeta(StringBuilder buf, String tag, String value) {
+ buf.append("<meta property='").append(tag).append("' content='").append(value).append("'/>");
+ }
+
+ private void writeCanonical(StringBuilder buf, Node node) throws RepositoryException {
+ buf.append("<div>");
+ if (node.hasProperty(JCR_TITLE))
+ buf.append("<p>").append(node.getProperty(JCR_TITLE).getString()).append("</p>");
+ if (node.hasProperty(JCR_DESCRIPTION))
+ buf.append("<p>").append(node.getProperty(JCR_DESCRIPTION).getString()).append("</p>");
+ NodeIterator children = node.getNodes();
+ while (children.hasNext()) {
+ writeCanonical(buf, children.nextNode());
+ }
+ buf.append("</div>");
+ }
+
+ // DATA
+ private StringBuilder getServerBaseUrl(HttpServletRequest request) {
+ try {
+ URL url = new URL(request.getRequestURL().toString());
+ StringBuilder buf = new StringBuilder();
+ buf.append(url.getProtocol()).append("://").append(url.getHost());
+ if (url.getPort() != -1)
+ buf.append(':').append(url.getPort());
+ return buf;
+ } catch (MalformedURLException e) {
+ throw new IllegalArgumentException("Cannot extract server base URL from " + request.getRequestURL(), e);
+ }
+ }
+
+ private String getDataUrl(Node node, HttpServletRequest request) throws RepositoryException {
+ try {
+ StringBuilder buf = getServerBaseUrl(request);
+ buf.append(CmsJcrUtils.getDataPath(CmsConstants.EGO_REPOSITORY, node));
+ return new URL(buf.toString()).toString();
+ } catch (MalformedURLException e) {
+ throw new IllegalArgumentException("Cannot build data URL for " + node, e);
+ }
+ }
+
+ // public static String getDataPath(Node node) throws
+ // RepositoryException {
+ // assert node != null;
+ // String userId = node.getSession().getUserID();
+ //// if (log.isTraceEnabled())
+ //// log.trace(userId + " : " + node.getPath());
+ // StringBuilder buf = new StringBuilder();
+ // boolean isAnonymous =
+ // userId.equalsIgnoreCase(NodeConstants.ROLE_ANONYMOUS);
+ // if (isAnonymous)
+ // buf.append(WEBDAV_PUBLIC);
+ // else
+ // buf.append(WEBDAV_PRIVATE);
+ // Session session = node.getSession();
+ // Repository repository = session.getRepository();
+ // String cn;
+ // if (repository.isSingleValueDescriptor(NodeConstants.CN)) {
+ // cn = repository.getDescriptor(NodeConstants.CN);
+ // } else {
+ //// log.warn("No cn defined in repository, using " +
+ // NodeConstants.NODE);
+ // cn = NodeConstants.NODE;
+ // }
+ // return
+ // buf.append('/').append(cn).append('/').append(session.getWorkspace().getName()).append(node.getPath())
+ // .toString();
+ // }
+
+ private String getCanonicalUrl(Node node, HttpServletRequest request) throws RepositoryException {
+ try {
+ StringBuilder buf = getServerBaseUrl(request);
+ buf.append('/').append('!').append(node.getPath());
+ return new URL(buf.toString()).toString();
+ } catch (MalformedURLException e) {
+ throw new IllegalArgumentException("Cannot build data URL for " + node, e);
+ }
+ // return request.getRequestURL().append('!').append(node.getPath())
+ // .toString();
+ }
+
+ private Subject anonymousLogin() {
+ Subject subject = new Subject();
+ LoginContext lc;
+ try {
+ lc = new LoginContext(CmsAuth.LOGIN_CONTEXT_ANONYMOUS, subject);
+ lc.login();
+ return subject;
+ } catch (LoginException e) {
+ throw new IllegalStateException("Cannot login as anonymous", e);
+ }
+ }
+
+}
--- /dev/null
+<config>
+ <protecteditemremovehandler>
+ <class name="org.apache.jackrabbit.server.remoting.davex.AclRemoveHandler" />
+ </protecteditemremovehandler>
+</config>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<!--
+<!DOCTYPE config [
+ <!ELEMENT config (iomanager , propertymanager, (collection | noncollection)? , filter?, mimetypeproperties?) >
+
+ <!ELEMENT iomanager (class, iohandler*) >
+ <!ELEMENT iohandler (class) >
+
+ <!ELEMENT propertymanager (class, propertyhandler*) >
+ <!ELEMENT propertyhandler (class) >
+
+ <!ELEMENT collection (nodetypes) >
+ <!ELEMENT noncollection (nodetypes) >
+
+ <!ELEMENT filter (class, namespaces?, nodetypes?) >
+
+ <!ELEMENT class >
+ <!ATTLIST class
+ name CDATA #REQUIRED
+ >
+ <!ELEMENT namespaces (prefix | uri)* >
+ <!ELEMENT prefix (CDATA) >
+ <!ELEMENT uri (CDATA) >
+
+ <!ELEMENT nodetypes (nodetype)* >
+ <!ELEMENT nodetype (CDATA) >
+
+ <!ELEMENT mimetypeproperties (mimemapping*, defaultmimetype) >
+
+ <!ELEMENT mimemapping >
+ <!ATTLIST mimemapping
+ extension CDATA #REQUIRED
+ mimetype CDATA #REQUIRED
+ >
+
+ <!ELEMENT defaultmimetype (CDATA) >
+]>
+-->
+
+<config>
+ <!--
+ Defines the IOManager implementation that is responsible for passing
+ import/export request to the individual IO-handlers.
+ -->
+ <iomanager>
+ <!-- class element defines the manager to be used. The specified class
+ must implement the IOManager interface.
+ Note, that the handlers are being added and called in the order
+ they appear in the configuration.
+ -->
+ <class name="org.apache.jackrabbit.server.io.IOManagerImpl" />
+ <iohandler>
+ <class name="org.apache.jackrabbit.server.io.VersionHandler" />
+ </iohandler>
+ <iohandler>
+ <class name="org.apache.jackrabbit.server.io.VersionHistoryHandler" />
+ </iohandler>
+<!-- <iohandler> -->
+<!-- <class name="org.apache.jackrabbit.server.io.ZipHandler" /> -->
+<!-- </iohandler> -->
+<!-- <iohandler> -->
+<!-- <class name="org.apache.jackrabbit.server.io.XmlHandler" /> -->
+<!-- </iohandler> -->
+ <iohandler>
+ <class name="org.apache.jackrabbit.server.io.DirListingExportHandler" />
+ </iohandler>
+ <iohandler>
+ <class name="org.apache.jackrabbit.server.io.DefaultHandler" />
+ </iohandler>
+ </iomanager>
+ <!--
+ Example config for iomanager that populates its list of handlers with
+ default values. Therefore the 'iohandler' elements are omited.
+ -->
+ <!--
+ <iomanager>
+ <class name="org.apache.jackrabbit.server.io.DefaultIOManager" />
+ </iomanager>
+ -->
+ <!--
+ Defines the PropertyManager implementation that is responsible for export
+ and import of resource properties.
+ -->
+ <propertymanager>
+ <!-- class element defines the manager to be used. The specified class
+ must implement the PropertyManager interface.
+ Note, that the handlers are being added and called in the order
+ they appear in the configuration.
+ -->
+ <class name="org.apache.jackrabbit.server.io.PropertyManagerImpl" />
+ <propertyhandler>
+ <class name="org.apache.jackrabbit.server.io.VersionHandler" />
+ </propertyhandler>
+ <propertyhandler>
+ <class name="org.apache.jackrabbit.server.io.VersionHistoryHandler" />
+ </propertyhandler>
+<!-- <propertyhandler> -->
+<!-- <class name="org.apache.jackrabbit.server.io.ZipHandler" /> -->
+<!-- </propertyhandler> -->
+<!-- <propertyhandler> -->
+<!-- <class name="org.apache.jackrabbit.server.io.XmlHandler" /> -->
+<!-- </propertyhandler> -->
+ <propertyhandler>
+ <class name="org.apache.jackrabbit.server.io.DefaultHandler" />
+ </propertyhandler>
+ </propertymanager>
+ <!--
+ Define nodetypes, that should never by displayed as 'collection'
+ -->
+ <noncollection>
+ <nodetypes>
+ <nodetype>nt:file</nodetype>
+ <nodetype>nt:resource</nodetype>
+ </nodetypes>
+ </noncollection>
+ <!--
+ Example: Defines nodetypes, that should always be displayed as 'collection'.
+ -->
+ <!--
+ <collection>
+ <nodetypes>
+ <nodetype>nt:folder</nodetype>
+ <nodetype>rep:root</nodetype>
+ </nodetypes>
+ </collection>
+ -->
+ <!--
+ Filter that allows to prevent certain items from being displayed.
+ Please note, that this has an effect on PROPFIND calls only and does not
+ provide limited access to those items matching any of the filters.
+
+ However specifying a filter may cause problems with PUT or MKCOL if the
+ resource to be created is being filtered out, thus resulting in inconsistent
+ responses (e.g. PUT followed by PROPFIND on parent).
+ -->
+ <filter>
+ <!-- class element defines the resource filter to be used. The specified class
+ must implement the ItemFilter interface -->
+ <class name="org.apache.jackrabbit.webdav.simple.DefaultItemFilter" />
+ <!--
+ Nodetype names to be used to filter child nodes.
+ A child node can be filtered if the declaring nodetype of its definition
+ is one of the nodetype names specified in the nodetypes Element.
+ E.g. defining 'rep:root' as filtered nodetype whould result in jcr:system
+ being hidden but no other child node of the root node, since those
+ are defined by the nodetype nt:unstructered.
+ -->
+ <!--
+ <nodetypes>
+ <nodetype>rep:root</nodetype>
+ </nodetypes>
+ -->
+ <!--
+ Namespace prefixes or uris. Items having a name that matches any of the
+ entries will be filtered.
+ -->
+ <namespaces>
+ <prefix>rep</prefix>
+ <prefix>jcr</prefix>
+ <!-- Argeo namespaces -->
+ <prefix>node</prefix>
+ <prefix>argeo</prefix>
+ <prefix>cms</prefix>
+ <prefix>slc</prefix>
+ <prefix>connect</prefix>
+ <prefix>activities</prefix>
+ <prefix>people</prefix>
+ <prefix>documents</prefix>
+ <prefix>tracker</prefix>
+ <!--
+ <uri>internal</uri>
+ <uri>http://www.jcp.org/jcr/1.0</uri>
+ -->
+ </namespaces>
+ </filter>
+
+ <!--
+ Optional 'mimetypeproperties' element.
+ It defines additional or replaces existing mappings for the MimeResolver
+ instance created by the ResourceConfig.
+ The default mappings are defined in org.apache.jackrabbit.server.io.mimetypes.properties.
+ If the default mime type defined by MimeResolver is 'application/octet-stream'.
+ -->
+ <!--
+ <mimetypeproperties>
+ <mimemapping extension="rtf" mimetype="application/rtf" />
+ <mimemapping extension="ott" mimetype="application/vnd.oasis.opendocument.text-template" />
+ <defaultmimetype>text/html</defaultmimetype>
+ </mimetypeproperties>
+ -->
+</config>
--- /dev/null
+<ldap = 'http://www.argeo.org/ns/ldap'>
--- /dev/null
+<node = 'http://www.argeo.org/ns/node'>
+
+[node:userHome]
+mixin
+- ldap:uid (STRING) m
+
+[node:groupHome]
+mixin
+- ldap:cn (STRING) m
--- /dev/null
+package org.argeo.cms.jcr.tabular;
+
+import java.io.OutputStream;
+
+import org.argeo.cms.tabular.TabularWriter;
+import org.argeo.util.CsvWriter;
+
+/** Write tabular content in a stream as CSV. Wraps a {@link CsvWriter}. */
+public class CsvTabularWriter implements TabularWriter {
+ private CsvWriter csvWriter;
+
+ public CsvTabularWriter(OutputStream out) {
+ this.csvWriter = new CsvWriter(out);
+ }
+
+ public void appendRow(Object[] row) {
+ csvWriter.writeLine(row);
+ }
+
+ public void close() {
+ }
+
+}
--- /dev/null
+package org.argeo.cms.jcr.tabular;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.cms.ArgeoTypes;
+import org.argeo.cms.tabular.ArrayTabularRow;
+import org.argeo.cms.tabular.TabularColumn;
+import org.argeo.cms.tabular.TabularRow;
+import org.argeo.cms.tabular.TabularRowIterator;
+import org.argeo.jcr.JcrException;
+import org.argeo.util.CsvParser;
+
+/** Iterates over the rows of a {@link ArgeoTypes#ARGEO_TABLE} node. */
+public class JcrTabularRowIterator implements TabularRowIterator {
+ private Boolean hasNext = null;
+ private Boolean parsingCompleted = false;
+
+ private Long currentRowNumber = 0l;
+
+ private List<TabularColumn> header = new ArrayList<TabularColumn>();
+
+ /** referenced so that we can close it */
+ private Binary binary;
+ private InputStream in;
+
+ private CsvParser csvParser;
+ private ArrayBlockingQueue<List<String>> textLines;
+
+ public JcrTabularRowIterator(Node tableNode) {
+ try {
+ for (NodeIterator it = tableNode.getNodes(); it.hasNext();) {
+ Node node = it.nextNode();
+ if (node.isNodeType(ArgeoTypes.ARGEO_COLUMN)) {
+ Integer type = PropertyType.valueFromName(node.getProperty(
+ Property.JCR_REQUIRED_TYPE).getString());
+ TabularColumn tc = new TabularColumn(node.getProperty(
+ Property.JCR_TITLE).getString(), type);
+ header.add(tc);
+ }
+ }
+ Node contentNode = tableNode.getNode(Property.JCR_CONTENT);
+ if (contentNode.isNodeType(ArgeoTypes.ARGEO_CSV)) {
+ textLines = new ArrayBlockingQueue<List<String>>(1000);
+ csvParser = new CsvParser() {
+ protected void processLine(Integer lineNumber,
+ List<String> header, List<String> tokens) {
+ try {
+ textLines.put(tokens);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ // textLines.add(tokens);
+ if (hasNext == null) {
+ hasNext = true;
+ synchronized (JcrTabularRowIterator.this) {
+ JcrTabularRowIterator.this.notifyAll();
+ }
+ }
+ }
+ };
+ csvParser.setNoHeader(true);
+ binary = contentNode.getProperty(Property.JCR_DATA).getBinary();
+ in = binary.getStream();
+ Thread thread = new Thread(contentNode.getPath() + " reader") {
+ public void run() {
+ try {
+ csvParser.parse(in);
+ } finally {
+ parsingCompleted = true;
+ IOUtils.closeQuietly(in);
+ }
+ }
+ };
+ thread.start();
+ }
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot read table " + tableNode, e);
+ }
+ }
+
+ public synchronized boolean hasNext() {
+ // we don't know if there is anything available
+ // while (hasNext == null)
+ // try {
+ // wait();
+ // } catch (InterruptedException e) {
+ // // silent
+ // // FIXME better deal with interruption
+ // Thread.currentThread().interrupt();
+ // break;
+ // }
+
+ // buffer not empty
+ if (!textLines.isEmpty())
+ return true;
+
+ // maybe the parsing is finished but the flag has not been set
+ while (!parsingCompleted && textLines.isEmpty())
+ try {
+ wait(100);
+ } catch (InterruptedException e) {
+ // silent
+ // FIXME better deal with interruption
+ Thread.currentThread().interrupt();
+ break;
+ }
+
+ // buffer not empty
+ if (!textLines.isEmpty())
+ return true;
+
+ // (parsingCompleted && textLines.isEmpty())
+ return false;
+
+ // if (!hasNext && textLines.isEmpty()) {
+ // if (in != null) {
+ // IOUtils.closeQuietly(in);
+ // in = null;
+ // }
+ // if (binary != null) {
+ // JcrUtils.closeQuietly(binary);
+ // binary = null;
+ // }
+ // return false;
+ // } else
+ // return true;
+ }
+
+ public synchronized TabularRow next() {
+ try {
+ List<String> tokens = textLines.take();
+ List<Object> objs = new ArrayList<Object>(tokens.size());
+ for (String token : tokens) {
+ // TODO convert to other formats using header
+ objs.add(token);
+ }
+ currentRowNumber++;
+ return new ArrayTabularRow(objs);
+ } catch (InterruptedException e) {
+ // silent
+ // FIXME better deal with interruption
+ }
+ return null;
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+
+ public Long getCurrentRowNumber() {
+ return currentRowNumber;
+ }
+
+ public List<TabularColumn> getHeader() {
+ return header;
+ }
+
+}
--- /dev/null
+package org.argeo.cms.jcr.tabular;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.util.List;
+
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.cms.ArgeoTypes;
+import org.argeo.cms.tabular.TabularColumn;
+import org.argeo.cms.tabular.TabularWriter;
+import org.argeo.jcr.JcrException;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.util.CsvWriter;
+
+/** Write / reference tabular content in a JCR repository. */
+public class JcrTabularWriter implements TabularWriter {
+ private Node contentNode;
+ private ByteArrayOutputStream out;
+ private CsvWriter csvWriter;
+
+ @SuppressWarnings("unused")
+ private final List<TabularColumn> columns;
+
+ /** Creates a table node */
+ public JcrTabularWriter(Node tableNode, List<TabularColumn> columns,
+ String contentNodeType) {
+ try {
+ this.columns = columns;
+ for (TabularColumn column : columns) {
+ String normalized = JcrUtils.replaceInvalidChars(column
+ .getName());
+ Node columnNode = tableNode.addNode(normalized,
+ ArgeoTypes.ARGEO_COLUMN);
+ columnNode.setProperty(Property.JCR_TITLE, column.getName());
+ if (column.getType() != null)
+ columnNode.setProperty(Property.JCR_REQUIRED_TYPE,
+ PropertyType.nameFromValue(column.getType()));
+ else
+ columnNode.setProperty(Property.JCR_REQUIRED_TYPE,
+ PropertyType.TYPENAME_STRING);
+ }
+ contentNode = tableNode.addNode(Property.JCR_CONTENT,
+ contentNodeType);
+ if (contentNodeType.equals(ArgeoTypes.ARGEO_CSV)) {
+ contentNode.setProperty(Property.JCR_MIMETYPE, "text/csv");
+ contentNode.setProperty(Property.JCR_ENCODING, "UTF-8");
+ out = new ByteArrayOutputStream();
+ csvWriter = new CsvWriter(out);
+ }
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot create table node " + tableNode, e);
+ }
+ }
+
+ public void appendRow(Object[] row) {
+ csvWriter.writeLine(row);
+ }
+
+ public void close() {
+ Binary binary = null;
+ InputStream in = null;
+ try {
+ // TODO parallelize with pipes and writing from another thread
+ in = new ByteArrayInputStream(out.toByteArray());
+ binary = contentNode.getSession().getValueFactory()
+ .createBinary(in);
+ contentNode.setProperty(Property.JCR_DATA, binary);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot store data in " + contentNode, e);
+ } finally {
+ IOUtils.closeQuietly(in);
+ JcrUtils.closeQuietly(binary);
+ }
+ }
+}
--- /dev/null
+/** Argeo CMS implementation of the Argeo Tabular API (CSV, JCR). */
+package org.argeo.cms.jcr.tabular;
\ No newline at end of file
--- /dev/null
+package org.argeo.jackrabbit;
+
+import java.util.Map;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+
+import org.apache.jackrabbit.core.security.SecurityConstants;
+import org.apache.jackrabbit.core.security.principal.AdminPrincipal;
+
+@Deprecated
+public class JackrabbitAdminLoginModule implements LoginModule {
+ private Subject subject;
+
+ @Override
+ public void initialize(Subject subject, CallbackHandler callbackHandler,
+ Map<String, ?> sharedState, Map<String, ?> options) {
+ this.subject = subject;
+ }
+
+ @Override
+ public boolean login() throws LoginException {
+ // TODO check permission?
+ return true;
+ }
+
+ @Override
+ public boolean commit() throws LoginException {
+ subject.getPrincipals().add(
+ new AdminPrincipal(SecurityConstants.ADMIN_ID));
+ return true;
+ }
+
+ @Override
+ public boolean abort() throws LoginException {
+ return true;
+ }
+
+ @Override
+ public boolean logout() throws LoginException {
+ subject.getPrincipals().removeAll(
+ subject.getPrincipals(AdminPrincipal.class));
+ return true;
+ }
+
+}
--- /dev/null
+package org.argeo.jackrabbit;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.URL;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.jackrabbit.commons.cnd.CndImporter;
+import org.apache.jackrabbit.commons.cnd.ParseException;
+import org.apache.jackrabbit.core.config.RepositoryConfig;
+import org.apache.jackrabbit.core.fs.FileSystemException;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrCallback;
+import org.argeo.jcr.JcrException;
+import org.argeo.jcr.JcrUtils;
+
+/** Migrate the data in a Jackrabbit repository. */
+@Deprecated
+public class JackrabbitDataModelMigration implements Comparable<JackrabbitDataModelMigration> {
+ private final static CmsLog log = CmsLog.getLog(JackrabbitDataModelMigration.class);
+
+ private String dataModelNodePath;
+ private String targetVersion;
+ private URL migrationCnd;
+ private JcrCallback dataModification;
+
+ /**
+ * Expects an already started repository with the old data model to migrate.
+ * Expects to be run with admin rights (Repository.login() will be used).
+ *
+ * @return true if a migration was performed and the repository needs to be
+ * restarted and its caches cleared.
+ */
+ public Boolean migrate(Session session) {
+ long begin = System.currentTimeMillis();
+ Reader reader = null;
+ try {
+ // check if already migrated
+ if (!session.itemExists(dataModelNodePath)) {
+// log.warn("Node " + dataModelNodePath + " does not exist: nothing to migrate.");
+ return false;
+ }
+// Node dataModelNode = session.getNode(dataModelNodePath);
+// if (dataModelNode.hasProperty(ArgeoNames.ARGEO_DATA_MODEL_VERSION)) {
+// String currentVersion = dataModelNode.getProperty(
+// ArgeoNames.ARGEO_DATA_MODEL_VERSION).getString();
+// if (compareVersions(currentVersion, targetVersion) >= 0) {
+// log.info("Data model at version " + currentVersion
+// + ", no need to migrate.");
+// return false;
+// }
+// }
+
+ // apply transitional CND
+ if (migrationCnd != null) {
+ reader = new InputStreamReader(migrationCnd.openStream());
+ CndImporter.registerNodeTypes(reader, session, true);
+ session.save();
+// log.info("Registered migration node types from " + migrationCnd);
+ }
+
+ // modify data
+ dataModification.execute(session);
+
+ // apply changes
+ session.save();
+
+ long duration = System.currentTimeMillis() - begin;
+// log.info("Migration of data model " + dataModelNodePath + " to " + targetVersion + " performed in "
+// + duration + "ms");
+ return true;
+ } catch (RepositoryException e) {
+ JcrUtils.discardQuietly(session);
+ throw new JcrException("Migration of data model " + dataModelNodePath + " to " + targetVersion + " failed.",
+ e);
+ } catch (ParseException | IOException e) {
+ JcrUtils.discardQuietly(session);
+ throw new RuntimeException(
+ "Migration of data model " + dataModelNodePath + " to " + targetVersion + " failed.", e);
+ } finally {
+ JcrUtils.logoutQuietly(session);
+ IOUtils.closeQuietly(reader);
+ }
+ }
+
+ protected static int compareVersions(String version1, String version2) {
+ // TODO do a proper version analysis and comparison
+ return version1.compareTo(version2);
+ }
+
+ /** To be called on a stopped repository. */
+ public static void clearRepositoryCaches(RepositoryConfig repositoryConfig) {
+ try {
+ String customeNodeTypesPath = "/nodetypes/custom_nodetypes.xml";
+ // FIXME causes weird error in Eclipse
+ repositoryConfig.getFileSystem().deleteFile(customeNodeTypesPath);
+ if (log.isDebugEnabled())
+ log.debug("Cleared " + customeNodeTypesPath);
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (RepositoryException e) {
+ throw new JcrException(e);
+ } catch (FileSystemException e) {
+ throw new RuntimeException("Cannot clear node types cache.",e);
+ }
+
+ // File customNodeTypes = new File(home.getPath()
+ // + "/repository/nodetypes/custom_nodetypes.xml");
+ // if (customNodeTypes.exists()) {
+ // customNodeTypes.delete();
+ // if (log.isDebugEnabled())
+ // log.debug("Cleared " + customNodeTypes);
+ // } else {
+ // log.warn("File " + customNodeTypes + " not found.");
+ // }
+ }
+
+ /*
+ * FOR USE IN (SORTED) SETS
+ */
+
+ public int compareTo(JackrabbitDataModelMigration dataModelMigration) {
+ // TODO make ordering smarter
+ if (dataModelNodePath.equals(dataModelMigration.dataModelNodePath))
+ return compareVersions(targetVersion, dataModelMigration.targetVersion);
+ else
+ return dataModelNodePath.compareTo(dataModelMigration.dataModelNodePath);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof JackrabbitDataModelMigration))
+ return false;
+ JackrabbitDataModelMigration dataModelMigration = (JackrabbitDataModelMigration) obj;
+ return dataModelNodePath.equals(dataModelMigration.dataModelNodePath)
+ && targetVersion.equals(dataModelMigration.targetVersion);
+ }
+
+ @Override
+ public int hashCode() {
+ return targetVersion.hashCode();
+ }
+
+ public void setDataModelNodePath(String dataModelNodePath) {
+ this.dataModelNodePath = dataModelNodePath;
+ }
+
+ public void setTargetVersion(String targetVersion) {
+ this.targetVersion = targetVersion;
+ }
+
+ public void setMigrationCnd(URL migrationCnd) {
+ this.migrationCnd = migrationCnd;
+ }
+
+ public void setDataModification(JcrCallback dataModification) {
+ this.dataModification = dataModification;
+ }
+
+ public String getDataModelNodePath() {
+ return dataModelNodePath;
+ }
+
+ public String getTargetVersion() {
+ return targetVersion;
+ }
+
+}
--- /dev/null
+package org.argeo.jackrabbit.client;
+
+import java.util.Map;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+
+import org.apache.jackrabbit.jcr2spi.Jcr2spiRepositoryFactory;
+import org.apache.jackrabbit.jcr2spi.RepositoryImpl;
+import org.apache.jackrabbit.spi.RepositoryServiceFactory;
+
+/** A customised {@link RepositoryFactory} access a remote DAVEX service. */
+public class ClientDavexRepositoryFactory implements RepositoryFactory {
+ public final static String JACKRABBIT_DAVEX_URI = ClientDavexRepositoryServiceFactory.PARAM_REPOSITORY_URI;
+ public final static String JACKRABBIT_REMOTE_DEFAULT_WORKSPACE = ClientDavexRepositoryServiceFactory.PARAM_WORKSPACE_NAME_DEFAULT;
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public Repository getRepository(Map parameters) throws RepositoryException {
+ RepositoryServiceFactory repositoryServiceFactory = new ClientDavexRepositoryServiceFactory();
+ return RepositoryImpl
+ .create(new Jcr2spiRepositoryFactory.RepositoryConfigImpl(repositoryServiceFactory, parameters));
+ }
+
+}
--- /dev/null
+package org.argeo.jackrabbit.client;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.protocol.HttpContext;
+import org.apache.jackrabbit.spi.SessionInfo;
+import org.apache.jackrabbit.spi2davex.BatchReadConfig;
+import org.apache.jackrabbit.spi2davex.RepositoryServiceImpl;
+
+/**
+ * Wrapper for {@link RepositoryServiceImpl} in order to access the underlying
+ * {@link HttpClientContext}.
+ */
+public class ClientDavexRepositoryService extends RepositoryServiceImpl {
+
+ public ClientDavexRepositoryService(String jcrServerURI, BatchReadConfig batchReadConfig)
+ throws RepositoryException {
+ super(jcrServerURI, batchReadConfig);
+ }
+
+
+
+// public ClientDavexRepositoryService(String jcrServerURI, String defaultWorkspaceName,
+// BatchReadConfig batchReadConfig, int itemInfoCacheSize, ConnectionOptions connectionOptions)
+// throws RepositoryException {
+// super(jcrServerURI, defaultWorkspaceName, batchReadConfig, itemInfoCacheSize, connectionOptions);
+// // TODO Auto-generated constructor stub
+// }
+
+
+
+// public ClientDavexRepositoryService(String jcrServerURI, String defaultWorkspaceName,
+// BatchReadConfig batchReadConfig, int itemInfoCacheSize, int maximumHttpConnections)
+// throws RepositoryException {
+// super(jcrServerURI, defaultWorkspaceName, batchReadConfig, itemInfoCacheSize, maximumHttpConnections);
+// }
+//
+// public ClientDavexRepositoryService(String jcrServerURI, String defaultWorkspaceName,
+// BatchReadConfig batchReadConfig, int itemInfoCacheSize) throws RepositoryException {
+// super(jcrServerURI, defaultWorkspaceName, batchReadConfig, itemInfoCacheSize);
+// }
+
+ @Override
+ protected HttpContext getContext(SessionInfo sessionInfo) throws RepositoryException {
+ HttpClientContext result = HttpClientContext.create();
+ result.setAuthCache(new NonSerialBasicAuthCache());
+ return result;
+ }
+
+}
--- /dev/null
+package org.argeo.jackrabbit.client;
+
+import java.util.Map;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.jackrabbit.spi.RepositoryService;
+import org.apache.jackrabbit.spi.commons.ItemInfoCacheImpl;
+import org.apache.jackrabbit.spi2davex.BatchReadConfig;
+import org.apache.jackrabbit.spi2davex.Spi2davexRepositoryServiceFactory;
+
+/**
+ * Wrapper for {@link Spi2davexRepositoryServiceFactory} in order to create a
+ * {@link ClientDavexRepositoryService}.
+ */
+public class ClientDavexRepositoryServiceFactory extends Spi2davexRepositoryServiceFactory {
+ @Override
+ public RepositoryService createRepositoryService(Map<?, ?> parameters) throws RepositoryException {
+ // retrieve the repository uri
+ String uri;
+ if (parameters == null) {
+ uri = System.getProperty(PARAM_REPOSITORY_URI);
+ } else {
+ Object repoUri = parameters.get(PARAM_REPOSITORY_URI);
+ uri = (repoUri == null) ? null : repoUri.toString();
+ }
+ if (uri == null) {
+ uri = DEFAULT_REPOSITORY_URI;
+ }
+
+ // load other optional configuration parameters
+ BatchReadConfig brc = null;
+ int itemInfoCacheSize = ItemInfoCacheImpl.DEFAULT_CACHE_SIZE;
+ int maximumHttpConnections = 0;
+
+ // since JCR-4120 the default workspace name is no longer set to 'default'
+ // note: if running with JCR Server < 1.5 a default workspace name must
+ // therefore be configured
+ String workspaceNameDefault = null;
+
+ if (parameters != null) {
+ // batchRead config
+ Object param = parameters.get(PARAM_BATCHREAD_CONFIG);
+ if (param != null && param instanceof BatchReadConfig) {
+ brc = (BatchReadConfig) param;
+ }
+
+ // itemCache size config
+ param = parameters.get(PARAM_ITEMINFO_CACHE_SIZE);
+ if (param != null) {
+ try {
+ itemInfoCacheSize = Integer.parseInt(param.toString());
+ } catch (NumberFormatException e) {
+ // ignore, use default
+ }
+ }
+
+ // max connections config
+ param = parameters.get(PARAM_MAX_CONNECTIONS);
+ if (param != null) {
+ try {
+ maximumHttpConnections = Integer.parseInt(param.toString());
+ } catch (NumberFormatException e) {
+ // using default
+ }
+ }
+
+ param = parameters.get(PARAM_WORKSPACE_NAME_DEFAULT);
+ if (param != null) {
+ workspaceNameDefault = param.toString();
+ }
+ }
+
+ // FIXME adapt to changes in Jackrabbit
+// if (maximumHttpConnections > 0) {
+// return new ClientDavexRepositoryService(uri, workspaceNameDefault, brc, itemInfoCacheSize,
+// maximumHttpConnections);
+// } else {
+// return new ClientDavexRepositoryService(uri, workspaceNameDefault, brc, itemInfoCacheSize);
+// }
+ return null;
+ }
+
+}
--- /dev/null
+package org.argeo.jackrabbit.client;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.protocol.HttpContext;
+import org.apache.jackrabbit.jcr2dav.Jcr2davRepositoryFactory;
+import org.apache.jackrabbit.jcr2spi.Jcr2spiRepositoryFactory;
+import org.apache.jackrabbit.jcr2spi.RepositoryImpl;
+import org.apache.jackrabbit.spi.RepositoryService;
+import org.apache.jackrabbit.spi.RepositoryServiceFactory;
+import org.apache.jackrabbit.spi.SessionInfo;
+import org.apache.jackrabbit.spi.commons.ItemInfoCacheImpl;
+import org.apache.jackrabbit.spi2davex.BatchReadConfig;
+import org.apache.jackrabbit.spi2davex.RepositoryServiceImpl;
+import org.apache.jackrabbit.spi2davex.Spi2davexRepositoryServiceFactory;
+import org.argeo.jcr.JcrUtils;
+
+/** Minimal client to test JCR DAVEX connectivity. */
+public class JackrabbitClient {
+ final static String JACKRABBIT_REPOSITORY_URI = "org.apache.jackrabbit.repository.uri";
+ final static String JACKRABBIT_DAVEX_URI = "org.apache.jackrabbit.spi2davex.uri";
+ final static String JACKRABBIT_REMOTE_DEFAULT_WORKSPACE = "org.apache.jackrabbit.spi2davex.WorkspaceNameDefault";
+
+ public static void main(String[] args) {
+ String repoUri = args.length == 0 ? "http://root:demo@localhost:7070/jcr/ego" : args[0];
+ String workspace = args.length < 2 ? "home" : args[1];
+
+ Repository repository = null;
+ Session session = null;
+
+ URI uri;
+ try {
+ uri = new URI(repoUri);
+ } catch (URISyntaxException e1) {
+ throw new IllegalArgumentException(e1);
+ }
+
+ if (uri.getScheme().equals("http") || uri.getScheme().equals("https")) {
+
+ RepositoryFactory repositoryFactory = new Jcr2davRepositoryFactory() {
+ @SuppressWarnings("rawtypes")
+ public Repository getRepository(Map parameters) throws RepositoryException {
+ RepositoryServiceFactory repositoryServiceFactory = new Spi2davexRepositoryServiceFactory() {
+
+ @Override
+ public RepositoryService createRepositoryService(Map<?, ?> parameters)
+ throws RepositoryException {
+ Object uri = parameters.get(JACKRABBIT_DAVEX_URI);
+ Object defaultWorkspace = parameters.get(JACKRABBIT_REMOTE_DEFAULT_WORKSPACE);
+ BatchReadConfig brc = null;
+ // FIXME adapt to change in Jackrabbit
+// return new RepositoryServiceImpl(uri.toString(), defaultWorkspace.toString(), brc,
+// ItemInfoCacheImpl.DEFAULT_CACHE_SIZE) {
+//
+// @Override
+// protected HttpContext getContext(SessionInfo sessionInfo) throws RepositoryException {
+// HttpClientContext result = HttpClientContext.create();
+// result.setAuthCache(new NonSerialBasicAuthCache());
+// return result;
+// }
+//
+// };
+ return null;
+ }
+ };
+ return RepositoryImpl.create(
+ new Jcr2spiRepositoryFactory.RepositoryConfigImpl(repositoryServiceFactory, parameters));
+ }
+ };
+ Map<String, String> params = new HashMap<String, String>();
+ params.put(JACKRABBIT_DAVEX_URI, repoUri.toString());
+ // FIXME make it configurable
+ params.put(JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, "sys");
+
+ try {
+ repository = repositoryFactory.getRepository(params);
+ if (repository != null)
+ session = repository.login(workspace);
+ else
+ throw new IllegalArgumentException("Repository " + repoUri + " not found");
+ } catch (RepositoryException e) {
+ e.printStackTrace();
+ }
+
+ } else {
+ Path path = Paths.get(uri.getPath());
+ }
+
+ try {
+ Node rootNode = session.getRootNode();
+ NodeIterator nit = rootNode.getNodes();
+ while (nit.hasNext()) {
+ System.out.println(nit.nextNode().getPath());
+ }
+
+ Node newNode = JcrUtils.mkdirs(rootNode, "dir/subdir");
+ System.out.println("Created folder " + newNode.getPath());
+ Node newFile = JcrUtils.copyBytesAsFile(newNode, "test.txt", "TEST".getBytes());
+ System.out.println("Created file " + newFile.getPath());
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(JcrUtils.getFileAsStream(newFile)))) {
+ System.out.println("Read " + reader.readLine());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ newNode.getParent().remove();
+ System.out.println("Removed new nodes");
+ } catch (RepositoryException e) {
+ e.printStackTrace();
+ }
+ }
+}
--- /dev/null
+package org.argeo.jackrabbit.client;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.http.HttpHost;
+import org.apache.http.auth.AuthScheme;
+import org.apache.http.client.AuthCache;
+
+/**
+ * Implementation of {@link AuthCache} which doesn't use serialization, as it is
+ * not supported by GraalVM at this stage.
+ */
+public class NonSerialBasicAuthCache implements AuthCache {
+ private final Map<HttpHost, AuthScheme> cache;
+
+ public NonSerialBasicAuthCache() {
+ cache = new ConcurrentHashMap<HttpHost, AuthScheme>();
+ }
+
+ @Override
+ public void put(HttpHost host, AuthScheme authScheme) {
+ cache.put(host, authScheme);
+ }
+
+ @Override
+ public AuthScheme get(HttpHost host) {
+ return cache.get(host);
+ }
+
+ @Override
+ public void remove(HttpHost host) {
+ cache.remove(host);
+ }
+
+ @Override
+ public void clear() {
+ cache.clear();
+ }
+
+}
--- /dev/null
+package org.argeo.jackrabbit.fs;
+
+import org.argeo.jcr.fs.JcrFileSystemProvider;
+
+public abstract class AbstractJackrabbitFsProvider extends JcrFileSystemProvider {
+
+}
--- /dev/null
+package org.argeo.jackrabbit.fs;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystemAlreadyExistsException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+
+import org.argeo.jackrabbit.client.ClientDavexRepositoryFactory;
+import org.argeo.jcr.fs.JcrFileSystem;
+import org.argeo.jcr.fs.JcrFsException;
+
+/**
+ * A file system provider based on a JCR repository remotely accessed via the
+ * DAVEX protocol.
+ */
+public class DavexFsProvider extends AbstractJackrabbitFsProvider {
+ final static String DEFAULT_JACKRABBIT_REMOTE_DEFAULT_WORKSPACE = "sys";
+
+ private Map<String, JcrFileSystem> fileSystems = new HashMap<>();
+
+ @Override
+ public String getScheme() {
+ return "davex";
+ }
+
+ @Override
+ public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
+ if (uri.getHost() == null)
+ throw new IllegalArgumentException("An host should be provided");
+ try {
+ URI repoUri = new URI("http", uri.getUserInfo(), uri.getHost(), uri.getPort(), uri.getPath(), null, null);
+ String repoKey = repoUri.toString();
+ if (fileSystems.containsKey(repoKey))
+ throw new FileSystemAlreadyExistsException("CMS file system already exists for " + repoKey);
+ RepositoryFactory repositoryFactory = new ClientDavexRepositoryFactory();
+ return tryGetRepo(repositoryFactory, repoUri, "home");
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Cannot open file system " + uri, e);
+ }
+ }
+
+ private JcrFileSystem tryGetRepo(RepositoryFactory repositoryFactory, URI repoUri, String workspace)
+ throws IOException {
+ Map<String, String> params = new HashMap<String, String>();
+ params.put(ClientDavexRepositoryFactory.JACKRABBIT_DAVEX_URI, repoUri.toString());
+ // TODO better integrate with OSGi or other configuration than system
+ // properties.
+ String remoteDefaultWorkspace = System.getProperty(
+ ClientDavexRepositoryFactory.JACKRABBIT_REMOTE_DEFAULT_WORKSPACE,
+ DEFAULT_JACKRABBIT_REMOTE_DEFAULT_WORKSPACE);
+ params.put(ClientDavexRepositoryFactory.JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, remoteDefaultWorkspace);
+ Repository repository = null;
+ Session session = null;
+ try {
+ repository = repositoryFactory.getRepository(params);
+ if (repository != null)
+ session = repository.login(workspace);
+ } catch (Exception e) {
+ // silent
+ }
+
+ if (session == null) {
+ if (repoUri.getPath() == null || repoUri.getPath().equals("/"))
+ return null;
+ String repoUriStr = repoUri.toString();
+ if (repoUriStr.endsWith("/"))
+ repoUriStr = repoUriStr.substring(0, repoUriStr.length() - 1);
+ String nextRepoUriStr = repoUriStr.substring(0, repoUriStr.lastIndexOf('/'));
+ String nextWorkspace = repoUriStr.substring(repoUriStr.lastIndexOf('/') + 1);
+ URI nextUri;
+ try {
+ nextUri = new URI(nextRepoUriStr);
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Badly formatted URI", e);
+ }
+ return tryGetRepo(repositoryFactory, nextUri, nextWorkspace);
+ } else {
+ JcrFileSystem fileSystem = new JcrFileSystem(this, repository);
+ fileSystems.put(repoUri.toString() + "/" + workspace, fileSystem);
+ return fileSystem;
+ }
+ }
+
+ @Override
+ public FileSystem getFileSystem(URI uri) {
+ return currentUserFileSystem(uri);
+ }
+
+ @Override
+ public Path getPath(URI uri) {
+ JcrFileSystem fileSystem = currentUserFileSystem(uri);
+ if (fileSystem == null)
+ try {
+ fileSystem = (JcrFileSystem) newFileSystem(uri, new HashMap<String, Object>());
+ if (fileSystem == null)
+ throw new IllegalArgumentException("No file system found for " + uri);
+ } catch (IOException e) {
+ throw new JcrFsException("Could not autocreate file system", e);
+ }
+ URI repoUri = null;
+ try {
+ repoUri = new URI("http", uri.getUserInfo(), uri.getHost(), uri.getPort(), uri.getPath(), null, null);
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ String uriStr = repoUri.toString();
+ String localPath = null;
+ for (String key : fileSystems.keySet()) {
+ if (uriStr.startsWith(key)) {
+ localPath = uriStr.toString().substring(key.length());
+ }
+ }
+ if ("".equals(localPath))
+ localPath = "/";
+ return fileSystem.getPath(localPath);
+ }
+
+ private JcrFileSystem currentUserFileSystem(URI uri) {
+ for (String key : fileSystems.keySet()) {
+ if (uri.toString().startsWith(key))
+ return fileSystems.get(key);
+ }
+ return null;
+ }
+
+ public static void main(String args[]) {
+ try {
+ DavexFsProvider fsProvider = new DavexFsProvider();
+ Path path = fsProvider.getPath(new URI("davex://root:demo@localhost:7070/jcr/ego/"));
+ System.out.println(path);
+ DirectoryStream<Path> ds = Files.newDirectoryStream(path);
+ for (Path p : ds) {
+ System.out.println("- " + p);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+}
--- /dev/null
+package org.argeo.jackrabbit.fs;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Credentials;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+
+import org.apache.jackrabbit.core.RepositoryImpl;
+import org.apache.jackrabbit.core.config.RepositoryConfig;
+import org.argeo.jcr.fs.JcrFileSystem;
+import org.argeo.jcr.fs.JcrFsException;
+
+public class JackrabbitMemoryFsProvider extends AbstractJackrabbitFsProvider {
+ private RepositoryImpl repository;
+ private JcrFileSystem fileSystem;
+
+ private Credentials credentials;
+
+ public JackrabbitMemoryFsProvider() {
+ String username = System.getProperty("user.name");
+ credentials = new SimpleCredentials(username, username.toCharArray());
+ }
+
+ @Override
+ public String getScheme() {
+ return "jcr+memory";
+ }
+
+ @Override
+ public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
+ try {
+ Path tempDir = Files.createTempDirectory("fs-memory");
+ URL confUrl = JackrabbitMemoryFsProvider.class.getResource("fs-memory.xml");
+ RepositoryConfig repositoryConfig = RepositoryConfig.create(confUrl.toURI(), tempDir.toString());
+ repository = RepositoryImpl.create(repositoryConfig);
+ postRepositoryCreation(repository);
+ fileSystem = new JcrFileSystem(this, repository, credentials);
+ return fileSystem;
+ } catch (RepositoryException | URISyntaxException e) {
+ throw new IOException("Cannot login to repository", e);
+ }
+ }
+
+ @Override
+ public FileSystem getFileSystem(URI uri) {
+ return fileSystem;
+ }
+
+ @Override
+ public Path getPath(URI uri) {
+ String path = uri.getPath();
+ if (fileSystem == null)
+ try {
+ newFileSystem(uri, new HashMap<String, Object>());
+ } catch (IOException e) {
+ throw new JcrFsException("Could not autocreate file system", e);
+ }
+ return fileSystem.getPath(path);
+ }
+
+ public Repository getRepository() {
+ return repository;
+ }
+
+ public Session login() throws RepositoryException {
+ return getRepository().login(credentials);
+ }
+
+ /**
+ * Called after the repository has been created and before the file system is
+ * created.
+ */
+ protected void postRepositoryCreation(RepositoryImpl repositoryImpl) throws RepositoryException {
+
+ }
+}
--- /dev/null
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+ <!-- File system and datastore -->
+ <FileSystem
+ class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+
+ <!-- Workspace templates -->
+ <Workspaces rootPath="${rep.home}/workspaces"
+ defaultWorkspace="main" configRootPath="/workspaces" />
+ <Workspace name="${wsp.name}">
+ <FileSystem
+ class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+ </PersistenceManager>
+ <SearchIndex
+ class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${wsp.home}/index" />
+ <param name="directoryManagerClass"
+ value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+ <param name="extractorPoolSize" value="0" />
+ <FileSystem
+ class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+ </SearchIndex>
+ </Workspace>
+
+ <!-- Versioning -->
+ <Versioning rootPath="${rep.home}/version">
+ <FileSystem
+ class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+ </PersistenceManager>
+ </Versioning>
+
+ <!-- Indexing -->
+ <SearchIndex
+ class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${rep.home}/index" />
+ <param name="directoryManagerClass"
+ value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+ <param name="extractorPoolSize" value="0" />
+ <FileSystem
+ class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+ </SearchIndex>
+
+ <!-- Security -->
+ <Security appName="Jackrabbit">
+ <LoginModule
+ class="org.apache.jackrabbit.core.security.SimpleLoginModule" />
+ <!-- <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager" -->
+ <!-- workspaceName="security" /> -->
+ <!-- <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager"
+ /> -->
+ </Security>
+</Repository>
\ No newline at end of file
--- /dev/null
+/** Java NIO file system implementation based on Jackrabbit. */
+package org.argeo.jackrabbit.fs;
\ No newline at end of file
--- /dev/null
+/** Generic Jackrabbit utilities. */
+package org.argeo.jackrabbit;
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+ <!-- Shared datasource -->
+ <DataSources>
+ <DataSource name="dataSource">
+ <param name="driver" value="org.h2.Driver" />
+ <param name="url" value="${dburl}" />
+ <param name="user" value="${dbuser}" />
+ <param name="password" value="${dbpassword}" />
+ <param name="databaseType" value="h2" />
+ <param name="maxPoolSize" value="${maxPoolSize}" />
+ </DataSource>
+ </DataSources>
+
+ <!-- File system and datastore -->
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="default" />
+ <param name="schemaObjectPrefix" value="fs_" />
+ </FileSystem>
+ <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
+ <param name="path" value="${rep.home}/datastore" />
+ </DataStore>
+
+ <!-- Workspace templates -->
+ <Workspaces rootPath="${rep.home}/workspaces"
+ defaultWorkspace="${defaultWorkspace}" />
+ <Workspace name="${wsp.name}">
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="default" />
+ <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${wsp.home}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+ </SearchIndex>
+ <WorkspaceSecurity>
+ <AccessControlProvider
+ class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+ </WorkspaceSecurity>
+ </Workspace>
+
+ <!-- Versioning -->
+ <Versioning rootPath="${rep.home}/version">
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="default" />
+ <param name="schemaObjectPrefix" value="fs_ver_" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schemaObjectPrefix" value="pm_ver_" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ </Versioning>
+
+ <!-- Indexing -->
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${rep.home}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+ </SearchIndex>
+
+ <!-- Security -->
+ <Security appName="Jackrabbit">
+ <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+ workspaceName="security" />
+ <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+ </Security>
+</Repository>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+ <!-- File system and datastore -->
+ <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+ <param name="path" value="${rep.home}/repository" />
+ </FileSystem>
+ <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
+ <param name="path" value="${rep.home}/datastore" />
+ </DataStore>
+
+ <!-- Workspace templates -->
+ <Workspaces rootPath="${rep.home}/workspaces"
+ defaultWorkspace="${defaultWorkspace}" />
+ <Workspace name="${wsp.name}">
+ <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+ <param name="path" value="${wsp.home}" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${wsp.home}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+ </SearchIndex>
+ <WorkspaceSecurity>
+ <AccessControlProvider
+ class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+ </WorkspaceSecurity>
+ </Workspace>
+
+ <!-- Versioning -->
+ <Versioning rootPath="${rep.home}/version">
+ <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+ <param name="path" value="${rep.home}/version" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ </Versioning>
+
+ <!-- Indexing -->
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${rep.home}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+ </SearchIndex>
+
+ <!-- Security -->
+ <Security appName="Jackrabbit">
+ <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+ workspaceName="security" />
+ <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+ </Security>
+</Repository>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+ <!-- File system and datastore -->
+ <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+
+ <!-- Workspace templates -->
+ <Workspaces rootPath="${rep.home}/workspaces"
+ defaultWorkspace="${defaultWorkspace}" configRootPath="/workspaces" />
+ <Workspace name="${wsp.name}">
+ <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+ <param name="blobFSBlockSize" value="1" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${wsp.home}/index" />
+ <param name="directoryManagerClass"
+ value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+ <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+ </SearchIndex>
+ </Workspace>
+
+ <!-- Versioning -->
+ <Versioning rootPath="${rep.home}/version">
+ <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+ <param name="blobFSBlockSize" value="1" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ </Versioning>
+
+ <!-- Indexing -->
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${rep.home}/index" />
+ <param name="directoryManagerClass"
+ value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+ <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+ </SearchIndex>
+
+ <!-- Security -->
+ <Security appName="Jackrabbit">
+ <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+ workspaceName="security" />
+ <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+ </Security>
+</Repository>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+ <!-- Shared datasource -->
+ <DataSources>
+ <DataSource name="dataSource">
+ <param name="driver" value="org.postgresql.Driver" />
+ <param name="url" value="${dburl}" />
+ <param name="user" value="${dbuser}" />
+ <param name="password" value="${dbpassword}" />
+ <param name="databaseType" value="postgresql" />
+ <param name="maxPoolSize" value="${maxPoolSize}" />
+ </DataSource>
+ </DataSources>
+
+ <!-- File system and datastore -->
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="postgresql" />
+ <param name="schemaObjectPrefix" value="fs_" />
+ </FileSystem>
+ <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
+ <param name="path" value="${rep.home}/datastore" />
+ </DataStore>
+
+ <!-- Workspace templates -->
+ <Workspaces rootPath="${rep.home}/workspaces"
+ defaultWorkspace="${defaultWorkspace}" />
+ <Workspace name="${wsp.name}">
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="postgresql" />
+ <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${wsp.home}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+ </SearchIndex>
+ <WorkspaceSecurity>
+ <AccessControlProvider
+ class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+ </WorkspaceSecurity>
+ </Workspace>
+
+ <!-- Versioning -->
+ <Versioning rootPath="${rep.home}/version">
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="postgresql" />
+ <param name="schemaObjectPrefix" value="fs_ver_" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schemaObjectPrefix" value="pm_ver_" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ </Versioning>
+
+ <!-- Indexing -->
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${rep.home}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+ </SearchIndex>
+
+ <!-- Security -->
+ <Security appName="Jackrabbit">
+ <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+ workspaceName="security" />
+ <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+ </Security>
+</Repository>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+ <!-- Shared datasource -->
+ <DataSources>
+ <DataSource name="dataSource">
+ <param name="driver" value="org.postgresql.Driver" />
+ <param name="url" value="${dburl}" />
+ <param name="user" value="${dbuser}" />
+ <param name="password" value="${dbpassword}" />
+ <param name="databaseType" value="postgresql" />
+ <param name="maxPoolSize" value="${maxPoolSize}" />
+ </DataSource>
+ </DataSources>
+
+ <!-- File system and datastore -->
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="postgresql" />
+ <param name="schemaObjectPrefix" value="fs_" />
+ </FileSystem>
+
+ <!-- Workspace templates -->
+ <Workspaces rootPath="${rep.home}/workspaces"
+ defaultWorkspace="${defaultWorkspace}" />
+ <Workspace name="${wsp.name}">
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="postgresql" />
+ <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${wsp.home}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+ </SearchIndex>
+ <WorkspaceSecurity>
+ <AccessControlProvider
+ class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+ </WorkspaceSecurity>
+ </Workspace>
+
+ <!-- Versioning -->
+ <Versioning rootPath="${rep.home}/version">
+ <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schema" value="postgresql" />
+ <param name="schemaObjectPrefix" value="fs_ver_" />
+ </FileSystem>
+ <PersistenceManager
+ class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+ <param name="dataSourceName" value="dataSource" />
+ <param name="schemaObjectPrefix" value="pm_ver_" />
+ <param name="bundleCacheSize" value="${bundleCacheMB}" />
+ </PersistenceManager>
+ </Versioning>
+
+ <!-- Indexing -->
+ <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+ <param name="path" value="${rep.home}/index" />
+ <param name="extractorPoolSize" value="${extractorPoolSize}" />
+ <param name="cacheSize" value="${searchCacheSize}" />
+ <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+ </SearchIndex>
+
+ <!-- Security -->
+ <Security appName="Jackrabbit">
+ <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+ workspaceName="security" />
+ <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+ </Security>
+</Repository>
\ No newline at end of file
--- /dev/null
+package org.argeo.jackrabbit.security;
+
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.security.Privilege;
+
+import org.apache.jackrabbit.api.security.JackrabbitAccessControlList;
+import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrUtils;
+
+/** Utilities around Jackrabbit security extensions. */
+public class JackrabbitSecurityUtils {
+ private final static CmsLog log = CmsLog.getLog(JackrabbitSecurityUtils.class);
+
+ /**
+ * Convenience method for denying a single privilege to a principal (user or
+ * role), typically jcr:all
+ */
+ public synchronized static void denyPrivilege(Session session, String path, String principal, String privilege)
+ throws RepositoryException {
+ List<Privilege> privileges = new ArrayList<Privilege>();
+ privileges.add(session.getAccessControlManager().privilegeFromName(privilege));
+ denyPrivileges(session, path, () -> principal, privileges);
+ }
+
+ /**
+ * Deny privileges on a path to a {@link Principal}. The path must already
+ * exist. Session is saved. Synchronized to prevent concurrent modifications of
+ * the same node.
+ */
+ public synchronized static Boolean denyPrivileges(Session session, String path, Principal principal,
+ List<Privilege> privs) throws RepositoryException {
+ // make sure the session is in line with the persisted state
+ session.refresh(false);
+ JackrabbitAccessControlManager acm = (JackrabbitAccessControlManager) session.getAccessControlManager();
+ JackrabbitAccessControlList acl = (JackrabbitAccessControlList) JcrUtils.getAccessControlList(acm, path);
+
+// accessControlEntries: for (AccessControlEntry ace : acl.getAccessControlEntries()) {
+// Principal currentPrincipal = ace.getPrincipal();
+// if (currentPrincipal.getName().equals(principal.getName())) {
+// Privilege[] currentPrivileges = ace.getPrivileges();
+// if (currentPrivileges.length != privs.size())
+// break accessControlEntries;
+// for (int i = 0; i < currentPrivileges.length; i++) {
+// Privilege currP = currentPrivileges[i];
+// Privilege p = privs.get(i);
+// if (!currP.getName().equals(p.getName())) {
+// break accessControlEntries;
+// }
+// }
+// return false;
+// }
+// }
+
+ Privilege[] privileges = privs.toArray(new Privilege[privs.size()]);
+ acl.addEntry(principal, privileges, false);
+ acm.setPolicy(path, acl);
+ if (log.isDebugEnabled()) {
+ StringBuffer privBuf = new StringBuffer();
+ for (Privilege priv : privs)
+ privBuf.append(priv.getName());
+ log.debug("Denied privileges " + privBuf + " to " + principal.getName() + " on " + path + " in '"
+ + session.getWorkspace().getName() + "'");
+ }
+ session.refresh(true);
+ session.save();
+ return true;
+ }
+
+ /** Singleton. */
+ private JackrabbitSecurityUtils() {
+
+ }
+}
--- /dev/null
+/** Generic Jackrabbit security utilities. */
+package org.argeo.jackrabbit.security;
\ No newline at end of file
--- /dev/null
+package org.argeo.jcr;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.jcr.Binary;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+
+/**
+ * A {@link Binary} wrapper implementing {@link AutoCloseable} for ease of use
+ * in try/catch blocks.
+ */
+public class Bin implements Binary, AutoCloseable {
+ private final Binary wrappedBinary;
+
+ public Bin(Property property) throws RepositoryException {
+ this(property.getBinary());
+ }
+
+ public Bin(Binary wrappedBinary) {
+ if (wrappedBinary == null)
+ throw new IllegalArgumentException("Wrapped binary cannot be null");
+ this.wrappedBinary = wrappedBinary;
+ }
+
+ // private static Binary getBinary(Property property) throws IOException {
+ // try {
+ // return property.getBinary();
+ // } catch (RepositoryException e) {
+ // throw new IOException("Cannot get binary from property " + property, e);
+ // }
+ // }
+
+ @Override
+ public void close() {
+ dispose();
+ }
+
+ @Override
+ public InputStream getStream() throws RepositoryException {
+ return wrappedBinary.getStream();
+ }
+
+ @Override
+ public int read(byte[] b, long position) throws IOException, RepositoryException {
+ return wrappedBinary.read(b, position);
+ }
+
+ @Override
+ public long getSize() throws RepositoryException {
+ return wrappedBinary.getSize();
+ }
+
+ @Override
+ public void dispose() {
+ wrappedBinary.dispose();
+ }
+
+}
--- /dev/null
+package org.argeo.jcr;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+
+/** Wraps a collection of nodes in order to read it as a {@link NodeIterator} */
+public class CollectionNodeIterator implements NodeIterator {
+ private final Long collectionSize;
+ private final Iterator<Node> iterator;
+ private Integer position = 0;
+
+ public CollectionNodeIterator(Collection<Node> nodes) {
+ super();
+ this.collectionSize = (long) nodes.size();
+ this.iterator = nodes.iterator();
+ }
+
+ public void skip(long skipNum) {
+ if (skipNum < 0)
+ throw new IllegalArgumentException(
+ "Skip count has to be positive: " + skipNum);
+
+ for (long i = 0; i < skipNum; i++) {
+ if (!hasNext())
+ throw new NoSuchElementException("Last element past (position="
+ + getPosition() + ")");
+ nextNode();
+ }
+ }
+
+ public long getSize() {
+ return collectionSize;
+ }
+
+ public long getPosition() {
+ return position;
+ }
+
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ public Object next() {
+ return nextNode();
+ }
+
+ public void remove() {
+ iterator.remove();
+ }
+
+ public Node nextNode() {
+ Node node = iterator.next();
+ position++;
+ return node;
+ }
+
+}
--- /dev/null
+package org.argeo.jcr;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.observation.Event;
+import javax.jcr.observation.EventIterator;
+import javax.jcr.observation.EventListener;
+import javax.jcr.observation.ObservationManager;
+
+import org.argeo.api.cms.CmsLog;
+
+/** To be overridden */
+public class DefaultJcrListener implements EventListener {
+ private final static CmsLog log = CmsLog.getLog(DefaultJcrListener.class);
+ private Session session;
+ private String path = "/";
+ private Boolean deep = true;
+
+ public void start() {
+ try {
+ addEventListener(session().getWorkspace().getObservationManager());
+ if (log.isDebugEnabled())
+ log.debug("Registered JCR event listener on " + path);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot register event listener", e);
+ }
+ }
+
+ public void stop() {
+ try {
+ session().getWorkspace().getObservationManager()
+ .removeEventListener(this);
+ if (log.isDebugEnabled())
+ log.debug("Unregistered JCR event listener on " + path);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot unregister event listener", e);
+ }
+ }
+
+ /** Default is listen to all events */
+ protected Integer getEvents() {
+ return Event.NODE_ADDED | Event.NODE_REMOVED | Event.PROPERTY_ADDED
+ | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED;
+ }
+
+ /** To be overidden */
+ public void onEvent(EventIterator events) {
+ while (events.hasNext()) {
+ Event event = events.nextEvent();
+ log.debug(event);
+ }
+ }
+
+ /** To be overidden */
+ protected void addEventListener(ObservationManager observationManager)
+ throws RepositoryException {
+ observationManager.addEventListener(this, getEvents(), path, deep,
+ null, null, false);
+ }
+
+ private Session session() {
+ return session;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public void setDeep(Boolean deep) {
+ this.deep = deep;
+ }
+
+ public void setSession(Session session) {
+ this.session = session;
+ }
+
+}
--- /dev/null
+package org.argeo.jcr;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.text.MessageFormat;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.jcr.Binary;
+import javax.jcr.ItemNotFoundException;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.Workspace;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.query.Query;
+import javax.jcr.query.QueryManager;
+import javax.jcr.query.Row;
+import javax.jcr.security.Privilege;
+import javax.jcr.version.Version;
+import javax.jcr.version.VersionHistory;
+import javax.jcr.version.VersionIterator;
+import javax.jcr.version.VersionManager;
+
+import org.apache.commons.io.IOUtils;
+
+/**
+ * Utility class whose purpose is to make using JCR less verbose by
+ * systematically using unchecked exceptions and returning <code>null</code>
+ * when something is not found. This is especially useful when writing user
+ * interfaces (such as with SWT) where listeners and callbacks expect unchecked
+ * exceptions. Loosely inspired by Java's <code>Files</code> singleton.
+ */
+public class Jcr {
+ /**
+ * The name of a node which will be serialized as XML text, as per section 7.3.1
+ * of the JCR 2.0 specifications.
+ */
+ public final static String JCR_XMLTEXT = "jcr:xmltext";
+ /**
+ * The name of a property which will be serialized as XML text, as per section
+ * 7.3.1 of the JCR 2.0 specifications.
+ */
+ public final static String JCR_XMLCHARACTERS = "jcr:xmlcharacters";
+ /**
+ * <code>jcr:name</code>, when used in another context than
+ * {@link Property#JCR_NAME}, typically to name a node rather than a property.
+ */
+ public final static String JCR_NAME = "jcr:name";
+ /**
+ * <code>jcr:path</code>, when used in another context than
+ * {@link Property#JCR_PATH}, typically to name a node rather than a property.
+ */
+ public final static String JCR_PATH = "jcr:path";
+ /**
+ * <code>jcr:primaryType</code> with prefix instead of namespace (as in
+ * {@link Property#JCR_PRIMARY_TYPE}.
+ */
+ public final static String JCR_PRIMARY_TYPE = "jcr:primaryType";
+ /**
+ * <code>jcr:mixinTypes</code> with prefix instead of namespace (as in
+ * {@link Property#JCR_MIXIN_TYPES}.
+ */
+ public final static String JCR_MIXIN_TYPES = "jcr:mixinTypes";
+ /**
+ * <code>jcr:uuid</code> with prefix instead of namespace (as in
+ * {@link Property#JCR_UUID}.
+ */
+ public final static String JCR_UUID = "jcr:uuid";
+ /**
+ * <code>jcr:created</code> with prefix instead of namespace (as in
+ * {@link Property#JCR_CREATED}.
+ */
+ public final static String JCR_CREATED = "jcr:created";
+ /**
+ * <code>jcr:createdBy</code> with prefix instead of namespace (as in
+ * {@link Property#JCR_CREATED_BY}.
+ */
+ public final static String JCR_CREATED_BY = "jcr:createdBy";
+ /**
+ * <code>jcr:lastModified</code> with prefix instead of namespace (as in
+ * {@link Property#JCR_LAST_MODIFIED}.
+ */
+ public final static String JCR_LAST_MODIFIED = "jcr:lastModified";
+ /**
+ * <code>jcr:lastModifiedBy</code> with prefix instead of namespace (as in
+ * {@link Property#JCR_LAST_MODIFIED_BY}.
+ */
+ public final static String JCR_LAST_MODIFIED_BY = "jcr:lastModifiedBy";
+
+ /**
+ * @see Node#isNodeType(String)
+ * @throws JcrException caused by {@link RepositoryException}
+ */
+ public static boolean isNodeType(Node node, String nodeTypeName) {
+ try {
+ return node.isNodeType(nodeTypeName);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get whether " + node + " is of type " + nodeTypeName, e);
+ }
+ }
+
+ /**
+ * @see Node#hasNodes()
+ * @throws JcrException caused by {@link RepositoryException}
+ */
+ public static boolean hasNodes(Node node) {
+ try {
+ return node.hasNodes();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get whether " + node + " has children.", e);
+ }
+ }
+
+ /**
+ * @see Node#getParent()
+ * @throws JcrException caused by {@link RepositoryException}
+ */
+ public static Node getParent(Node node) {
+ try {
+ return isRoot(node) ? null : node.getParent();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get parent of " + node, e);
+ }
+ }
+
+ /**
+ * @see Node#getParent()
+ * @throws JcrException caused by {@link RepositoryException}
+ */
+ public static String getParentPath(Node node) {
+ return getPath(getParent(node));
+ }
+
+ /**
+ * Whether this node is the root node.
+ *
+ * @throws JcrException caused by {@link RepositoryException}
+ */
+ public static boolean isRoot(Node node) {
+ try {
+ return node.getDepth() == 0;
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get depth of " + node, e);
+ }
+ }
+
+ /**
+ * @see Node#getPath()
+ * @throws JcrException caused by {@link RepositoryException}
+ */
+ public static String getPath(Node node) {
+ try {
+ return node.getPath();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get path of " + node, e);
+ }
+ }
+
+ /**
+ * @see Node#getSession()
+ * @see Session#getWorkspace()
+ * @see Workspace#getName()
+ */
+ public static String getWorkspaceName(Node node) {
+ return session(node).getWorkspace().getName();
+ }
+
+ /**
+ * @see Node#getIdentifier()
+ * @throws JcrException caused by {@link RepositoryException}
+ */
+ public static String getIdentifier(Node node) {
+ try {
+ return node.getIdentifier();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get identifier of " + node, e);
+ }
+ }
+
+ /**
+ * @see Node#getName()
+ * @throws JcrException caused by {@link RepositoryException}
+ */
+ public static String getName(Node node) {
+ try {
+ return node.getName();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get name of " + node, e);
+ }
+ }
+
+ /**
+ * Returns the node name with its current index (useful for re-ordering).
+ *
+ * @see Node#getName()
+ * @see Node#getIndex()
+ * @throws JcrException caused by {@link RepositoryException}
+ */
+ public static String getIndexedName(Node node) {
+ try {
+ return node.getName() + "[" + node.getIndex() + "]";
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get name of " + node, e);
+ }
+ }
+
+ /**
+ * @see Node#getProperty(String)
+ * @throws JcrException caused by {@link RepositoryException}
+ */
+ public static Property getProperty(Node node, String property) {
+ try {
+ if (node.hasProperty(property))
+ return node.getProperty(property);
+ else
+ return null;
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get property " + property + " of " + node, e);
+ }
+ }
+
+ /**
+ * @see Node#getIndex()
+ * @throws JcrException caused by {@link RepositoryException}
+ */
+ public static int getIndex(Node node) {
+ try {
+ return node.getIndex();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get index of " + node, e);
+ }
+ }
+
+ /**
+ * If node has mixin {@link NodeType#MIX_TITLE}, return
+ * {@link Property#JCR_TITLE}, otherwise return {@link #getName(Node)}.
+ */
+ public static String getTitle(Node node) {
+ if (Jcr.isNodeType(node, NodeType.MIX_TITLE))
+ return get(node, Property.JCR_TITLE);
+ else
+ return Jcr.getName(node);
+ }
+
+ /** Accesses a {@link NodeIterator} as an {@link Iterable}. */
+ @SuppressWarnings("unchecked")
+ public static Iterable<Node> iterate(NodeIterator nodeIterator) {
+ return new Iterable<Node>() {
+
+ @Override
+ public Iterator<Node> iterator() {
+ return nodeIterator;
+ }
+ };
+ }
+
+ /**
+ * @return the children as an {@link Iterable} for use in for-each llops.
+ * @see Node#getNodes()
+ * @throws JcrException caused by {@link RepositoryException}
+ */
+ public static Iterable<Node> nodes(Node node) {
+ try {
+ return iterate(node.getNodes());
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get children of " + node, e);
+ }
+ }
+
+ /**
+ * @return the children as a (possibly empty) {@link List}.
+ * @see Node#getNodes()
+ * @throws JcrException caused by {@link RepositoryException}
+ */
+ public static List<Node> getNodes(Node node) {
+ List<Node> nodes = new ArrayList<>();
+ try {
+ if (node.hasNodes()) {
+ NodeIterator nit = node.getNodes();
+ while (nit.hasNext())
+ nodes.add(nit.nextNode());
+ return nodes;
+ } else
+ return nodes;
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get children of " + node, e);
+ }
+ }
+
+ /**
+ * @return the child or <code>null</node> if not found
+ * @see Node#getNode(String)
+ * @throws JcrException caused by {@link RepositoryException}
+ */
+ public static Node getNode(Node node, String child) {
+ try {
+ if (node.hasNode(child))
+ return node.getNode(child);
+ else
+ return null;
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get child of " + node, e);
+ }
+ }
+
+ /**
+ * @return the node at this path or <code>null</node> if not found
+ * @see Session#getNode(String)
+ * @throws JcrException caused by {@link RepositoryException}
+ */
+ public static Node getNode(Session session, String path) {
+ try {
+ if (session.nodeExists(path))
+ return session.getNode(path);
+ else
+ return null;
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get node " + path, e);
+ }
+ }
+
+ /**
+ * Add a node to this parent, setting its primary type and its mixins.
+ *
+ * @param parent the parent node
+ * @param name the name of the node, if <code>null</code>, the primary
+ * type will be used (typically for XML structures)
+ * @param primaryType the primary type, if <code>null</code>
+ * {@link NodeType#NT_UNSTRUCTURED} will be used.
+ * @param mixins the mixins
+ * @return the created node
+ * @see Node#addNode(String, String)
+ * @see Node#addMixin(String)
+ */
+ public static Node addNode(Node parent, String name, String primaryType, String... mixins) {
+ if (name == null && primaryType == null)
+ throw new IllegalArgumentException("Both node name and primary type cannot be null");
+ try {
+ Node newNode = parent.addNode(name == null ? primaryType : name,
+ primaryType == null ? NodeType.NT_UNSTRUCTURED : primaryType);
+ for (String mixin : mixins) {
+ newNode.addMixin(mixin);
+ }
+ return newNode;
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot add node " + name + " to " + parent, e);
+ }
+ }
+
+ /**
+ * Add an {@link NodeType#NT_BASE} node to this parent.
+ *
+ * @param parent the parent node
+ * @param name the name of the node, cannot be <code>null</code>
+ * @return the created node
+ *
+ * @see Node#addNode(String)
+ */
+ public static Node addNode(Node parent, String name) {
+ if (name == null)
+ throw new IllegalArgumentException("Node name cannot be null");
+ try {
+ Node newNode = parent.addNode(name);
+ return newNode;
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot add node " + name + " to " + parent, e);
+ }
+ }
+
+ /**
+ * Add mixins to a node.
+ *
+ * @param node the node
+ * @param mixins the mixins
+ * @see Node#addMixin(String)
+ */
+ public static void addMixin(Node node, String... mixins) {
+ try {
+ for (String mixin : mixins) {
+ node.addMixin(mixin);
+ }
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot add mixins " + Arrays.asList(mixins) + " to " + node, e);
+ }
+ }
+
+ /**
+ * Removes this node.
+ *
+ * @see Node#remove()
+ */
+ public static void remove(Node node) {
+ try {
+ node.remove();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot remove node " + node, e);
+ }
+ }
+
+ /**
+ * @return the node with htis id or <code>null</node> if not found
+ * @see Session#getNodeByIdentifier(String)
+ * @throws JcrException caused by {@link RepositoryException}
+ */
+ public static Node getNodeById(Session session, String id) {
+ try {
+ return session.getNodeByIdentifier(id);
+ } catch (ItemNotFoundException e) {
+ return null;
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get node with id " + id, e);
+ }
+ }
+
+ /**
+ * Set a property to the given value, or remove it if the value is
+ * <code>null</code>.
+ *
+ * @throws JcrException caused by {@link RepositoryException}
+ */
+ public static void set(Node node, String property, Object value) {
+ try {
+ if (!node.hasProperty(property)) {
+ if (value != null) {
+ if (value instanceof List) {// multiple
+ List<?> lst = (List<?>) value;
+ String[] values = new String[lst.size()];
+ for (int i = 0; i < lst.size(); i++) {
+ values[i] = lst.get(i).toString();
+ }
+ node.setProperty(property, values);
+ } else {
+ node.setProperty(property, value.toString());
+ }
+ }
+ return;
+ }
+ Property prop = node.getProperty(property);
+ if (value == null) {
+ prop.remove();
+ return;
+ }
+
+ // multiple
+ if (value instanceof List) {
+ List<?> lst = (List<?>) value;
+ String[] values = new String[lst.size()];
+ // TODO better cast?
+ for (int i = 0; i < lst.size(); i++) {
+ values[i] = lst.get(i).toString();
+ }
+ if (!prop.isMultiple())
+ prop.remove();
+ node.setProperty(property, values);
+ return;
+ }
+
+ // single
+ if (prop.isMultiple()) {
+ prop.remove();
+ node.setProperty(property, value.toString());
+ return;
+ }
+
+ if (value instanceof String)
+ prop.setValue((String) value);
+ else if (value instanceof Long)
+ prop.setValue((Long) value);
+ else if (value instanceof Integer)
+ prop.setValue(((Integer) value).longValue());
+ else if (value instanceof Double)
+ prop.setValue((Double) value);
+ else if (value instanceof Float)
+ prop.setValue(((Float) value).doubleValue());
+ else if (value instanceof Calendar)
+ prop.setValue((Calendar) value);
+ else if (value instanceof BigDecimal)
+ prop.setValue((BigDecimal) value);
+ else if (value instanceof Boolean)
+ prop.setValue((Boolean) value);
+ else if (value instanceof byte[])
+ JcrUtils.setBinaryAsBytes(prop, (byte[]) value);
+ else if (value instanceof Instant) {
+ Instant instant = (Instant) value;
+ GregorianCalendar calendar = new GregorianCalendar();
+ calendar.setTime(Date.from(instant));
+ prop.setValue(calendar);
+ } else // try with toString()
+ prop.setValue(value.toString());
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot set property " + property + " of " + node + " to " + value, e);
+ }
+ }
+
+ /**
+ * Get property as {@link String}.
+ *
+ * @return the value of
+ * {@link Node#getProperty(String)}.{@link Property#getString()} or
+ * <code>null</code> if the property does not exist.
+ * @throws JcrException caused by {@link RepositoryException}
+ */
+ public static String get(Node node, String property) {
+ return get(node, property, null);
+ }
+
+ /**
+ * Get property as a {@link String}. If the property is multiple it returns the
+ * first value.
+ *
+ * @return the value of
+ * {@link Node#getProperty(String)}.{@link Property#getString()} or
+ * <code>defaultValue</code> if the property does not exist.
+ * @throws JcrException caused by {@link RepositoryException}
+ */
+ public static String get(Node node, String property, String defaultValue) {
+ try {
+ if (node.hasProperty(property)) {
+ Property p = node.getProperty(property);
+ if (!p.isMultiple())
+ return p.getString();
+ else {
+ Value[] values = p.getValues();
+ if (values.length == 0)
+ return defaultValue;
+ else
+ return values[0].getString();
+ }
+ } else
+ return defaultValue;
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot retrieve property " + property + " from " + node, e);
+ }
+ }
+
+ /**
+ * Get property as a {@link Value}.
+ *
+ * @return {@link Node#getProperty(String)} or <code>null</code> if the property
+ * does not exist.
+ * @throws JcrException caused by {@link RepositoryException}
+ */
+ public static Value getValue(Node node, String property) {
+ try {
+ if (node.hasProperty(property))
+ return node.getProperty(property).getValue();
+ else
+ return null;
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot retrieve property " + property + " from " + node, e);
+ }
+ }
+
+ /**
+ * Get property doing a best effort to cast it as the target object.
+ *
+ * @return the value of {@link Node#getProperty(String)} or
+ * <code>defaultValue</code> if the property does not exist.
+ * @throws IllegalArgumentException if the value could not be cast
+ * @throws JcrException in case of unexpected
+ * {@link RepositoryException}
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T getAs(Node node, String property, T defaultValue) {
+ try {
+ // TODO deal with multiple
+ if (node.hasProperty(property)) {
+ Property p = node.getProperty(property);
+ try {
+ if (p.isMultiple()) {
+ throw new UnsupportedOperationException("Multiple values properties are not supported");
+ }
+ Value value = p.getValue();
+ return (T) get(value);
+ } catch (ClassCastException e) {
+ throw new IllegalArgumentException(
+ "Cannot cast property of type " + PropertyType.nameFromValue(p.getType()), e);
+ }
+ } else {
+ return defaultValue;
+ }
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot retrieve property " + property + " from " + node, e);
+ }
+ }
+
+ public static <T> T getAs(Node node, String property, Class<T> clss) {
+ if (String.class.isAssignableFrom(clss)) {
+ return (T) get(node, property);
+ } else if (Long.class.isAssignableFrom(clss)) {
+ return (T) get(node, property);
+ } else {
+ throw new IllegalArgumentException("Unsupported format " + clss);
+ }
+ }
+
+ /**
+ * Get a multiple property as a list, doing a best effort to cast it as the
+ * target list.
+ *
+ * @return the value of {@link Node#getProperty(String)}.
+ * @throws IllegalArgumentException if the value could not be cast
+ * @throws JcrException in case of unexpected
+ * {@link RepositoryException}
+ */
+ public static <T> List<T> getMultiple(Node node, String property) {
+ try {
+ if (node.hasProperty(property)) {
+ Property p = node.getProperty(property);
+ return getMultiple(p);
+ } else {
+ return null;
+ }
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot retrieve multiple values property " + property + " from " + node, e);
+ }
+ }
+
+ /**
+ * Get a multiple property as a list, doing a best effort to cast it as the
+ * target list.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> List<T> getMultiple(Property p) {
+ try {
+ List<T> res = new ArrayList<>();
+ if (!p.isMultiple()) {
+ res.add((T) get(p.getValue()));
+ return res;
+ }
+ Value[] values = p.getValues();
+ for (Value value : values) {
+ res.add((T) get(value));
+ }
+ return res;
+ } catch (ClassCastException | RepositoryException e) {
+ throw new IllegalArgumentException("Cannot get property " + p, e);
+ }
+ }
+
+ /** Cast a {@link Value} to a standard Java object. */
+ public static Object get(Value value) {
+ Binary binary = null;
+ try {
+ switch (value.getType()) {
+ case PropertyType.STRING:
+ return value.getString();
+ case PropertyType.DOUBLE:
+ return (Double) value.getDouble();
+ case PropertyType.LONG:
+ return (Long) value.getLong();
+ case PropertyType.BOOLEAN:
+ return (Boolean) value.getBoolean();
+ case PropertyType.DATE:
+ return value.getDate();
+ case PropertyType.BINARY:
+ binary = value.getBinary();
+ byte[] arr = null;
+ try (InputStream in = binary.getStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();) {
+ IOUtils.copy(in, out);
+ arr = out.toByteArray();
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot read binary from " + value, e);
+ }
+ return arr;
+ default:
+ return value.getString();
+ }
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot cast value from " + value, e);
+ } finally {
+ if (binary != null)
+ binary.dispose();
+ }
+ }
+
+ /**
+ * Retrieves the {@link Session} related to this node.
+ *
+ * @deprecated Use {@link #getSession(Node)} instead.
+ */
+ @Deprecated
+ public static Session session(Node node) {
+ return getSession(node);
+ }
+
+ /** Retrieves the {@link Session} related to this node. */
+ public static Session getSession(Node node) {
+ try {
+ return node.getSession();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot retrieve session related to " + node, e);
+ }
+ }
+
+ /** Retrieves the root node related to this session. */
+ public static Node getRootNode(Session session) {
+ try {
+ return session.getRootNode();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get root node for " + session, e);
+ }
+ }
+
+ /** Whether this item exists. */
+ public static boolean itemExists(Session session, String path) {
+ try {
+ return session.itemExists(path);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot check whether " + path + " exists", e);
+ }
+ }
+
+ /**
+ * Saves the {@link Session} related to this node. Note that all other unrelated
+ * modifications in this session will also be saved.
+ */
+ public static void save(Node node) {
+ try {
+ Session session = node.getSession();
+// if (node.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
+// set(node, Property.JCR_LAST_MODIFIED, Instant.now());
+// set(node, Property.JCR_LAST_MODIFIED_BY, session.getUserID());
+// }
+ if (session.hasPendingChanges())
+ session.save();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot save session related to " + node + " in workspace "
+ + session(node).getWorkspace().getName(), e);
+ }
+ }
+
+ /** Login to a JCR repository. */
+ public static Session login(Repository repository, String workspace) {
+ try {
+ return repository.login(workspace);
+ } catch (RepositoryException e) {
+ throw new IllegalArgumentException("Cannot login to repository", e);
+ }
+ }
+
+ /** Safely and silently logs out a session. */
+ public static void logout(Session session) {
+ try {
+ if (session != null)
+ if (session.isLive())
+ session.logout();
+ } catch (Exception e) {
+ // silent
+ }
+ }
+
+ /** Safely and silently logs out the underlying session. */
+ public static void logout(Node node) {
+ Jcr.logout(session(node));
+ }
+
+ /*
+ * SECURITY
+ */
+ /**
+ * Add a single privilege to a node.
+ *
+ * @see Privilege
+ */
+ public static void addPrivilege(Node node, String principal, String privilege) {
+ try {
+ Session session = node.getSession();
+ JcrUtils.addPrivilege(session, node.getPath(), principal, privilege);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot add privilege " + privilege + " to " + node, e);
+ }
+ }
+
+ /*
+ * VERSIONING
+ */
+ /** Get checked out status. */
+ public static boolean isCheckedOut(Node node) {
+ try {
+ return node.isCheckedOut();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot retrieve checked out status of " + node, e);
+ }
+ }
+
+ /** @see VersionManager#checkpoint(String) */
+ public static void checkpoint(Node node) {
+ try {
+ versionManager(node).checkpoint(node.getPath());
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot check in " + node, e);
+ }
+ }
+
+ /** @see VersionManager#checkin(String) */
+ public static void checkin(Node node) {
+ try {
+ versionManager(node).checkin(node.getPath());
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot check in " + node, e);
+ }
+ }
+
+ /** @see VersionManager#checkout(String) */
+ public static void checkout(Node node) {
+ try {
+ versionManager(node).checkout(node.getPath());
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot check out " + node, e);
+ }
+ }
+
+ /** Get the {@link VersionManager} related to this node. */
+ public static VersionManager versionManager(Node node) {
+ try {
+ return node.getSession().getWorkspace().getVersionManager();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get version manager from " + node, e);
+ }
+ }
+
+ /** Get the {@link VersionHistory} related to this node. */
+ public static VersionHistory getVersionHistory(Node node) {
+ try {
+ return versionManager(node).getVersionHistory(node.getPath());
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get version history from " + node, e);
+ }
+ }
+
+ /**
+ * The linear versions of this version history in reverse order and without the
+ * root version.
+ */
+ public static List<Version> getLinearVersions(VersionHistory versionHistory) {
+ try {
+ List<Version> lst = new ArrayList<>();
+ VersionIterator vit = versionHistory.getAllLinearVersions();
+ while (vit.hasNext())
+ lst.add(vit.nextVersion());
+ lst.remove(0);
+ Collections.reverse(lst);
+ return lst;
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get linear versions from " + versionHistory, e);
+ }
+ }
+
+ /** The frozen node related to this {@link Version}. */
+ public static Node getFrozenNode(Version version) {
+ try {
+ return version.getFrozenNode();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get frozen node from " + version, e);
+ }
+ }
+
+ /** Get the base {@link Version} related to this node. */
+ public static Version getBaseVersion(Node node) {
+ try {
+ return versionManager(node).getBaseVersion(node.getPath());
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get base version from " + node, e);
+ }
+ }
+
+ /*
+ * FILES
+ */
+ /**
+ * Returns the size of this file.
+ *
+ * @see NodeType#NT_FILE
+ */
+ public static long getFileSize(Node fileNode) {
+ try {
+ if (!fileNode.isNodeType(NodeType.NT_FILE))
+ throw new IllegalArgumentException(fileNode + " must be a file.");
+ return getBinarySize(fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary());
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get file size of " + fileNode, e);
+ }
+ }
+
+ /** Returns the size of this {@link Binary}. */
+ public static long getBinarySize(Binary binaryArg) {
+ try {
+ try (Bin binary = new Bin(binaryArg)) {
+ return binary.getSize();
+ }
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get file size of binary " + binaryArg, e);
+ }
+ }
+
+ // QUERY
+ /** Creates a JCR-SQL2 query using {@link MessageFormat}. */
+ public static Query createQuery(QueryManager qm, String sql, Object... args) {
+ // fix single quotes
+ sql = sql.replaceAll("'", "''");
+ String query = MessageFormat.format(sql, args);
+ try {
+ return qm.createQuery(query, Query.JCR_SQL2);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot create JCR-SQL2 query from " + query, e);
+ }
+ }
+
+ /** Executes a JCR-SQL2 query using {@link MessageFormat}. */
+ public static NodeIterator executeQuery(QueryManager qm, String sql, Object... args) {
+ Query query = createQuery(qm, sql, args);
+ try {
+ return query.execute().getNodes();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot execute query " + sql + " with arguments " + Arrays.asList(args), e);
+ }
+ }
+
+ /** Executes a JCR-SQL2 query using {@link MessageFormat}. */
+ public static NodeIterator executeQuery(Session session, String sql, Object... args) {
+ QueryManager queryManager;
+ try {
+ queryManager = session.getWorkspace().getQueryManager();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get query manager from session " + session, e);
+ }
+ return executeQuery(queryManager, sql, args);
+ }
+
+ /**
+ * Executes a JCR-SQL2 query using {@link MessageFormat}, which must return a
+ * single node at most.
+ *
+ * @return the node or <code>null</code> if not found.
+ */
+ public static Node getNode(QueryManager qm, String sql, Object... args) {
+ NodeIterator nit = executeQuery(qm, sql, args);
+ if (nit.hasNext()) {
+ Node node = nit.nextNode();
+ if (nit.hasNext())
+ throw new IllegalStateException(
+ "Query " + sql + " with arguments " + Arrays.asList(args) + " returned more than one node.");
+ return node;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Executes a JCR-SQL2 query using {@link MessageFormat}, which must return a
+ * single node at most.
+ *
+ * @return the node or <code>null</code> if not found.
+ */
+ public static Node getNode(Session session, String sql, Object... args) {
+ QueryManager queryManager;
+ try {
+ queryManager = session.getWorkspace().getQueryManager();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get query manager from session " + session, e);
+ }
+ return getNode(queryManager, sql, args);
+ }
+
+ public static Node getRowNode(Row row, String selectorName) {
+ try {
+ return row.getNode(selectorName);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get node " + selectorName + " from row", e);
+ }
+ }
+
+ /** Singleton. */
+ private Jcr() {
+
+ }
+}
--- /dev/null
+package org.argeo.jcr;
+
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.security.AccessControlManager;
+import javax.jcr.security.Privilege;
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+
+/** Apply authorizations to a JCR repository. */
+public class JcrAuthorizations implements Runnable {
+ // private final static Log log =
+ // LogFactory.getLog(JcrAuthorizations.class);
+
+ private Repository repository;
+ private String workspace = null;
+
+ private String securityWorkspace = "security";
+
+ /**
+ * key := privilege1,privilege2/path/to/node<br/>
+ * value := group1,group2,user1
+ */
+ private Map<String, String> principalPrivileges = new HashMap<String, String>();
+
+ public void run() {
+ String currentWorkspace = workspace;
+ Session session = null;
+ try {
+ if (workspace != null && workspace.equals("*")) {
+ session = repository.login();
+ String[] workspaces = session.getWorkspace().getAccessibleWorkspaceNames();
+ JcrUtils.logoutQuietly(session);
+ for (String wksp : workspaces) {
+ currentWorkspace = wksp;
+ if (currentWorkspace.equals(securityWorkspace))
+ continue;
+ session = repository.login(currentWorkspace);
+ initAuthorizations(session);
+ JcrUtils.logoutQuietly(session);
+ }
+ } else {
+ session = repository.login(workspace);
+ initAuthorizations(session);
+ }
+ } catch (RepositoryException e) {
+ JcrUtils.discardQuietly(session);
+ throw new JcrException(
+ "Cannot set authorizations " + principalPrivileges + " on workspace " + currentWorkspace, e);
+ } finally {
+ JcrUtils.logoutQuietly(session);
+ }
+ }
+
+ protected void processWorkspace(String workspace) {
+ Session session = null;
+ try {
+ session = repository.login(workspace);
+ initAuthorizations(session);
+ } catch (RepositoryException e) {
+ JcrUtils.discardQuietly(session);
+ throw new JcrException(
+ "Cannot set authorizations " + principalPrivileges + " on repository " + repository, e);
+ } finally {
+ JcrUtils.logoutQuietly(session);
+ }
+ }
+
+ /** @deprecated call {@link #run()} instead. */
+ @Deprecated
+ public void init() {
+ run();
+ }
+
+ protected void initAuthorizations(Session session) throws RepositoryException {
+ AccessControlManager acm = session.getAccessControlManager();
+
+ for (String privileges : principalPrivileges.keySet()) {
+ String path = null;
+ int slashIndex = privileges.indexOf('/');
+ if (slashIndex == 0) {
+ throw new IllegalArgumentException("Privilege " + privileges + " badly formatted it starts with /");
+ } else if (slashIndex > 0) {
+ path = privileges.substring(slashIndex);
+ privileges = privileges.substring(0, slashIndex);
+ }
+
+ if (path == null)
+ path = "/";
+
+ List<Privilege> privs = new ArrayList<Privilege>();
+ for (String priv : privileges.split(",")) {
+ privs.add(acm.privilegeFromName(priv));
+ }
+
+ String principalNames = principalPrivileges.get(privileges);
+ try {
+ new LdapName(principalNames);
+ // TODO differentiate groups and users ?
+ Principal principal = getOrCreatePrincipal(session, principalNames);
+ JcrUtils.addPrivileges(session, path, principal, privs);
+ } catch (InvalidNameException e) {
+ for (String principalName : principalNames.split(",")) {
+ Principal principal = getOrCreatePrincipal(session, principalName);
+ JcrUtils.addPrivileges(session, path, principal, privs);
+ // if (log.isDebugEnabled()) {
+ // StringBuffer privBuf = new StringBuffer();
+ // for (Privilege priv : privs)
+ // privBuf.append(priv.getName());
+ // log.debug("Added privileges " + privBuf + " to "
+ // + principal.getName() + " on " + path + " in '"
+ // + session.getWorkspace().getName() + "'");
+ // }
+ }
+ }
+ }
+
+ // if (log.isDebugEnabled())
+ // log.debug("JCR authorizations applied on '"
+ // + session.getWorkspace().getName() + "'");
+ }
+
+ /**
+ * Returns a {@link SimplePrincipal}, does not check whether it exists since
+ * such capabilities is not provided by the standard JCR API. Can be
+ * overridden to provide smarter handling
+ */
+ protected Principal getOrCreatePrincipal(Session session, String principalName) throws RepositoryException {
+ return new SimplePrincipal(principalName);
+ }
+
+ // public static void addPrivileges(Session session, Principal principal,
+ // String path, List<Privilege> privs) throws RepositoryException {
+ // AccessControlManager acm = session.getAccessControlManager();
+ // // search for an access control list
+ // AccessControlList acl = null;
+ // AccessControlPolicyIterator policyIterator = acm
+ // .getApplicablePolicies(path);
+ // if (policyIterator.hasNext()) {
+ // while (policyIterator.hasNext()) {
+ // AccessControlPolicy acp = policyIterator
+ // .nextAccessControlPolicy();
+ // if (acp instanceof AccessControlList)
+ // acl = ((AccessControlList) acp);
+ // }
+ // } else {
+ // AccessControlPolicy[] existingPolicies = acm.getPolicies(path);
+ // for (AccessControlPolicy acp : existingPolicies) {
+ // if (acp instanceof AccessControlList)
+ // acl = ((AccessControlList) acp);
+ // }
+ // }
+ //
+ // if (acl != null) {
+ // acl.addAccessControlEntry(principal,
+ // privs.toArray(new Privilege[privs.size()]));
+ // acm.setPolicy(path, acl);
+ // session.save();
+ // if (log.isDebugEnabled()) {
+ // StringBuffer buf = new StringBuffer("");
+ // for (int i = 0; i < privs.size(); i++) {
+ // if (i != 0)
+ // buf.append(',');
+ // buf.append(privs.get(i).getName());
+ // }
+ // log.debug("Added privilege(s) '" + buf + "' to '"
+ // + principal.getName() + "' on " + path
+ // + " from workspace '"
+ // + session.getWorkspace().getName() + "'");
+ // }
+ // } else {
+ // throw new ArgeoJcrException("Don't know how to apply privileges "
+ // + privs + " to " + principal + " on " + path
+ // + " from workspace '" + session.getWorkspace().getName()
+ // + "'");
+ // }
+ // }
+
+ @Deprecated
+ public void setGroupPrivileges(Map<String, String> groupPrivileges) {
+ this.principalPrivileges = groupPrivileges;
+ }
+
+ public void setPrincipalPrivileges(Map<String, String> principalPrivileges) {
+ this.principalPrivileges = principalPrivileges;
+ }
+
+ public void setRepository(Repository repository) {
+ this.repository = repository;
+ }
+
+ public void setWorkspace(String workspace) {
+ this.workspace = workspace;
+ }
+
+ public void setSecurityWorkspace(String securityWorkspace) {
+ this.securityWorkspace = securityWorkspace;
+ }
+
+}
--- /dev/null
+package org.argeo.jcr;
+
+import java.util.function.Function;
+
+import javax.jcr.Session;
+
+/** An arbitrary execution on a JCR session, optionally returning a result. */
+@FunctionalInterface
+public interface JcrCallback extends Function<Session, Object> {
+ /** @deprecated Use {@link #apply(Session)} instead. */
+ @Deprecated
+ public default Object execute(Session session) {
+ return apply(session);
+ }
+}
--- /dev/null
+package org.argeo.jcr;
+
+import javax.jcr.RepositoryException;
+
+/**
+ * Wraps a {@link RepositoryException} in a {@link RuntimeException}.
+ */
+public class JcrException extends IllegalStateException {
+ private static final long serialVersionUID = -4530350094877964989L;
+
+ public JcrException(String message, RepositoryException e) {
+ super(message, e);
+ }
+
+ public JcrException(RepositoryException e) {
+ super(e);
+ }
+
+ public RepositoryException getRepositoryCause() {
+ return (RepositoryException) getCause();
+ }
+}
--- /dev/null
+package org.argeo.jcr;
+
+
+/**
+ * Simple monitor abstraction. Inspired by Eclipse IProgressMOnitor, but without
+ * dependency to it.
+ */
+public interface JcrMonitor {
+ /**
+ * Constant indicating an unknown amount of work.
+ */
+ public final static int UNKNOWN = -1;
+
+ /**
+ * Notifies that the main task is beginning. This must only be called once
+ * on a given progress monitor instance.
+ *
+ * @param name
+ * the name (or description) of the main task
+ * @param totalWork
+ * the total number of work units into which the main task is
+ * been subdivided. If the value is <code>UNKNOWN</code> the
+ * implementation is free to indicate progress in a way which
+ * doesn't require the total number of work units in advance.
+ */
+ public void beginTask(String name, int totalWork);
+
+ /**
+ * Notifies that the work is done; that is, either the main task is
+ * completed or the user canceled it. This method may be called more than
+ * once (implementations should be prepared to handle this case).
+ */
+ public void done();
+
+ /**
+ * Returns whether cancelation of current operation has been requested.
+ * Long-running operations should poll to see if cancelation has been
+ * requested.
+ *
+ * @return <code>true</code> if cancellation has been requested, and
+ * <code>false</code> otherwise
+ * @see #setCanceled(boolean)
+ */
+ public boolean isCanceled();
+
+ /**
+ * Sets the cancel state to the given value.
+ *
+ * @param value
+ * <code>true</code> indicates that cancelation has been
+ * requested (but not necessarily acknowledged);
+ * <code>false</code> clears this flag
+ * @see #isCanceled()
+ */
+ public void setCanceled(boolean value);
+
+ /**
+ * Sets the task name to the given value. This method is used to restore the
+ * task label after a nested operation was executed. Normally there is no
+ * need for clients to call this method.
+ *
+ * @param name
+ * the name (or description) of the main task
+ * @see #beginTask(java.lang.String, int)
+ */
+ public void setTaskName(String name);
+
+ /**
+ * Notifies that a subtask of the main task is beginning. Subtasks are
+ * optional; the main task might not have subtasks.
+ *
+ * @param name
+ * the name (or description) of the subtask
+ */
+ public void subTask(String name);
+
+ /**
+ * Notifies that a given number of work unit of the main task has been
+ * completed. Note that this amount represents an installment, as opposed to
+ * a cumulative amount of work done to date.
+ *
+ * @param work
+ * a non-negative number of work units just completed
+ */
+ public void worked(int work);
+
+}
--- /dev/null
+package org.argeo.jcr;
+
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.jcr.Binary;
+import javax.jcr.Credentials;
+import javax.jcr.LoginException;
+import javax.jcr.NoSuchWorkspaceException;
+import javax.jcr.PropertyType;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.ValueFormatException;
+
+/**
+ * Wrapper around a JCR repository which allows to simplify configuration and
+ * intercept some actions. It exposes itself as a {@link Repository}.
+ */
+public abstract class JcrRepositoryWrapper implements Repository {
+ // private final static Log log = LogFactory
+ // .getLog(JcrRepositoryWrapper.class);
+
+ // wrapped repository
+ private Repository repository;
+
+ private Map<String, String> additionalDescriptors = new HashMap<>();
+
+ private Boolean autocreateWorkspaces = false;
+
+ public JcrRepositoryWrapper(Repository repository) {
+ setRepository(repository);
+ }
+
+ /**
+ * Empty constructor
+ */
+ public JcrRepositoryWrapper() {
+ }
+
+ // /** Initializes */
+ // public void init() {
+ // }
+ //
+ // /** Shutdown the repository */
+ // public void destroy() throws Exception {
+ // }
+
+ protected void putDescriptor(String key, String value) {
+ if (Arrays.asList(getRepository().getDescriptorKeys()).contains(key))
+ throw new IllegalArgumentException("Descriptor key " + key + " is already defined in wrapped repository");
+ if (value == null)
+ additionalDescriptors.remove(key);
+ else
+ additionalDescriptors.put(key, value);
+ }
+
+ /*
+ * DELEGATED JCR REPOSITORY METHODS
+ */
+
+ public String getDescriptor(String key) {
+ if (additionalDescriptors.containsKey(key))
+ return additionalDescriptors.get(key);
+ return getRepository().getDescriptor(key);
+ }
+
+ public String[] getDescriptorKeys() {
+ if (additionalDescriptors.size() == 0)
+ return getRepository().getDescriptorKeys();
+ List<String> keys = Arrays.asList(getRepository().getDescriptorKeys());
+ keys.addAll(additionalDescriptors.keySet());
+ return keys.toArray(new String[keys.size()]);
+ }
+
+ /** Central login method */
+ public Session login(Credentials credentials, String workspaceName)
+ throws LoginException, NoSuchWorkspaceException, RepositoryException {
+ Session session;
+ try {
+ session = getRepository(workspaceName).login(credentials, workspaceName);
+ } catch (NoSuchWorkspaceException e) {
+ if (autocreateWorkspaces && workspaceName != null)
+ session = createWorkspaceAndLogsIn(credentials, workspaceName);
+ else
+ throw e;
+ }
+ processNewSession(session, workspaceName);
+ return session;
+ }
+
+ public Session login() throws LoginException, RepositoryException {
+ return login(null, null);
+ }
+
+ public Session login(Credentials credentials) throws LoginException, RepositoryException {
+ return login(credentials, null);
+ }
+
+ public Session login(String workspaceName) throws LoginException, NoSuchWorkspaceException, RepositoryException {
+ return login(null, workspaceName);
+ }
+
+ /** Called after a session has been created, does nothing by default. */
+ protected void processNewSession(Session session, String workspaceName) {
+ }
+
+ /**
+ * Wraps access to the repository, making sure it is available.
+ *
+ * @deprecated Use {@link #getDefaultRepository()} instead.
+ */
+ @Deprecated
+ protected synchronized Repository getRepository() {
+ return getDefaultRepository();
+ }
+
+ protected synchronized Repository getDefaultRepository() {
+ return repository;
+ }
+
+ protected synchronized Repository getRepository(String workspaceName) {
+ return getDefaultRepository();
+ }
+
+ /**
+ * Logs in to the default workspace, creates the required workspace, logs out,
+ * logs in to the required workspace.
+ */
+ protected Session createWorkspaceAndLogsIn(Credentials credentials, String workspaceName)
+ throws RepositoryException {
+ if (workspaceName == null)
+ throw new IllegalArgumentException("No workspace specified.");
+ Session session = getRepository(workspaceName).login(credentials);
+ session.getWorkspace().createWorkspace(workspaceName);
+ session.logout();
+ return getRepository(workspaceName).login(credentials, workspaceName);
+ }
+
+ public boolean isStandardDescriptor(String key) {
+ return getRepository().isStandardDescriptor(key);
+ }
+
+ public boolean isSingleValueDescriptor(String key) {
+ if (additionalDescriptors.containsKey(key))
+ return true;
+ return getRepository().isSingleValueDescriptor(key);
+ }
+
+ public Value getDescriptorValue(String key) {
+ if (additionalDescriptors.containsKey(key))
+ return new StrValue(additionalDescriptors.get(key));
+ return getRepository().getDescriptorValue(key);
+ }
+
+ public Value[] getDescriptorValues(String key) {
+ return getRepository().getDescriptorValues(key);
+ }
+
+ public synchronized void setRepository(Repository repository) {
+ this.repository = repository;
+ }
+
+ public void setAutocreateWorkspaces(Boolean autocreateWorkspaces) {
+ this.autocreateWorkspaces = autocreateWorkspaces;
+ }
+
+ protected static class StrValue implements Value {
+ private final String str;
+
+ public StrValue(String str) {
+ this.str = str;
+ }
+
+ @Override
+ public String getString() throws ValueFormatException, IllegalStateException, RepositoryException {
+ return str;
+ }
+
+ @Override
+ public InputStream getStream() throws RepositoryException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Binary getBinary() throws RepositoryException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getLong() throws ValueFormatException, RepositoryException {
+ try {
+ return Long.parseLong(str);
+ } catch (NumberFormatException e) {
+ throw new ValueFormatException("Cannot convert", e);
+ }
+ }
+
+ @Override
+ public double getDouble() throws ValueFormatException, RepositoryException {
+ try {
+ return Double.parseDouble(str);
+ } catch (NumberFormatException e) {
+ throw new ValueFormatException("Cannot convert", e);
+ }
+ }
+
+ @Override
+ public BigDecimal getDecimal() throws ValueFormatException, RepositoryException {
+ try {
+ return new BigDecimal(str);
+ } catch (NumberFormatException e) {
+ throw new ValueFormatException("Cannot convert", e);
+ }
+ }
+
+ @Override
+ public Calendar getDate() throws ValueFormatException, RepositoryException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean getBoolean() throws ValueFormatException, RepositoryException {
+ try {
+ return Boolean.parseBoolean(str);
+ } catch (NumberFormatException e) {
+ throw new ValueFormatException("Cannot convert", e);
+ }
+ }
+
+ @Override
+ public int getType() {
+ return PropertyType.STRING;
+ }
+
+ }
+
+}
--- /dev/null
+package org.argeo.jcr;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+
+import javax.jcr.Item;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+
+/** URL stream handler able to deal with nt:file node and properties. NOT FINISHED */
+public class JcrUrlStreamHandler extends URLStreamHandler {
+ private final Session session;
+
+ public JcrUrlStreamHandler(Session session) {
+ this.session = session;
+ }
+
+ @Override
+ protected URLConnection openConnection(final URL u) throws IOException {
+ // TODO Auto-generated method stub
+ return new URLConnection(u) {
+
+ @Override
+ public void connect() throws IOException {
+ String itemPath = u.getPath();
+ try {
+ if (!session.itemExists(itemPath))
+ throw new IOException("No item under " + itemPath);
+
+ Item item = session.getItem(u.getPath());
+ if (item.isNode()) {
+ // this should be a nt:file node
+ Node node = (Node) item;
+ if (!node.getPrimaryNodeType().isNodeType(
+ NodeType.NT_FILE))
+ throw new IOException("Node " + node + " is not a "
+ + NodeType.NT_FILE);
+
+ } else {
+ Property property = (Property) item;
+ if(property.getType()==PropertyType.BINARY){
+ //Binary binary = property.getBinary();
+
+ }
+ }
+ } catch (RepositoryException e) {
+ IOException ioe = new IOException(
+ "Unexpected JCR exception");
+ ioe.initCause(e);
+ throw ioe;
+ }
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ // TODO Auto-generated method stub
+ return super.getInputStream();
+ }
+
+ };
+ }
+
+}
--- /dev/null
+package org.argeo.jcr;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import javax.jcr.Binary;
+import javax.jcr.Credentials;
+import javax.jcr.ImportUUIDBehavior;
+import javax.jcr.NamespaceRegistry;
+import javax.jcr.NoSuchWorkspaceException;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.PropertyType;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.Workspace;
+import javax.jcr.nodetype.NoSuchNodeTypeException;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.observation.EventListener;
+import javax.jcr.query.Query;
+import javax.jcr.query.QueryResult;
+import javax.jcr.security.AccessControlEntry;
+import javax.jcr.security.AccessControlList;
+import javax.jcr.security.AccessControlManager;
+import javax.jcr.security.AccessControlPolicy;
+import javax.jcr.security.AccessControlPolicyIterator;
+import javax.jcr.security.Privilege;
+
+import org.apache.commons.io.IOUtils;
+
+/** Utility methods to simplify common JCR operations. */
+public class JcrUtils {
+
+// final private static Log log = LogFactory.getLog(JcrUtils.class);
+
+ /**
+ * Not complete yet. See
+ * http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.2.2%20Local
+ * %20Names
+ */
+ public final static char[] INVALID_NAME_CHARACTERS = { '/', ':', '[', ']', '|', '*', /* invalid for XML: */ '<',
+ '>', '&' };
+
+ /** Prevents instantiation */
+ private JcrUtils() {
+ }
+
+ /**
+ * Queries one single node.
+ *
+ * @return one single node or null if none was found
+ * @throws JcrException if more than one node was found
+ */
+ public static Node querySingleNode(Query query) {
+ NodeIterator nodeIterator;
+ try {
+ QueryResult queryResult = query.execute();
+ nodeIterator = queryResult.getNodes();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot execute query " + query, e);
+ }
+ Node node;
+ if (nodeIterator.hasNext())
+ node = nodeIterator.nextNode();
+ else
+ return null;
+
+ if (nodeIterator.hasNext())
+ throw new IllegalArgumentException("Query returned more than one node.");
+ return node;
+ }
+
+ /** Retrieves the node name from the provided path */
+ public static String nodeNameFromPath(String path) {
+ if (path.equals("/"))
+ return "";
+ if (path.charAt(0) != '/')
+ throw new IllegalArgumentException("Path " + path + " must start with a '/'");
+ String pathT = path;
+ if (pathT.charAt(pathT.length() - 1) == '/')
+ pathT = pathT.substring(0, pathT.length() - 2);
+
+ int index = pathT.lastIndexOf('/');
+ return pathT.substring(index + 1);
+ }
+
+ /** Retrieves the parent path of the provided path */
+ public static String parentPath(String path) {
+ if (path.equals("/"))
+ throw new IllegalArgumentException("Root path '/' has no parent path");
+ if (path.charAt(0) != '/')
+ throw new IllegalArgumentException("Path " + path + " must start with a '/'");
+ String pathT = path;
+ if (pathT.charAt(pathT.length() - 1) == '/')
+ pathT = pathT.substring(0, pathT.length() - 2);
+
+ int index = pathT.lastIndexOf('/');
+ return pathT.substring(0, index);
+ }
+
+ /** The provided data as a path ('/' at the end, not the beginning) */
+ public static String dateAsPath(Calendar cal) {
+ return dateAsPath(cal, false);
+ }
+
+ /**
+ * Creates a deep path based on a URL:
+ * http://subdomain.example.com/to/content?args becomes
+ * com/example/subdomain/to/content
+ */
+ public static String urlAsPath(String url) {
+ try {
+ URL u = new URL(url);
+ StringBuffer path = new StringBuffer(url.length());
+ // invert host
+ path.append(hostAsPath(u.getHost()));
+ // we don't put port since it may not always be there and may change
+ path.append(u.getPath());
+ return path.toString();
+ } catch (MalformedURLException e) {
+ throw new IllegalArgumentException("Cannot generate URL path for " + url, e);
+ }
+ }
+
+ /** Set the {@link NodeType#NT_ADDRESS} properties based on this URL. */
+ public static void urlToAddressProperties(Node node, String url) {
+ try {
+ URL u = new URL(url);
+ node.setProperty(Property.JCR_PROTOCOL, u.getProtocol());
+ node.setProperty(Property.JCR_HOST, u.getHost());
+ node.setProperty(Property.JCR_PORT, Integer.toString(u.getPort()));
+ node.setProperty(Property.JCR_PATH, normalizePath(u.getPath()));
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot set URL " + url + " as nt:address properties", e);
+ } catch (MalformedURLException e) {
+ throw new IllegalArgumentException("Cannot set URL " + url + " as nt:address properties", e);
+ }
+ }
+
+ /** Build URL based on the {@link NodeType#NT_ADDRESS} properties. */
+ public static String urlFromAddressProperties(Node node) {
+ try {
+ URL u = new URL(node.getProperty(Property.JCR_PROTOCOL).getString(),
+ node.getProperty(Property.JCR_HOST).getString(),
+ (int) node.getProperty(Property.JCR_PORT).getLong(),
+ node.getProperty(Property.JCR_PATH).getString());
+ return u.toString();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get URL from nt:address properties of " + node, e);
+ } catch (MalformedURLException e) {
+ throw new IllegalArgumentException("Cannot get URL from nt:address properties of " + node, e);
+ }
+ }
+
+ /*
+ * PATH UTILITIES
+ */
+
+ /**
+ * Make sure that: starts with '/', do not end with '/', do not have '//'
+ */
+ public static String normalizePath(String path) {
+ List<String> tokens = tokenize(path);
+ StringBuffer buf = new StringBuffer(path.length());
+ for (String token : tokens) {
+ buf.append('/');
+ buf.append(token);
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Creates a path from a FQDN, inverting the order of the component:
+ * www.argeo.org becomes org.argeo.www
+ */
+ public static String hostAsPath(String host) {
+ StringBuffer path = new StringBuffer(host.length());
+ String[] hostTokens = host.split("\\.");
+ for (int i = hostTokens.length - 1; i >= 0; i--) {
+ path.append(hostTokens[i]);
+ if (i != 0)
+ path.append('/');
+ }
+ return path.toString();
+ }
+
+ /**
+ * Creates a path from a UUID (e.g. 6ebda899-217d-4bf1-abe4-2839085c8f3c becomes
+ * 6ebda899-217d/4bf1/abe4/2839085c8f3c/). '/' at the end, not the beginning
+ */
+ public static String uuidAsPath(String uuid) {
+ StringBuffer path = new StringBuffer(uuid.length());
+ String[] tokens = uuid.split("-");
+ for (int i = 0; i < tokens.length; i++) {
+ path.append(tokens[i]);
+ if (i != 0)
+ path.append('/');
+ }
+ return path.toString();
+ }
+
+ /**
+ * The provided data as a path ('/' at the end, not the beginning)
+ *
+ * @param cal the date
+ * @param addHour whether to add hour as well
+ */
+ public static String dateAsPath(Calendar cal, Boolean addHour) {
+ StringBuffer buf = new StringBuffer(14);
+ buf.append('Y');
+ buf.append(cal.get(Calendar.YEAR));
+ buf.append('/');
+
+ int month = cal.get(Calendar.MONTH) + 1;
+ buf.append('M');
+ if (month < 10)
+ buf.append(0);
+ buf.append(month);
+ buf.append('/');
+
+ int day = cal.get(Calendar.DAY_OF_MONTH);
+ buf.append('D');
+ if (day < 10)
+ buf.append(0);
+ buf.append(day);
+ buf.append('/');
+
+ if (addHour) {
+ int hour = cal.get(Calendar.HOUR_OF_DAY);
+ buf.append('H');
+ if (hour < 10)
+ buf.append(0);
+ buf.append(hour);
+ buf.append('/');
+ }
+ return buf.toString();
+
+ }
+
+ /** Converts in one call a string into a gregorian calendar. */
+ public static Calendar parseCalendar(DateFormat dateFormat, String value) {
+ try {
+ Date date = dateFormat.parse(value);
+ Calendar calendar = new GregorianCalendar();
+ calendar.setTime(date);
+ return calendar;
+ } catch (ParseException e) {
+ throw new IllegalArgumentException("Cannot parse " + value + " with date format " + dateFormat, e);
+ }
+
+ }
+
+ /** The last element of a path. */
+ public static String lastPathElement(String path) {
+ if (path.charAt(path.length() - 1) == '/')
+ throw new IllegalArgumentException("Path " + path + " cannot end with '/'");
+ int index = path.lastIndexOf('/');
+ if (index < 0)
+ return path;
+ return path.substring(index + 1);
+ }
+
+ /**
+ * Call {@link Node#getName()} without exceptions (useful in super
+ * constructors).
+ */
+ public static String getNameQuietly(Node node) {
+ try {
+ return node.getName();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get name from " + node, e);
+ }
+ }
+
+ /**
+ * Call {@link Node#getProperty(String)} without exceptions (useful in super
+ * constructors).
+ */
+ public static String getStringPropertyQuietly(Node node, String propertyName) {
+ try {
+ return node.getProperty(propertyName).getString();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get name from " + node, e);
+ }
+ }
+
+// /**
+// * Routine that get the child with this name, adding it if it does not already
+// * exist
+// */
+// public static Node getOrAdd(Node parent, String name, String primaryNodeType) throws RepositoryException {
+// return parent.hasNode(name) ? parent.getNode(name) : parent.addNode(name, primaryNodeType);
+// }
+
+ /**
+ * Routine that get the child with this name, adding it if it does not already
+ * exist
+ */
+ public static Node getOrAdd(Node parent, String name, String primaryNodeType, String... mixinNodeTypes)
+ throws RepositoryException {
+ Node node;
+ if (parent.hasNode(name)) {
+ node = parent.getNode(name);
+ if (primaryNodeType != null && !node.isNodeType(primaryNodeType))
+ throw new IllegalArgumentException("Node " + node + " exists but is of primary node type "
+ + node.getPrimaryNodeType().getName() + ", not " + primaryNodeType);
+ for (String mixin : mixinNodeTypes) {
+ if (!node.isNodeType(mixin))
+ node.addMixin(mixin);
+ }
+ return node;
+ } else {
+ node = primaryNodeType != null ? parent.addNode(name, primaryNodeType) : parent.addNode(name);
+ for (String mixin : mixinNodeTypes) {
+ node.addMixin(mixin);
+ }
+ return node;
+ }
+ }
+
+ /**
+ * Routine that get the child with this name, adding it if it does not already
+ * exist
+ */
+ public static Node getOrAdd(Node parent, String name) throws RepositoryException {
+ return parent.hasNode(name) ? parent.getNode(name) : parent.addNode(name);
+ }
+
+ /** Convert a {@link NodeIterator} to a list of {@link Node} */
+ public static List<Node> nodeIteratorToList(NodeIterator nodeIterator) {
+ List<Node> nodes = new ArrayList<Node>();
+ while (nodeIterator.hasNext()) {
+ nodes.add(nodeIterator.nextNode());
+ }
+ return nodes;
+ }
+
+ /*
+ * PROPERTIES
+ */
+
+ /**
+ * Concisely get the string value of a property or null if this node doesn't
+ * have this property
+ */
+ public static String get(Node node, String propertyName) {
+ try {
+ if (!node.hasProperty(propertyName))
+ return null;
+ return node.getProperty(propertyName).getString();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get property " + propertyName + " of " + node, e);
+ }
+ }
+
+ /** Concisely get the path of the given node. */
+ public static String getPath(Node node) {
+ try {
+ return node.getPath();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get path of " + node, e);
+ }
+ }
+
+ /** Concisely get the boolean value of a property */
+ public static Boolean check(Node node, String propertyName) {
+ try {
+ return node.getProperty(propertyName).getBoolean();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get property " + propertyName + " of " + node, e);
+ }
+ }
+
+ /** Concisely get the bytes array value of a property */
+ public static byte[] getBytes(Node node, String propertyName) {
+ try {
+ return getBinaryAsBytes(node.getProperty(propertyName));
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get property " + propertyName + " of " + node, e);
+ }
+ }
+
+ /*
+ * MKDIRS
+ */
+
+ /**
+ * Create sub nodes relative to a parent node
+ */
+ public static Node mkdirs(Node parentNode, String relativePath) {
+ return mkdirs(parentNode, relativePath, null, null);
+ }
+
+ /**
+ * Create sub nodes relative to a parent node
+ *
+ * @param nodeType the type of the leaf node
+ */
+ public static Node mkdirs(Node parentNode, String relativePath, String nodeType) {
+ return mkdirs(parentNode, relativePath, nodeType, null);
+ }
+
+ /**
+ * Create sub nodes relative to a parent node
+ *
+ * @param nodeType the type of the leaf node
+ */
+ public static Node mkdirs(Node parentNode, String relativePath, String nodeType, String intermediaryNodeType) {
+ List<String> tokens = tokenize(relativePath);
+ Node currParent = parentNode;
+ try {
+ for (int i = 0; i < tokens.size(); i++) {
+ String name = tokens.get(i);
+ if (currParent.hasNode(name)) {
+ currParent = currParent.getNode(name);
+ } else {
+ if (i != (tokens.size() - 1)) {// intermediary
+ currParent = currParent.addNode(name, intermediaryNodeType);
+ } else {// leaf
+ currParent = currParent.addNode(name, nodeType);
+ }
+ }
+ }
+ return currParent;
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot mkdirs relative path " + relativePath + " from " + parentNode, e);
+ }
+ }
+
+ /**
+ * Synchronized and save is performed, to avoid race conditions in initializers
+ * leading to duplicate nodes.
+ */
+ public synchronized static Node mkdirsSafe(Session session, String path, String type) {
+ try {
+ if (session.hasPendingChanges())
+ throw new IllegalStateException("Session has pending changes, save them first.");
+ Node node = mkdirs(session, path, type);
+ session.save();
+ return node;
+ } catch (RepositoryException e) {
+ discardQuietly(session);
+ throw new JcrException("Cannot safely make directories", e);
+ }
+ }
+
+ public synchronized static Node mkdirsSafe(Session session, String path) {
+ return mkdirsSafe(session, path, null);
+ }
+
+ /** Creates the nodes making path, if they don't exist. */
+ public static Node mkdirs(Session session, String path) {
+ return mkdirs(session, path, null, null, false);
+ }
+
+ /**
+ * @param type the type of the leaf node
+ */
+ public static Node mkdirs(Session session, String path, String type) {
+ return mkdirs(session, path, type, null, false);
+ }
+
+ /**
+ * Creates the nodes making path, if they don't exist. This is up to the caller
+ * to save the session. Use with caution since it can create duplicate nodes if
+ * used concurrently. Requires read access to the root node of the workspace.
+ */
+ public static Node mkdirs(Session session, String path, String type, String intermediaryNodeType,
+ Boolean versioning) {
+ try {
+ if (path.equals("/"))
+ return session.getRootNode();
+
+ if (session.itemExists(path)) {
+ Node node = session.getNode(path);
+ // check type
+ if (type != null && !node.isNodeType(type) && !node.getPath().equals("/"))
+ throw new IllegalArgumentException("Node " + node + " exists but is of type "
+ + node.getPrimaryNodeType().getName() + " not of type " + type);
+ // TODO: check versioning
+ return node;
+ }
+
+ // StringBuffer current = new StringBuffer("/");
+ // Node currentNode = session.getRootNode();
+
+ Node currentNode = findClosestExistingParent(session, path);
+ String closestExistingParentPath = currentNode.getPath();
+ StringBuffer current = new StringBuffer(closestExistingParentPath);
+ if (!closestExistingParentPath.endsWith("/"))
+ current.append('/');
+ Iterator<String> it = tokenize(path.substring(closestExistingParentPath.length())).iterator();
+ while (it.hasNext()) {
+ String part = it.next();
+ current.append(part).append('/');
+ if (!session.itemExists(current.toString())) {
+ if (!it.hasNext() && type != null)
+ currentNode = currentNode.addNode(part, type);
+ else if (it.hasNext() && intermediaryNodeType != null)
+ currentNode = currentNode.addNode(part, intermediaryNodeType);
+ else
+ currentNode = currentNode.addNode(part);
+ if (versioning)
+ currentNode.addMixin(NodeType.MIX_VERSIONABLE);
+// if (log.isTraceEnabled())
+// log.debug("Added folder " + part + " as " + current);
+ } else {
+ currentNode = (Node) session.getItem(current.toString());
+ }
+ }
+ return currentNode;
+ } catch (RepositoryException e) {
+ discardQuietly(session);
+ throw new JcrException("Cannot mkdirs " + path, e);
+ } finally {
+ }
+ }
+
+ private static Node findClosestExistingParent(Session session, String path) throws RepositoryException {
+ int idx = path.lastIndexOf('/');
+ if (idx == 0)
+ return session.getRootNode();
+ String parentPath = path.substring(0, idx);
+ if (session.itemExists(parentPath))
+ return session.getNode(parentPath);
+ else
+ return findClosestExistingParent(session, parentPath);
+ }
+
+ /** Convert a path to the list of its tokens */
+ public static List<String> tokenize(String path) {
+ List<String> tokens = new ArrayList<String>();
+ boolean optimized = false;
+ if (!optimized) {
+ String[] rawTokens = path.split("/");
+ for (String token : rawTokens) {
+ if (!token.equals(""))
+ tokens.add(token);
+ }
+ } else {
+ StringBuffer curr = new StringBuffer();
+ char[] arr = path.toCharArray();
+ chars: for (int i = 0; i < arr.length; i++) {
+ char c = arr[i];
+ if (c == '/') {
+ if (i == 0 || (i == arr.length - 1))
+ continue chars;
+ if (curr.length() > 0) {
+ tokens.add(curr.toString());
+ curr = new StringBuffer();
+ }
+ } else
+ curr.append(c);
+ }
+ if (curr.length() > 0) {
+ tokens.add(curr.toString());
+ curr = new StringBuffer();
+ }
+ }
+ return Collections.unmodifiableList(tokens);
+ }
+
+ // /**
+ // * use {@link #mkdirs(Session, String, String, String, Boolean)} instead.
+ // *
+ // * @deprecated
+ // */
+ // @Deprecated
+ // public static Node mkdirs(Session session, String path, String type,
+ // Boolean versioning) {
+ // return mkdirs(session, path, type, type, false);
+ // }
+
+ /**
+ * Safe and repository implementation independent registration of a namespace.
+ */
+ public static void registerNamespaceSafely(Session session, String prefix, String uri) {
+ try {
+ registerNamespaceSafely(session.getWorkspace().getNamespaceRegistry(), prefix, uri);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot find namespace registry", e);
+ }
+ }
+
+ /**
+ * Safe and repository implementation independent registration of a namespace.
+ */
+ public static void registerNamespaceSafely(NamespaceRegistry nr, String prefix, String uri) {
+ try {
+ String[] prefixes = nr.getPrefixes();
+ for (String pref : prefixes)
+ if (pref.equals(prefix)) {
+ String registeredUri = nr.getURI(pref);
+ if (!registeredUri.equals(uri))
+ throw new IllegalArgumentException("Prefix " + pref + " already registered for URI "
+ + registeredUri + " which is different from provided URI " + uri);
+ else
+ return;// skip
+ }
+ nr.registerNamespace(prefix, uri);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot register namespace " + uri + " under prefix " + prefix, e);
+ }
+ }
+
+// /** Recursively outputs the contents of the given node. */
+// public static void debug(Node node) {
+// debug(node, log);
+// }
+//
+// /** Recursively outputs the contents of the given node. */
+// public static void debug(Node node, Log log) {
+// try {
+// // First output the node path
+// log.debug(node.getPath());
+// // Skip the virtual (and large!) jcr:system subtree
+// if (node.getName().equals("jcr:system")) {
+// return;
+// }
+//
+// // Then the children nodes (recursive)
+// NodeIterator it = node.getNodes();
+// while (it.hasNext()) {
+// Node childNode = it.nextNode();
+// debug(childNode, log);
+// }
+//
+// // Then output the properties
+// PropertyIterator properties = node.getProperties();
+// // log.debug("Property are : ");
+//
+// properties: while (properties.hasNext()) {
+// Property property = properties.nextProperty();
+// if (property.getType() == PropertyType.BINARY)
+// continue properties;// skip
+// if (property.getDefinition().isMultiple()) {
+// // A multi-valued property, print all values
+// Value[] values = property.getValues();
+// for (int i = 0; i < values.length; i++) {
+// log.debug(property.getPath() + "=" + values[i].getString());
+// }
+// } else {
+// // A single-valued property
+// log.debug(property.getPath() + "=" + property.getString());
+// }
+// }
+// } catch (Exception e) {
+// log.error("Could not debug " + node, e);
+// }
+//
+// }
+
+// /** Logs the effective access control policies */
+// public static void logEffectiveAccessPolicies(Node node) {
+// try {
+// logEffectiveAccessPolicies(node.getSession(), node.getPath());
+// } catch (RepositoryException e) {
+// log.error("Cannot log effective access policies of " + node, e);
+// }
+// }
+//
+// /** Logs the effective access control policies */
+// public static void logEffectiveAccessPolicies(Session session, String path) {
+// if (!log.isDebugEnabled())
+// return;
+//
+// try {
+// AccessControlPolicy[] effectivePolicies = session.getAccessControlManager().getEffectivePolicies(path);
+// if (effectivePolicies.length > 0) {
+// for (AccessControlPolicy policy : effectivePolicies) {
+// if (policy instanceof AccessControlList) {
+// AccessControlList acl = (AccessControlList) policy;
+// log.debug("Access control list for " + path + "\n" + accessControlListSummary(acl));
+// }
+// }
+// } else {
+// log.debug("No effective access control policy for " + path);
+// }
+// } catch (RepositoryException e) {
+// log.error("Cannot log effective access policies of " + path, e);
+// }
+// }
+
+ /** Returns a human-readable summary of this access control list. */
+ public static String accessControlListSummary(AccessControlList acl) {
+ StringBuffer buf = new StringBuffer("");
+ try {
+ for (AccessControlEntry ace : acl.getAccessControlEntries()) {
+ buf.append('\t').append(ace.getPrincipal().getName()).append('\n');
+ for (Privilege priv : ace.getPrivileges())
+ buf.append("\t\t").append(priv.getName()).append('\n');
+ }
+ return buf.toString();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot write summary of " + acl, e);
+ }
+ }
+
+ /** Copy the whole workspace via a system view XML. */
+ public static void copyWorkspaceXml(Session fromSession, Session toSession) {
+ Workspace fromWorkspace = fromSession.getWorkspace();
+ Workspace toWorkspace = toSession.getWorkspace();
+ String errorMsg = "Cannot copy workspace " + fromWorkspace + " to " + toWorkspace + " via XML.";
+
+ try (PipedInputStream in = new PipedInputStream(1024 * 1024);) {
+ new Thread(() -> {
+ try (PipedOutputStream out = new PipedOutputStream(in)) {
+ fromSession.exportSystemView("/", out, false, false);
+ out.flush();
+ } catch (IOException e) {
+ throw new RuntimeException(errorMsg, e);
+ } catch (RepositoryException e) {
+ throw new JcrException(errorMsg, e);
+ }
+ }, "Copy workspace" + fromWorkspace + " to " + toWorkspace).start();
+
+ toSession.importXML("/", in, ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING);
+ toSession.save();
+ } catch (IOException e) {
+ throw new RuntimeException(errorMsg, e);
+ } catch (RepositoryException e) {
+ throw new JcrException(errorMsg, e);
+ }
+ }
+
+ /**
+ * Copies recursively the content of a node to another one. Do NOT copy the
+ * property values of {@link NodeType#MIX_CREATED} and
+ * {@link NodeType#MIX_LAST_MODIFIED}, but update the
+ * {@link Property#JCR_LAST_MODIFIED} and {@link Property#JCR_LAST_MODIFIED_BY}
+ * properties if the target node has the {@link NodeType#MIX_LAST_MODIFIED}
+ * mixin.
+ */
+ public static void copy(Node fromNode, Node toNode) {
+ try {
+ if (toNode.getDefinition().isProtected())
+ return;
+
+ // add mixins
+ for (NodeType mixinType : fromNode.getMixinNodeTypes()) {
+ try {
+ toNode.addMixin(mixinType.getName());
+ } catch (NoSuchNodeTypeException e) {
+ // ignore unknown mixins
+ // TODO log it
+ }
+ }
+
+ // process properties
+ PropertyIterator pit = fromNode.getProperties();
+ properties: while (pit.hasNext()) {
+ Property fromProperty = pit.nextProperty();
+ String propertyName = fromProperty.getName();
+ if (toNode.hasProperty(propertyName) && toNode.getProperty(propertyName).getDefinition().isProtected())
+ continue properties;
+
+ if (fromProperty.getDefinition().isProtected())
+ continue properties;
+
+ if (propertyName.equals("jcr:created") || propertyName.equals("jcr:createdBy")
+ || propertyName.equals("jcr:lastModified") || propertyName.equals("jcr:lastModifiedBy"))
+ continue properties;
+
+ if (fromProperty.isMultiple()) {
+ toNode.setProperty(propertyName, fromProperty.getValues());
+ } else {
+ toNode.setProperty(propertyName, fromProperty.getValue());
+ }
+ }
+
+ // update jcr:lastModified and jcr:lastModifiedBy in toNode in case
+ // they existed, before adding the mixins
+ updateLastModified(toNode, true);
+
+ // process children nodes
+ NodeIterator nit = fromNode.getNodes();
+ while (nit.hasNext()) {
+ Node fromChild = nit.nextNode();
+ Integer index = fromChild.getIndex();
+ String nodeRelPath = fromChild.getName() + "[" + index + "]";
+ Node toChild;
+ if (toNode.hasNode(nodeRelPath))
+ toChild = toNode.getNode(nodeRelPath);
+ else {
+ try {
+ toChild = toNode.addNode(fromChild.getName(), fromChild.getPrimaryNodeType().getName());
+ } catch (NoSuchNodeTypeException e) {
+ // ignore unknown primary types
+ // TODO log it
+ return;
+ }
+ }
+ copy(fromChild, toChild);
+ }
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot copy " + fromNode + " to " + toNode, e);
+ }
+ }
+
+ /**
+ * Check whether all first-level properties (except jcr:* properties) are equal.
+ * Skip jcr:* properties
+ */
+ public static Boolean allPropertiesEquals(Node reference, Node observed, Boolean onlyCommonProperties) {
+ try {
+ PropertyIterator pit = reference.getProperties();
+ props: while (pit.hasNext()) {
+ Property propReference = pit.nextProperty();
+ String propName = propReference.getName();
+ if (propName.startsWith("jcr:"))
+ continue props;
+
+ if (!observed.hasProperty(propName))
+ if (onlyCommonProperties)
+ continue props;
+ else
+ return false;
+ // TODO: deal with multiple property values?
+ if (!observed.getProperty(propName).getValue().equals(propReference.getValue()))
+ return false;
+ }
+ return true;
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot check all properties equals of " + reference + " and " + observed, e);
+ }
+ }
+
+ public static Map<String, PropertyDiff> diffProperties(Node reference, Node observed) {
+ Map<String, PropertyDiff> diffs = new TreeMap<String, PropertyDiff>();
+ diffPropertiesLevel(diffs, null, reference, observed);
+ return diffs;
+ }
+
+ /**
+ * Compare the properties of two nodes. Recursivity to child nodes is not yet
+ * supported. Skip jcr:* properties.
+ */
+ static void diffPropertiesLevel(Map<String, PropertyDiff> diffs, String baseRelPath, Node reference,
+ Node observed) {
+ try {
+ // check removed and modified
+ PropertyIterator pit = reference.getProperties();
+ props: while (pit.hasNext()) {
+ Property p = pit.nextProperty();
+ String name = p.getName();
+ if (name.startsWith("jcr:"))
+ continue props;
+
+ if (!observed.hasProperty(name)) {
+ String relPath = propertyRelPath(baseRelPath, name);
+ PropertyDiff pDiff = new PropertyDiff(PropertyDiff.REMOVED, relPath, p.getValue(), null);
+ diffs.put(relPath, pDiff);
+ } else {
+ if (p.isMultiple()) {
+ // FIXME implement multiple
+ } else {
+ Value referenceValue = p.getValue();
+ Value newValue = observed.getProperty(name).getValue();
+ if (!referenceValue.equals(newValue)) {
+ String relPath = propertyRelPath(baseRelPath, name);
+ PropertyDiff pDiff = new PropertyDiff(PropertyDiff.MODIFIED, relPath, referenceValue,
+ newValue);
+ diffs.put(relPath, pDiff);
+ }
+ }
+ }
+ }
+ // check added
+ pit = observed.getProperties();
+ props: while (pit.hasNext()) {
+ Property p = pit.nextProperty();
+ String name = p.getName();
+ if (name.startsWith("jcr:"))
+ continue props;
+ if (!reference.hasProperty(name)) {
+ if (p.isMultiple()) {
+ // FIXME implement multiple
+ } else {
+ String relPath = propertyRelPath(baseRelPath, name);
+ PropertyDiff pDiff = new PropertyDiff(PropertyDiff.ADDED, relPath, null, p.getValue());
+ diffs.put(relPath, pDiff);
+ }
+ }
+ }
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot diff " + reference + " and " + observed, e);
+ }
+ }
+
+ /**
+ * Compare only a restricted list of properties of two nodes. No recursivity.
+ *
+ */
+ public static Map<String, PropertyDiff> diffProperties(Node reference, Node observed, List<String> properties) {
+ Map<String, PropertyDiff> diffs = new TreeMap<String, PropertyDiff>();
+ try {
+ Iterator<String> pit = properties.iterator();
+
+ props: while (pit.hasNext()) {
+ String name = pit.next();
+ if (!reference.hasProperty(name)) {
+ if (!observed.hasProperty(name))
+ continue props;
+ Value val = observed.getProperty(name).getValue();
+ try {
+ // empty String but not null
+ if ("".equals(val.getString()))
+ continue props;
+ } catch (Exception e) {
+ // not parseable as String, silent
+ }
+ PropertyDiff pDiff = new PropertyDiff(PropertyDiff.ADDED, name, null, val);
+ diffs.put(name, pDiff);
+ } else if (!observed.hasProperty(name)) {
+ PropertyDiff pDiff = new PropertyDiff(PropertyDiff.REMOVED, name,
+ reference.getProperty(name).getValue(), null);
+ diffs.put(name, pDiff);
+ } else {
+ Value referenceValue = reference.getProperty(name).getValue();
+ Value newValue = observed.getProperty(name).getValue();
+ if (!referenceValue.equals(newValue)) {
+ PropertyDiff pDiff = new PropertyDiff(PropertyDiff.MODIFIED, name, referenceValue, newValue);
+ diffs.put(name, pDiff);
+ }
+ }
+ }
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot diff " + reference + " and " + observed, e);
+ }
+ return diffs;
+ }
+
+ /** Builds a property relPath to be used in the diff. */
+ private static String propertyRelPath(String baseRelPath, String propertyName) {
+ if (baseRelPath == null)
+ return propertyName;
+ else
+ return baseRelPath + '/' + propertyName;
+ }
+
+ /**
+ * Normalizes a name so that it can be stored in contexts not supporting names
+ * with ':' (typically databases). Replaces ':' by '_'.
+ */
+ public static String normalize(String name) {
+ return name.replace(':', '_');
+ }
+
+ /**
+ * Replaces characters which are invalid in a JCR name by '_'. Currently not
+ * exhaustive.
+ *
+ * @see JcrUtils#INVALID_NAME_CHARACTERS
+ */
+ public static String replaceInvalidChars(String name) {
+ return replaceInvalidChars(name, '_');
+ }
+
+ /**
+ * Replaces characters which are invalid in a JCR name. Currently not
+ * exhaustive.
+ *
+ * @see JcrUtils#INVALID_NAME_CHARACTERS
+ */
+ public static String replaceInvalidChars(String name, char replacement) {
+ boolean modified = false;
+ char[] arr = name.toCharArray();
+ for (int i = 0; i < arr.length; i++) {
+ char c = arr[i];
+ invalid: for (char invalid : INVALID_NAME_CHARACTERS) {
+ if (c == invalid) {
+ arr[i] = replacement;
+ modified = true;
+ break invalid;
+ }
+ }
+ }
+ if (modified)
+ return new String(arr);
+ else
+ // do not create new object if unnecessary
+ return name;
+ }
+
+ // /**
+ // * Removes forbidden characters from a path, replacing them with '_'
+ // *
+ // * @deprecated use {@link #replaceInvalidChars(String)} instead
+ // */
+ // public static String removeForbiddenCharacters(String str) {
+ // return str.replace('[', '_').replace(']', '_').replace('/', '_').replace('*',
+ // '_');
+ //
+ // }
+
+ /** Cleanly disposes a {@link Binary} even if it is null. */
+ public static void closeQuietly(Binary binary) {
+ if (binary == null)
+ return;
+ binary.dispose();
+ }
+
+ /** Retrieve a {@link Binary} as a byte array */
+ public static byte[] getBinaryAsBytes(Property property) {
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream();
+ Bin binary = new Bin(property);
+ InputStream in = binary.getStream()) {
+ IOUtils.copy(in, out);
+ return out.toByteArray();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot read binary " + property + " as bytes", e);
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot read binary " + property + " as bytes", e);
+ }
+ }
+
+ /** Writes a {@link Binary} from a byte array */
+ public static void setBinaryAsBytes(Node node, String property, byte[] bytes) {
+ Binary binary = null;
+ try (InputStream in = new ByteArrayInputStream(bytes)) {
+ binary = node.getSession().getValueFactory().createBinary(in);
+ node.setProperty(property, binary);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot set binary " + property + " as bytes", e);
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot set binary " + property + " as bytes", e);
+ } finally {
+ closeQuietly(binary);
+ }
+ }
+
+ /** Writes a {@link Binary} from a byte array */
+ public static void setBinaryAsBytes(Property prop, byte[] bytes) {
+ Binary binary = null;
+ try (InputStream in = new ByteArrayInputStream(bytes)) {
+ binary = prop.getSession().getValueFactory().createBinary(in);
+ prop.setValue(binary);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot set binary " + prop + " as bytes", e);
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot set binary " + prop + " as bytes", e);
+ } finally {
+ closeQuietly(binary);
+ }
+ }
+
+ /**
+ * Creates depth from a string (typically a username) by adding levels based on
+ * its first characters: "aBcD",2 becomes a/aB
+ */
+ public static String firstCharsToPath(String str, Integer nbrOfChars) {
+ if (str.length() < nbrOfChars)
+ throw new IllegalArgumentException("String " + str + " length must be greater or equal than " + nbrOfChars);
+ StringBuffer path = new StringBuffer("");
+ StringBuffer curr = new StringBuffer("");
+ for (int i = 0; i < nbrOfChars; i++) {
+ curr.append(str.charAt(i));
+ path.append(curr);
+ if (i < nbrOfChars - 1)
+ path.append('/');
+ }
+ return path.toString();
+ }
+
+ /**
+ * Discards the current changes in the session attached to this node. To be used
+ * typically in a catch block.
+ *
+ * @see #discardQuietly(Session)
+ */
+ public static void discardUnderlyingSessionQuietly(Node node) {
+ try {
+ discardQuietly(node.getSession());
+ } catch (RepositoryException e) {
+ // silent
+ }
+ }
+
+ /**
+ * Discards the current changes in a session by calling
+ * {@link Session#refresh(boolean)} with <code>false</code>, only logging
+ * potential errors when doing so. To be used typically in a catch block.
+ */
+ public static void discardQuietly(Session session) {
+ try {
+ if (session != null)
+ session.refresh(false);
+ } catch (RepositoryException e) {
+ // silent
+ }
+ }
+
+ /**
+ * Login to a workspace with implicit credentials, creates the workspace with
+ * these credentials if it does not already exist.
+ */
+ public static Session loginOrCreateWorkspace(Repository repository, String workspaceName)
+ throws RepositoryException {
+ return loginOrCreateWorkspace(repository, workspaceName, null);
+ }
+
+ /**
+ * Login to a workspace with implicit credentials, creates the workspace with
+ * these credentials if it does not already exist.
+ */
+ public static Session loginOrCreateWorkspace(Repository repository, String workspaceName, Credentials credentials)
+ throws RepositoryException {
+ Session workspaceSession = null;
+ Session defaultSession = null;
+ try {
+ try {
+ workspaceSession = repository.login(credentials, workspaceName);
+ } catch (NoSuchWorkspaceException e) {
+ // try to create workspace
+ defaultSession = repository.login(credentials);
+ defaultSession.getWorkspace().createWorkspace(workspaceName);
+
+ // work around non-atomicity of workspace creation in Jackrabbit
+// try {
+// Thread.sleep(5000);
+// } catch (InterruptedException e1) {
+// // ignore
+// }
+
+ workspaceSession = repository.login(credentials, workspaceName);
+ }
+ return workspaceSession;
+ } finally {
+ logoutQuietly(defaultSession);
+ }
+ }
+
+ /**
+ * Logs out the session, not throwing any exception, even if it is null.
+ * {@link Jcr#logout(Session)} should rather be used.
+ */
+ public static void logoutQuietly(Session session) {
+ Jcr.logout(session);
+// try {
+// if (session != null)
+// if (session.isLive())
+// session.logout();
+// } catch (Exception e) {
+// // silent
+// }
+ }
+
+ /**
+ * Convenient method to add a listener. uuids passed as null, deep=true,
+ * local=true, only one node type
+ */
+ public static void addListener(Session session, EventListener listener, int eventTypes, String basePath,
+ String nodeType) {
+ try {
+ session.getWorkspace().getObservationManager().addEventListener(listener, eventTypes, basePath, true, null,
+ nodeType == null ? null : new String[] { nodeType }, true);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot add JCR listener " + listener + " to session " + session, e);
+ }
+ }
+
+ /** Removes a listener without throwing exception */
+ public static void removeListenerQuietly(Session session, EventListener listener) {
+ if (session == null || !session.isLive())
+ return;
+ try {
+ session.getWorkspace().getObservationManager().removeEventListener(listener);
+ } catch (RepositoryException e) {
+ // silent
+ }
+ }
+
+ /**
+ * Quietly unregisters an {@link EventListener} from the udnerlying workspace of
+ * this node.
+ */
+ public static void unregisterQuietly(Node node, EventListener eventListener) {
+ try {
+ unregisterQuietly(node.getSession().getWorkspace(), eventListener);
+ } catch (RepositoryException e) {
+ // silent
+ }
+ }
+
+ /** Quietly unregisters an {@link EventListener} from this workspace */
+ public static void unregisterQuietly(Workspace workspace, EventListener eventListener) {
+ if (eventListener == null)
+ return;
+ try {
+ workspace.getObservationManager().removeEventListener(eventListener);
+ } catch (RepositoryException e) {
+ // silent
+ }
+ }
+
+ /**
+ * Checks whether {@link Property#JCR_LAST_MODIFIED} or (afterwards)
+ * {@link Property#JCR_CREATED} are set and returns it as an {@link Instant}.
+ */
+ public static Instant getModified(Node node) {
+ Calendar calendar = null;
+ try {
+ if (node.hasProperty(Property.JCR_LAST_MODIFIED))
+ calendar = node.getProperty(Property.JCR_LAST_MODIFIED).getDate();
+ else if (node.hasProperty(Property.JCR_CREATED))
+ calendar = node.getProperty(Property.JCR_CREATED).getDate();
+ else
+ throw new IllegalArgumentException("No modification time found in " + node);
+ return calendar.toInstant();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get modification time for " + node, e);
+ }
+
+ }
+
+ /**
+ * Get {@link Property#JCR_CREATED} as an {@link Instant}, if it is set.
+ */
+ public static Instant getCreated(Node node) {
+ Calendar calendar = null;
+ try {
+ if (node.hasProperty(Property.JCR_CREATED))
+ calendar = node.getProperty(Property.JCR_CREATED).getDate();
+ else
+ throw new IllegalArgumentException("No created time found in " + node);
+ return calendar.toInstant();
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get created time for " + node, e);
+ }
+
+ }
+
+ /**
+ * Updates the {@link Property#JCR_LAST_MODIFIED} property with the current time
+ * and the {@link Property#JCR_LAST_MODIFIED_BY} property with the underlying
+ * session user id.
+ */
+ public static void updateLastModified(Node node) {
+ updateLastModified(node, false);
+ }
+
+ /**
+ * Updates the {@link Property#JCR_LAST_MODIFIED} property with the current time
+ * and the {@link Property#JCR_LAST_MODIFIED_BY} property with the underlying
+ * session user id. In Jackrabbit 2.x,
+ * <a href="https://issues.apache.org/jira/browse/JCR-2233">these properties are
+ * not automatically updated</a>, hence the need for manual update. The session
+ * is not saved.
+ */
+ public static void updateLastModified(Node node, boolean addMixin) {
+ try {
+ if (addMixin && !node.isNodeType(NodeType.MIX_LAST_MODIFIED))
+ node.addMixin(NodeType.MIX_LAST_MODIFIED);
+ node.setProperty(Property.JCR_LAST_MODIFIED, new GregorianCalendar());
+ node.setProperty(Property.JCR_LAST_MODIFIED_BY, node.getSession().getUserID());
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot update last modified on " + node, e);
+ }
+ }
+
+ /**
+ * Update lastModified recursively until this parent.
+ *
+ * @param node the node
+ * @param untilPath the base path, null is equivalent to "/"
+ */
+ public static void updateLastModifiedAndParents(Node node, String untilPath) {
+ updateLastModifiedAndParents(node, untilPath, true);
+ }
+
+ /**
+ * Update lastModified recursively until this parent.
+ *
+ * @param node the node
+ * @param untilPath the base path, null is equivalent to "/"
+ */
+ public static void updateLastModifiedAndParents(Node node, String untilPath, boolean addMixin) {
+ try {
+ if (untilPath != null && !node.getPath().startsWith(untilPath))
+ throw new IllegalArgumentException(node + " is not under " + untilPath);
+ updateLastModified(node, addMixin);
+ if (untilPath == null) {
+ if (!node.getPath().equals("/"))
+ updateLastModifiedAndParents(node.getParent(), untilPath, addMixin);
+ } else {
+ if (!node.getPath().equals(untilPath))
+ updateLastModifiedAndParents(node.getParent(), untilPath, addMixin);
+ }
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot update lastModified from " + node + " until " + untilPath, e);
+ }
+ }
+
+ /**
+ * Returns a String representing the short version (see
+ * <a href="http://jackrabbit.apache.org/node-type-notation.html"> Node type
+ * Notation </a> attributes grammar) of the main business attributes of this
+ * property definition
+ *
+ * @param prop
+ */
+ public static String getPropertyDefinitionAsString(Property prop) {
+ StringBuffer sbuf = new StringBuffer();
+ try {
+ if (prop.getDefinition().isAutoCreated())
+ sbuf.append("a");
+ if (prop.getDefinition().isMandatory())
+ sbuf.append("m");
+ if (prop.getDefinition().isProtected())
+ sbuf.append("p");
+ if (prop.getDefinition().isMultiple())
+ sbuf.append("*");
+ } catch (RepositoryException re) {
+ throw new JcrException("unexpected error while getting property definition as String", re);
+ }
+ return sbuf.toString();
+ }
+
+ /**
+ * Estimate the sub tree size from current node. Computation is based on the Jcr
+ * {@link Property#getLength()} method. Note : it is not the exact size used on
+ * the disk by the current part of the JCR Tree.
+ */
+
+ public static long getNodeApproxSize(Node node) {
+ long curNodeSize = 0;
+ try {
+ PropertyIterator pi = node.getProperties();
+ while (pi.hasNext()) {
+ Property prop = pi.nextProperty();
+ if (prop.isMultiple()) {
+ int nb = prop.getLengths().length;
+ for (int i = 0; i < nb; i++) {
+ curNodeSize += (prop.getLengths()[i] > 0 ? prop.getLengths()[i] : 0);
+ }
+ } else
+ curNodeSize += (prop.getLength() > 0 ? prop.getLength() : 0);
+ }
+
+ NodeIterator ni = node.getNodes();
+ while (ni.hasNext())
+ curNodeSize += getNodeApproxSize(ni.nextNode());
+ return curNodeSize;
+ } catch (RepositoryException re) {
+ throw new JcrException("Unexpected error while recursively determining node size.", re);
+ }
+ }
+
+ /*
+ * SECURITY
+ */
+
+ /**
+ * Convenience method for adding a single privilege to a principal (user or
+ * role), typically jcr:all
+ */
+ public synchronized static void addPrivilege(Session session, String path, String principal, String privilege)
+ throws RepositoryException {
+ List<Privilege> privileges = new ArrayList<Privilege>();
+ privileges.add(session.getAccessControlManager().privilegeFromName(privilege));
+ addPrivileges(session, path, new SimplePrincipal(principal), privileges);
+ }
+
+ /**
+ * Add privileges on a path to a {@link Principal}. The path must already exist.
+ * Session is saved. Synchronized to prevent concurrent modifications of the
+ * same node.
+ */
+ public synchronized static Boolean addPrivileges(Session session, String path, Principal principal,
+ List<Privilege> privs) throws RepositoryException {
+ // make sure the session is in line with the persisted state
+ session.refresh(false);
+ AccessControlManager acm = session.getAccessControlManager();
+ AccessControlList acl = getAccessControlList(acm, path);
+
+ accessControlEntries: for (AccessControlEntry ace : acl.getAccessControlEntries()) {
+ Principal currentPrincipal = ace.getPrincipal();
+ if (currentPrincipal.getName().equals(principal.getName())) {
+ Privilege[] currentPrivileges = ace.getPrivileges();
+ if (currentPrivileges.length != privs.size())
+ break accessControlEntries;
+ for (int i = 0; i < currentPrivileges.length; i++) {
+ Privilege currP = currentPrivileges[i];
+ Privilege p = privs.get(i);
+ if (!currP.getName().equals(p.getName())) {
+ break accessControlEntries;
+ }
+ }
+ return false;
+ }
+ }
+
+ Privilege[] privileges = privs.toArray(new Privilege[privs.size()]);
+ acl.addAccessControlEntry(principal, privileges);
+ acm.setPolicy(path, acl);
+// if (log.isDebugEnabled()) {
+// StringBuffer privBuf = new StringBuffer();
+// for (Privilege priv : privs)
+// privBuf.append(priv.getName());
+// log.debug("Added privileges " + privBuf + " to " + principal.getName() + " on " + path + " in '"
+// + session.getWorkspace().getName() + "'");
+// }
+ session.refresh(true);
+ session.save();
+ return true;
+ }
+
+ /**
+ * Gets the first available access control list for this path, throws exception
+ * if not found
+ */
+ public synchronized static AccessControlList getAccessControlList(AccessControlManager acm, String path)
+ throws RepositoryException {
+ // search for an access control list
+ AccessControlList acl = null;
+ AccessControlPolicyIterator policyIterator = acm.getApplicablePolicies(path);
+ applicablePolicies: if (policyIterator.hasNext()) {
+ while (policyIterator.hasNext()) {
+ AccessControlPolicy acp = policyIterator.nextAccessControlPolicy();
+ if (acp instanceof AccessControlList) {
+ acl = ((AccessControlList) acp);
+ break applicablePolicies;
+ }
+ }
+ } else {
+ AccessControlPolicy[] existingPolicies = acm.getPolicies(path);
+ existingPolicies: for (AccessControlPolicy acp : existingPolicies) {
+ if (acp instanceof AccessControlList) {
+ acl = ((AccessControlList) acp);
+ break existingPolicies;
+ }
+ }
+ }
+ if (acl != null)
+ return acl;
+ else
+ throw new IllegalArgumentException("ACL not found at " + path);
+ }
+
+ /** Clear authorizations for a user at this path */
+ public synchronized static void clearAccessControList(Session session, String path, String username)
+ throws RepositoryException {
+ AccessControlManager acm = session.getAccessControlManager();
+ AccessControlList acl = getAccessControlList(acm, path);
+ for (AccessControlEntry ace : acl.getAccessControlEntries()) {
+ if (ace.getPrincipal().getName().equals(username)) {
+ acl.removeAccessControlEntry(ace);
+ }
+ }
+ // the new access control list must be applied otherwise this call:
+ // acl.removeAccessControlEntry(ace); has no effect
+ acm.setPolicy(path, acl);
+ session.refresh(true);
+ session.save();
+ }
+
+ /*
+ * FILES UTILITIES
+ */
+ /**
+ * Creates the nodes making the path as {@link NodeType#NT_FOLDER}
+ */
+ public static Node mkfolders(Session session, String path) {
+ return mkdirs(session, path, NodeType.NT_FOLDER, NodeType.NT_FOLDER, false);
+ }
+
+ /**
+ * Copy only nt:folder and nt:file, without their additional types and
+ * properties.
+ *
+ * @param recursive if true copies folders as well, otherwise only first level
+ * files
+ * @return how many files were copied
+ */
+ public static Long copyFiles(Node fromNode, Node toNode, Boolean recursive, JcrMonitor monitor, boolean onlyAdd) {
+ long count = 0l;
+
+ // Binary binary = null;
+ // InputStream in = null;
+ try {
+ NodeIterator fromChildren = fromNode.getNodes();
+ children: while (fromChildren.hasNext()) {
+ if (monitor != null && monitor.isCanceled())
+ throw new IllegalStateException("Copy cancelled before it was completed");
+
+ Node fromChild = fromChildren.nextNode();
+ String fileName = fromChild.getName();
+ if (fromChild.isNodeType(NodeType.NT_FILE)) {
+ if (onlyAdd && toNode.hasNode(fileName)) {
+ monitor.subTask("Skip existing " + fileName);
+ continue children;
+ }
+
+ if (monitor != null)
+ monitor.subTask("Copy " + fileName);
+ try (Bin binary = new Bin(fromChild.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA));
+ InputStream in = binary.getStream();) {
+ copyStreamAsFile(toNode, fileName, in);
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot copy " + fileName + " to " + toNode, e);
+ }
+
+ // save session
+ toNode.getSession().save();
+ count++;
+
+// if (log.isDebugEnabled())
+// log.debug("Copied file " + fromChild.getPath());
+ if (monitor != null)
+ monitor.worked(1);
+ } else if (fromChild.isNodeType(NodeType.NT_FOLDER) && recursive) {
+ Node toChildFolder;
+ if (toNode.hasNode(fileName)) {
+ toChildFolder = toNode.getNode(fileName);
+ if (!toChildFolder.isNodeType(NodeType.NT_FOLDER))
+ throw new IllegalArgumentException(toChildFolder + " is not of type nt:folder");
+ } else {
+ toChildFolder = toNode.addNode(fileName, NodeType.NT_FOLDER);
+
+ // save session
+ toNode.getSession().save();
+ }
+ count = count + copyFiles(fromChild, toChildFolder, recursive, monitor, onlyAdd);
+ }
+ }
+ return count;
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot copy files between " + fromNode + " and " + toNode, e);
+ } finally {
+ // in case there was an exception
+ // IOUtils.closeQuietly(in);
+ // closeQuietly(binary);
+ }
+ }
+
+ /**
+ * Iteratively count all file nodes in subtree, inefficient but can be useful
+ * when query are poorly supported, such as in remoting.
+ */
+ public static Long countFiles(Node node) {
+ Long localCount = 0l;
+ try {
+ for (NodeIterator nit = node.getNodes(); nit.hasNext();) {
+ Node child = nit.nextNode();
+ if (child.isNodeType(NodeType.NT_FOLDER))
+ localCount = localCount + countFiles(child);
+ else if (child.isNodeType(NodeType.NT_FILE))
+ localCount = localCount + 1;
+ }
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot count all children of " + node, e);
+ }
+ return localCount;
+ }
+
+ /**
+ * Copy a file as an nt:file, assuming an nt:folder hierarchy. The session is
+ * NOT saved.
+ *
+ * @return the created file node
+ */
+ @Deprecated
+ public static Node copyFile(Node folderNode, File file) {
+ try (InputStream in = new FileInputStream(file)) {
+ return copyStreamAsFile(folderNode, file.getName(), in);
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot copy file " + file + " under " + folderNode, e);
+ }
+ }
+
+ /** Copy bytes as an nt:file */
+ public static Node copyBytesAsFile(Node folderNode, String fileName, byte[] bytes) {
+ // InputStream in = null;
+ try (InputStream in = new ByteArrayInputStream(bytes)) {
+ // in = new ByteArrayInputStream(bytes);
+ return copyStreamAsFile(folderNode, fileName, in);
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot copy file " + fileName + " under " + folderNode, e);
+ // } finally {
+ // IOUtils.closeQuietly(in);
+ }
+ }
+
+ /**
+ * Copy a stream as an nt:file, assuming an nt:folder hierarchy. The session is
+ * NOT saved.
+ *
+ * @return the created file node
+ */
+ public static Node copyStreamAsFile(Node folderNode, String fileName, InputStream in) {
+ Binary binary = null;
+ try {
+ Node fileNode;
+ Node contentNode;
+ if (folderNode.hasNode(fileName)) {
+ fileNode = folderNode.getNode(fileName);
+ if (!fileNode.isNodeType(NodeType.NT_FILE))
+ throw new IllegalArgumentException(fileNode + " is not of type nt:file");
+ // we assume that the content node is already there
+ contentNode = fileNode.getNode(Node.JCR_CONTENT);
+ } else {
+ fileNode = folderNode.addNode(fileName, NodeType.NT_FILE);
+ contentNode = fileNode.addNode(Node.JCR_CONTENT, NodeType.NT_UNSTRUCTURED);
+ }
+ binary = contentNode.getSession().getValueFactory().createBinary(in);
+ contentNode.setProperty(Property.JCR_DATA, binary);
+ updateLastModified(contentNode);
+ return fileNode;
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot create file node " + fileName + " under " + folderNode, e);
+ } finally {
+ closeQuietly(binary);
+ }
+ }
+
+ /** Read an an nt:file as an {@link InputStream}. */
+ public static InputStream getFileAsStream(Node fileNode) throws RepositoryException {
+ return fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary().getStream();
+ }
+
+ /**
+ * Set the properties of {@link NodeType#MIX_MIMETYPE} on the content of this
+ * file node.
+ */
+ public static void setFileMimeType(Node fileNode, String mimeType, String encoding) throws RepositoryException {
+ Node contentNode = fileNode.getNode(Node.JCR_CONTENT);
+ if (mimeType != null)
+ contentNode.setProperty(Property.JCR_MIMETYPE, mimeType);
+ if (encoding != null)
+ contentNode.setProperty(Property.JCR_ENCODING, encoding);
+ // TODO remove properties if args are null?
+ }
+
+ public static void copyFilesToFs(Node baseNode, Path targetDir, boolean recursive) {
+ try {
+ Files.createDirectories(targetDir);
+ for (NodeIterator nit = baseNode.getNodes(); nit.hasNext();) {
+ Node node = nit.nextNode();
+ if (node.isNodeType(NodeType.NT_FILE)) {
+ Path filePath = targetDir.resolve(node.getName());
+ try (OutputStream out = Files.newOutputStream(filePath); InputStream in = getFileAsStream(node)) {
+ IOUtils.copy(in, out);
+ }
+ } else if (recursive && node.isNodeType(NodeType.NT_FOLDER)) {
+ Path dirPath = targetDir.resolve(node.getName());
+ copyFilesToFs(node, dirPath, true);
+ }
+ }
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot copy " + baseNode + " to " + targetDir, e);
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot copy " + baseNode + " to " + targetDir, e);
+ }
+ }
+
+ /**
+ * Computes the checksum of an nt:file.
+ *
+ * @deprecated use separate digest utilities
+ */
+ @Deprecated
+ public static String checksumFile(Node fileNode, String algorithm) {
+ try (InputStream in = fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary()
+ .getStream()) {
+ return digest(algorithm, in);
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot checksum file " + fileNode + " with algorithm " + algorithm, e);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot checksum file " + fileNode + " with algorithm " + algorithm, e);
+ }
+ }
+
+ @Deprecated
+ private static String digest(String algorithm, InputStream in) {
+ final Integer byteBufferCapacity = 100 * 1024;// 100 KB
+ try {
+ MessageDigest digest = MessageDigest.getInstance(algorithm);
+ byte[] buffer = new byte[byteBufferCapacity];
+ int read = 0;
+ while ((read = in.read(buffer)) > 0) {
+ digest.update(buffer, 0, read);
+ }
+
+ byte[] checksum = digest.digest();
+ String res = encodeHexString(checksum);
+ return res;
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot digest with algorithm " + algorithm, e);
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e);
+ }
+ }
+
+ /**
+ * From
+ * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to
+ * -a-hex-string-in-java
+ */
+ @Deprecated
+ private static String encodeHexString(byte[] bytes) {
+ final char[] hexArray = "0123456789abcdef".toCharArray();
+ char[] hexChars = new char[bytes.length * 2];
+ for (int j = 0; j < bytes.length; j++) {
+ int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = hexArray[v >>> 4];
+ hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+
+ /** Export a subtree as a compact XML without namespaces. */
+ public static void toSimpleXml(Node node, StringBuilder sb) throws RepositoryException {
+ sb.append('<');
+ String nodeName = node.getName();
+ int colIndex = nodeName.indexOf(':');
+ if (colIndex > 0) {
+ nodeName = nodeName.substring(colIndex + 1);
+ }
+ sb.append(nodeName);
+ PropertyIterator pit = node.getProperties();
+ properties: while (pit.hasNext()) {
+ Property p = pit.nextProperty();
+ // skip multiple properties
+ if (p.isMultiple())
+ continue properties;
+ String propertyName = p.getName();
+ int pcolIndex = propertyName.indexOf(':');
+ // skip properties with namespaces
+ if (pcolIndex > 0)
+ continue properties;
+ // skip binaries
+ if (p.getType() == PropertyType.BINARY) {
+ continue properties;
+ // TODO retrieve identifier?
+ }
+ sb.append(' ');
+ sb.append(propertyName);
+ sb.append('=');
+ sb.append('\"').append(p.getString()).append('\"');
+ }
+
+ if (node.hasNodes()) {
+ sb.append('>');
+ NodeIterator children = node.getNodes();
+ while (children.hasNext()) {
+ toSimpleXml(children.nextNode(), sb);
+ }
+ sb.append("</");
+ sb.append(nodeName);
+ sb.append('>');
+ } else {
+ sb.append("/>");
+ }
+ }
+
+}
--- /dev/null
+package org.argeo.jcr;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+
+/** Uilities around the JCR extensions. */
+public class JcrxApi {
+ public final static String MD5 = "MD5";
+ public final static String SHA1 = "SHA1";
+ public final static String SHA256 = "SHA-256";
+ public final static String SHA512 = "SHA-512";
+
+ public final static String EMPTY_MD5 = "d41d8cd98f00b204e9800998ecf8427e";
+ public final static String EMPTY_SHA1 = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
+ public final static String EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
+ public final static String EMPTY_SHA512 = "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e";
+
+ public final static int LENGTH_MD5 = EMPTY_MD5.length();
+ public final static int LENGTH_SHA1 = EMPTY_SHA1.length();
+ public final static int LENGTH_SHA256 = EMPTY_SHA256.length();
+ public final static int LENGTH_SHA512 = EMPTY_SHA512.length();
+
+ /*
+ * XML
+ */
+ /**
+ * Get the XML text of this child node.
+ */
+ public static String getXmlValue(Node node, String name) {
+ try {
+ if (!node.hasNode(name))
+ return null;
+ Node child = node.getNode(name);
+ return getXmlValue(child);
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Cannot get " + name + " as XML text", e);
+ }
+ }
+
+ /**
+ * Get the XML text of this node.
+ */
+ public static String getXmlValue(Node node) {
+ try {
+ if (!node.hasNode(Jcr.JCR_XMLTEXT))
+ return null;
+ Node xmlText = node.getNode(Jcr.JCR_XMLTEXT);
+ if (!xmlText.hasProperty(Jcr.JCR_XMLCHARACTERS))
+ throw new IllegalArgumentException(
+ "Node " + xmlText + " has no " + Jcr.JCR_XMLCHARACTERS + " property");
+ return xmlText.getProperty(Jcr.JCR_XMLCHARACTERS).getString();
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Cannot get " + node + " as XML text", e);
+ }
+ }
+
+ /**
+ * Set as a subnode which will be exported as an XML element.
+ */
+ public static void setXmlValue(Node node, String name, String value) {
+ try {
+ if (node.hasNode(name)) {
+ Node child = node.getNode(name);
+ setXmlValue(node, child, value);
+ } else
+ node.addNode(name, JcrxType.JCRX_XMLVALUE).addNode(Jcr.JCR_XMLTEXT, JcrxType.JCRX_XMLTEXT)
+ .setProperty(Jcr.JCR_XMLCHARACTERS, value);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot set " + name + " as XML text", e);
+ }
+ }
+
+ public static void setXmlValue(Node node, Node child, String value) {
+ try {
+ if (!child.hasNode(Jcr.JCR_XMLTEXT))
+ child.addNode(Jcr.JCR_XMLTEXT, JcrxType.JCRX_XMLTEXT);
+ child.getNode(Jcr.JCR_XMLTEXT).setProperty(Jcr.JCR_XMLCHARACTERS, value);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot set " + child + " as XML text", e);
+ }
+ }
+
+ /**
+ * Add a checksum replacing the one which was previously set with the same
+ * length.
+ */
+ public static void addChecksum(Node node, String checksum) {
+ try {
+ if (!node.hasProperty(JcrxName.JCRX_SUM)) {
+ node.setProperty(JcrxName.JCRX_SUM, new String[] { checksum });
+ return;
+ } else {
+ int stringLength = checksum.length();
+ Property property = node.getProperty(JcrxName.JCRX_SUM);
+ List<Value> values = Arrays.asList(property.getValues());
+ Integer indexToRemove = null;
+ values: for (int i = 0; i < values.size(); i++) {
+ Value value = values.get(i);
+ if (value.getString().length() == stringLength) {
+ indexToRemove = i;
+ break values;
+ }
+ }
+ if (indexToRemove != null)
+ values.set(indexToRemove, node.getSession().getValueFactory().createValue(checksum));
+ else
+ values.add(0, node.getSession().getValueFactory().createValue(checksum));
+ property.setValue(values.toArray(new Value[values.size()]));
+ }
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot set checksum on " + node, e);
+ }
+ }
+
+ /** Replace all checksums. */
+ public static void setChecksums(Node node, List<String> checksums) {
+ try {
+ node.setProperty(JcrxName.JCRX_SUM, checksums.toArray(new String[checksums.size()]));
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot set checksums on " + node, e);
+ }
+ }
+
+ /** Replace all checksums. */
+ public static List<String> getChecksums(Node node) {
+ try {
+ List<String> res = new ArrayList<>();
+ if (!node.hasProperty(JcrxName.JCRX_SUM))
+ return res;
+ Property property = node.getProperty(JcrxName.JCRX_SUM);
+ for (Value value : property.getValues()) {
+ res.add(value.getString());
+ }
+ return res;
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot get checksums from " + node, e);
+ }
+ }
+
+// /** Replace all checksums with this single one. */
+// public static void setChecksum(Node node, String checksum) {
+// setChecksums(node, Collections.singletonList(checksum));
+// }
+
+ /** Retrieves the checksum with this algorithm, or null if not found. */
+ public static String getChecksum(Node node, String algorithm) {
+ int stringLength;
+ switch (algorithm) {
+ case MD5:
+ stringLength = LENGTH_MD5;
+ break;
+ case SHA1:
+ stringLength = LENGTH_SHA1;
+ break;
+ case SHA256:
+ stringLength = LENGTH_SHA256;
+ break;
+ case SHA512:
+ stringLength = LENGTH_SHA512;
+ break;
+ default:
+ throw new IllegalArgumentException("Unkown algorithm " + algorithm);
+ }
+ return getChecksum(node, stringLength);
+ }
+
+ /** Retrieves the checksum with this string length, or null if not found. */
+ public static String getChecksum(Node node, int stringLength) {
+ try {
+ if (!node.hasProperty(JcrxName.JCRX_SUM))
+ return null;
+ Property property = node.getProperty(JcrxName.JCRX_SUM);
+ for (Value value : property.getValues()) {
+ String str = value.getString();
+ if (str.length() == stringLength)
+ return str;
+ }
+ return null;
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Cannot get checksum for " + node, e);
+ }
+ }
+
+}
--- /dev/null
+package org.argeo.jcr;
+
+/** Names declared by the JCR extensions. */
+public interface JcrxName {
+ /** The multiple property holding various coherent checksums. */
+ public final static String JCRX_SUM = "{http://www.argeo.org/ns/jcrx}sum";
+}
--- /dev/null
+package org.argeo.jcr;
+
+/** Node types declared by the JCR extensions. */
+public interface JcrxType {
+ /**
+ * Node type for an XML value, which will be serialized in XML as an element
+ * containing text.
+ */
+ public final static String JCRX_XMLVALUE = "{http://www.argeo.org/ns/jcrx}xmlvalue";
+
+ /** Node type for the node containing the text. */
+ public final static String JCRX_XMLTEXT = "{http://www.argeo.org/ns/jcrx}xmltext";
+
+ /** Mixin node type for a set of checksums. */
+ public final static String JCRX_CSUM = "{http://www.argeo.org/ns/jcrx}csum";
+
+}
--- /dev/null
+package org.argeo.jcr;
+
+import javax.jcr.Value;
+
+/** The result of the comparison of two JCR properties. */
+public class PropertyDiff {
+ public final static Integer MODIFIED = 0;
+ public final static Integer ADDED = 1;
+ public final static Integer REMOVED = 2;
+
+ private final Integer type;
+ private final String relPath;
+ private final Value referenceValue;
+ private final Value newValue;
+
+ public PropertyDiff(Integer type, String relPath, Value referenceValue, Value newValue) {
+ super();
+
+ if (type == MODIFIED) {
+ if (referenceValue == null || newValue == null)
+ throw new IllegalArgumentException("Reference and new values must be specified.");
+ } else if (type == ADDED) {
+ if (referenceValue != null || newValue == null)
+ throw new IllegalArgumentException("New value and only it must be specified.");
+ } else if (type == REMOVED) {
+ if (referenceValue == null || newValue != null)
+ throw new IllegalArgumentException("Reference value and only it must be specified.");
+ } else {
+ throw new IllegalArgumentException("Unkown diff type " + type);
+ }
+
+ if (relPath == null)
+ throw new IllegalArgumentException("Relative path must be specified");
+
+ this.type = type;
+ this.relPath = relPath;
+ this.referenceValue = referenceValue;
+ this.newValue = newValue;
+ }
+
+ public Integer getType() {
+ return type;
+ }
+
+ public String getRelPath() {
+ return relPath;
+ }
+
+ public Value getReferenceValue() {
+ return referenceValue;
+ }
+
+ public Value getNewValue() {
+ return newValue;
+ }
+
+}
--- /dev/null
+package org.argeo.jcr;
+
+import java.security.Principal;
+
+/** Canonical implementation of a {@link Principal} */
+class SimplePrincipal implements Principal {
+ private final String name;
+
+ public SimplePrincipal(String name) {
+ if (name == null)
+ throw new IllegalArgumentException("Principal name cannot be null");
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null)
+ return false;
+ if (obj instanceof Principal)
+ return name.equals((((Principal) obj).getName()));
+ return name.equals(obj.toString());
+ }
+
+ @Override
+ protected Object clone() throws CloneNotSupportedException {
+ return new SimplePrincipal(name);
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+}
--- /dev/null
+package org.argeo.jcr;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.jcr.LoginException;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+
+import org.argeo.api.cms.CmsLog;
+
+/** Proxy JCR sessions and attach them to calling threads. */
+@Deprecated
+public abstract class ThreadBoundJcrSessionFactory {
+ private final static CmsLog log = CmsLog.getLog(ThreadBoundJcrSessionFactory.class);
+
+ private Repository repository;
+ /** can be injected as list, only used if repository is null */
+ private List<Repository> repositories;
+
+ private ThreadLocal<Session> session = new ThreadLocal<Session>();
+ private final Session proxiedSession;
+ /** If workspace is null, default will be used. */
+ private String workspace = null;
+
+ private String defaultUsername = "demo";
+ private String defaultPassword = "demo";
+ private Boolean forceDefaultCredentials = false;
+
+ private boolean active = true;
+
+ // monitoring
+ private final List<Thread> threads = Collections.synchronizedList(new ArrayList<Thread>());
+ private final Map<Long, Session> activeSessions = Collections.synchronizedMap(new HashMap<Long, Session>());
+ private MonitoringThread monitoringThread;
+
+ public ThreadBoundJcrSessionFactory() {
+ Class<?>[] interfaces = { Session.class };
+ proxiedSession = (Session) Proxy.newProxyInstance(ThreadBoundJcrSessionFactory.class.getClassLoader(),
+ interfaces, new JcrSessionInvocationHandler());
+ }
+
+ /** Logs in to the repository using various strategies. */
+ protected synchronized Session login() {
+ if (!isActive())
+ throw new IllegalStateException("Thread bound session factory inactive");
+
+ // discard session previously attached to this thread
+ Thread thread = Thread.currentThread();
+ if (activeSessions.containsKey(thread.getId())) {
+ Session oldSession = activeSessions.remove(thread.getId());
+ oldSession.logout();
+ session.remove();
+ }
+
+ Session newSession = null;
+ // first try to login without credentials, assuming the underlying login
+ // module will have dealt with authentication (typically using Spring
+ // Security)
+ if (!forceDefaultCredentials)
+ try {
+ newSession = repository().login(workspace);
+ } catch (LoginException e1) {
+ log.warn("Cannot login without credentials: " + e1.getMessage());
+ // invalid credentials, go to the next step
+ } catch (RepositoryException e1) {
+ // other kind of exception, fail
+ throw new JcrException("Cannot log in to repository", e1);
+ }
+
+ // log using default username / password (useful for testing purposes)
+ if (newSession == null)
+ try {
+ SimpleCredentials sc = new SimpleCredentials(defaultUsername, defaultPassword.toCharArray());
+ newSession = repository().login(sc, workspace);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot log in to repository", e);
+ }
+
+ session.set(newSession);
+ // Log and monitor new session
+ if (log.isTraceEnabled())
+ log.trace("Logged in to JCR session " + newSession + "; userId=" + newSession.getUserID());
+
+ // monitoring
+ activeSessions.put(thread.getId(), newSession);
+ threads.add(thread);
+ return newSession;
+ }
+
+ public Object getObject() {
+ return proxiedSession;
+ }
+
+ public void init() throws Exception {
+ // log.error("SHOULD NOT BE USED ANYMORE");
+ monitoringThread = new MonitoringThread();
+ monitoringThread.start();
+ }
+
+ public void dispose() throws Exception {
+ // if (activeSessions.size() == 0)
+ // return;
+
+ if (log.isTraceEnabled())
+ log.trace("Cleaning up " + activeSessions.size() + " active JCR sessions...");
+
+ deactivate();
+ for (Session sess : activeSessions.values()) {
+ JcrUtils.logoutQuietly(sess);
+ }
+ activeSessions.clear();
+ }
+
+ protected Boolean isActive() {
+ return active;
+ }
+
+ protected synchronized void deactivate() {
+ active = false;
+ notifyAll();
+ }
+
+ protected synchronized void removeSession(Thread thread) {
+ if (!isActive())
+ return;
+ activeSessions.remove(thread.getId());
+ threads.remove(thread);
+ }
+
+ protected synchronized void cleanDeadThreads() {
+ if (!isActive())
+ return;
+ Iterator<Thread> it = threads.iterator();
+ while (it.hasNext()) {
+ Thread thread = it.next();
+ if (!thread.isAlive() && isActive()) {
+ if (activeSessions.containsKey(thread.getId())) {
+ Session session = activeSessions.get(thread.getId());
+ activeSessions.remove(thread.getId());
+ session.logout();
+ if (log.isTraceEnabled())
+ log.trace("Cleaned up JCR session (userID=" + session.getUserID() + ") from dead thread "
+ + thread.getId());
+ }
+ it.remove();
+ }
+ }
+ try {
+ wait(1000);
+ } catch (InterruptedException e) {
+ // silent
+ }
+ }
+
+ public Class<? extends Session> getObjectType() {
+ return Session.class;
+ }
+
+ public boolean isSingleton() {
+ return true;
+ }
+
+ /**
+ * Called before a method is actually called, allowing to check the session or
+ * re-login it (e.g. if authentication has changed). The default implementation
+ * returns the session.
+ */
+ protected Session preCall(Session session) {
+ return session;
+ }
+
+ protected Repository repository() {
+ if (repository != null)
+ return repository;
+ if (repositories != null) {
+ // hardened for OSGi dynamic services
+ Iterator<Repository> it = repositories.iterator();
+ if (it.hasNext())
+ return it.next();
+ }
+ throw new IllegalStateException("No repository injected");
+ }
+
+ // /** Useful for declarative registration of OSGi services (blueprint) */
+ // public void register(Repository repository, Map<?, ?> params) {
+ // this.repository = repository;
+ // }
+ //
+ // /** Useful for declarative registration of OSGi services (blueprint) */
+ // public void unregister(Repository repository, Map<?, ?> params) {
+ // this.repository = null;
+ // }
+
+ public void setRepository(Repository repository) {
+ this.repository = repository;
+ }
+
+ public void setRepositories(List<Repository> repositories) {
+ this.repositories = repositories;
+ }
+
+ public void setDefaultUsername(String defaultUsername) {
+ this.defaultUsername = defaultUsername;
+ }
+
+ public void setDefaultPassword(String defaultPassword) {
+ this.defaultPassword = defaultPassword;
+ }
+
+ public void setForceDefaultCredentials(Boolean forceDefaultCredentials) {
+ this.forceDefaultCredentials = forceDefaultCredentials;
+ }
+
+ public void setWorkspace(String workspace) {
+ this.workspace = workspace;
+ }
+
+ protected class JcrSessionInvocationHandler implements InvocationHandler {
+
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable, RepositoryException {
+ Session threadSession = session.get();
+ if (threadSession == null) {
+ if ("logout".equals(method.getName()))// no need to login
+ return Void.TYPE;
+ else if ("toString".equals(method.getName()))// maybe logging
+ return "Uninitialized Argeo thread bound JCR session";
+ threadSession = login();
+ }
+
+ preCall(threadSession);
+ Object ret;
+ try {
+ ret = method.invoke(threadSession, args);
+ } catch (InvocationTargetException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof RepositoryException)
+ throw (RepositoryException) cause;
+ else
+ throw cause;
+ }
+ if ("logout".equals(method.getName())) {
+ session.remove();
+ Thread thread = Thread.currentThread();
+ removeSession(thread);
+ if (log.isTraceEnabled())
+ log.trace("Logged out JCR session (userId=" + threadSession.getUserID() + ") on thread "
+ + thread.getId());
+ }
+ return ret;
+ }
+ }
+
+ /** Monitors registered thread in order to clean up dead ones. */
+ private class MonitoringThread extends Thread {
+
+ public MonitoringThread() {
+ super("ThreadBound JCR Session Monitor");
+ }
+
+ @Override
+ public void run() {
+ while (isActive()) {
+ cleanDeadThreads();
+ }
+ }
+
+ }
+}
--- /dev/null
+package org.argeo.jcr;
+
+import java.util.Calendar;
+import java.util.Map;
+
+/**
+ * Generic Object that enables the creation of history reports based on a JCR
+ * versionable node. userId and creation date are added to the map of
+ * PropertyDiff.
+ *
+ * These two fields might be null
+ *
+ */
+public class VersionDiff {
+
+ private String userId;
+ private Map<String, PropertyDiff> diffs;
+ private Calendar updateTime;
+
+ public VersionDiff(String userId, Calendar updateTime,
+ Map<String, PropertyDiff> diffs) {
+ this.userId = userId;
+ this.updateTime = updateTime;
+ this.diffs = diffs;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public Map<String, PropertyDiff> getDiffs() {
+ return diffs;
+ }
+
+ public Calendar getUpdateTime() {
+ return updateTime;
+ }
+}
--- /dev/null
+package org.argeo.jcr.fs;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.FileChannel;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.jcr.JcrUtils;
+
+/** A read/write {@link SeekableByteChannel} based on a {@link Binary}. */
+public class BinaryChannel implements SeekableByteChannel {
+ private final Node file;
+ private Binary binary;
+ private boolean open = true;
+
+ private long position = 0;
+
+ private FileChannel fc = null;
+
+ public BinaryChannel(Node file, Path path) throws RepositoryException, IOException {
+ this.file = file;
+ Session session = file.getSession();
+ synchronized (session) {
+ if (file.isNodeType(NodeType.NT_FILE)) {
+ if (file.hasNode(Node.JCR_CONTENT)) {
+ Node data = file.getNode(Property.JCR_CONTENT);
+ this.binary = data.getProperty(Property.JCR_DATA).getBinary();
+ } else {
+ Node data = file.addNode(Node.JCR_CONTENT, NodeType.NT_UNSTRUCTURED);
+ data.addMixin(NodeType.MIX_LAST_MODIFIED);
+ try (InputStream in = new ByteArrayInputStream(new byte[0])) {
+ this.binary = data.getSession().getValueFactory().createBinary(in);
+ }
+ data.setProperty(Property.JCR_DATA, this.binary);
+
+ // MIME type
+ String mime = Files.probeContentType(path);
+ // String mime = fileTypeMap.getContentType(file.getName());
+ data.setProperty(Property.JCR_MIMETYPE, mime);
+
+ session.refresh(true);
+ session.save();
+ session.notifyAll();
+ }
+ } else {
+ throw new IllegalArgumentException(
+ "Unsupported file node " + file + " (" + file.getPrimaryNodeType() + ")");
+ }
+ }
+ }
+
+ @Override
+ public synchronized boolean isOpen() {
+ return open;
+ }
+
+ @Override
+ public synchronized void close() throws IOException {
+ if (isModified()) {
+ Binary newBinary = null;
+ try {
+ Session session = file.getSession();
+ synchronized (session) {
+ fc.position(0);
+ InputStream in = Channels.newInputStream(fc);
+ newBinary = session.getValueFactory().createBinary(in);
+ file.getNode(Property.JCR_CONTENT).setProperty(Property.JCR_DATA, newBinary);
+ session.refresh(true);
+ session.save();
+ open = false;
+ session.notifyAll();
+ }
+ } catch (RepositoryException e) {
+ throw new IOException("Cannot close " + file, e);
+ } finally {
+ JcrUtils.closeQuietly(newBinary);
+ // IOUtils.closeQuietly(fc);
+ if (fc != null) {
+ fc.close();
+ }
+ }
+ } else {
+ clearReadState();
+ open = false;
+ }
+ }
+
+ @Override
+ public int read(ByteBuffer dst) throws IOException {
+ if (isModified()) {
+ return fc.read(dst);
+ } else {
+
+ try {
+ int read;
+ byte[] arr = dst.array();
+ read = binary.read(arr, position);
+
+ if (read != -1)
+ position = position + read;
+ return read;
+ } catch (RepositoryException e) {
+ throw new IOException("Cannot read into buffer", e);
+ }
+ }
+ }
+
+ @Override
+ public int write(ByteBuffer src) throws IOException {
+ int written = getFileChannel().write(src);
+ return written;
+ }
+
+ @Override
+ public long position() throws IOException {
+ if (isModified())
+ return getFileChannel().position();
+ else
+ return position;
+ }
+
+ @Override
+ public SeekableByteChannel position(long newPosition) throws IOException {
+ if (isModified()) {
+ getFileChannel().position(position);
+ } else {
+ this.position = newPosition;
+ }
+ return this;
+ }
+
+ @Override
+ public long size() throws IOException {
+ if (isModified()) {
+ return getFileChannel().size();
+ } else {
+ try {
+ return binary.getSize();
+ } catch (RepositoryException e) {
+ throw new IOException("Cannot get size", e);
+ }
+ }
+ }
+
+ @Override
+ public SeekableByteChannel truncate(long size) throws IOException {
+ getFileChannel().truncate(size);
+ return this;
+ }
+
+ private FileChannel getFileChannel() throws IOException {
+ try {
+ if (fc == null) {
+ Path tempPath = Files.createTempFile(getClass().getSimpleName(), null);
+ fc = FileChannel.open(tempPath, StandardOpenOption.WRITE, StandardOpenOption.READ,
+ StandardOpenOption.DELETE_ON_CLOSE, StandardOpenOption.SPARSE);
+ ReadableByteChannel readChannel = Channels.newChannel(binary.getStream());
+ fc.transferFrom(readChannel, 0, binary.getSize());
+ clearReadState();
+ }
+ return fc;
+ } catch (RepositoryException e) {
+ throw new IOException("Cannot get temp file channel", e);
+ }
+ }
+
+ private boolean isModified() {
+ return fc != null;
+ }
+
+ private void clearReadState() {
+ position = -1;
+ JcrUtils.closeQuietly(binary);
+ binary = null;
+ }
+}
--- /dev/null
+package org.argeo.jcr.fs;
+
+import static javax.jcr.Property.JCR_CREATED;
+import static javax.jcr.Property.JCR_LAST_MODIFIED;
+
+import java.nio.file.attribute.FileTime;
+import java.time.Instant;
+
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.jcr.JcrUtils;
+
+public class JcrBasicfileAttributes implements NodeFileAttributes {
+ private final Node node;
+
+ private final static FileTime EPOCH = FileTime.fromMillis(0);
+
+ public JcrBasicfileAttributes(Node node) {
+ if (node == null)
+ throw new JcrFsException("Node underlying the attributes cannot be null");
+ this.node = node;
+ }
+
+ @Override
+ public FileTime lastModifiedTime() {
+ try {
+ if (node.hasProperty(JCR_LAST_MODIFIED)) {
+ Instant instant = node.getProperty(JCR_LAST_MODIFIED).getDate().toInstant();
+ return FileTime.from(instant);
+ } else if (node.hasProperty(JCR_CREATED)) {
+ Instant instant = node.getProperty(JCR_CREATED).getDate().toInstant();
+ return FileTime.from(instant);
+ }
+// if (node.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
+// Instant instant = node.getProperty(Property.JCR_LAST_MODIFIED).getDate().toInstant();
+// return FileTime.from(instant);
+// }
+ return EPOCH;
+ } catch (RepositoryException e) {
+ throw new JcrFsException("Cannot get last modified time", e);
+ }
+ }
+
+ @Override
+ public FileTime lastAccessTime() {
+ return lastModifiedTime();
+ }
+
+ @Override
+ public FileTime creationTime() {
+ try {
+ if (node.hasProperty(JCR_CREATED)) {
+ Instant instant = node.getProperty(JCR_CREATED).getDate().toInstant();
+ return FileTime.from(instant);
+ } else if (node.hasProperty(JCR_LAST_MODIFIED)) {
+ Instant instant = node.getProperty(JCR_LAST_MODIFIED).getDate().toInstant();
+ return FileTime.from(instant);
+ }
+// if (node.isNodeType(NodeType.MIX_CREATED)) {
+// Instant instant = node.getProperty(JCR_CREATED).getDate().toInstant();
+// return FileTime.from(instant);
+// }
+ return EPOCH;
+ } catch (RepositoryException e) {
+ throw new JcrFsException("Cannot get creation time", e);
+ }
+ }
+
+ @Override
+ public boolean isRegularFile() {
+ try {
+ return node.isNodeType(NodeType.NT_FILE);
+ } catch (RepositoryException e) {
+ throw new JcrFsException("Cannot check if regular file", e);
+ }
+ }
+
+ @Override
+ public boolean isDirectory() {
+ try {
+ if (node.isNodeType(NodeType.NT_FOLDER))
+ return true;
+ // all other non file nodes
+ return !(node.isNodeType(NodeType.NT_FILE) || node.isNodeType(NodeType.NT_LINKED_FILE));
+ } catch (RepositoryException e) {
+ throw new JcrFsException("Cannot check if directory", e);
+ }
+ }
+
+ @Override
+ public boolean isSymbolicLink() {
+ try {
+ return node.isNodeType(NodeType.NT_LINKED_FILE);
+ } catch (RepositoryException e) {
+ throw new JcrFsException("Cannot check if linked file", e);
+ }
+ }
+
+ @Override
+ public boolean isOther() {
+ return !(isDirectory() || isRegularFile() || isSymbolicLink());
+ }
+
+ @Override
+ public long size() {
+ if (isRegularFile()) {
+ Binary binary = null;
+ try {
+ binary = node.getNode(Property.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary();
+ return binary.getSize();
+ } catch (RepositoryException e) {
+ throw new JcrFsException("Cannot check size", e);
+ } finally {
+ JcrUtils.closeQuietly(binary);
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public Object fileKey() {
+ try {
+ return node.getIdentifier();
+ } catch (RepositoryException e) {
+ throw new JcrFsException("Cannot get identifier", e);
+ }
+ }
+
+ @Override
+ public Node getNode() {
+ return node;
+ }
+
+}
--- /dev/null
+package org.argeo.jcr.fs;
+
+import java.io.IOException;
+import java.nio.file.FileStore;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.WatchService;
+import java.nio.file.attribute.UserPrincipalLookupService;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+import javax.jcr.Credentials;
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.api.acr.fs.AbstractFsStore;
+import org.argeo.api.acr.fs.AbstractFsSystem;
+import org.argeo.jcr.Jcr;
+import org.argeo.jcr.JcrUtils;
+
+public class JcrFileSystem extends AbstractFsSystem<WorkspaceFileStore> {
+ private final JcrFileSystemProvider provider;
+
+ private final Repository repository;
+ private Session session;
+ private WorkspaceFileStore baseFileStore;
+
+ private Map<String, WorkspaceFileStore> mounts = new TreeMap<>();
+
+ private String userHomePath = null;
+
+ @Deprecated
+ public JcrFileSystem(JcrFileSystemProvider provider, Session session) throws IOException {
+ super();
+ this.provider = provider;
+ baseFileStore = new WorkspaceFileStore(null, session.getWorkspace());
+ this.session = session;
+// Node userHome = provider.getUserHome(session);
+// if (userHome != null)
+// try {
+// userHomePath = userHome.getPath();
+// } catch (RepositoryException e) {
+// throw new IOException("Cannot retrieve user home path", e);
+// }
+ this.repository = null;
+ }
+
+ public JcrFileSystem(JcrFileSystemProvider provider, Repository repository) throws IOException {
+ this(provider, repository, null);
+ }
+
+ public JcrFileSystem(JcrFileSystemProvider provider, Repository repository, Credentials credentials)
+ throws IOException {
+ super();
+ this.provider = provider;
+ this.repository = repository;
+ try {
+ this.session = credentials == null ? repository.login() : repository.login(credentials);
+ baseFileStore = new WorkspaceFileStore(null, session.getWorkspace());
+ workspaces: for (String workspaceName : baseFileStore.getWorkspace().getAccessibleWorkspaceNames()) {
+ if (workspaceName.equals(baseFileStore.getWorkspace().getName()))
+ continue workspaces;// do not mount base
+ if (workspaceName.equals("security")) {
+ continue workspaces;// do not mount security workspace
+ // TODO make it configurable
+ }
+ Session mountSession = credentials == null ? repository.login(workspaceName)
+ : repository.login(credentials, workspaceName);
+ String mountPath = JcrPath.separator + workspaceName;
+ mounts.put(mountPath, new WorkspaceFileStore(mountPath, mountSession.getWorkspace()));
+ }
+ } catch (RepositoryException e) {
+ throw new IOException("Cannot initialise file system", e);
+ }
+
+ Node userHome = provider.getUserHome(repository);
+ if (userHome != null)
+ try {
+ userHomePath = toFsPath(userHome);
+ } catch (RepositoryException e) {
+ throw new IOException("Cannot retrieve user home path", e);
+ } finally {
+ JcrUtils.logoutQuietly(Jcr.session(userHome));
+ }
+ }
+
+ public String toFsPath(Node node) throws RepositoryException {
+ return getFileStore(node).toFsPath(node);
+ }
+
+ /** Whether this node should be skipped in directory listings */
+ public boolean skipNode(Node node) throws RepositoryException {
+ if (node.isNodeType(NodeType.NT_HIERARCHY_NODE))
+ return false;
+ return true;
+ }
+
+ public String getUserHomePath() {
+ return userHomePath;
+ }
+
+ public WorkspaceFileStore getFileStore(String path) {
+ WorkspaceFileStore res = baseFileStore;
+ for (String mountPath : mounts.keySet()) {
+ if (path.equals(mountPath))
+ return mounts.get(mountPath);
+ if (path.startsWith(mountPath + JcrPath.separator)) {
+ res = mounts.get(mountPath);
+ // we keep the last one
+ }
+ }
+ assert res != null;
+ return res;
+ }
+
+ public WorkspaceFileStore getFileStore(Node node) throws RepositoryException {
+ String workspaceName = node.getSession().getWorkspace().getName();
+ if (workspaceName.equals(baseFileStore.getWorkspace().getName()))
+ return baseFileStore;
+ for (String mountPath : mounts.keySet()) {
+ WorkspaceFileStore fileStore = mounts.get(mountPath);
+ if (workspaceName.equals(fileStore.getWorkspace().getName()))
+ return fileStore;
+ }
+ throw new IllegalStateException("No workspace mount found for " + node + " in workspace " + workspaceName);
+ }
+
+ public Iterator<JcrPath> listDirectMounts(Path base) {
+ String baseStr = base.toString();
+ Set<JcrPath> res = new HashSet<>();
+ mounts: for (String mountPath : mounts.keySet()) {
+ if (mountPath.equals(baseStr))
+ continue mounts;
+ if (mountPath.startsWith(baseStr)) {
+ JcrPath path = new JcrPath(this, mountPath);
+ Path relPath = base.relativize(path);
+ if (relPath.getNameCount() == 1)
+ res.add(path);
+ }
+ }
+ return res.iterator();
+ }
+
+ public WorkspaceFileStore getBaseFileStore() {
+ return baseFileStore;
+ }
+
+ @Override
+ public FileSystemProvider provider() {
+ return provider;
+ }
+
+ @Override
+ public void close() throws IOException {
+ JcrUtils.logoutQuietly(session);
+ for (String mountPath : mounts.keySet()) {
+ WorkspaceFileStore fileStore = mounts.get(mountPath);
+ try {
+ fileStore.close();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public boolean isOpen() {
+ return session.isLive();
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return false;
+ }
+
+ @Override
+ public String getSeparator() {
+ return JcrPath.separator;
+ }
+
+ @Override
+ public Iterable<Path> getRootDirectories() {
+ Set<Path> single = new HashSet<>();
+ single.add(new JcrPath(this, JcrPath.separator));
+ return single;
+ }
+
+ @Override
+ public Iterable<FileStore> getFileStores() {
+ List<FileStore> stores = new ArrayList<>();
+ stores.add(baseFileStore);
+ stores.addAll(mounts.values());
+ return stores;
+ }
+
+ @Override
+ public Set<String> supportedFileAttributeViews() {
+ try {
+ String[] prefixes = session.getNamespacePrefixes();
+ Set<String> res = new HashSet<>();
+ for (String prefix : prefixes)
+ res.add(prefix);
+ res.add("basic");
+ return res;
+ } catch (RepositoryException e) {
+ throw new JcrFsException("Cannot get supported file attributes views", e);
+ }
+ }
+
+ @Override
+ public Path getPath(String first, String... more) {
+ StringBuilder sb = new StringBuilder(first);
+ // TODO Make it more robust
+ for (String part : more)
+ sb.append('/').append(part);
+ return new JcrPath(this, sb.toString());
+ }
+
+ @Override
+ public PathMatcher getPathMatcher(String syntaxAndPattern) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public UserPrincipalLookupService getUserPrincipalLookupService() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public WatchService newWatchService() throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+// public Session getSession() {
+// return session;
+// }
+
+ public Repository getRepository() {
+ return repository;
+ }
+
+}
--- /dev/null
+package org.argeo.jcr.fs;
+
+import java.io.IOException;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.AccessMode;
+import java.nio.file.CopyOption;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.DirectoryStream.Filter;
+import java.nio.file.FileStore;
+import java.nio.file.LinkOption;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.PropertyType;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.nodetype.PropertyDefinition;
+
+import org.argeo.jcr.JcrUtils;
+
+/** Operations on a {@link JcrFileSystem}. */
+public abstract class JcrFileSystemProvider extends FileSystemProvider {
+
+ @Override
+ public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs)
+ throws IOException {
+ Node node = toNode(path);
+ try {
+ if (node == null) {
+ Node parent = toNode(path.getParent());
+ if (parent == null)
+ throw new IOException("No parent directory for " + path);
+ if (parent.getPrimaryNodeType().isNodeType(NodeType.NT_FILE)
+ || parent.getPrimaryNodeType().isNodeType(NodeType.NT_LINKED_FILE))
+ throw new IOException(path + " parent is a file");
+
+ String fileName = path.getFileName().toString();
+ fileName = Text.escapeIllegalJcrChars(fileName);
+ node = parent.addNode(fileName, NodeType.NT_FILE);
+ node.addMixin(NodeType.MIX_CREATED);
+// node.addMixin(NodeType.MIX_LAST_MODIFIED);
+ }
+ if (!node.isNodeType(NodeType.NT_FILE))
+ throw new UnsupportedOperationException(node + " must be a file");
+ return new BinaryChannel(node, path);
+ } catch (RepositoryException e) {
+ discardChanges(node);
+ throw new IOException("Cannot read file", e);
+ }
+ }
+
+ @Override
+ public DirectoryStream<Path> newDirectoryStream(Path dir, Filter<? super Path> filter) throws IOException {
+ try {
+ Node base = toNode(dir);
+ if (base == null)
+ throw new IOException(dir + " is not a JCR node");
+ JcrFileSystem fileSystem = (JcrFileSystem) dir.getFileSystem();
+ return new NodeDirectoryStream(fileSystem, base.getNodes(), fileSystem.listDirectMounts(dir), filter);
+ } catch (RepositoryException e) {
+ throw new IOException("Cannot list directory", e);
+ }
+ }
+
+ @Override
+ public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
+ Node node = toNode(dir);
+ try {
+ if (node == null) {
+ Node parent = toNode(dir.getParent());
+ if (parent == null)
+ throw new IOException("Parent of " + dir + " does not exist");
+ Session session = parent.getSession();
+ synchronized (session) {
+ if (parent.getPrimaryNodeType().isNodeType(NodeType.NT_FILE)
+ || parent.getPrimaryNodeType().isNodeType(NodeType.NT_LINKED_FILE))
+ throw new IOException(dir + " parent is a file");
+ String fileName = dir.getFileName().toString();
+ fileName = Text.escapeIllegalJcrChars(fileName);
+ node = parent.addNode(fileName, NodeType.NT_FOLDER);
+ node.addMixin(NodeType.MIX_CREATED);
+ node.addMixin(NodeType.MIX_LAST_MODIFIED);
+ save(session);
+ }
+ } else {
+ // if (!node.getPrimaryNodeType().isNodeType(NodeType.NT_FOLDER))
+ // throw new FileExistsException(dir + " exists and is not a directory");
+ }
+ } catch (RepositoryException e) {
+ discardChanges(node);
+ throw new IOException("Cannot create directory " + dir, e);
+ }
+ }
+
+ @Override
+ public void delete(Path path) throws IOException {
+ Node node = toNode(path);
+ try {
+ if (node == null)
+ throw new NoSuchFileException(path + " does not exist");
+ Session session = node.getSession();
+ synchronized (session) {
+ session.refresh(false);
+ if (node.getPrimaryNodeType().isNodeType(NodeType.NT_FILE))
+ node.remove();
+ else if (node.getPrimaryNodeType().isNodeType(NodeType.NT_FOLDER)) {
+ if (node.hasNodes())// TODO check only files
+ throw new DirectoryNotEmptyException(path.toString());
+ node.remove();
+ }
+ save(session);
+ }
+ } catch (RepositoryException e) {
+ discardChanges(node);
+ throw new IOException("Cannot delete " + path, e);
+ }
+
+ }
+
+ @Override
+ public void copy(Path source, Path target, CopyOption... options) throws IOException {
+ Node sourceNode = toNode(source);
+ Node targetNode = toNode(target);
+ try {
+ Session targetSession = targetNode.getSession();
+ synchronized (targetSession) {
+ JcrUtils.copy(sourceNode, targetNode);
+ save(targetSession);
+ }
+ } catch (RepositoryException e) {
+ discardChanges(sourceNode);
+ discardChanges(targetNode);
+ throw new IOException("Cannot copy from " + source + " to " + target, e);
+ }
+ }
+
+ @Override
+ public void move(Path source, Path target, CopyOption... options) throws IOException {
+ JcrFileSystem sourceFileSystem = (JcrFileSystem) source.getFileSystem();
+ WorkspaceFileStore sourceStore = sourceFileSystem.getFileStore(source.toString());
+ WorkspaceFileStore targetStore = sourceFileSystem.getFileStore(target.toString());
+ try {
+ if (sourceStore.equals(targetStore)) {
+ sourceStore.getWorkspace().move(sourceStore.toJcrPath(source.toString()),
+ targetStore.toJcrPath(target.toString()));
+ } else {
+ // TODO implement it
+ throw new UnsupportedOperationException("Can only move paths within the same workspace.");
+ }
+ } catch (RepositoryException e) {
+ throw new IOException("Cannot move from " + source + " to " + target, e);
+ }
+
+// Node sourceNode = toNode(source);
+// try {
+// Session session = sourceNode.getSession();
+// synchronized (session) {
+// session.move(sourceNode.getPath(), target.toString());
+// save(session);
+// }
+// } catch (RepositoryException e) {
+// discardChanges(sourceNode);
+// throw new IOException("Cannot move from " + source + " to " + target, e);
+// }
+ }
+
+ @Override
+ public boolean isSameFile(Path path, Path path2) throws IOException {
+ if (path.getFileSystem() != path2.getFileSystem())
+ return false;
+ boolean equ = path.equals(path2);
+ if (equ)
+ return true;
+ else {
+ try {
+ Node node = toNode(path);
+ Node node2 = toNode(path2);
+ return node.isSame(node2);
+ } catch (RepositoryException e) {
+ throw new IOException("Cannot check whether " + path + " and " + path2 + " are same", e);
+ }
+ }
+
+ }
+
+ @Override
+ public boolean isHidden(Path path) throws IOException {
+ return path.getFileName().toString().charAt(0) == '.';
+ }
+
+ @Override
+ public FileStore getFileStore(Path path) throws IOException {
+ JcrFileSystem fileSystem = (JcrFileSystem) path.getFileSystem();
+ return fileSystem.getFileStore(path.toString());
+ }
+
+ @Override
+ public void checkAccess(Path path, AccessMode... modes) throws IOException {
+ Node node = toNode(path);
+ if (node == null)
+ throw new NoSuchFileException(path + " does not exist");
+ // TODO check access via JCR api
+ }
+
+ @Override
+ public <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) {
+ throw new UnsupportedOperationException();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options)
+ throws IOException {
+ // TODO check if assignable
+ Node node = toNode(path);
+ if (node == null) {
+ throw new IOException("JCR node not found for " + path);
+ }
+ return (A) new JcrBasicfileAttributes(node);
+ }
+
+ @Override
+ public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
+ try {
+ Node node = toNode(path);
+ String pattern = attributes.replace(',', '|');
+ Map<String, Object> res = new HashMap<String, Object>();
+ PropertyIterator it = node.getProperties(pattern);
+ props: while (it.hasNext()) {
+ Property prop = it.nextProperty();
+ PropertyDefinition pd = prop.getDefinition();
+ if (pd.isMultiple())
+ continue props;
+ int requiredType = pd.getRequiredType();
+ switch (requiredType) {
+ case PropertyType.LONG:
+ res.put(prop.getName(), prop.getLong());
+ break;
+ case PropertyType.DOUBLE:
+ res.put(prop.getName(), prop.getDouble());
+ break;
+ case PropertyType.BOOLEAN:
+ res.put(prop.getName(), prop.getBoolean());
+ break;
+ case PropertyType.DATE:
+ res.put(prop.getName(), prop.getDate());
+ break;
+ case PropertyType.BINARY:
+ byte[] arr = JcrUtils.getBinaryAsBytes(prop);
+ res.put(prop.getName(), arr);
+ break;
+ default:
+ res.put(prop.getName(), prop.getString());
+ }
+ }
+ return res;
+ } catch (RepositoryException e) {
+ throw new IOException("Cannot read attributes of " + path, e);
+ }
+ }
+
+ @Override
+ public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException {
+ Node node = toNode(path);
+ try {
+ Session session = node.getSession();
+ synchronized (session) {
+ if (value instanceof byte[]) {
+ JcrUtils.setBinaryAsBytes(node, attribute, (byte[]) value);
+ } else if (value instanceof Calendar) {
+ node.setProperty(attribute, (Calendar) value);
+ } else {
+ node.setProperty(attribute, value.toString());
+ }
+ save(session);
+ }
+ } catch (RepositoryException e) {
+ discardChanges(node);
+ throw new IOException("Cannot set attribute " + attribute + " on " + path, e);
+ }
+ }
+
+ protected Node toNode(Path path) {
+ try {
+ return ((JcrPath) path).getNode();
+ } catch (RepositoryException e) {
+ throw new JcrFsException("Cannot convert path " + path + " to JCR Node", e);
+ }
+ }
+
+ /** Discard changes in the underlying session */
+ protected void discardChanges(Node node) {
+ if (node == null)
+ return;
+ try {
+ // discard changes
+ node.getSession().refresh(false);
+ } catch (RepositoryException e) {
+ e.printStackTrace();
+ // TODO log out session?
+ // TODO use Commons logging?
+ }
+ }
+
+ /** Make sure save is robust. */
+ protected void save(Session session) throws RepositoryException {
+ session.refresh(true);
+ session.save();
+ session.notifyAll();
+ }
+
+ /**
+ * To be overriden in order to support the ~ path, with an implementation
+ * specific concept of user home.
+ *
+ * @return null by default
+ */
+ public Node getUserHome(Repository session) {
+ return null;
+ }
+
+}
--- /dev/null
+package org.argeo.jcr.fs;
+
+
+/** Exception related to the JCR FS */
+public class JcrFsException extends RuntimeException {
+ private static final long serialVersionUID = -7973896038244922980L;
+
+ public JcrFsException(String message, Throwable e) {
+ super(message, e);
+ }
+
+ public JcrFsException(String message) {
+ super(message);
+ }
+}
--- /dev/null
+package org.argeo.jcr.fs;
+
+import java.nio.file.Path;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.api.acr.fs.AbstractFsPath;
+
+/** A {@link Path} which contains a reference to a JCR {@link Node}. */
+public class JcrPath extends AbstractFsPath<JcrFileSystem, WorkspaceFileStore> {
+ final static String separator = "/";
+ final static char separatorChar = '/';
+
+// private final JcrFileSystem fs;
+// /** null for non absolute paths */
+// private final WorkspaceFileStore fileStore;
+// private final String[] path;// null means root
+// private final boolean absolute;
+//
+// // optim
+// private final int hashCode;
+
+ public JcrPath(JcrFileSystem filesSystem, String path) {
+ super(filesSystem, path);
+// this.fs = filesSystem;
+// if (path == null)
+// throw new JcrFsException("Path cannot be null");
+// if (path.equals(separator)) {// root
+// this.path = null;
+// this.absolute = true;
+// this.hashCode = 0;
+// this.fileStore = fs.getBaseFileStore();
+// return;
+// } else if (path.equals("")) {// empty path
+// this.path = new String[] { "" };
+// this.absolute = false;
+// this.fileStore = null;
+// this.hashCode = "".hashCode();
+// return;
+// }
+//
+// if (path.equals("~")) {// home
+// path = filesSystem.getUserHomePath();
+// if (path == null)
+// throw new JcrFsException("No home directory available");
+// }
+//
+// this.absolute = path.charAt(0) == separatorChar ? true : false;
+//
+// this.fileStore = absolute ? fs.getFileStore(path) : null;
+//
+// String trimmedPath = path.substring(absolute ? 1 : 0,
+// path.charAt(path.length() - 1) == separatorChar ? path.length() - 1 : path.length());
+// this.path = trimmedPath.split(separator);
+// for (int i = 0; i < this.path.length; i++) {
+// this.path[i] = Text.unescapeIllegalJcrChars(this.path[i]);
+// }
+// this.hashCode = this.path[this.path.length - 1].hashCode();
+// assert !(absolute && fileStore == null);
+ }
+
+ public JcrPath(JcrFileSystem filesSystem, Node node) throws RepositoryException {
+ this(filesSystem, filesSystem.getFileStore(node).toFsPath(node));
+ }
+
+ /** Internal optimisation */
+ private JcrPath(JcrFileSystem filesSystem, WorkspaceFileStore fileStore, String[] path, boolean absolute) {
+ super(filesSystem, fileStore, path, absolute);
+// this.fs = filesSystem;
+// this.path = path;
+// this.absolute = path == null ? true : absolute;
+// if (this.absolute && fileStore == null)
+// throw new IllegalArgumentException("Absolute path requires a file store");
+// if (!this.absolute && fileStore != null)
+// throw new IllegalArgumentException("A file store should not be provided for a relative path");
+// this.fileStore = fileStore;
+// this.hashCode = path == null ? 0 : path[path.length - 1].hashCode();
+// assert !(absolute && fileStore == null);
+ }
+
+ protected String cleanUpSegment(String segment) {
+ return Text.unescapeIllegalJcrChars(segment);
+ }
+
+ @Override
+ protected JcrPath newInstance(String path) {
+ return new JcrPath(getFileSystem(), path);
+ }
+
+ @Override
+ protected JcrPath newInstance(String[] segments, boolean absolute) {
+ return new JcrPath(getFileSystem(), getFileStore(), segments, absolute);
+
+ }
+
+// @Override
+// public FileSystem getFileSystem() {
+// return fs;
+// }
+//
+// @Override
+// public boolean isAbsolute() {
+// return absolute;
+// }
+//
+// @Override
+// public Path getRoot() {
+// if (path == null)
+// return this;
+// return new JcrPath(fs, separator);
+// }
+//
+// @Override
+// public String toString() {
+// return toFsPath(path);
+// }
+//
+// private String toFsPath(String[] path) {
+// if (path == null)
+// return "/";
+// StringBuilder sb = new StringBuilder();
+// if (isAbsolute())
+// sb.append('/');
+// for (int i = 0; i < path.length; i++) {
+// if (i != 0)
+// sb.append('/');
+// sb.append(path[i]);
+// }
+// return sb.toString();
+// }
+
+// @Deprecated
+// private String toJcrPath() {
+// return toJcrPath(path);
+// }
+//
+// @Deprecated
+// private String toJcrPath(String[] path) {
+// if (path == null)
+// return "/";
+// StringBuilder sb = new StringBuilder();
+// if (isAbsolute())
+// sb.append('/');
+// for (int i = 0; i < path.length; i++) {
+// if (i != 0)
+// sb.append('/');
+// sb.append(Text.escapeIllegalJcrChars(path[i]));
+// }
+// return sb.toString();
+// }
+
+// @Override
+// public Path getFileName() {
+// if (path == null)
+// return null;
+// return new JcrPath(fs, path[path.length - 1]);
+// }
+//
+// @Override
+// public Path getParent() {
+// if (path == null)
+// return null;
+// if (path.length == 1)// root
+// return new JcrPath(fs, separator);
+// String[] parentPath = Arrays.copyOfRange(path, 0, path.length - 1);
+// if (!absolute)
+// return new JcrPath(fs, null, parentPath, absolute);
+// else
+// return new JcrPath(fs, toFsPath(parentPath));
+// }
+//
+// @Override
+// public int getNameCount() {
+// if (path == null)
+// return 0;
+// return path.length;
+// }
+//
+// @Override
+// public Path getName(int index) {
+// if (path == null)
+// return null;
+// return new JcrPath(fs, path[index]);
+// }
+//
+// @Override
+// public Path subpath(int beginIndex, int endIndex) {
+// if (path == null)
+// return null;
+// String[] parentPath = Arrays.copyOfRange(path, beginIndex, endIndex);
+// return new JcrPath(fs, null, parentPath, false);
+// }
+//
+// @Override
+// public boolean startsWith(Path other) {
+// return toString().startsWith(other.toString());
+// }
+//
+// @Override
+// public boolean startsWith(String other) {
+// return toString().startsWith(other);
+// }
+//
+// @Override
+// public boolean endsWith(Path other) {
+// return toString().endsWith(other.toString());
+// }
+//
+// @Override
+// public boolean endsWith(String other) {
+// return toString().endsWith(other);
+// }
+
+// @Override
+// public Path normalize() {
+// // always normalized
+// return this;
+// }
+
+// @Override
+// public Path resolve(Path other) {
+// JcrPath otherPath = (JcrPath) other;
+// if (otherPath.isAbsolute())
+// return other;
+// String[] newPath;
+// if (path == null) {
+// newPath = new String[otherPath.path.length];
+// System.arraycopy(otherPath.path, 0, newPath, 0, otherPath.path.length);
+// } else {
+// newPath = new String[path.length + otherPath.path.length];
+// System.arraycopy(path, 0, newPath, 0, path.length);
+// System.arraycopy(otherPath.path, 0, newPath, path.length, otherPath.path.length);
+// }
+// if (!absolute)
+// return new JcrPath(fs, null, newPath, absolute);
+// else {
+// return new JcrPath(fs, toFsPath(newPath));
+// }
+// }
+//
+// @Override
+// public final Path resolve(String other) {
+// return resolve(getFileSystem().getPath(other));
+// }
+//
+// @Override
+// public final Path resolveSibling(Path other) {
+// if (other == null)
+// throw new NullPointerException();
+// Path parent = getParent();
+// return (parent == null) ? other : parent.resolve(other);
+// }
+//
+// @Override
+// public final Path resolveSibling(String other) {
+// return resolveSibling(getFileSystem().getPath(other));
+// }
+//
+// @Override
+// public final Iterator<Path> iterator() {
+// return new Iterator<Path>() {
+// private int i = 0;
+//
+// @Override
+// public boolean hasNext() {
+// return (i < getNameCount());
+// }
+//
+// @Override
+// public Path next() {
+// if (i < getNameCount()) {
+// Path result = getName(i);
+// i++;
+// return result;
+// } else {
+// throw new NoSuchElementException();
+// }
+// }
+//
+// @Override
+// public void remove() {
+// throw new UnsupportedOperationException();
+// }
+// };
+// }
+//
+// @Override
+// public Path relativize(Path other) {
+// if (equals(other))
+// return new JcrPath(fs, "");
+// if (other.startsWith(this)) {
+// String p1 = toString();
+// String p2 = other.toString();
+// String relative = p2.substring(p1.length(), p2.length());
+// if (relative.charAt(0) == '/')
+// relative = relative.substring(1);
+// return new JcrPath(fs, relative);
+// }
+// throw new IllegalArgumentException(other + " cannot be relativized against " + this);
+// }
+
+// @Override
+// public URI toUri() {
+// try {
+// return new URI(fs.provider().getScheme(), toString(), null);
+// } catch (URISyntaxException e) {
+// throw new JcrFsException("Cannot create URI for " + toString(), e);
+// }
+// }
+//
+// @Override
+// public Path toAbsolutePath() {
+// if (isAbsolute())
+// return this;
+// return new JcrPath(fs, fileStore, path, true);
+// }
+//
+// @Override
+// public Path toRealPath(LinkOption... options) throws IOException {
+// return this;
+// }
+//
+// @Override
+// public File toFile() {
+// throw new UnsupportedOperationException();
+// }
+
+ public Node getNode() throws RepositoryException {
+ if (!isAbsolute())// TODO default dir
+ throw new JcrFsException("Cannot get a JCR node from a relative path");
+ assert getFileStore() != null;
+ return getFileStore().toNode(getSegments());
+// String pathStr = toJcrPath();
+// Session session = fs.getSession();
+// // TODO synchronize on the session ?
+// if (!session.itemExists(pathStr))
+// return null;
+// return session.getNode(pathStr);
+ }
+//
+// @Override
+// public boolean equals(Object obj) {
+// if (!(obj instanceof JcrPath))
+// return false;
+// JcrPath other = (JcrPath) obj;
+//
+// if (path == null) {// root
+// if (other.path == null)// root
+// return true;
+// else
+// return false;
+// } else {
+// if (other.path == null)// root
+// return false;
+// }
+// // non root
+// if (path.length != other.path.length)
+// return false;
+// for (int i = 0; i < path.length; i++) {
+// if (!path[i].equals(other.path[i]))
+// return false;
+// }
+// return true;
+// }
+
+// @Override
+// public int hashCode() {
+// return hashCode;
+// }
+
+// @Override
+// protected Object clone() throws CloneNotSupportedException {
+// return new JcrPath(fs, toString());
+// }
+
+// @Override
+// protected void finalize() throws Throwable {
+// Arrays.fill(path, null);
+// }
+
+
+
+}
--- /dev/null
+package org.argeo.jcr.fs;
+
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Path;
+import java.util.Iterator;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+
+public class NodeDirectoryStream implements DirectoryStream<Path> {
+ private final JcrFileSystem fs;
+ private final NodeIterator nodeIterator;
+ private final Iterator<JcrPath> additionalPaths;
+ private final Filter<? super Path> filter;
+
+ public NodeDirectoryStream(JcrFileSystem fs, NodeIterator nodeIterator, Iterator<JcrPath> additionalPaths,
+ Filter<? super Path> filter) {
+ this.fs = fs;
+ this.nodeIterator = nodeIterator;
+ this.additionalPaths = additionalPaths;
+ this.filter = filter;
+ }
+
+ @Override
+ public void close() throws IOException {
+ }
+
+ @Override
+ public Iterator<Path> iterator() {
+ return new Iterator<Path>() {
+ private JcrPath next = null;
+
+ @Override
+ public synchronized boolean hasNext() {
+ if (next != null)
+ return true;
+ nodes: while (nodeIterator.hasNext()) {
+ try {
+ Node node = nodeIterator.nextNode();
+ String nodeName = node.getName();
+ if (nodeName.startsWith("rep:") || nodeName.startsWith("jcr:"))
+ continue nodes;
+ if (fs.skipNode(node))
+ continue nodes;
+ next = new JcrPath(fs, node);
+ if (filter != null) {
+ if (filter.accept(next))
+ break nodes;
+ } else
+ break nodes;
+ } catch (Exception e) {
+ throw new JcrFsException("Could not get next path", e);
+ }
+ }
+
+ if (next == null) {
+ if (additionalPaths.hasNext())
+ next = additionalPaths.next();
+ }
+
+ return next != null;
+ }
+
+ @Override
+ public synchronized Path next() {
+ if (!hasNext())// make sure has next has been called
+ return null;
+ JcrPath res = next;
+ next = null;
+ return res;
+ }
+
+ };
+ }
+
+}
--- /dev/null
+package org.argeo.jcr.fs;
+
+import java.nio.file.attribute.BasicFileAttributes;
+
+import javax.jcr.Node;
+
+public interface NodeFileAttributes extends BasicFileAttributes {
+ public Node getNode();
+}
--- /dev/null
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jcr.fs;
+
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Properties;
+
+/**
+ * <b>Hacked from org.apache.jackrabbit.util.Text in Jackrabbit JCR Commons</b>
+ * This Class provides some text related utilities
+ */
+class Text {
+
+ /**
+ * Hidden constructor.
+ */
+ private Text() {
+ }
+
+ /**
+ * used for the md5
+ */
+ public static final char[] hexTable = "0123456789abcdef".toCharArray();
+
+ /**
+ * Calculate an MD5 hash of the string given.
+ *
+ * @param data
+ * the data to encode
+ * @param enc
+ * the character encoding to use
+ * @return a hex encoded string of the md5 digested input
+ */
+ public static String md5(String data, String enc) throws UnsupportedEncodingException {
+ try {
+ return digest("MD5", data.getBytes(enc));
+ } catch (NoSuchAlgorithmException e) {
+ throw new InternalError("MD5 digest not available???");
+ }
+ }
+
+ /**
+ * Calculate an MD5 hash of the string given using 'utf-8' encoding.
+ *
+ * @param data
+ * the data to encode
+ * @return a hex encoded string of the md5 digested input
+ */
+ public static String md5(String data) {
+ try {
+ return md5(data, "utf-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new InternalError("UTF8 digest not available???");
+ }
+ }
+
+ /**
+ * Digest the plain string using the given algorithm.
+ *
+ * @param algorithm
+ * The alogrithm for the digest. This algorithm must be supported
+ * by the MessageDigest class.
+ * @param data
+ * The plain text String to be digested.
+ * @param enc
+ * The character encoding to use
+ * @return The digested plain text String represented as Hex digits.
+ * @throws java.security.NoSuchAlgorithmException
+ * if the desired algorithm is not supported by the
+ * MessageDigest class.
+ * @throws java.io.UnsupportedEncodingException
+ * if the encoding is not supported
+ */
+ public static String digest(String algorithm, String data, String enc)
+ throws NoSuchAlgorithmException, UnsupportedEncodingException {
+
+ return digest(algorithm, data.getBytes(enc));
+ }
+
+ /**
+ * Digest the plain string using the given algorithm.
+ *
+ * @param algorithm
+ * The algorithm for the digest. This algorithm must be supported
+ * by the MessageDigest class.
+ * @param data
+ * the data to digest with the given algorithm
+ * @return The digested plain text String represented as Hex digits.
+ * @throws java.security.NoSuchAlgorithmException
+ * if the desired algorithm is not supported by the
+ * MessageDigest class.
+ */
+ public static String digest(String algorithm, byte[] data) throws NoSuchAlgorithmException {
+
+ MessageDigest md = MessageDigest.getInstance(algorithm);
+ byte[] digest = md.digest(data);
+ StringBuilder res = new StringBuilder(digest.length * 2);
+ for (byte b : digest) {
+ res.append(hexTable[(b >> 4) & 15]);
+ res.append(hexTable[b & 15]);
+ }
+ return res.toString();
+ }
+
+ /**
+ * returns an array of strings decomposed of the original string, split at
+ * every occurrence of 'ch'. if 2 'ch' follow each other with no
+ * intermediate characters, empty "" entries are avoided.
+ *
+ * @param str
+ * the string to decompose
+ * @param ch
+ * the character to use a split pattern
+ * @return an array of strings
+ */
+ public static String[] explode(String str, int ch) {
+ return explode(str, ch, false);
+ }
+
+ /**
+ * returns an array of strings decomposed of the original string, split at
+ * every occurrence of 'ch'.
+ *
+ * @param str
+ * the string to decompose
+ * @param ch
+ * the character to use a split pattern
+ * @param respectEmpty
+ * if <code>true</code>, empty elements are generated
+ * @return an array of strings
+ */
+ public static String[] explode(String str, int ch, boolean respectEmpty) {
+ if (str == null || str.length() == 0) {
+ return new String[0];
+ }
+
+ ArrayList<String> strings = new ArrayList<String>();
+ int pos;
+ int lastpos = 0;
+
+ // add snipples
+ while ((pos = str.indexOf(ch, lastpos)) >= 0) {
+ if (pos - lastpos > 0 || respectEmpty) {
+ strings.add(str.substring(lastpos, pos));
+ }
+ lastpos = pos + 1;
+ }
+ // add rest
+ if (lastpos < str.length()) {
+ strings.add(str.substring(lastpos));
+ } else if (respectEmpty && lastpos == str.length()) {
+ strings.add("");
+ }
+
+ // return string array
+ return strings.toArray(new String[strings.size()]);
+ }
+
+ /**
+ * Concatenates all strings in the string array using the specified
+ * delimiter.
+ *
+ * @param arr
+ * @param delim
+ * @return the concatenated string
+ */
+ public static String implode(String[] arr, String delim) {
+ StringBuilder buf = new StringBuilder();
+ for (int i = 0; i < arr.length; i++) {
+ if (i > 0) {
+ buf.append(delim);
+ }
+ buf.append(arr[i]);
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Replaces all occurrences of <code>oldString</code> in <code>text</code>
+ * with <code>newString</code>.
+ *
+ * @param text
+ * @param oldString
+ * old substring to be replaced with <code>newString</code>
+ * @param newString
+ * new substring to replace occurrences of <code>oldString</code>
+ * @return a string
+ */
+ public static String replace(String text, String oldString, String newString) {
+ if (text == null || oldString == null || newString == null) {
+ throw new IllegalArgumentException("null argument");
+ }
+ int pos = text.indexOf(oldString);
+ if (pos == -1) {
+ return text;
+ }
+ int lastPos = 0;
+ StringBuilder sb = new StringBuilder(text.length());
+ while (pos != -1) {
+ sb.append(text.substring(lastPos, pos));
+ sb.append(newString);
+ lastPos = pos + oldString.length();
+ pos = text.indexOf(oldString, lastPos);
+ }
+ if (lastPos < text.length()) {
+ sb.append(text.substring(lastPos));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Replaces XML characters in the given string that might need escaping as
+ * XML text or attribute
+ *
+ * @param text
+ * text to be escaped
+ * @return a string
+ */
+ public static String encodeIllegalXMLCharacters(String text) {
+ return encodeMarkupCharacters(text, false);
+ }
+
+ /**
+ * Replaces HTML characters in the given string that might need escaping as
+ * HTML text or attribute
+ *
+ * @param text
+ * text to be escaped
+ * @return a string
+ */
+ public static String encodeIllegalHTMLCharacters(String text) {
+ return encodeMarkupCharacters(text, true);
+ }
+
+ private static String encodeMarkupCharacters(String text, boolean isHtml) {
+ if (text == null) {
+ throw new IllegalArgumentException("null argument");
+ }
+ StringBuilder buf = null;
+ int length = text.length();
+ int pos = 0;
+ for (int i = 0; i < length; i++) {
+ int ch = text.charAt(i);
+ switch (ch) {
+ case '<':
+ case '>':
+ case '&':
+ case '"':
+ case '\'':
+ if (buf == null) {
+ buf = new StringBuilder();
+ }
+ if (i > 0) {
+ buf.append(text.substring(pos, i));
+ }
+ pos = i + 1;
+ break;
+ default:
+ continue;
+ }
+ if (ch == '<') {
+ buf.append("<");
+ } else if (ch == '>') {
+ buf.append(">");
+ } else if (ch == '&') {
+ buf.append("&");
+ } else if (ch == '"') {
+ buf.append(""");
+ } else if (ch == '\'') {
+ buf.append(isHtml ? "'" : "'");
+ }
+ }
+ if (buf == null) {
+ return text;
+ } else {
+ if (pos < length) {
+ buf.append(text.substring(pos));
+ }
+ return buf.toString();
+ }
+ }
+
+ /**
+ * The list of characters that are not encoded by the <code>escape()</code>
+ * and <code>unescape()</code> METHODS. They contains the characters as
+ * defined 'unreserved' in section 2.3 of the RFC 2396 'URI generic syntax':
+ * <p>
+ *
+ * <pre>
+ * unreserved = alphanum | mark
+ * mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
+ * </pre>
+ */
+ public static BitSet URISave;
+
+ /**
+ * Same as {@link #URISave} but also contains the '/'
+ */
+ public static BitSet URISaveEx;
+
+ static {
+ URISave = new BitSet(256);
+ int i;
+ for (i = 'a'; i <= 'z'; i++) {
+ URISave.set(i);
+ }
+ for (i = 'A'; i <= 'Z'; i++) {
+ URISave.set(i);
+ }
+ for (i = '0'; i <= '9'; i++) {
+ URISave.set(i);
+ }
+ URISave.set('-');
+ URISave.set('_');
+ URISave.set('.');
+ URISave.set('!');
+ URISave.set('~');
+ URISave.set('*');
+ URISave.set('\'');
+ URISave.set('(');
+ URISave.set(')');
+
+ URISaveEx = (BitSet) URISave.clone();
+ URISaveEx.set('/');
+ }
+
+ /**
+ * Does an URL encoding of the <code>string</code> using the
+ * <code>escape</code> character. The characters that don't need encoding
+ * are those defined 'unreserved' in section 2.3 of the 'URI generic syntax'
+ * RFC 2396, but without the escape character.
+ *
+ * @param string
+ * the string to encode.
+ * @param escape
+ * the escape character.
+ * @return the escaped string
+ * @throws NullPointerException
+ * if <code>string</code> is <code>null</code>.
+ */
+ public static String escape(String string, char escape) {
+ return escape(string, escape, false);
+ }
+
+ /**
+ * Does an URL encoding of the <code>string</code> using the
+ * <code>escape</code> character. The characters that don't need encoding
+ * are those defined 'unreserved' in section 2.3 of the 'URI generic syntax'
+ * RFC 2396, but without the escape character. If <code>isPath</code> is
+ * <code>true</code>, additionally the slash '/' is ignored, too.
+ *
+ * @param string
+ * the string to encode.
+ * @param escape
+ * the escape character.
+ * @param isPath
+ * if <code>true</code>, the string is treated as path
+ * @return the escaped string
+ * @throws NullPointerException
+ * if <code>string</code> is <code>null</code>.
+ */
+ public static String escape(String string, char escape, boolean isPath) {
+ try {
+ BitSet validChars = isPath ? URISaveEx : URISave;
+ byte[] bytes = string.getBytes("utf-8");
+ StringBuilder out = new StringBuilder(bytes.length);
+ for (byte aByte : bytes) {
+ int c = aByte & 0xff;
+ if (validChars.get(c) && c != escape) {
+ out.append((char) c);
+ } else {
+ out.append(escape);
+ out.append(hexTable[(c >> 4) & 0x0f]);
+ out.append(hexTable[(c) & 0x0f]);
+ }
+ }
+ return out.toString();
+ } catch (UnsupportedEncodingException e) {
+ throw new InternalError(e.toString());
+ }
+ }
+
+ /**
+ * Does a URL encoding of the <code>string</code>. The characters that don't
+ * need encoding are those defined 'unreserved' in section 2.3 of the 'URI
+ * generic syntax' RFC 2396.
+ *
+ * @param string
+ * the string to encode
+ * @return the escaped string
+ * @throws NullPointerException
+ * if <code>string</code> is <code>null</code>.
+ */
+ public static String escape(String string) {
+ return escape(string, '%');
+ }
+
+ /**
+ * Does a URL encoding of the <code>path</code>. The characters that don't
+ * need encoding are those defined 'unreserved' in section 2.3 of the 'URI
+ * generic syntax' RFC 2396. In contrast to the {@link #escape(String)}
+ * method, not the entire path string is escaped, but every individual part
+ * (i.e. the slashes are not escaped).
+ *
+ * @param path
+ * the path to encode
+ * @return the escaped path
+ * @throws NullPointerException
+ * if <code>path</code> is <code>null</code>.
+ */
+ public static String escapePath(String path) {
+ return escape(path, '%', true);
+ }
+
+ /**
+ * Does a URL decoding of the <code>string</code> using the
+ * <code>escape</code> character. Please note that in opposite to the
+ * {@link java.net.URLDecoder} it does not transform the + into spaces.
+ *
+ * @param string
+ * the string to decode
+ * @param escape
+ * the escape character
+ * @return the decoded string
+ * @throws NullPointerException
+ * if <code>string</code> is <code>null</code>.
+ * @throws IllegalArgumentException
+ * if the 2 characters following the escape character do not
+ * represent a hex-number or if not enough characters follow an
+ * escape character
+ */
+ public static String unescape(String string, char escape) {
+ try {
+ byte[] utf8 = string.getBytes("utf-8");
+
+ // Check whether escape occurs at invalid position
+ if ((utf8.length >= 1 && utf8[utf8.length - 1] == escape)
+ || (utf8.length >= 2 && utf8[utf8.length - 2] == escape)) {
+ throw new IllegalArgumentException("Premature end of escape sequence at end of input");
+ }
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream(utf8.length);
+ for (int k = 0; k < utf8.length; k++) {
+ byte b = utf8[k];
+ if (b == escape) {
+ out.write((decodeDigit(utf8[++k]) << 4) + decodeDigit(utf8[++k]));
+ } else {
+ out.write(b);
+ }
+ }
+
+ return new String(out.toByteArray(), "utf-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new InternalError(e.toString());
+ }
+ }
+
+ /**
+ * Does a URL decoding of the <code>string</code>. Please note that in
+ * opposite to the {@link java.net.URLDecoder} it does not transform the +
+ * into spaces.
+ *
+ * @param string
+ * the string to decode
+ * @return the decoded string
+ * @throws NullPointerException
+ * if <code>string</code> is <code>null</code>.
+ * @throws ArrayIndexOutOfBoundsException
+ * if not enough character follow an escape character
+ * @throws IllegalArgumentException
+ * if the 2 characters following the escape character do not
+ * represent a hex-number.
+ */
+ public static String unescape(String string) {
+ return unescape(string, '%');
+ }
+
+ /**
+ * Escapes all illegal JCR name characters of a string. The encoding is
+ * loosely modeled after URI encoding, but only encodes the characters it
+ * absolutely needs to in order to make the resulting string a valid JCR
+ * name. Use {@link #unescapeIllegalJcrChars(String)} for decoding.
+ * <p>
+ * QName EBNF:<br>
+ * <xmp> simplename ::= onecharsimplename | twocharsimplename |
+ * threeormorecharname onecharsimplename ::= (* Any Unicode character
+ * except: '.', '/', ':', '[', ']', '*', '|' or any whitespace character *)
+ * twocharsimplename ::= '.' onecharsimplename | onecharsimplename '.' |
+ * onecharsimplename onecharsimplename threeormorecharname ::= nonspace
+ * string nonspace string ::= char | string char char ::= nonspace | ' '
+ * nonspace ::= (* Any Unicode character except: '/', ':', '[', ']', '*',
+ * '|' or any whitespace character *) </xmp>
+ *
+ * @param name
+ * the name to escape
+ * @return the escaped name
+ */
+ public static String escapeIllegalJcrChars(String name) {
+ return escapeIllegalChars(name, "%/:[]*|\t\r\n");
+ }
+
+ /**
+ * Escapes all illegal JCR 1.0 name characters of a string. Use
+ * {@link #unescapeIllegalJcrChars(String)} for decoding.
+ * <p>
+ * QName EBNF:<br>
+ * <xmp> simplename ::= onecharsimplename | twocharsimplename |
+ * threeormorecharname onecharsimplename ::= (* Any Unicode character
+ * except: '.', '/', ':', '[', ']', '*', ''', '"', '|' or any whitespace
+ * character *) twocharsimplename ::= '.' onecharsimplename |
+ * onecharsimplename '.' | onecharsimplename onecharsimplename
+ * threeormorecharname ::= nonspace string nonspace string ::= char | string
+ * char char ::= nonspace | ' ' nonspace ::= (* Any Unicode character
+ * except: '/', ':', '[', ']', '*', ''', '"', '|' or any whitespace
+ * character *) </xmp>
+ *
+ * @since Apache Jackrabbit 2.3.2 and 2.2.10
+ * @see <a href=
+ * "https://issues.apache.org/jira/browse/JCR-3128">JCR-3128</a>
+ * @param name
+ * the name to escape
+ * @return the escaped name
+ */
+ public static String escapeIllegalJcr10Chars(String name) {
+ return escapeIllegalChars(name, "%/:[]*'\"|\t\r\n");
+ }
+
+ private static String escapeIllegalChars(String name, String illegal) {
+ StringBuilder buffer = new StringBuilder(name.length() * 2);
+ for (int i = 0; i < name.length(); i++) {
+ char ch = name.charAt(i);
+ if (illegal.indexOf(ch) != -1 || (ch == '.' && name.length() < 3)
+ || (ch == ' ' && (i == 0 || i == name.length() - 1))) {
+ buffer.append('%');
+ buffer.append(Character.toUpperCase(Character.forDigit(ch / 16, 16)));
+ buffer.append(Character.toUpperCase(Character.forDigit(ch % 16, 16)));
+ } else {
+ buffer.append(ch);
+ }
+ }
+ return buffer.toString();
+ }
+
+ /**
+ * Escapes illegal XPath search characters at the end of a string.
+ * <p>
+ * Example:<br>
+ * A search string like 'test?' will run into a ParseException documented in
+ * http://issues.apache.org/jira/browse/JCR-1248
+ *
+ * @param s
+ * the string to encode
+ * @return the escaped string
+ */
+ public static String escapeIllegalXpathSearchChars(String s) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(s.substring(0, (s.length() - 1)));
+ char c = s.charAt(s.length() - 1);
+ // NOTE: keep this in sync with _ESCAPED_CHAR below!
+ if (c == '!' || c == '(' || c == ':' || c == '^' || c == '[' || c == ']' || c == '{' || c == '}' || c == '?') {
+ sb.append('\\');
+ }
+ sb.append(c);
+ return sb.toString();
+ }
+
+ /**
+ * Unescapes previously escaped jcr chars.
+ * <p>
+ * Please note, that this does not exactly the same as the url related
+ * {@link #unescape(String)}, since it handles the byte-encoding
+ * differently.
+ *
+ * @param name
+ * the name to unescape
+ * @return the unescaped name
+ */
+ public static String unescapeIllegalJcrChars(String name) {
+ StringBuilder buffer = new StringBuilder(name.length());
+ int i = name.indexOf('%');
+ while (i > -1 && i + 2 < name.length()) {
+ buffer.append(name.toCharArray(), 0, i);
+ int a = Character.digit(name.charAt(i + 1), 16);
+ int b = Character.digit(name.charAt(i + 2), 16);
+ if (a > -1 && b > -1) {
+ buffer.append((char) (a * 16 + b));
+ name = name.substring(i + 3);
+ } else {
+ buffer.append('%');
+ name = name.substring(i + 1);
+ }
+ i = name.indexOf('%');
+ }
+ buffer.append(name);
+ return buffer.toString();
+ }
+
+ /**
+ * Returns the name part of the path. If the given path is already a name
+ * (i.e. contains no slashes) it is returned.
+ *
+ * @param path
+ * the path
+ * @return the name part or <code>null</code> if <code>path</code> is
+ * <code>null</code>.
+ */
+ public static String getName(String path) {
+ return getName(path, '/');
+ }
+
+ /**
+ * Returns the name part of the path, delimited by the given
+ * <code>delim</code>. If the given path is already a name (i.e. contains no
+ * <code>delim</code> characters) it is returned.
+ *
+ * @param path
+ * the path
+ * @param delim
+ * the delimiter
+ * @return the name part or <code>null</code> if <code>path</code> is
+ * <code>null</code>.
+ */
+ public static String getName(String path, char delim) {
+ return path == null ? null : path.substring(path.lastIndexOf(delim) + 1);
+ }
+
+ /**
+ * Same as {@link #getName(String)} but adding the possibility to pass paths
+ * that end with a trailing '/'
+ *
+ * @see #getName(String)
+ */
+ public static String getName(String path, boolean ignoreTrailingSlash) {
+ if (ignoreTrailingSlash && path != null && path.endsWith("/") && path.length() > 1) {
+ path = path.substring(0, path.length() - 1);
+ }
+ return getName(path);
+ }
+
+ /**
+ * Returns the namespace prefix of the given <code>qname</code>. If the
+ * prefix is missing, an empty string is returned. Please note, that this
+ * method does not validate the name or prefix.
+ * </p>
+ * the qname has the format: qname := [prefix ':'] local;
+ *
+ * @param qname
+ * a qualified name
+ * @return the prefix of the name or "".
+ *
+ * @see #getLocalName(String)
+ *
+ * @throws NullPointerException
+ * if <code>qname</code> is <code>null</code>
+ */
+ public static String getNamespacePrefix(String qname) {
+ int pos = qname.indexOf(':');
+ return pos >= 0 ? qname.substring(0, pos) : "";
+ }
+
+ /**
+ * Returns the local name of the given <code>qname</code>. Please note, that
+ * this method does not validate the name.
+ * </p>
+ * the qname has the format: qname := [prefix ':'] local;
+ *
+ * @param qname
+ * a qualified name
+ * @return the localname
+ *
+ * @see #getNamespacePrefix(String)
+ *
+ * @throws NullPointerException
+ * if <code>qname</code> is <code>null</code>
+ */
+ public static String getLocalName(String qname) {
+ int pos = qname.indexOf(':');
+ return pos >= 0 ? qname.substring(pos + 1) : qname;
+ }
+
+ /**
+ * Determines, if two paths denote hierarchical siblins.
+ *
+ * @param p1
+ * first path
+ * @param p2
+ * second path
+ * @return true if on same level, false otherwise
+ */
+ public static boolean isSibling(String p1, String p2) {
+ int pos1 = p1.lastIndexOf('/');
+ int pos2 = p2.lastIndexOf('/');
+ return (pos1 == pos2 && pos1 >= 0 && p1.regionMatches(0, p2, 0, pos1));
+ }
+
+ /**
+ * Determines if the <code>descendant</code> path is hierarchical a
+ * descendant of <code>path</code>.
+ *
+ * @param path
+ * the current path
+ * @param descendant
+ * the potential descendant
+ * @return <code>true</code> if the <code>descendant</code> is a descendant;
+ * <code>false</code> otherwise.
+ */
+ public static boolean isDescendant(String path, String descendant) {
+ String pattern = path.endsWith("/") ? path : path + "/";
+ return !pattern.equals(descendant) && descendant.startsWith(pattern);
+ }
+
+ /**
+ * Determines if the <code>descendant</code> path is hierarchical a
+ * descendant of <code>path</code> or equal to it.
+ *
+ * @param path
+ * the path to check
+ * @param descendant
+ * the potential descendant
+ * @return <code>true</code> if the <code>descendant</code> is a descendant
+ * or equal; <code>false</code> otherwise.
+ */
+ public static boolean isDescendantOrEqual(String path, String descendant) {
+ if (path.equals(descendant)) {
+ return true;
+ } else {
+ String pattern = path.endsWith("/") ? path : path + "/";
+ return descendant.startsWith(pattern);
+ }
+ }
+
+ /**
+ * Returns the n<sup>th</sup> relative parent of the path, where n=level.
+ * <p>
+ * Example:<br>
+ * <code>
+ * Text.getRelativeParent("/foo/bar/test", 1) == "/foo/bar"
+ * </code>
+ *
+ * @param path
+ * the path of the page
+ * @param level
+ * the level of the parent
+ */
+ public static String getRelativeParent(String path, int level) {
+ int idx = path.length();
+ while (level > 0) {
+ idx = path.lastIndexOf('/', idx - 1);
+ if (idx < 0) {
+ return "";
+ }
+ level--;
+ }
+ return (idx == 0) ? "/" : path.substring(0, idx);
+ }
+
+ /**
+ * Same as {@link #getRelativeParent(String, int)} but adding the
+ * possibility to pass paths that end with a trailing '/'
+ *
+ * @see #getRelativeParent(String, int)
+ */
+ public static String getRelativeParent(String path, int level, boolean ignoreTrailingSlash) {
+ if (ignoreTrailingSlash && path.endsWith("/") && path.length() > 1) {
+ path = path.substring(0, path.length() - 1);
+ }
+ return getRelativeParent(path, level);
+ }
+
+ /**
+ * Returns the n<sup>th</sup> absolute parent of the path, where n=level.
+ * <p>
+ * Example:<br>
+ * <code>
+ * Text.getAbsoluteParent("/foo/bar/test", 1) == "/foo/bar"
+ * </code>
+ *
+ * @param path
+ * the path of the page
+ * @param level
+ * the level of the parent
+ */
+ public static String getAbsoluteParent(String path, int level) {
+ int idx = 0;
+ int len = path.length();
+ while (level >= 0 && idx < len) {
+ idx = path.indexOf('/', idx + 1);
+ if (idx < 0) {
+ idx = len;
+ }
+ level--;
+ }
+ return level >= 0 ? "" : path.substring(0, idx);
+ }
+
+ /**
+ * Performs variable replacement on the given string value. Each
+ * <code>${...}</code> sequence within the given value is replaced with the
+ * value of the named parser variable. If a variable is not found in the
+ * properties an IllegalArgumentException is thrown unless
+ * <code>ignoreMissing</code> is <code>true</code>. In the later case, the
+ * missing variable is replaced by the empty string.
+ *
+ * @param value
+ * the original value
+ * @param ignoreMissing
+ * if <code>true</code>, missing variables are replaced by the
+ * empty string.
+ * @return value after variable replacements
+ * @throws IllegalArgumentException
+ * if the replacement of a referenced variable is not found
+ */
+ public static String replaceVariables(Properties variables, String value, boolean ignoreMissing)
+ throws IllegalArgumentException {
+ StringBuilder result = new StringBuilder();
+
+ // Value:
+ // +--+-+--------+-+-----------------+
+ // | |p|--> |q|--> |
+ // +--+-+--------+-+-----------------+
+ int p = 0, q = value.indexOf("${"); // Find first ${
+ while (q != -1) {
+ result.append(value.substring(p, q)); // Text before ${
+ p = q;
+ q = value.indexOf("}", q + 2); // Find }
+ if (q != -1) {
+ String variable = value.substring(p + 2, q);
+ String replacement = variables.getProperty(variable);
+ if (replacement == null) {
+ if (ignoreMissing) {
+ replacement = "";
+ } else {
+ throw new IllegalArgumentException("Replacement not found for ${" + variable + "}.");
+ }
+ }
+ result.append(replacement);
+ p = q + 1;
+ q = value.indexOf("${", p); // Find next ${
+ }
+ }
+ result.append(value.substring(p, value.length())); // Trailing text
+
+ return result.toString();
+ }
+
+ private static byte decodeDigit(byte b) {
+ if (b >= 0x30 && b <= 0x39) {
+ return (byte) (b - 0x30);
+ } else if (b >= 0x41 && b <= 0x46) {
+ return (byte) (b - 0x37);
+ } else if (b >= 0x61 && b <= 0x66) {
+ return (byte) (b - 0x57);
+ } else {
+ throw new IllegalArgumentException("Escape sequence is not hexadecimal: " + (char) b);
+ }
+ }
+
+}
--- /dev/null
+package org.argeo.jcr.fs;
+
+import java.io.IOException;
+import java.nio.file.FileStore;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.attribute.FileStoreAttributeView;
+import java.util.Arrays;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Workspace;
+
+import org.argeo.api.acr.fs.AbstractFsStore;
+import org.argeo.jcr.JcrUtils;
+
+/** A {@link FileStore} implementation based on JCR {@link Workspace}. */
+public class WorkspaceFileStore extends AbstractFsStore {
+ private final String mountPath;
+ private final Workspace workspace;
+ private final String workspaceName;
+ private final int mountDepth;
+
+ public WorkspaceFileStore(String mountPath, Workspace workspace) {
+ if ("/".equals(mountPath) || "".equals(mountPath))
+ throw new IllegalArgumentException(
+ "Mount path '" + mountPath + "' is unsupported, use null for the base file store");
+ if (mountPath != null && !mountPath.startsWith(JcrPath.separator))
+ throw new IllegalArgumentException("Mount path '" + mountPath + "' cannot end with /");
+ if (mountPath != null && mountPath.endsWith(JcrPath.separator))
+ throw new IllegalArgumentException("Mount path '" + mountPath + "' cannot end with /");
+ this.mountPath = mountPath;
+ if (mountPath == null)
+ mountDepth = 0;
+ else {
+ mountDepth = mountPath.split(JcrPath.separator).length - 1;
+ }
+ this.workspace = workspace;
+ this.workspaceName = workspace.getName();
+ }
+
+ public void close() {
+ JcrUtils.logoutQuietly(workspace.getSession());
+ }
+
+ @Override
+ public String name() {
+ return workspace.getName();
+ }
+
+ @Override
+ public String type() {
+ return "workspace";
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return false;
+ }
+
+ @Override
+ public long getTotalSpace() throws IOException {
+ return 0;
+ }
+
+ @Override
+ public long getUsableSpace() throws IOException {
+ return 0;
+ }
+
+ @Override
+ public long getUnallocatedSpace() throws IOException {
+ return 0;
+ }
+
+ @Override
+ public boolean supportsFileAttributeView(Class<? extends FileAttributeView> type) {
+ return false;
+ }
+
+ @Override
+ public boolean supportsFileAttributeView(String name) {
+ return false;
+ }
+
+ @Override
+ public <V extends FileStoreAttributeView> V getFileStoreAttributeView(Class<V> type) {
+ return null;
+ }
+
+ @Override
+ public Object getAttribute(String attribute) throws IOException {
+ return workspace.getSession().getRepository().getDescriptor(attribute);
+ }
+
+ public Workspace getWorkspace() {
+ return workspace;
+ }
+
+ public String toFsPath(Node node) throws RepositoryException {
+ String nodeWorkspaceName = node.getSession().getWorkspace().getName();
+ if (!nodeWorkspaceName.equals(workspace.getName()))
+ throw new IllegalArgumentException("Icompatible " + node + " from workspace '" + nodeWorkspaceName
+ + "' in file store '" + workspace.getName() + "'");
+ return mountPath == null ? node.getPath() : mountPath + node.getPath();
+ }
+
+ public boolean isBase() {
+ return mountPath == null;
+ }
+
+ Node toNode(String[] fullPath) throws RepositoryException {
+ String jcrPath = toJcrPath(fullPath);
+ Session session = workspace.getSession();
+ if (!session.itemExists(jcrPath))
+ return null;
+ Node node = session.getNode(jcrPath);
+ return node;
+ }
+
+ String toJcrPath(String fsPath) {
+ if (fsPath.length() == 1)
+ return toJcrPath((String[]) null);// root
+ String[] arr = fsPath.substring(1).split("/");
+// if (arr.length == 0 || (arr.length == 1 && arr[0].equals("")))
+// return toJcrPath((String[]) null);// root
+// else
+ return toJcrPath(arr);
+ }
+
+ private String toJcrPath(String[] path) {
+ if (path == null)
+ return "/";
+ if (path.length < mountDepth)
+ throw new IllegalArgumentException(
+ "Path " + Arrays.asList(path) + " is no compatible with mount " + mountPath);
+
+ if (!isBase()) {
+ // check mount compatibility
+ StringBuilder mount = new StringBuilder();
+ mount.append('/');
+ for (int i = 0; i < mountDepth; i++) {
+ if (i != 0)
+ mount.append('/');
+ mount.append(Text.escapeIllegalJcrChars(path[i]));
+ }
+ if (!mountPath.equals(mount.toString()))
+ throw new IllegalArgumentException(
+ "Path " + Arrays.asList(path) + " is no compatible with mount " + mountPath);
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append('/');
+ for (int i = mountDepth; i < path.length; i++) {
+ if (i != mountDepth)
+ sb.append('/');
+ sb.append(Text.escapeIllegalJcrChars(path[i]));
+ }
+ return sb.toString();
+ }
+
+ public String getMountPath() {
+ return mountPath;
+ }
+
+ public String getWorkspaceName() {
+ return workspaceName;
+ }
+
+ public int getMountDepth() {
+ return mountDepth;
+ }
+
+ @Override
+ public int hashCode() {
+ return workspaceName.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof WorkspaceFileStore))
+ return false;
+ WorkspaceFileStore other = (WorkspaceFileStore) obj;
+ return workspaceName.equals(other.workspaceName);
+ }
+
+ @Override
+ public String toString() {
+ return "WorkspaceFileStore " + workspaceName;
+ }
+
+}
--- /dev/null
+/** Java NIO file system implementation based on plain JCR. */
+package org.argeo.jcr.fs;
\ No newline at end of file
--- /dev/null
+//
+// JCR EXTENSIONS
+//
+<jcrx = "http://www.argeo.org/ns/jcrx">
+
+[jcrx:xmlvalue]
+- *
++ jcr:xmltext (jcrx:xmltext) = jcrx:xmltext
+
+[jcrx:xmltext]
+ - jcr:xmlcharacters (STRING) mandatory
+
+[jcrx:csum]
+mixin
+ - jcrx:sum (STRING) *
+
\ No newline at end of file
--- /dev/null
+/** Generic JCR utilities. */
+package org.argeo.jcr;
\ No newline at end of file
--- /dev/null
+package org.argeo.jcr.proxy;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.JcrException;
+import org.argeo.jcr.JcrUtils;
+
+/** Base class for URL based proxys. */
+public abstract class AbstractUrlProxy implements ResourceProxy {
+ private final static CmsLog log = CmsLog.getLog(AbstractUrlProxy.class);
+
+ private Repository jcrRepository;
+ private Session jcrAdminSession;
+ private String proxyWorkspace = "proxy";
+
+ protected abstract Node retrieve(Session session, String path);
+
+ void init() {
+ try {
+ jcrAdminSession = JcrUtils.loginOrCreateWorkspace(jcrRepository, proxyWorkspace);
+ beforeInitSessionSave(jcrAdminSession);
+ if (jcrAdminSession.hasPendingChanges())
+ jcrAdminSession.save();
+ } catch (RepositoryException e) {
+ JcrUtils.discardQuietly(jcrAdminSession);
+ throw new JcrException("Cannot initialize URL proxy", e);
+ }
+ }
+
+ /**
+ * Called before the (admin) session is saved at the end of the initialization.
+ * Does nothing by default, to be overridden.
+ */
+ protected void beforeInitSessionSave(Session session) throws RepositoryException {
+ }
+
+ void destroy() {
+ JcrUtils.logoutQuietly(jcrAdminSession);
+ }
+
+ /**
+ * Called before the (admin) session is logged out when resources are released.
+ * Does nothing by default, to be overridden.
+ */
+ protected void beforeDestroySessionLogout() throws RepositoryException {
+ }
+
+ public Node proxy(String path) {
+ // we open a JCR session with client credentials in order not to use the
+ // admin session in multiple thread or make it a bottleneck.
+ Node nodeAdmin = null;
+ Node nodeClient = null;
+ Session clientSession = null;
+ try {
+ clientSession = jcrRepository.login(proxyWorkspace);
+ if (!clientSession.itemExists(path) || shouldUpdate(clientSession, path)) {
+ nodeAdmin = retrieveAndSave(path);
+ if (nodeAdmin != null)
+ nodeClient = clientSession.getNode(path);
+ } else
+ nodeClient = clientSession.getNode(path);
+ return nodeClient;
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot proxy " + path, e);
+ } finally {
+ if (nodeClient == null)
+ JcrUtils.logoutQuietly(clientSession);
+ }
+ }
+
+ protected synchronized Node retrieveAndSave(String path) {
+ try {
+ Node node = retrieve(jcrAdminSession, path);
+ if (node == null)
+ return null;
+ jcrAdminSession.save();
+ return node;
+ } catch (RepositoryException e) {
+ JcrUtils.discardQuietly(jcrAdminSession);
+ throw new JcrException("Cannot retrieve and save " + path, e);
+ } finally {
+ notifyAll();
+ }
+ }
+
+ /** Session is not saved */
+ protected synchronized Node proxyUrl(Session session, String remoteUrl, String path) throws RepositoryException {
+ Node node = null;
+ if (session.itemExists(path)) {
+ // throw new ArgeoJcrException("Node " + path + " already exists");
+ }
+ try (InputStream in = new URL(remoteUrl).openStream()) {
+ // URL u = new URL(remoteUrl);
+ // in = u.openStream();
+ node = importFile(session, path, in);
+ } catch (IOException e) {
+ if (log.isDebugEnabled()) {
+ log.debug("Cannot read " + remoteUrl + ", skipping... " + e.getMessage());
+ // log.trace("Cannot read because of ", e);
+ }
+ JcrUtils.discardQuietly(session);
+ // } finally {
+ // IOUtils.closeQuietly(in);
+ }
+ return node;
+ }
+
+ protected synchronized Node importFile(Session session, String path, InputStream in) throws RepositoryException {
+ Binary binary = null;
+ try {
+ Node content = null;
+ Node node = null;
+ if (!session.itemExists(path)) {
+ node = JcrUtils.mkdirs(session, path, NodeType.NT_FILE, NodeType.NT_FOLDER, false);
+ content = node.addNode(Node.JCR_CONTENT, NodeType.NT_UNSTRUCTURED);
+ } else {
+ node = session.getNode(path);
+ content = node.getNode(Node.JCR_CONTENT);
+ }
+ binary = session.getValueFactory().createBinary(in);
+ content.setProperty(Property.JCR_DATA, binary);
+ JcrUtils.updateLastModifiedAndParents(node, null, true);
+ return node;
+ } finally {
+ JcrUtils.closeQuietly(binary);
+ }
+ }
+
+ /** Whether the file should be updated. */
+ protected Boolean shouldUpdate(Session clientSession, String nodePath) {
+ return false;
+ }
+
+ public void setJcrRepository(Repository jcrRepository) {
+ this.jcrRepository = jcrRepository;
+ }
+
+ public void setProxyWorkspace(String localWorkspace) {
+ this.proxyWorkspace = localWorkspace;
+ }
+
+}
--- /dev/null
+package org.argeo.jcr.proxy;
+
+import javax.jcr.Node;
+
+/** A proxy which nows how to resolve and synchronize relative URLs */
+public interface ResourceProxy {
+ /**
+ * Proxy the file referenced by this relative path in the underlying
+ * repository. A new session is created by each call, so the underlying
+ * session of the returned node must be closed by the caller.
+ *
+ * @return the proxied Node, <code>null</code> if the resource was not found
+ * (e.g. HTTP 404)
+ */
+ public Node proxy(String relativePath);
+}
--- /dev/null
+package org.argeo.jcr.proxy;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.argeo.jcr.JcrException;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.jcr.Bin;
+import org.argeo.jcr.JcrUtils;
+
+/** Wraps a proxy via HTTP */
+public class ResourceProxyServlet extends HttpServlet {
+ private static final long serialVersionUID = -8886549549223155801L;
+
+ private final static CmsLog log = CmsLog
+ .getLog(ResourceProxyServlet.class);
+
+ private ResourceProxy proxy;
+
+ private String contentTypeCharset = "UTF-8";
+
+ @Override
+ protected void doGet(HttpServletRequest request,
+ HttpServletResponse response) throws ServletException, IOException {
+ String path = request.getPathInfo();
+
+ if (log.isTraceEnabled()) {
+ log.trace("path=" + path);
+ log.trace("UserPrincipal = " + request.getUserPrincipal().getName());
+ log.trace("SessionID = " + request.getSession(false).getId());
+ log.trace("ContextPath = " + request.getContextPath());
+ log.trace("ServletPath = " + request.getServletPath());
+ log.trace("PathInfo = " + request.getPathInfo());
+ log.trace("Method = " + request.getMethod());
+ log.trace("User-Agent = " + request.getHeader("User-Agent"));
+ }
+
+ Node node = null;
+ try {
+ node = proxy.proxy(path);
+ if (node == null)
+ response.sendError(404);
+ else
+ processResponse(node, response);
+ } finally {
+ if (node != null)
+ try {
+ JcrUtils.logoutQuietly(node.getSession());
+ } catch (RepositoryException e) {
+ // silent
+ }
+ }
+
+ }
+
+ /** Retrieve the content of the node. */
+ protected void processResponse(Node node, HttpServletResponse response) {
+// Binary binary = null;
+// InputStream in = null;
+ try(Bin binary = new Bin( node.getNode(Property.JCR_CONTENT)
+ .getProperty(Property.JCR_DATA));InputStream in = binary.getStream()) {
+ String fileName = node.getName();
+ String ext = FilenameUtils.getExtension(fileName);
+
+ // TODO use a more generic / standard approach
+ // see http://svn.apache.org/viewvc/tomcat/trunk/conf/web.xml
+ String contentType;
+ if ("xml".equals(ext))
+ contentType = "text/xml;charset=" + contentTypeCharset;
+ else if ("jar".equals(ext))
+ contentType = "application/java-archive";
+ else if ("zip".equals(ext))
+ contentType = "application/zip";
+ else if ("gz".equals(ext))
+ contentType = "application/x-gzip";
+ else if ("bz2".equals(ext))
+ contentType = "application/x-bzip2";
+ else if ("tar".equals(ext))
+ contentType = "application/x-tar";
+ else if ("rpm".equals(ext))
+ contentType = "application/x-redhat-package-manager";
+ else
+ contentType = "application/octet-stream";
+ contentType = contentType + ";name=\"" + fileName + "\"";
+ response.setHeader("Content-Disposition", "attachment; filename=\""
+ + fileName + "\"");
+ response.setHeader("Expires", "0");
+ response.setHeader("Cache-Control", "no-cache, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+
+ response.setContentType(contentType);
+
+ IOUtils.copy(in, response.getOutputStream());
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot download " + node, e);
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot download " + node, e);
+ }
+ }
+
+ public void setProxy(ResourceProxy resourceProxy) {
+ this.proxy = resourceProxy;
+ }
+
+}
--- /dev/null
+/** Components to build proxys based on JCR. */
+package org.argeo.jcr.proxy;
\ No newline at end of file
--- /dev/null
+package org.argeo.jcr.xml;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Map;
+import java.util.TreeMap;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.jcr.Jcr;
+
+/** Utilities around JCR and XML. */
+public class JcrXmlUtils {
+ /**
+ * Convenience method calling {@link #toXmlElements(Writer, Node, boolean)} with
+ * <code>false</code>.
+ */
+ public static void toXmlElements(Writer writer, Node node) throws RepositoryException, IOException {
+ toXmlElements(writer, node, null, false, false, false);
+ }
+
+ /**
+ * Write JCR properties as XML elements in a tree structure whose elements are
+ * named by node primary type.
+ *
+ * @param writer the writer to use
+ * @param node the subtree
+ * @param depth maximal depth, or if <code>null</code> the whole
+ * subtree. It must be positive, with depth 0
+ * describing just the node without its children.
+ * @param withMetadata whether to write the primary type and mixins as
+ * elements
+ * @param withPrefix whether to keep the namespace prefixes
+ * @param propertiesAsElements whether single properties should be written as
+ * elements rather than attributes. If
+ * <code>false</code>, multiple properties will be
+ * skipped.
+ */
+ public static void toXmlElements(Writer writer, Node node, Integer depth, boolean withMetadata, boolean withPrefix,
+ boolean propertiesAsElements) throws RepositoryException, IOException {
+ if (depth != null && depth < 0)
+ throw new IllegalArgumentException("Depth " + depth + " is negative.");
+
+ if (node.getName().equals(Jcr.JCR_XMLTEXT)) {
+ writer.write(node.getProperty(Jcr.JCR_XMLCHARACTERS).getString());
+ return;
+ }
+
+ if (!propertiesAsElements) {
+ Map<String, String> attrs = new TreeMap<>();
+ PropertyIterator pit = node.getProperties();
+ properties: while (pit.hasNext()) {
+ Property p = pit.nextProperty();
+ if (!p.isMultiple()) {
+ String pName = p.getName();
+ if (!withMetadata && (pName.equals(Jcr.JCR_PRIMARY_TYPE) || pName.equals(Jcr.JCR_UUID)
+ || pName.equals(Jcr.JCR_CREATED) || pName.equals(Jcr.JCR_CREATED_BY)
+ || pName.equals(Jcr.JCR_LAST_MODIFIED) || pName.equals(Jcr.JCR_LAST_MODIFIED_BY)))
+ continue properties;
+ attrs.put(withPrefix(p.getName(), withPrefix), p.getString());
+ }
+ }
+ if (withMetadata && node.hasProperty(Property.JCR_UUID))
+ attrs.put("id", "urn:uuid:" + node.getProperty(Property.JCR_UUID).getString());
+ attrs.put(withPrefix ? Jcr.JCR_NAME : "name", node.getName());
+ writeStart(writer, withPrefix(node.getPrimaryNodeType().getName(), withPrefix), attrs, node.hasNodes());
+ } else {
+ if (withMetadata && node.hasProperty(Property.JCR_UUID)) {
+ writeStart(writer, withPrefix(node.getPrimaryNodeType().getName(), withPrefix), "id",
+ "urn:uuid:" + node.getProperty(Property.JCR_UUID).getString());
+ } else {
+ writeStart(writer, withPrefix(node.getPrimaryNodeType().getName(), withPrefix));
+ }
+ // name
+ writeStart(writer, withPrefix ? Jcr.JCR_NAME : "name");
+ writer.append(node.getName());
+ writeEnd(writer, withPrefix ? Jcr.JCR_NAME : "name");
+ }
+
+ // mixins
+ if (withMetadata) {
+ for (NodeType mixin : node.getMixinNodeTypes()) {
+ writeStart(writer, withPrefix ? Jcr.JCR_MIXIN_TYPES : "mixinTypes");
+ writer.append(mixin.getName());
+ writeEnd(writer, withPrefix ? Jcr.JCR_MIXIN_TYPES : "mixinTypes");
+ }
+ }
+
+ // properties as elements
+ if (propertiesAsElements) {
+ PropertyIterator pit = node.getProperties();
+ properties: while (pit.hasNext()) {
+ Property p = pit.nextProperty();
+ if (p.isMultiple()) {
+ for (Value value : p.getValues()) {
+ writeStart(writer, withPrefix(p.getName(), withPrefix));
+ writer.write(value.getString());
+ writeEnd(writer, withPrefix(p.getName(), withPrefix));
+ }
+ } else {
+ Value value = p.getValue();
+ String pName = p.getName();
+ if (!withMetadata && (pName.equals(Jcr.JCR_PRIMARY_TYPE) || pName.equals(Jcr.JCR_UUID)
+ || pName.equals(Jcr.JCR_CREATED) || pName.equals(Jcr.JCR_CREATED_BY)
+ || pName.equals(Jcr.JCR_LAST_MODIFIED) || pName.equals(Jcr.JCR_LAST_MODIFIED_BY)))
+ continue properties;
+ writeStart(writer, withPrefix(p.getName(), withPrefix));
+ writer.write(value.getString());
+ writeEnd(writer, withPrefix(p.getName(), withPrefix));
+ }
+ }
+ }
+
+ // children
+ if (node.hasNodes()) {
+ if (depth == null || depth > 0) {
+ NodeIterator nit = node.getNodes();
+ while (nit.hasNext()) {
+ toXmlElements(writer, nit.nextNode(), depth == null ? null : depth - 1, withMetadata, withPrefix,
+ propertiesAsElements);
+ }
+ }
+ writeEnd(writer, withPrefix(node.getPrimaryNodeType().getName(), withPrefix));
+ }
+ }
+
+ private static String withPrefix(String str, boolean withPrefix) {
+ if (withPrefix)
+ return str;
+ int index = str.indexOf(':');
+ if (index < 0)
+ return str;
+ return str.substring(index + 1);
+ }
+
+ private static void writeStart(Writer writer, String tagName) throws IOException {
+ writer.append('<');
+ writer.append(tagName);
+ writer.append('>');
+ }
+
+ private static void writeStart(Writer writer, String tagName, String attr, String value) throws IOException {
+ writer.append('<');
+ writer.append(tagName);
+ writer.append(' ');
+ writer.append(attr);
+ writer.append("=\"");
+ writer.append(value);
+ writer.append("\">");
+ }
+
+ private static void writeStart(Writer writer, String tagName, Map<String, String> attrs, boolean hasChildren)
+ throws IOException {
+ writer.append('<');
+ writer.append(tagName);
+ for (String attr : attrs.keySet()) {
+ writer.append(' ');
+ writer.append(attr);
+ writer.append("=\"");
+ writer.append(attrs.get(attr));
+ writer.append('\"');
+ }
+ if (hasChildren)
+ writer.append('>');
+ else
+ writer.append("/>");
+ }
+
+ private static void writeEnd(Writer writer, String tagName) throws IOException {
+ writer.append("</");
+ writer.append(tagName);
+ writer.append('>');
+ }
+
+ /** Singleton. */
+ private JcrXmlUtils() {
+
+ }
+
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
+ <xsl:output method="xml" indent="yes"/>
+ <xsl:template match="/|comment()|processing-instruction()">
+ <xsl:copy>
+ <xsl:apply-templates/>
+ </xsl:copy>
+ </xsl:template>
+ <xsl:template match="*">
+ <xsl:element name="{local-name()}">
+ <xsl:apply-templates select="@*|node()"/>
+ </xsl:element>
+ </xsl:template>
+ <xsl:template match="@*">
+ <xsl:attribute name="{local-name()}">
+ <xsl:value-of select="."/>
+ </xsl:attribute>
+ </xsl:template>
+</xsl:stylesheet>
\ No newline at end of file
--- /dev/null
+package org.argeo.maintenance;
+
+import java.io.IOException;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.jcr.NoSuchWorkspaceException;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.api.acr.spi.ProvidedRepository;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.jcr.CmsJcrUtils;
+import org.argeo.jcr.Jcr;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.util.naming.Distinguished;
+import org.argeo.util.transaction.WorkTransaction;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdmin;
+
+/** Make sure roles and access rights are properly configured. */
+public abstract class AbstractMaintenanceService {
+ private final static CmsLog log = CmsLog.getLog(AbstractMaintenanceService.class);
+
+ private Repository repository;
+// private UserAdminService userAdminService;
+ private UserAdmin userAdmin;
+ private WorkTransaction userTransaction;
+
+ private ProvidedRepository contentRepository;
+
+ public void init() {
+ makeSureRolesExists(getRequiredRoles());
+ configureStandardRoles();
+
+ Set<String> workspaceNames = getWorkspaceNames();
+ if (workspaceNames == null || workspaceNames.isEmpty()) {
+ configureJcr(repository, null);
+ } else {
+ for (String workspaceName : workspaceNames)
+ configureJcr(repository, workspaceName);
+ }
+ }
+
+ /** Configures a workspace. */
+ protected void configureJcr(Repository repository, String workspaceName) {
+ Session adminSession;
+ try {
+ adminSession = CmsJcrUtils.openDataAdminSession(repository, workspaceName);
+ } catch (RuntimeException e1) {
+ if (e1.getCause() != null && e1.getCause() instanceof NoSuchWorkspaceException) {
+ Session defaultAdminSession = CmsJcrUtils.openDataAdminSession(repository, null);
+ try {
+ defaultAdminSession.getWorkspace().createWorkspace(workspaceName);
+ log.info("Created JCR workspace " + workspaceName);
+ } catch (RepositoryException e) {
+ throw new IllegalStateException("Cannot create workspace " + workspaceName, e);
+ } finally {
+ Jcr.logout(defaultAdminSession);
+ }
+ adminSession = CmsJcrUtils.openDataAdminSession(repository, workspaceName);
+ } else
+ throw e1;
+ }
+ try {
+ if (prepareJcrTree(adminSession)) {
+ configurePrivileges(adminSession);
+ }
+ } catch (RepositoryException | IOException e) {
+ throw new IllegalStateException("Cannot initialise JCR data layer.", e);
+ } finally {
+ JcrUtils.logoutQuietly(adminSession);
+ }
+ }
+
+ /** To be overridden. */
+ protected Set<String> getWorkspaceNames() {
+ return null;
+ }
+
+ /**
+ * To be overridden in order to programmatically set relationships between
+ * roles. Does nothing by default.
+ */
+ protected void configureStandardRoles() {
+ }
+
+ /**
+ * Creates the base JCR tree structure expected for this app if necessary.
+ *
+ * Expects a clean session ({@link Session#hasPendingChanges()} should return
+ * false) and saves it once the changes have been done. Thus the session can be
+ * rolled back if an exception occurs.
+ *
+ * @return true if something as been updated
+ */
+ public boolean prepareJcrTree(Session adminSession) throws RepositoryException, IOException {
+ return false;
+ }
+
+ /**
+ * Adds app specific default privileges.
+ *
+ * Expects a clean session ({@link Session#hasPendingChanges()} should return
+ * false} and saves it once the changes have been done. Thus the session can be
+ * rolled back if an exception occurs.
+ *
+ * Warning: no check is done and corresponding privileges are always added, so
+ * only call this when necessary
+ */
+ public void configurePrivileges(Session session) throws RepositoryException {
+ }
+
+ /** The system roles that must be available in the system. */
+ protected Set<String> getRequiredRoles() {
+ return new HashSet<>();
+ }
+
+ public void destroy() {
+
+ }
+
+ /*
+ * UTILITIES
+ */
+
+ /** Create these roles as group if they don't exist. */
+ protected void makeSureRolesExists(EnumSet<? extends Distinguished> enumSet) {
+ makeSureRolesExists(Distinguished.enumToDns(enumSet));
+ }
+
+ /** Create these roles as group if they don't exist. */
+ protected void makeSureRolesExists(Set<String> requiredRoles) {
+ if (requiredRoles == null)
+ return;
+ if (getUserAdmin() == null) {
+ log.warn("No user admin service available, cannot make sure that role exists");
+ return;
+ }
+ for (String role : requiredRoles) {
+ Role systemRole = getUserAdmin().getRole(role);
+ if (systemRole == null) {
+ try {
+ getUserTransaction().begin();
+ getUserAdmin().createRole(role, Role.GROUP);
+ getUserTransaction().commit();
+ log.info("Created role " + role);
+ } catch (Exception e) {
+ try {
+ getUserTransaction().rollback();
+ } catch (Exception e1) {
+ // silent
+ }
+ throw new IllegalStateException("Cannot create role " + role, e);
+ }
+ }
+ }
+ }
+
+ /** Add a user or group to a group. */
+ protected void addToGroup(String groupToAddDn, String groupDn) {
+ if (groupToAddDn.contentEquals(groupDn)) {
+ if (log.isTraceEnabled())
+ log.trace("Ignore adding group " + groupDn + " to itself");
+ return;
+ }
+
+ if (getUserAdmin() == null) {
+ log.warn("No user admin service available, cannot add group " + groupToAddDn + " to " + groupDn);
+ return;
+ }
+ Group groupToAdd = (Group) getUserAdmin().getRole(groupToAddDn);
+ if (groupToAdd == null)
+ throw new IllegalArgumentException("Group " + groupToAddDn + " not found");
+ Group group = (Group) getUserAdmin().getRole(groupDn);
+ if (group == null)
+ throw new IllegalArgumentException("Group " + groupDn + " not found");
+ try {
+ getUserTransaction().begin();
+ if (group.addMember(groupToAdd))
+ log.info("Added " + groupToAddDn + " to " + group);
+ getUserTransaction().commit();
+ } catch (Exception e) {
+ try {
+ getUserTransaction().rollback();
+ } catch (Exception e1) {
+ // silent
+ }
+ throw new IllegalStateException("Cannot add " + groupToAddDn + " to " + groupDn);
+ }
+ }
+
+ /*
+ * DEPENDENCY INJECTION
+ */
+ public void setRepository(Repository repository) {
+ this.repository = repository;
+ }
+
+// public void setUserAdminService(UserAdminService userAdminService) {
+// this.userAdminService = userAdminService;
+// }
+
+ protected WorkTransaction getUserTransaction() {
+ return userTransaction;
+ }
+
+ protected UserAdmin getUserAdmin() {
+ return userAdmin;
+ }
+
+ public void setUserAdmin(UserAdmin userAdmin) {
+ this.userAdmin = userAdmin;
+ }
+
+ public void setUserTransaction(WorkTransaction userTransaction) {
+ this.userTransaction = userTransaction;
+ }
+
+ public void setContentRepository(ProvidedRepository contentRepository) {
+ this.contentRepository = contentRepository;
+ }
+
+ protected ProvidedRepository getContentRepository() {
+ return contentRepository;
+ }
+
+
+}
--- /dev/null
+package org.argeo.maintenance;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.util.transaction.WorkTransaction;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdmin;
+
+/**
+ * Register one or many roles via a user admin service. Does nothing if the role
+ * is already registered.
+ */
+public class SimpleRoleRegistration implements Runnable {
+ private final static CmsLog log = CmsLog.getLog(SimpleRoleRegistration.class);
+
+ private String role;
+ private List<String> roles = new ArrayList<String>();
+ private UserAdmin userAdmin;
+ private WorkTransaction userTransaction;
+
+ @Override
+ public void run() {
+ try {
+ userTransaction.begin();
+ if (role != null && !roleExists(role))
+ newRole(toDn(role));
+
+ for (String r : roles)
+ if (!roleExists(r))
+ newRole(toDn(r));
+ userTransaction.commit();
+ } catch (Exception e) {
+ try {
+ userTransaction.rollback();
+ } catch (Exception e1) {
+ log.error("Cannot rollback", e1);
+ }
+ throw new IllegalArgumentException("Cannot add roles", e);
+ }
+ }
+
+ private boolean roleExists(String role) {
+ return userAdmin.getRole(toDn(role).toString()) != null;
+ }
+
+ protected void newRole(LdapName r) {
+ userAdmin.createRole(r.toString(), Role.GROUP);
+ log.info("Added role " + r + " required by application.");
+ }
+
+ public void register(UserAdmin userAdminService, Map<?, ?> properties) {
+ this.userAdmin = userAdminService;
+ run();
+ }
+
+ protected LdapName toDn(String name) {
+ try {
+ return new LdapName("cn=" + name + ",ou=roles,ou=node");
+ } catch (InvalidNameException e) {
+ throw new IllegalArgumentException("Badly formatted role name " + name, e);
+ }
+ }
+
+ public void setRole(String role) {
+ this.role = role;
+ }
+
+ public void setRoles(List<String> roles) {
+ this.roles = roles;
+ }
+
+ public void setUserAdmin(UserAdmin userAdminService) {
+ this.userAdmin = userAdminService;
+ }
+
+ public void setUserTransaction(WorkTransaction userTransaction) {
+ this.userTransaction = userTransaction;
+ }
+
+}
--- /dev/null
+package org.argeo.maintenance.backup;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Writer;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Set;
+import java.util.TreeSet;
+
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.jcr.Jcr;
+import org.argeo.jcr.JcrException;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/** XML handler serialising a JCR system view. */
+public class BackupContentHandler extends DefaultHandler {
+ final static int MAX_DEPTH = 1024;
+ final static String SV_NAMESPACE_URI = "http://www.jcp.org/jcr/sv/1.0";
+ final static String SV_PREFIX = "sv";
+ // elements
+ final static String NODE = "node";
+ final static String PROPERTY = "property";
+ final static String VALUE = "value";
+ // attributes
+ final static String NAME = "name";
+ final static String MULTIPLE = "multiple";
+ final static String TYPE = "type";
+
+ // values
+ final static String BINARY = "Binary";
+ final static String JCR_CONTENT = "jcr:content";
+
+ private Writer out;
+ private Session session;
+ private Set<String> contentPaths = new TreeSet<>();
+
+ boolean prettyPrint = true;
+
+ private final String parentPath;
+
+// private boolean inSystem = false;
+
+ public BackupContentHandler(Writer out, Node node) {
+ super();
+ this.out = out;
+ this.session = Jcr.getSession(node);
+ parentPath = Jcr.getParentPath(node);
+ }
+
+ private int currentDepth = -1;
+ private String[] currentPath = new String[MAX_DEPTH];
+
+ private boolean currentPropertyIsMultiple = false;
+ private String currentEncoded = null;
+ private Base64.Encoder base64encore = Base64.getEncoder();
+
+ @Override
+ public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+ boolean isNode;
+ boolean isProperty;
+ switch (localName) {
+ case NODE:
+ isNode = true;
+ isProperty = false;
+ break;
+ case PROPERTY:
+ isNode = false;
+ isProperty = true;
+ break;
+ default:
+ isNode = false;
+ isProperty = false;
+ }
+
+ if (isNode) {
+ String nodeName = attributes.getValue(SV_NAMESPACE_URI, NAME);
+ currentDepth = currentDepth + 1;
+// if (currentDepth >= 0)
+ currentPath[currentDepth] = nodeName;
+// System.out.println(getCurrentPath() + " , depth=" + currentDepth);
+// if ("jcr:system".equals(nodeName)) {
+// inSystem = true;
+// }
+ }
+// if (inSystem)
+// return;
+
+ if (SV_NAMESPACE_URI.equals(uri))
+ try {
+ if (prettyPrint) {
+ if (isNode) {
+ out.write(spaces());
+ out.write("<!-- ");
+ out.write(getCurrentJcrPath());
+ out.write(" -->\n");
+ out.write(spaces());
+ } else if (isProperty)
+ out.write(spaces());
+ else if (currentPropertyIsMultiple)
+ out.write(spaces());
+ }
+
+ out.write("<");
+ out.write(SV_PREFIX + ":" + localName);
+ if (isProperty)
+ currentPropertyIsMultiple = false; // always reset
+ for (int i = 0; i < attributes.getLength(); i++) {
+ String ns = attributes.getURI(i);
+ if (SV_NAMESPACE_URI.equals(ns)) {
+ String attrName = attributes.getLocalName(i);
+ String attrValue = attributes.getValue(i);
+ out.write(" ");
+ out.write(SV_PREFIX + ":" + attrName);
+ out.write("=");
+ out.write("\"");
+ out.write(attrValue);
+ out.write("\"");
+ if (isProperty) {
+ if (MULTIPLE.equals(attrName))
+ currentPropertyIsMultiple = Boolean.parseBoolean(attrValue);
+ else if (TYPE.equals(attrName)) {
+ if (BINARY.equals(attrValue)) {
+ if (JCR_CONTENT.equals(getCurrentName())) {
+ contentPaths.add(getCurrentJcrPath());
+ } else {
+ Binary binary = session.getNode(getCurrentJcrPath()).getProperty(attrName)
+ .getBinary();
+ try (InputStream in = binary.getStream()) {
+ currentEncoded = base64encore.encodeToString(IOUtils.toByteArray(in));
+ } finally {
+
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ if (isNode && currentDepth == 0) {
+ // out.write(" xmlns=\"" + SV_NAMESPACE_URI + "\"");
+ out.write(" xmlns:" + SV_PREFIX + "=\"" + SV_NAMESPACE_URI + "\"");
+ }
+ out.write(">");
+
+ if (prettyPrint)
+ if (isNode)
+ out.write("\n");
+ else if (isProperty && currentPropertyIsMultiple)
+ out.write("\n");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } catch (RepositoryException e) {
+ throw new JcrException(e);
+ }
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+ boolean isNode = localName.equals(NODE);
+ boolean isValue = localName.equals(VALUE);
+ if (prettyPrint)
+ if (!isValue)
+ try {
+ if (isNode || currentPropertyIsMultiple)
+ out.write(spaces());
+ } catch (IOException e1) {
+ throw new RuntimeException(e1);
+ }
+ if (isNode) {
+// System.out.println("endElement " + getCurrentPath() + " , depth=" + currentDepth);
+// if (currentDepth > 0)
+ currentPath[currentDepth] = null;
+ currentDepth = currentDepth - 1;
+// if (inSystem) {
+// // System.out.println("Skip " + getCurrentPath()+" ,
+// // currentDepth="+currentDepth);
+// if (currentDepth == 0) {
+// inSystem = false;
+// return;
+// }
+// }
+ }
+// if (inSystem)
+// return;
+ if (SV_NAMESPACE_URI.equals(uri))
+ try {
+ if (isValue && currentEncoded != null) {
+ out.write(currentEncoded);
+ }
+ currentEncoded = null;
+ out.write("</");
+ out.write(SV_PREFIX + ":" + localName);
+ out.write(">");
+ if (prettyPrint)
+ if (!isValue)
+ out.write("\n");
+ else {
+ if (currentPropertyIsMultiple)
+ out.write("\n");
+ }
+ if (currentDepth == 0)
+ out.flush();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ private char[] spaces() {
+ char[] arr = new char[currentDepth];
+ Arrays.fill(arr, ' ');
+ return arr;
+ }
+
+ @Override
+ public void characters(char[] ch, int start, int length) throws SAXException {
+// if (inSystem)
+// return;
+ try {
+ out.write(ch, start, length);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ protected String getCurrentName() {
+ assert currentDepth >= 0;
+// if (currentDepth == 0)
+// return "jcr:root";
+ return currentPath[currentDepth];
+ }
+
+ protected String getCurrentJcrPath() {
+// if (currentDepth == 0)
+// return "/";
+ StringBuilder sb = new StringBuilder(parentPath.equals("/") ? "" : parentPath);
+ for (int i = 0; i <= currentDepth; i++) {
+// if (i != 0)
+ sb.append('/');
+ sb.append(currentPath[i]);
+ }
+ return sb.toString();
+ }
+
+ public Set<String> getContentPaths() {
+ return contentPaths;
+ }
+
+}
--- /dev/null
+package org.argeo.maintenance.backup;
+
+import java.io.BufferedWriter;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.net.URI;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipOutputStream;
+
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.jackrabbit.api.JackrabbitSession;
+import org.apache.jackrabbit.api.JackrabbitValue;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.jcr.CmsJcrUtils;
+import org.argeo.jackrabbit.client.ClientDavexRepositoryFactory;
+import org.argeo.jcr.Jcr;
+import org.argeo.jcr.JcrException;
+import org.argeo.jcr.JcrUtils;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+
+/**
+ * Performs a backup of the data based only on programmatic interfaces. Useful
+ * for migration or live backup. Physical backups of the underlying file
+ * systems, databases, LDAP servers, etc. should be performed for disaster
+ * recovery.
+ */
+public class LogicalBackup implements Runnable {
+ private final static CmsLog log = CmsLog.getLog(LogicalBackup.class);
+
+ public final static String WORKSPACES_BASE = "workspaces/";
+ public final static String FILES_BASE = "files/";
+ public final static String OSGI_BASE = "share/osgi/";
+
+ public final static String JCR_SYSTEM = "jcr:system";
+ public final static String JCR_VERSION_STORAGE_PATH = "/jcr:system/jcr:versionStorage";
+
+ private final Repository repository;
+ private String defaultWorkspace;
+ private final BundleContext bundleContext;
+
+ private final ZipOutputStream zout;
+ private final Path basePath;
+
+ private ExecutorService executorService;
+
+ private boolean performSoftwareBackup = false;
+
+ private Map<String, String> checksums = new TreeMap<>();
+
+ private int threadCount = 5;
+
+ private boolean backupFailed = false;
+
+ public LogicalBackup(BundleContext bundleContext, Repository repository, Path basePath) {
+ this.repository = repository;
+ this.zout = null;
+ this.basePath = basePath;
+ this.bundleContext = bundleContext;
+ }
+
+ @Override
+ public void run() {
+ try {
+ log.info("Start logical backup to " + basePath);
+ perform();
+ } catch (Exception e) {
+ log.error("Unexpected exception when performing logical backup", e);
+ throw new IllegalStateException("Logical backup failed", e);
+ }
+
+ }
+
+ public void perform() throws RepositoryException, IOException {
+ if (executorService != null && !executorService.isTerminated())
+ throw new IllegalStateException("Another backup is running");
+ executorService = Executors.newFixedThreadPool(threadCount);
+ long begin = System.currentTimeMillis();
+ // software backup
+ if (bundleContext != null && performSoftwareBackup)
+ executorService.submit(() -> performSoftwareBackup(bundleContext));
+
+ // data backup
+ Session defaultSession = login(null);
+ defaultWorkspace = defaultSession.getWorkspace().getName();
+ try {
+ String[] workspaceNames = defaultSession.getWorkspace().getAccessibleWorkspaceNames();
+ workspaces: for (String workspaceName : workspaceNames) {
+ if ("security".equals(workspaceName))
+ continue workspaces;
+ performDataBackup(workspaceName);
+ }
+ } finally {
+ JcrUtils.logoutQuietly(defaultSession);
+ executorService.shutdown();
+ try {
+ executorService.awaitTermination(24, TimeUnit.HOURS);
+ } catch (InterruptedException e) {
+ // silent
+ throw new IllegalStateException("Backup was interrupted before completion", e);
+ }
+ }
+ // versions
+ executorService = Executors.newFixedThreadPool(threadCount);
+ try {
+ performVersionsBackup();
+ } finally {
+ executorService.shutdown();
+ try {
+ executorService.awaitTermination(24, TimeUnit.HOURS);
+ } catch (InterruptedException e) {
+ // silent
+ throw new IllegalStateException("Backup was interrupted before completion", e);
+ }
+ }
+ long duration = System.currentTimeMillis() - begin;
+ if (isBackupFailed())
+ log.info("System logical backup failed after " + (duration / 60000) + "min " + (duration / 1000) + "s");
+ else
+ log.info("System logical backup completed in " + (duration / 60000) + "min " + (duration / 1000) + "s");
+ }
+
+ protected void performDataBackup(String workspaceName) throws RepositoryException, IOException {
+ Session session = login(workspaceName);
+ try {
+ nodes: for (NodeIterator nit = session.getRootNode().getNodes(); nit.hasNext();) {
+ if (isBackupFailed())
+ return;
+ Node nodeToExport = nit.nextNode();
+ if (JCR_SYSTEM.equals(nodeToExport.getName()))
+ continue nodes;
+ String nodePath = nodeToExport.getPath();
+ Future<Set<String>> contentPathsFuture = executorService
+ .submit(() -> performNodeBackup(workspaceName, nodePath));
+ executorService.submit(() -> performFilesBackup(workspaceName, contentPathsFuture));
+ }
+ } finally {
+ Jcr.logout(session);
+ }
+ }
+
+ protected void performVersionsBackup() throws RepositoryException, IOException {
+ Session session = login(defaultWorkspace);
+ Node versionStorageNode = session.getNode(JCR_VERSION_STORAGE_PATH);
+ try {
+ for (NodeIterator nit = versionStorageNode.getNodes(); nit.hasNext();) {
+ Node nodeToExport = nit.nextNode();
+ String nodePath = nodeToExport.getPath();
+ if (isBackupFailed())
+ return;
+ Future<Set<String>> contentPathsFuture = executorService
+ .submit(() -> performNodeBackup(defaultWorkspace, nodePath));
+ executorService.submit(() -> performFilesBackup(defaultWorkspace, contentPathsFuture));
+ }
+ } finally {
+ Jcr.logout(session);
+ }
+
+ }
+
+ protected Set<String> performNodeBackup(String workspaceName, String nodePath) {
+ Session session = login(workspaceName);
+ try {
+ Node nodeToExport = session.getNode(nodePath);
+// String nodeName = nodeToExport.getName();
+// if (nodeName.startsWith("jcr:") || nodeName.startsWith("rep:"))
+// continue nodes;
+// // TODO make it more robust / configurable
+// if (nodeName.equals("user"))
+// continue nodes;
+ String relativePath = WORKSPACES_BASE + workspaceName + nodePath + ".xml";
+ OutputStream xmlOut = openOutputStream(relativePath);
+ BackupContentHandler contentHandler;
+ try (Writer writer = new BufferedWriter(new OutputStreamWriter(xmlOut, StandardCharsets.UTF_8))) {
+ contentHandler = new BackupContentHandler(writer, nodeToExport);
+ session.exportSystemView(nodeToExport.getPath(), contentHandler, true, false);
+ if (log.isDebugEnabled())
+ log.debug(workspaceName + ":" + nodePath + " metadata exported to " + relativePath);
+ }
+
+ // Files
+ Set<String> contentPaths = contentHandler.getContentPaths();
+ return contentPaths;
+ } catch (Exception e) {
+ markBackupFailed("Cannot backup node " + workspaceName + ":" + nodePath, e);
+ throw new ThreadDeath();
+ } finally {
+ Jcr.logout(session);
+ }
+ }
+
+ protected void performFilesBackup(String workspaceName, Future<Set<String>> contentPathsFuture) {
+ Set<String> contentPaths;
+ try {
+ contentPaths = contentPathsFuture.get(24, TimeUnit.HOURS);
+ } catch (InterruptedException | ExecutionException | TimeoutException e1) {
+ markBackupFailed("Cannot retrieve content paths for workspace " + workspaceName, e1);
+ return;
+ }
+ if (contentPaths == null || contentPaths.size() == 0)
+ return;
+ Session session = login(workspaceName);
+ try {
+ String workspacesFilesBasePath = FILES_BASE + workspaceName;
+ for (String path : contentPaths) {
+ if (isBackupFailed())
+ return;
+ Node contentNode = session.getNode(path);
+ Binary binary = null;
+ try {
+ binary = contentNode.getProperty(Property.JCR_DATA).getBinary();
+ String fileRelativePath = workspacesFilesBasePath + contentNode.getParent().getPath();
+
+ // checksum
+ boolean skip = false;
+ String checksum = null;
+ if (session instanceof JackrabbitSession) {
+ JackrabbitValue value = (JackrabbitValue) contentNode.getProperty(Property.JCR_DATA).getValue();
+// ReferenceBinary referenceBinary = (ReferenceBinary) binary;
+ checksum = value.getContentIdentity();
+ }
+ if (checksum != null) {
+ if (!checksums.containsKey(checksum)) {
+ checksums.put(checksum, fileRelativePath);
+ } else {
+ skip = true;
+ String sourcePath = checksums.get(checksum);
+ if (log.isTraceEnabled())
+ log.trace(fileRelativePath + " : already " + sourcePath + " with checksum " + checksum);
+ createLink(sourcePath, fileRelativePath);
+ try (Writer writerSum = new OutputStreamWriter(
+ openOutputStream(fileRelativePath + ".sha256"), StandardCharsets.UTF_8)) {
+ writerSum.write(checksum);
+ }
+ }
+ }
+
+ // copy file
+ if (!skip)
+ try (InputStream in = binary.getStream();
+ OutputStream out = openOutputStream(fileRelativePath)) {
+ IOUtils.copy(in, out);
+ if (log.isTraceEnabled())
+ log.trace("Workspace " + workspaceName + ": file content exported to "
+ + fileRelativePath);
+ }
+ } finally {
+ JcrUtils.closeQuietly(binary);
+ }
+ }
+ if (log.isDebugEnabled())
+ log.debug(workspaceName + ":" + contentPaths.size() + " files exported to " + workspacesFilesBasePath);
+ } catch (Exception e) {
+ markBackupFailed("Cannot backup files from " + workspaceName + ":", e);
+ } finally {
+ Jcr.logout(session);
+ }
+ }
+
+ protected OutputStream openOutputStream(String relativePath) throws IOException {
+ if (zout != null) {
+ ZipEntry entry = new ZipEntry(relativePath);
+ zout.putNextEntry(entry);
+ return zout;
+ } else if (basePath != null) {
+ Path targetPath = basePath.resolve(Paths.get(relativePath));
+ Files.createDirectories(targetPath.getParent());
+ return Files.newOutputStream(targetPath);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ protected void createLink(String source, String target) throws IOException {
+ if (zout != null) {
+ // TODO implement for zip
+ throw new UnsupportedOperationException();
+ } else if (basePath != null) {
+ Path sourcePath = basePath.resolve(Paths.get(source));
+ Path targetPath = basePath.resolve(Paths.get(target));
+ Path relativeSource = targetPath.getParent().relativize(sourcePath);
+ Files.createDirectories(targetPath.getParent());
+ Files.createSymbolicLink(targetPath, relativeSource);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ protected void closeOutputStream(String relativePath, OutputStream out) throws IOException {
+ if (zout != null) {
+ zout.closeEntry();
+ } else if (basePath != null) {
+ out.close();
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ protected Session login(String workspaceName) {
+ if (bundleContext != null) {// local
+ return CmsJcrUtils.openDataAdminSession(repository, workspaceName);
+ } else {// remote
+ try {
+ return repository.login(workspaceName);
+ } catch (RepositoryException e) {
+ throw new JcrException(e);
+ }
+ }
+ }
+
+ public final static void main(String[] args) throws Exception {
+ if (args.length == 0) {
+ printUsage("No argument");
+ System.exit(1);
+ }
+ URI uri = new URI(args[0]);
+ Repository repository = createRemoteRepository(uri);
+ Path basePath = args.length > 1 ? Paths.get(args[1]) : Paths.get(System.getProperty("user.dir"));
+ if (!Files.exists(basePath))
+ Files.createDirectories(basePath);
+ LogicalBackup backup = new LogicalBackup(null, repository, basePath);
+ backup.run();
+ }
+
+ private static void printUsage(String errorMessage) {
+ if (errorMessage != null)
+ System.err.println(errorMessage);
+ System.out.println("Usage: LogicalBackup <remote URL> [<target directory>]");
+
+ }
+
+ protected static Repository createRemoteRepository(URI uri) throws RepositoryException {
+ RepositoryFactory repositoryFactory = new ClientDavexRepositoryFactory();
+ Map<String, String> params = new HashMap<String, String>();
+ params.put(ClientDavexRepositoryFactory.JACKRABBIT_DAVEX_URI, uri.toString());
+ // TODO make it configurable
+ params.put(ClientDavexRepositoryFactory.JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, CmsConstants.SYS_WORKSPACE);
+ return repositoryFactory.getRepository(params);
+ }
+
+ public void performSoftwareBackup(BundleContext bundleContext) {
+ String bootBasePath = OSGI_BASE + "boot";
+ Bundle[] bundles = bundleContext.getBundles();
+ for (Bundle bundle : bundles) {
+ String relativePath = bootBasePath + "/" + bundle.getSymbolicName() + ".jar";
+ Dictionary<String, String> headers = bundle.getHeaders();
+ Manifest manifest = new Manifest();
+ Enumeration<String> headerKeys = headers.keys();
+ while (headerKeys.hasMoreElements()) {
+ String headerKey = headerKeys.nextElement();
+ String headerValue = headers.get(headerKey);
+ manifest.getMainAttributes().putValue(headerKey, headerValue);
+ }
+ try (JarOutputStream jarOut = new JarOutputStream(openOutputStream(relativePath), manifest)) {
+ Enumeration<URL> resourcePaths = bundle.findEntries("/", "*", true);
+ resources: while (resourcePaths.hasMoreElements()) {
+ URL entryUrl = resourcePaths.nextElement();
+ String entryPath = entryUrl.getPath();
+ if (entryPath.equals(""))
+ continue resources;
+ if (entryPath.endsWith("/"))
+ continue resources;
+ String entryName = entryPath.substring(1);// remove first '/'
+ if (entryUrl.getPath().equals("/META-INF/"))
+ continue resources;
+ if (entryUrl.getPath().equals("/META-INF/MANIFEST.MF"))
+ continue resources;
+ // dev
+ if (entryUrl.getPath().startsWith("/target"))
+ continue resources;
+ if (entryUrl.getPath().startsWith("/src"))
+ continue resources;
+ if (entryUrl.getPath().startsWith("/ext"))
+ continue resources;
+
+ if (entryName.startsWith("bin/")) {// dev
+ entryName = entryName.substring("bin/".length());
+ }
+
+ ZipEntry entry = new ZipEntry(entryName);
+ try (InputStream in = entryUrl.openStream()) {
+ try {
+ jarOut.putNextEntry(entry);
+ } catch (ZipException e) {// duplicate
+ continue resources;
+ }
+ IOUtils.copy(in, jarOut);
+ jarOut.closeEntry();
+// log.info(entryUrl);
+ } catch (FileNotFoundException e) {
+ log.warn(entryUrl + ": " + e.getMessage());
+ }
+ }
+ } catch (IOException e1) {
+ throw new RuntimeException("Cannot export bundle " + bundle, e1);
+ }
+ }
+ if (log.isDebugEnabled())
+ log.debug(bundles.length + " OSGi bundles exported to " + bootBasePath);
+
+ }
+
+ protected synchronized void markBackupFailed(Object message, Exception e) {
+ log.error(message, e);
+ backupFailed = true;
+ notifyAll();
+ if (executorService != null)
+ executorService.shutdownNow();
+ }
+
+ protected boolean isBackupFailed() {
+ return backupFailed;
+ }
+}
--- /dev/null
+package org.argeo.maintenance.backup;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import javax.jcr.ImportUUIDBehavior;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.jcr.CmsJcrUtils;
+import org.argeo.jcr.Jcr;
+import org.argeo.jcr.JcrException;
+import org.argeo.jcr.JcrUtils;
+import org.osgi.framework.BundleContext;
+
+/** Restores a backup in the format defined by {@link LogicalBackup}. */
+public class LogicalRestore implements Runnable {
+ private final static CmsLog log = CmsLog.getLog(LogicalRestore.class);
+
+ private final Repository repository;
+ private final BundleContext bundleContext;
+ private final Path basePath;
+
+ public LogicalRestore(BundleContext bundleContext, Repository repository, Path basePath) {
+ this.repository = repository;
+ this.basePath = basePath;
+ this.bundleContext = bundleContext;
+ }
+
+ @Override
+ public void run() {
+ Path workspaces = basePath.resolve(LogicalBackup.WORKSPACES_BASE);
+ try {
+ // import jcr:system first
+// Session defaultSession = NodeUtils.openDataAdminSession(repository, null);
+// try (DirectoryStream<Path> xmls = Files.newDirectoryStream(
+// workspaces.resolve(NodeConstants.SYS_WORKSPACE + LogicalBackup.JCR_VERSION_STORAGE_PATH),
+// "*.xml")) {
+// for (Path xml : xmls) {
+// try (InputStream in = Files.newInputStream(xml)) {
+// defaultSession.getWorkspace().importXML(LogicalBackup.JCR_VERSION_STORAGE_PATH, in,
+// ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING);
+// if (log.isDebugEnabled())
+// log.debug("Restored " + xml + " to " + defaultSession.getWorkspace().getName() + ":");
+// }
+// }
+// } finally {
+// Jcr.logout(defaultSession);
+// }
+
+ // non-system content
+ try (DirectoryStream<Path> workspaceDirs = Files.newDirectoryStream(workspaces)) {
+ for (Path workspacePath : workspaceDirs) {
+ String workspaceName = workspacePath.getFileName().toString();
+ Session session = JcrUtils.loginOrCreateWorkspace(repository, workspaceName);
+ try (DirectoryStream<Path> xmls = Files.newDirectoryStream(workspacePath, "*.xml")) {
+ xmls: for (Path xml : xmls) {
+ if (xml.getFileName().toString().startsWith("rep:"))
+ continue xmls;
+ try (InputStream in = Files.newInputStream(xml)) {
+ session.getWorkspace().importXML("/", in,
+ ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING);
+ if (log.isDebugEnabled())
+ log.debug("Restored " + xml + " to workspace " + workspaceName);
+ }
+ }
+ } finally {
+ Jcr.logout(session);
+ }
+ }
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot restore backup from " + basePath, e);
+ } catch (RepositoryException e) {
+ throw new JcrException("Cannot restore backup from " + basePath, e);
+ }
+ }
+
+}
--- /dev/null
+/** Argeo Node backup utilities. */
+package org.argeo.maintenance.backup;
\ No newline at end of file
--- /dev/null
+package org.argeo.maintenance.internal;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import javax.jcr.Repository;
+
+import org.argeo.maintenance.backup.LogicalBackup;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+
+public class Activator implements BundleActivator {
+
+ @Override
+ public void start(BundleContext context) throws Exception {
+ // Start backup
+ Repository repository = context.getService(context.getServiceReference(Repository.class));
+ Path basePath = Paths.get(System.getProperty("user.dir"), "backup");
+ LogicalBackup backup = new LogicalBackup(context, repository, basePath);
+ backup.run();
+ }
+
+ @Override
+ public void stop(BundleContext context) throws Exception {
+ }
+
+}
--- /dev/null
+/** Utilities for the maintenance of an Argeo Node. */
+package org.argeo.maintenance;
\ No newline at end of file
--- /dev/null
+package org.argeo.security.jackrabbit;
+
+import java.security.Principal;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.jackrabbit.core.security.authorization.acl.ACLProvider;
+
+/** Argeo specific access control provider */
+public class ArgeoAccessControlProvider extends ACLProvider {
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ @Override
+ public void init(Session systemSession, Map configuration) throws RepositoryException {
+ if (!configuration.containsKey(PARAM_ALLOW_UNKNOWN_PRINCIPALS))
+ configuration.put(PARAM_ALLOW_UNKNOWN_PRINCIPALS, "true");
+ if (!configuration.containsKey(PARAM_OMIT_DEFAULT_PERMISSIONS))
+ configuration.put(PARAM_OMIT_DEFAULT_PERMISSIONS, "true");
+ super.init(systemSession, configuration);
+ }
+
+ @Override
+ public boolean canAccessRoot(Set<Principal> principals) throws RepositoryException {
+ return super.canAccessRoot(principals);
+ }
+
+}
--- /dev/null
+package org.argeo.security.jackrabbit;
+
+import javax.jcr.PathNotFoundException;
+import javax.jcr.RepositoryException;
+import javax.jcr.security.Privilege;
+
+import org.apache.jackrabbit.core.id.ItemId;
+import org.apache.jackrabbit.core.security.DefaultAccessManager;
+import org.apache.jackrabbit.spi.Path;
+
+/**
+ * Intermediary class in order to have a consistent naming in config files. Does
+ * nothing for the time being, but may in the future.
+ */
+public class ArgeoAccessManager extends DefaultAccessManager {
+
+ @Override
+ public boolean canRead(Path itemPath, ItemId itemId)
+ throws RepositoryException {
+ return super.canRead(itemPath, itemId);
+ }
+
+ @Override
+ public Privilege[] getPrivileges(String absPath)
+ throws PathNotFoundException, RepositoryException {
+ return super.getPrivileges(absPath);
+ }
+
+ @Override
+ public boolean hasPrivileges(String absPath, Privilege[] privileges)
+ throws PathNotFoundException, RepositoryException {
+ return super.hasPrivileges(absPath, privileges);
+ }
+
+}
--- /dev/null
+package org.argeo.security.jackrabbit;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.apache.jackrabbit.core.security.authentication.AuthContext;
+
+/** Wraps a regular {@link LoginContext}, using the proper class loader. */
+class ArgeoAuthContext implements AuthContext {
+ private LoginContext lc;
+
+ public ArgeoAuthContext(String appName, Subject subject, CallbackHandler callbackHandler) {
+ try {
+ lc = new LoginContext(appName, subject, callbackHandler);
+ } catch (LoginException e) {
+ throw new IllegalStateException("Cannot configure Jackrabbit login context", e);
+ }
+ }
+
+ @Override
+ public void login() throws LoginException {
+ lc.login();
+ }
+
+ @Override
+ public Subject getSubject() {
+ return lc.getSubject();
+ }
+
+ @Override
+ public void logout() throws LoginException {
+ lc.logout();
+ }
+
+}
--- /dev/null
+package org.argeo.security.jackrabbit;
+
+import java.security.Principal;
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.Set;
+
+import javax.jcr.Credentials;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.x500.X500Principal;
+
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.jackrabbit.core.DefaultSecurityManager;
+import org.apache.jackrabbit.core.security.AMContext;
+import org.apache.jackrabbit.core.security.AccessManager;
+import org.apache.jackrabbit.core.security.SecurityConstants;
+import org.apache.jackrabbit.core.security.SystemPrincipal;
+import org.apache.jackrabbit.core.security.authentication.AuthContext;
+import org.apache.jackrabbit.core.security.authentication.CallbackHandlerImpl;
+import org.apache.jackrabbit.core.security.authorization.WorkspaceAccessManager;
+import org.apache.jackrabbit.core.security.principal.AdminPrincipal;
+import org.apache.jackrabbit.core.security.principal.PrincipalProvider;
+import org.argeo.api.cms.AnonymousPrincipal;
+import org.argeo.api.cms.CmsConstants;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.api.cms.DataAdminPrincipal;
+
+/** Customises Jackrabbit security. */
+public class ArgeoSecurityManager extends DefaultSecurityManager {
+ private final static CmsLog log = CmsLog.getLog(ArgeoSecurityManager.class);
+
+// private BundleContext cmsBundleContext = null;
+
+ public ArgeoSecurityManager() {
+// if (FrameworkUtil.getBundle(CmsSession.class) != null) {
+// cmsBundleContext = FrameworkUtil.getBundle(CmsSession.class).getBundleContext();
+// }
+ }
+
+ public AuthContext getAuthContext(Credentials creds, Subject subject, String workspaceName)
+ throws RepositoryException {
+ checkInitialized();
+
+ CallbackHandler cbHandler = new CallbackHandlerImpl(creds, getSystemSession(), getPrincipalProviderRegistry(),
+ adminId, anonymousId);
+ String appName = "Jackrabbit";
+ return new ArgeoAuthContext(appName, subject, cbHandler);
+ }
+
+ @Override
+ public AccessManager getAccessManager(Session session, AMContext amContext) throws RepositoryException {
+ synchronized (getSystemSession()) {
+ return super.getAccessManager(session, amContext);
+ }
+ }
+
+ @Override
+ public UserManager getUserManager(Session session) throws RepositoryException {
+ synchronized (getSystemSession()) {
+ return super.getUserManager(session);
+ }
+ }
+
+ @Override
+ protected PrincipalProvider createDefaultPrincipalProvider(Properties[] moduleConfig) throws RepositoryException {
+ return super.createDefaultPrincipalProvider(moduleConfig);
+ }
+
+ /** Called once when the session is created */
+ @Override
+ public String getUserID(Subject subject, String workspaceName) throws RepositoryException {
+ boolean isAnonymous = !subject.getPrincipals(AnonymousPrincipal.class).isEmpty();
+ boolean isDataAdmin = !subject.getPrincipals(DataAdminPrincipal.class).isEmpty();
+ boolean isJackrabbitSystem = !subject.getPrincipals(SystemPrincipal.class).isEmpty();
+ Set<X500Principal> userPrincipal = subject.getPrincipals(X500Principal.class);
+ boolean isRegularUser = !userPrincipal.isEmpty();
+// CmsSession cmsSession = null;
+// if (cmsBundleContext != null) {
+// cmsSession = CmsOsgiUtils.getCmsSession(cmsBundleContext, subject);
+// if (log.isTraceEnabled())
+// log.trace("Opening JCR session for CMS session " + cmsSession);
+// }
+
+ if (isAnonymous) {
+ if (isDataAdmin || isJackrabbitSystem || isRegularUser)
+ throw new IllegalStateException("Inconsistent " + subject);
+ else
+ return CmsConstants.ROLE_ANONYMOUS;
+ } else if (isRegularUser) {// must be before DataAdmin
+ if (isAnonymous || isJackrabbitSystem)
+ throw new IllegalStateException("Inconsistent " + subject);
+ else {
+ if (userPrincipal.size() > 1) {
+ StringBuilder buf = new StringBuilder();
+ for (X500Principal principal : userPrincipal)
+ buf.append(' ').append('\"').append(principal).append('\"');
+ throw new RuntimeException("Multiple user principals:" + buf);
+ }
+ return userPrincipal.iterator().next().getName();
+ }
+ } else if (isDataAdmin) {
+ if (isAnonymous || isJackrabbitSystem || isRegularUser)
+ throw new IllegalStateException("Inconsistent " + subject);
+ else {
+ assert !subject.getPrincipals(AdminPrincipal.class).isEmpty();
+ return CmsConstants.ROLE_DATA_ADMIN;
+ }
+ } else if (isJackrabbitSystem) {
+ if (isAnonymous || isDataAdmin || isRegularUser)
+ throw new IllegalStateException("Inconsistent " + subject);
+ else
+ return super.getUserID(subject, workspaceName);
+ } else {
+ throw new IllegalStateException("Unrecognized subject type: " + subject);
+ }
+ }
+
+ @Override
+ protected WorkspaceAccessManager createDefaultWorkspaceAccessManager() {
+ WorkspaceAccessManager wam = super.createDefaultWorkspaceAccessManager();
+ ArgeoWorkspaceAccessManagerImpl workspaceAccessManager = new ArgeoWorkspaceAccessManagerImpl(wam);
+ if (log.isTraceEnabled())
+ log.trace("Created workspace access manager");
+ return workspaceAccessManager;
+ }
+
+ private class ArgeoWorkspaceAccessManagerImpl implements SecurityConstants, WorkspaceAccessManager {
+ private final WorkspaceAccessManager wam;
+
+ public ArgeoWorkspaceAccessManagerImpl(WorkspaceAccessManager wam) {
+ super();
+ this.wam = wam;
+ }
+
+ public void init(Session systemSession) throws RepositoryException {
+ wam.init(systemSession);
+ Repository repository = systemSession.getRepository();
+ if (log.isTraceEnabled())
+ log.trace("Initialised workspace access manager on repository " + repository
+ + ", systemSession workspace: " + systemSession.getWorkspace().getName());
+ }
+
+ public void close() throws RepositoryException {
+ }
+
+ public boolean grants(Set<Principal> principals, String workspaceName) throws RepositoryException {
+ // TODO: implements finer access to workspaces
+ if (log.isTraceEnabled())
+ log.trace("Grants " + new HashSet<>(principals) + " access to workspace '" + workspaceName + "'");
+ return true;
+ // return wam.grants(principals, workspaceName);
+ }
+ }
+
+}
--- /dev/null
+package org.argeo.security.jackrabbit;
+
+import java.util.Map;
+import java.util.Set;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+import javax.security.auth.x500.X500Principal;
+
+import org.apache.jackrabbit.core.security.AnonymousPrincipal;
+import org.apache.jackrabbit.core.security.SecurityConstants;
+import org.apache.jackrabbit.core.security.principal.AdminPrincipal;
+import org.argeo.api.cms.DataAdminPrincipal;
+
+/** JAAS login module used when initiating a new Jackrabbit session. */
+public class SystemJackrabbitLoginModule implements LoginModule {
+ private Subject subject;
+
+ @Override
+ public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
+ Map<String, ?> options) {
+ this.subject = subject;
+ }
+
+ @Override
+ public boolean login() throws LoginException {
+ return true;
+ }
+
+ @Override
+ public boolean commit() throws LoginException {
+ Set<org.argeo.api.cms.AnonymousPrincipal> anonPrincipal = subject
+ .getPrincipals(org.argeo.api.cms.AnonymousPrincipal.class);
+ if (!anonPrincipal.isEmpty()) {
+ subject.getPrincipals().add(new AnonymousPrincipal());
+ return true;
+ }
+
+ Set<DataAdminPrincipal> initPrincipal = subject.getPrincipals(DataAdminPrincipal.class);
+ if (!initPrincipal.isEmpty()) {
+ subject.getPrincipals().add(new AdminPrincipal(SecurityConstants.ADMIN_ID));
+ return true;
+ }
+
+ Set<X500Principal> userPrincipal = subject.getPrincipals(X500Principal.class);
+ if (userPrincipal.isEmpty())
+ throw new LoginException("Subject must be pre-authenticated");
+ if (userPrincipal.size() > 1)
+ throw new LoginException("Multiple user principals " + userPrincipal);
+
+ return true;
+ }
+
+ @Override
+ public boolean abort() throws LoginException {
+ return true;
+ }
+
+ @Override
+ public boolean logout() throws LoginException {
+ subject.getPrincipals().removeAll(subject.getPrincipals(AnonymousPrincipal.class));
+ subject.getPrincipals().removeAll(subject.getPrincipals(AdminPrincipal.class));
+ return true;
+ }
+}
--- /dev/null
+/** Integration of Jackrabbit with Argeo security model. */
+package org.argeo.security.jackrabbit;
\ No newline at end of file
--- /dev/null
+Subproject commit 872f63170bb639abab6e64d6e3d6a13318d38e32