Jcr Explorer refactoring and packaging
authorBruno Sinou <bsinou@argeo.org>
Tue, 6 Sep 2011 10:12:29 +0000 (10:12 +0000)
committerBruno Sinou <bsinou@argeo.org>
Tue, 6 Sep 2011 10:12:29 +0000 (10:12 +0000)
git-svn-id: https://svn.argeo.org/commons/trunk@4725 4cfe0d0a-d680-48aa-b62c-e0a02a3f76cc

36 files changed:
server/plugins/org.argeo.jcr.ui.explorer/.classpath
server/plugins/org.argeo.jcr.ui.explorer/META-INF/MANIFEST.MF
server/plugins/org.argeo.jcr.ui.explorer/META-INF/spring/commands.xml
server/plugins/org.argeo.jcr.ui.explorer/META-INF/spring/editors.xml
server/plugins/org.argeo.jcr.ui.explorer/META-INF/spring/views.xml
server/plugins/org.argeo.jcr.ui.explorer/build.properties
server/plugins/org.argeo.jcr.ui.explorer/icons/repositories.gif [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/plugin.xml
server/plugins/org.argeo.jcr.ui.explorer/pom.xml
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/JcrExplorerConstants.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/JcrExplorerPerspective.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/JcrExplorerPlugin.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/JcrExplorerView.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/HomeContentProvider.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/ItemComparator.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/NodeContentProvider.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/NodeLabelProvider.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/PropertiesContentProvider.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/RepositoryNode.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/WorkspaceNode.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/commands/EditNode.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/commands/ImportFileSystem.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/commands/OpenGenericJcrQueryEditor.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/commands/OpenGenericNodeEditor.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/dialogs/ChooseNameDialog.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/editors/EmptyNodePage.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/editors/GenericJcrQueryEditor.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/editors/GenericNodeEditor.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/editors/GenericNodeEditorInput.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/editors/GenericNodePage.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/utils/GenericNodeDoubleClickListener.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/utils/JcrFileProvider.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/utils/JcrUiUtils.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/views/GenericJcrBrowser.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/wizards/ImportFileSystemWizard.java [new file with mode: 0644]
server/plugins/org.argeo.jcr.ui.explorer/src/main/resources/org/argeo/jcr/ui/explorer/messages.properties [new file with mode: 0644]

index 92f19d2ff95b83a87f5210157582c70bb070ed48..d3d5c80958677f1f8de5b580c289b3baf7c5452c 100644 (file)
@@ -3,5 +3,6 @@
        <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/J2SE-1.5"/>
        <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
        <classpathentry kind="src" path="src/main/java"/>
+       <classpathentry kind="src" path="src/main/resources"/>
        <classpathentry kind="output" path="target/classes"/>
 </classpath>
index 13fdc29a9cc67983a3e6d1037419836930ac877d..896f7fa3691fef33d6292db1a6865cc89d66e0c9 100644 (file)
@@ -3,17 +3,64 @@ Bundle-ManifestVersion: 2
 Bundle-Name: JCR Explorer
 Bundle-SymbolicName: org.argeo.jcr.ui.explorer;singleton:=true
 Bundle-Version: 0.3.4.SNAPSHOT
-Bundle-Activator: org.argeo.eclipse.ui.jcr.explorer.JcrExplorerPlugin
+Bundle-Activator: org.argeo.jcr.ui.explorer.JcrExplorerPlugin
 Bundle-Vendor: Argeo
 Require-Bundle: org.eclipse.ui;resolution:=optional,
- org.eclipse.core.runtime,
- org.eclipse.rap.ui;resolution:=optional
+ org.eclipse.core.runtime;resolution:=optional,
+ org.eclipse.rap.ui;resolution:=optional,
+ org.eclipse.rap.ui.workbench;resolution:=optional
 Bundle-RequiredExecutionEnvironment: J2SE-1.5
 Bundle-ActivationPolicy: lazy
-Import-Package: javax.jcr;version="2.0.0",
+Import-Package: javax.jcr,
+ javax.jcr.nodetype,
+ javax.jcr.observation,
+ org.apache.commons.io,
+ org.apache.commons.logging,
+ org.argeo,
  org.argeo.eclipse.spring,
+ org.argeo.eclipse.ui,
+ org.argeo.eclipse.ui.jcr,
  org.argeo.eclipse.ui.jcr.commands,
  org.argeo.eclipse.ui.jcr.editors,
+ org.argeo.eclipse.ui.jcr.utils,
  org.argeo.eclipse.ui.jcr.views,
- org.argeo.jcr
-Export-Package: org.argeo.eclipse.ui.jcr.explorer
+ org.argeo.eclipse.ui.specific,
+ org.argeo.eclipse.ui.utils,
+ org.argeo.jcr,
+ org.argeo.jcr.spring,
+ org.argeo.util,
+ org.eclipse.ui.forms,
+ org.eclipse.ui.forms.editor,
+ org.eclipse.ui.forms.events,
+ org.eclipse.ui.forms.widgets,
+ org.springframework.context
+Export-Package: org.argeo.jcr.ui.explorer,
+ org.argeo.jcr.ui.explorer.browser;
+  uses:="javax.jcr,
+   org.argeo.eclipse.ui.jcr,
+   org.argeo.jcr,
+   org.argeo,
+   org.eclipse.jface.viewers,
+   org.eclipse.jface.resource,
+   org.eclipse.swt.graphics,
+   javax.jcr.nodetype,
+   org.argeo.eclipse.ui,
+   javax.jcr.observation",
+ org.argeo.jcr.ui.explorer.dialogs;
+  uses:="org.eclipse.swt.graphics,
+   org.eclipse.swt.widgets,
+   org.eclipse.swt.layout,
+   org.eclipse.jface.dialogs",
+ org.argeo.jcr.ui.explorer.wizards;
+  uses:="javax.jcr,
+   org.argeo,
+   org.eclipse.jface.operation,
+   org.eclipse.core.runtime,
+   org.eclipse.jface.wizard,
+   org.apache.commons.logging,
+   org.argeo.eclipse.ui.dialogs,
+   org.argeo.eclipse.ui.specific,
+   org.apache.commons.io,
+   org.eclipse.swt.widgets,
+   org.eclipse.jface.dialogs"
+
index 0a9a4bb611a2c52ebae5344fb991c3d9abe387c0..16f4adfa6ace06a5b6228f887c236bb0b1ff7cb4 100644 (file)
@@ -5,12 +5,17 @@
         http://www.springframework.org/schema/beans/spring-beans.xsd">
 
        <bean id="openGenericJcrQueryEditor"
-               class="org.argeo.eclipse.ui.jcr.commands.OpenGenericJcrQueryEditor"
+               class="org.argeo.jcr.ui.explorer.commands.OpenGenericJcrQueryEditor"
                scope="prototype">
                <property name="editorId"
                        value="org.argeo.slc.client.ui.dist.genericJcrQueryEditor" />
        </bean>
 
+       <bean id="openGenericNodeEditor"
+               class="org.argeo.jcr.ui.explorer.commands.OpenGenericNodeEditor"
+               scope="prototype">
+       </bean>
+
        <bean id="addFileFolder" class="org.argeo.eclipse.ui.jcr.commands.AddFileFolder"
                scope="prototype" />
 
@@ -20,6 +25,6 @@
        <bean id="deleteNode" class="org.argeo.eclipse.ui.jcr.commands.DeleteNode"
                scope="prototype" />
 
-       <bean id="importFileSystem" class="org.argeo.eclipse.ui.jcr.commands.ImportFileSystem"
+       <bean id="importFileSystem" class="org.argeo.jcr.ui.explorer.commands.ImportFileSystem"
                scope="prototype" />
 </beans>
index 6e88ffb3fc4397262c5c5a9c952bae7d6c8f38a0..0dc6c91a4864db9e3c8caadf6312299af4e924d5 100644 (file)
@@ -5,7 +5,10 @@
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd">
 
-       <bean id="genericJcrQueryEditor"
-               class="org.argeo.eclipse.ui.jcr.editors.GenericJcrQueryEditor" scope="prototype">
+       <bean id="genericJcrQueryEditor" class="org.argeo.jcr.ui.explorer.editors.GenericJcrQueryEditor"
+               scope="prototype">
+       </bean>
+       <bean id="genericNodeEditor" class="org.argeo.jcr.ui.explorer.editors.GenericNodeEditor"
+               scope="prototype">
        </bean>
 </beans>
index eed1f4844ceeed7d275757c8d70da564fd2b6491..975dc5404716321ecfe6cce6e7abc94297512d8d 100644 (file)
@@ -5,7 +5,7 @@
         http://www.springframework.org/schema/beans/spring-beans.xsd">
 
        <!-- Views -->
-       <bean id="browserView" class="org.argeo.eclipse.ui.jcr.views.GenericJcrBrowser"
+       <bean id="browserView" class="org.argeo.jcr.ui.explorer.views.GenericJcrBrowser"
                scope="prototype">
                <property name="repositoryRegister" ref="repositoryRegister" />
        </bean>
index 7b6db787bdf89f9f733acba7a946bf64854580c7..1f316c6fa1769e68276d245bdf9d93be02bd54da 100644 (file)
@@ -1,4 +1,5 @@
-source.. = src/main/java/
+source.. =     src/main/java/,\
+                       src/main/resources
 output.. = target/classes/
 bin.includes = META-INF/,\
                .,\
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/icons/repositories.gif b/server/plugins/org.argeo.jcr.ui.explorer/icons/repositories.gif
new file mode 100644 (file)
index 0000000..c13bea1
Binary files /dev/null and b/server/plugins/org.argeo.jcr.ui.explorer/icons/repositories.gif differ
index ebec87a123401ea8156368759ea76928b56831a2..933aad083ebf7951a75bcdaf3626a7fa4bed1adc 100644 (file)
@@ -1,15 +1,17 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?eclipse version="3.4"?>
 <plugin>
+       <!-- Perspectives -->
    <extension
          point="org.eclipse.ui.perspectives">
       <perspective
-            class="org.argeo.eclipse.ui.jcr.explorer.JcrExplorerPerspective"
+            class="org.argeo.jcr.ui.explorer.JcrExplorerPerspective"
             icon="icons/nodes.gif"
             id="org.argeo.jcr.ui.explorer.perspective"
             name="JCR Explorer">
       </perspective>
    </extension>
+   <!-- Views --> 
    <extension
          point="org.eclipse.ui.views">
           <view
@@ -19,6 +21,7 @@
           name="JCR Browser">
           </view>
    </extension>
+   <!-- Editors --> 
    <extension
            point="org.eclipse.ui.editors">
             <editor
               name="JCR Query"
               icon="icons/query.png"
               default="false">
+        </editor>
+            <editor
+                 class="org.argeo.eclipse.spring.SpringExtensionFactory"
+              id="org.argeo.jcr.ui.explorer.genericNodeEditor"
+              name="Node Editor"
+              icon="icons/query.png"
+              default="false">
         </editor>
      </extension>
+       <!-- Commands --> 
        <extension
          point="org.eclipse.ui.commands">
                <command
             id="org.argeo.jcr.ui.explorer.openGenericJcrQueryEditor"
             name="New generic JCR query">
        </command>
+       <command
+                       defaultHandler="org.argeo.eclipse.spring.SpringCommandHandler"
+                       id="org.argeo.jcr.ui.explorer.openGenericNodeEditor"
+                       name="Open generic node Editor">
+                       <commandParameter
+                               id="org.argeo.jcr.ui.explorer.nodePath"
+                               name="Node path">
+                       </commandParameter>
+               </command>    
          <command
                defaultHandler="org.argeo.eclipse.spring.SpringCommandHandler"
                id="org.argeo.jcr.ui.explorer.addFileFolder"
index 2ef9fcf6e5077e4f031086261fda0780c19f4720..0500514c3247d5dd7a4caa1bd6748b5187dcd86d 100644 (file)
@@ -1,4 +1,5 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
                <groupId>org.argeo.commons.server</groupId>
                        <version>0.3.4-SNAPSHOT</version>
                        <scope>provided</scope>
                </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons.eclipse</groupId>
+                       <artifactId>org.argeo.eclipse.ui.rcp</artifactId>
+                       <version>0.3.4-SNAPSHOT</version>
+                       <scope>provided</scope>
+               </dependency>
        </dependencies>
 </project>
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/JcrExplorerConstants.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/JcrExplorerConstants.java
new file mode 100644 (file)
index 0000000..a410fca
--- /dev/null
@@ -0,0 +1,11 @@
+package org.argeo.jcr.ui.explorer;
+
+/** Constants used across the application. */
+public interface JcrExplorerConstants {
+
+       /*
+        * MISCEALLENEOUS
+        */
+       public final static String DATE_TIME_FORMAT = "dd/MM/yyyy, HH:mm";
+
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/JcrExplorerPerspective.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/JcrExplorerPerspective.java
new file mode 100644 (file)
index 0000000..487784d
--- /dev/null
@@ -0,0 +1,19 @@
+package org.argeo.jcr.ui.explorer;
+
+import org.eclipse.ui.IFolderLayout;
+import org.eclipse.ui.IPageLayout;
+import org.eclipse.ui.IPerspectiveFactory;
+
+public class JcrExplorerPerspective implements IPerspectiveFactory {
+       public static String BROWSER_VIEW_ID = JcrExplorerPlugin.ID
+                       + ".browserView";
+
+       public void createInitialLayout(IPageLayout layout) {
+               layout.setEditorAreaVisible(true);
+
+               IFolderLayout upperLeft = layout.createFolder(JcrExplorerPlugin.ID
+                               + ".upperLeft", IPageLayout.LEFT, 0.4f, layout.getEditorArea());
+               upperLeft.addView(BROWSER_VIEW_ID);
+       }
+
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/JcrExplorerPlugin.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/JcrExplorerPlugin.java
new file mode 100644 (file)
index 0000000..c8aaab1
--- /dev/null
@@ -0,0 +1,92 @@
+package org.argeo.jcr.ui.explorer;
+
+import java.util.ResourceBundle;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.ui.plugin.AbstractUIPlugin;
+import org.osgi.framework.BundleContext;
+
+/**
+ * The activator class controls the plug-in life cycle
+ */
+public class JcrExplorerPlugin extends AbstractUIPlugin {
+       private final static Log log = LogFactory.getLog(JcrExplorerPlugin.class);
+       private ResourceBundle messages;
+
+       // The plug-in ID
+       public static final String ID = "org.argeo.jcr.ui.explorer"; //$NON-NLS-1$
+
+       // The shared instance
+       private static JcrExplorerPlugin plugin;
+
+       /**
+        * The constructor
+        */
+       public JcrExplorerPlugin() {
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see
+        * org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext
+        * )
+        */
+       public void start(BundleContext context) throws Exception {
+               super.start(context);
+               plugin = this;
+               messages = ResourceBundle
+                               .getBundle("org.argeo.jcr.ui.explorer.messages");
+
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see
+        * org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext
+        * )
+        */
+       public void stop(BundleContext context) throws Exception {
+               plugin = null;
+               super.stop(context);
+       }
+
+       /**
+        * Returns the shared instance
+        * 
+        * @return the shared instance
+        */
+       public static JcrExplorerPlugin getDefault() {
+               return plugin;
+       }
+
+       public static ImageDescriptor getImageDescriptor(String path) {
+               return imageDescriptorFromPlugin(ID, path);
+       }
+
+       /** Returns the internationalized label for the given key */
+       public static String getMessage(String key) {
+               try {
+                       return getDefault().messages.getString(key);
+               } catch (NullPointerException npe) {
+                       log.warn(key + " not found.");
+                       return key;
+               }
+       }
+
+       /**
+        * Gives access to the internationalization message bundle. Returns null in
+        * case the ClientUiPlugin is not started (for JUnit tests, by instance)
+        */
+       public static ResourceBundle getMessagesBundle() {
+               if (getDefault() != null)
+                       // To avoid NPE
+                       return getDefault().messages;
+               else
+                       return null;
+       }
+
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/JcrExplorerView.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/JcrExplorerView.java
new file mode 100644 (file)
index 0000000..ac9c895
--- /dev/null
@@ -0,0 +1,7 @@
+package org.argeo.jcr.ui.explorer;
+
+import org.argeo.jcr.ui.explorer.views.GenericJcrBrowser;
+
+public class JcrExplorerView extends GenericJcrBrowser {
+
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/HomeContentProvider.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/HomeContentProvider.java
new file mode 100644 (file)
index 0000000..123fdaa
--- /dev/null
@@ -0,0 +1,14 @@
+package org.argeo.jcr.ui.explorer.browser;
+
+import javax.jcr.Session;
+
+import org.argeo.eclipse.ui.jcr.SimpleNodeContentProvider;
+import org.argeo.jcr.JcrUtils;
+
+public class HomeContentProvider extends SimpleNodeContentProvider {
+
+       public HomeContentProvider(Session session) {
+               super(session, new String[] { JcrUtils.getUserHomePath(session) });
+       }
+
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/ItemComparator.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/ItemComparator.java
new file mode 100644 (file)
index 0000000..156aa7b
--- /dev/null
@@ -0,0 +1,20 @@
+package org.argeo.jcr.ui.explorer.browser;
+
+import java.util.Comparator;
+
+import javax.jcr.Item;
+import javax.jcr.RepositoryException;
+
+import org.argeo.ArgeoException;
+
+public class ItemComparator implements Comparator<Item> {
+       public int compare(Item o1, Item o2) {
+               try {
+                       // TODO: put folder before files
+                       return o1.getName().compareTo(o2.getName());
+               } catch (RepositoryException e) {
+                       throw new ArgeoException("Cannot compare " + o1 + " and " + o2, e);
+               }
+       }
+
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/NodeContentProvider.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/NodeContentProvider.java
new file mode 100644 (file)
index 0000000..b2bd4b7
--- /dev/null
@@ -0,0 +1,132 @@
+package org.argeo.jcr.ui.explorer.browser;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.ArgeoException;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.jcr.RepositoryRegister;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+public class NodeContentProvider implements ITreeContentProvider {
+       private ItemComparator itemComparator = new ItemComparator();
+
+       private RepositoryRegister repositoryRegister;
+       private Session userSession;
+
+       public NodeContentProvider(Session userSession,
+                       RepositoryRegister repositoryRegister) {
+               this.userSession = userSession;
+               this.repositoryRegister = repositoryRegister;
+       }
+
+       /**
+        * 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 (userSession != null) {
+                       Node userHome = JcrUtils.getUserHome(userSession);
+                       if (userHome != null)
+                               objs.add(userHome);
+               }
+               if (repositoryRegister != null)
+                       objs.add(repositoryRegister);
+               return objs.toArray();
+       }
+
+       public Object[] getChildren(Object parentElement) {
+               if (parentElement instanceof Node) {
+                       return childrenNodes((Node) parentElement);
+               } else if (parentElement instanceof RepositoryNode) {
+                       return ((RepositoryNode) parentElement).getChildren();
+               } else if (parentElement instanceof WorkspaceNode) {
+                       Session session = ((WorkspaceNode) parentElement).getSession();
+                       if (session == null)
+                               return new Object[0];
+
+                       try {
+                               return childrenNodes(session.getRootNode());
+                       } catch (RepositoryException e) {
+                               throw new ArgeoException("Cannot retrieve root node of "
+                                               + session, e);
+                       }
+               } else if (parentElement instanceof RepositoryRegister) {
+                       RepositoryRegister repositoryRegister = (RepositoryRegister) parentElement;
+                       List<RepositoryNode> nodes = new ArrayList<RepositoryNode>();
+                       Map<String, Repository> repositories = repositoryRegister
+                                       .getRepositories();
+                       for (String name : repositories.keySet()) {
+                               nodes.add(new RepositoryNode(name, repositories.get(name)));
+                       }
+                       return nodes.toArray();
+               } else {
+                       return new Object[0];
+               }
+       }
+
+       public Object getParent(Object element) {
+               try {
+                       if (element instanceof Node) {
+                               Node node = (Node) element;
+                               if (!node.getPath().equals("/"))
+                                       return node.getParent();
+                               else
+                                       return null;
+                       }
+                       return null;
+               } catch (RepositoryException e) {
+                       return null;
+               }
+       }
+
+       public boolean hasChildren(Object element) {
+               try {
+                       if (element instanceof Node) {
+                               return ((Node) element).hasNodes();
+                       } else if (element instanceof RepositoryNode) {
+                               return ((RepositoryNode) element).hasChildren();
+                       } else if (element instanceof WorkspaceNode) {
+                               return ((WorkspaceNode) element).getSession() != null;
+                       } else if (element instanceof RepositoryRegister) {
+                               return ((RepositoryRegister) element).getRepositories().size() > 0;
+                       }
+                       return false;
+               } catch (RepositoryException e) {
+                       throw new ArgeoException("Cannot check children of " + element, e);
+               }
+       }
+
+       public void dispose() {
+       }
+
+       public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+       }
+
+       protected Object[] childrenNodes(Node parentNode) {
+               try {
+                       List<Node> children = new ArrayList<Node>();
+                       NodeIterator nit = parentNode.getNodes();
+                       while (nit.hasNext()) {
+                               Node node = nit.nextNode();
+                               children.add(node);
+                       }
+                       Node[] arr = children.toArray(new Node[children.size()]);
+                       Arrays.sort(arr, itemComparator);
+                       return arr;
+               } catch (RepositoryException e) {
+                       throw new ArgeoException("Cannot list children of " + parentNode, e);
+               }
+       }
+
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/NodeLabelProvider.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/NodeLabelProvider.java
new file mode 100644 (file)
index 0000000..6b9af8f
--- /dev/null
@@ -0,0 +1,52 @@
+package org.argeo.jcr.ui.explorer.browser;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.eclipse.ui.jcr.DefaultNodeLabelProvider;
+import org.argeo.eclipse.ui.jcr.JcrUiPlugin;
+import org.argeo.jcr.RepositoryRegister;
+import org.eclipse.swt.graphics.Image;
+
+public class NodeLabelProvider extends DefaultNodeLabelProvider {
+       // Images
+       public final static Image REPOSITORIES = JcrUiPlugin.getImageDescriptor(
+                       "icons/repositories.gif").createImage();
+
+       public String getText(Object element) {
+               if (element instanceof RepositoryRegister) {
+                       return "Repositories";
+               }
+               return super.getText(element);
+       }
+
+       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 RepositoryNode) {
+                       if (((RepositoryNode) element).getDefaultSession() == null)
+                               return RepositoryNode.REPOSITORY_DISCONNECTED;
+                       else
+                               return RepositoryNode.REPOSITORY_CONNECTED;
+               } else if (element instanceof WorkspaceNode) {
+                       if (((WorkspaceNode) element).getSession() == null)
+                               return WorkspaceNode.WORKSPACE_DISCONNECTED;
+                       else
+                               return WorkspaceNode.WORKSPACE_CONNECTED;
+               } else if (element instanceof RepositoryRegister) {
+                       return REPOSITORIES;
+               }
+               return super.getImage(element);
+       }
+
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/PropertiesContentProvider.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/PropertiesContentProvider.java
new file mode 100644 (file)
index 0000000..04cd699
--- /dev/null
@@ -0,0 +1,40 @@
+package org.argeo.jcr.ui.explorer.browser;
+
+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.ArgeoException;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+public class PropertiesContentProvider implements IStructuredContentProvider {
+       private ItemComparator itemComparator = new ItemComparator();
+
+       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 ArgeoException("Cannot get element for " + inputElement,
+                                       e);
+               }
+       }
+
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/RepositoryNode.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/RepositoryNode.java
new file mode 100644 (file)
index 0000000..a09661e
--- /dev/null
@@ -0,0 +1,49 @@
+package org.argeo.jcr.ui.explorer.browser;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.ArgeoException;
+import org.argeo.eclipse.ui.TreeParent;
+import org.argeo.eclipse.ui.jcr.JcrUiPlugin;
+import org.eclipse.swt.graphics.Image;
+
+public class RepositoryNode extends TreeParent {
+       private final String name;
+       private final Repository repository;
+       private Session defaultSession = null;
+       public final static Image REPOSITORY_DISCONNECTED = JcrUiPlugin
+       .getImageDescriptor("icons/repository_disconnected.gif")
+       .createImage();
+       public final static Image REPOSITORY_CONNECTED = JcrUiPlugin
+       .getImageDescriptor("icons/repository_connected.gif").createImage();
+
+       public RepositoryNode(String name, Repository repository) {
+               super(name);
+               this.name = name;
+               this.repository = repository;
+       }
+
+       public void login() {
+               try {
+                       defaultSession = repository.login();
+                       String[] wkpNames = defaultSession.getWorkspace()
+                                       .getAccessibleWorkspaceNames();
+                       for (String wkpName : wkpNames) {
+                               if (wkpName.equals(defaultSession.getWorkspace().getName()))
+                                       addChild(new WorkspaceNode(repository, wkpName,
+                                                       defaultSession));
+                               else
+                                       addChild(new WorkspaceNode(repository, wkpName));
+                       }
+               } catch (RepositoryException e) {
+                       throw new ArgeoException("Cannot connect to repository " + name, e);
+               }
+       }
+
+       public Session getDefaultSession() {
+               return defaultSession;
+       }
+
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/WorkspaceNode.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/WorkspaceNode.java
new file mode 100644 (file)
index 0000000..6c8b7db
--- /dev/null
@@ -0,0 +1,107 @@
+package org.argeo.jcr.ui.explorer.browser;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.observation.EventIterator;
+import javax.jcr.observation.EventListener;
+
+import org.argeo.ArgeoException;
+import org.argeo.eclipse.ui.TreeParent;
+import org.argeo.eclipse.ui.jcr.JcrUiPlugin;
+import org.eclipse.swt.graphics.Image;
+
+public class WorkspaceNode extends TreeParent implements EventListener {
+       private final String name;
+       private final Repository repository;
+       private Session session = null;
+       public final static Image WORKSPACE_DISCONNECTED = JcrUiPlugin
+       .getImageDescriptor("icons/workspace_disconnected.png")
+       .createImage();
+       public final static Image WORKSPACE_CONNECTED = JcrUiPlugin
+       .getImageDescriptor("icons/workspace_connected.png").createImage();
+
+       public WorkspaceNode(Repository repository, String name) {
+               this(repository, name, null); 
+       }
+
+       public WorkspaceNode(Repository repository, String name, Session session) {
+               super(name);
+               this.name = name;
+               this.repository = repository;
+               this.session = session;
+               if (session != null)
+                       processNewSession(session);
+       }
+
+       public Session getSession() {
+               return session;
+       }
+
+       public void login() {
+               try {
+                       logout();
+                       session = repository.login(name);
+                       processNewSession(session);
+               } catch (RepositoryException e) {
+                       throw new ArgeoException("Cannot connect to repository " + name, e);
+               }
+       }
+
+       public void logout() {
+               try {
+                       if (session != null && session.isLive()) {
+                               session.getWorkspace().getObservationManager()
+                                               .removeEventListener(this);
+                               session.logout();
+                       }
+               } catch (RepositoryException e) {
+                       throw new ArgeoException("Cannot connect to repository " + name, e);
+               }
+       }
+
+       public void onEvent(final EventIterator events) {
+               // if (session == null)
+               // return;
+               // Display.getDefault().syncExec(new Runnable() {
+               // public void run() {
+               // while (events.hasNext()) {
+               // Event event = events.nextEvent();
+               // try {
+               // String path = event.getPath();
+               // String parentPath = path.substring(0,
+               // path.lastIndexOf('/'));
+               // final Object parent;
+               // if (parentPath.equals("/") || parentPath.equals(""))
+               // parent = this;
+               // else if (session.itemExists(parentPath)){
+               // parent = session.getItem(parentPath);
+               // ((Item)parent).refresh(false);
+               // }
+               // else
+               // parent = null;
+               // if (parent != null) {
+               // nodesViewer.refresh(parent);
+               // }
+               //
+               // } catch (RepositoryException e) {
+               // log.warn("Error processing event " + event, e);
+               // }
+               // }
+               // }
+               // });
+       }
+
+       protected void processNewSession(Session session) {
+               // try {
+               // ObservationManager observationManager = session.getWorkspace()
+               // .getObservationManager();
+               // observationManager.addEventListener(this, Event.NODE_ADDED
+               // | Event.NODE_REMOVED, "/", true, null, null, false);
+               // } catch (RepositoryException e) {
+               // throw new ArgeoException("Cannot process new session "
+               // + session, e);
+               // }
+       }
+
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/commands/EditNode.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/commands/EditNode.java
new file mode 100644 (file)
index 0000000..16bb1b3
--- /dev/null
@@ -0,0 +1,54 @@
+package org.argeo.jcr.ui.explorer.commands;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Property;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.eclipse.ui.dialogs.Error;
+import org.argeo.eclipse.ui.jcr.editors.NodeEditorInput;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+/** Generic command to open a path in an editor. */
+public class EditNode extends AbstractHandler {
+       public final static String EDITOR_PARAM = "editor";
+
+       private String defaultEditorId;
+
+       private Map<String, String> nodeTypeToEditor = new HashMap<String, String>();
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               String path = event.getParameter(Property.JCR_PATH);
+
+               String type = event.getParameter(NodeType.NT_NODE_TYPE);
+               if (type == null)
+                       type = NodeType.NT_UNSTRUCTURED;
+
+               String editorId = event.getParameter(NodeType.NT_NODE_TYPE);
+               if (editorId == null)
+                       editorId = nodeTypeToEditor.containsKey(type) ? nodeTypeToEditor
+                                       .get(type) : defaultEditorId;
+                                       
+               NodeEditorInput nei = new NodeEditorInput(path);
+
+               try {
+                       HandlerUtil.getActiveWorkbenchWindow(event).getActivePage()
+                                       .openEditor(nei, editorId);
+               } catch (PartInitException e) {
+                       Error.show("Cannot open " + editorId + " with " + path
+                                       + " of type " + type, e);
+               }
+               // TODO Auto-generated method stub
+               return null;
+       }
+
+       public void setDefaultEditorId(String defaultEditorId) {
+               this.defaultEditorId = defaultEditorId;
+       }
+
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/commands/ImportFileSystem.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/commands/ImportFileSystem.java
new file mode 100644 (file)
index 0000000..12880ba
--- /dev/null
@@ -0,0 +1,54 @@
+package org.argeo.jcr.ui.explorer.commands;
+
+import javax.jcr.Node;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.eclipse.ui.dialogs.Error;
+import org.argeo.eclipse.ui.jcr.views.AbstractJcrBrowser;
+import org.argeo.jcr.ui.explorer.wizards.ImportFileSystemWizard;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+public class ImportFileSystem extends AbstractHandler {
+       private static Log log = LogFactory.getLog(ImportFileSystem.class);
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               ISelection selection = HandlerUtil.getActiveWorkbenchWindow(event)
+                               .getActivePage().getSelection();
+               AbstractJcrBrowser view = (AbstractJcrBrowser) HandlerUtil
+                               .getActiveWorkbenchWindow(event).getActivePage()
+                               .findView(HandlerUtil.getActivePartId(event));
+               if (selection != null && !selection.isEmpty()
+                               && selection instanceof IStructuredSelection) {
+                       Object obj = ((IStructuredSelection) selection).getFirstElement();
+                       try {
+                               if (obj instanceof Node) {
+                                       Node folder = (Node) obj;
+                                       // if (!folder.getPrimaryNodeType().getName()
+                                       // .equals(NodeType.NT_FOLDER)) {
+                                       // Error.show("Can only import to a folder node");
+                                       // return null;
+                                       // }
+                                       ImportFileSystemWizard wizard = new ImportFileSystemWizard(
+                                                       folder);
+                                       WizardDialog dialog = new WizardDialog(
+                                                       HandlerUtil.getActiveShell(event), wizard);
+                                       dialog.open();
+                                       view.refresh(folder);
+                               } else {
+                                       Error.show("Can only import to a node");
+                               }
+                       } catch (Exception e) {
+                               Error.show("Cannot import files to " + obj, e);
+                       }
+               }
+               return null;
+       }
+
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/commands/OpenGenericJcrQueryEditor.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/commands/OpenGenericJcrQueryEditor.java
new file mode 100644 (file)
index 0000000..177dfd2
--- /dev/null
@@ -0,0 +1,30 @@
+package org.argeo.jcr.ui.explorer.commands;
+
+import org.argeo.eclipse.ui.jcr.editors.JcrQueryEditorInput;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+/** Open a JCR query editor. */
+public class OpenGenericJcrQueryEditor extends AbstractHandler {
+       private String editorId;
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               try {
+                       JcrQueryEditorInput editorInput = new JcrQueryEditorInput("", null);
+                       IWorkbenchPage activePage = HandlerUtil.getActiveWorkbenchWindow(
+                                       event).getActivePage();
+                       activePage.openEditor(editorInput, editorId);
+               } catch (Exception e) {
+                       throw new ExecutionException("Cannot open editor", e);
+               }
+               return null;
+       }
+
+       public void setEditorId(String editorId) {
+               this.editorId = editorId;
+       }
+
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/commands/OpenGenericNodeEditor.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/commands/OpenGenericNodeEditor.java
new file mode 100644 (file)
index 0000000..63d53fa
--- /dev/null
@@ -0,0 +1,31 @@
+package org.argeo.jcr.ui.explorer.commands;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.ArgeoException;
+import org.argeo.eclipse.ui.jcr.editors.NodeEditorInput;
+import org.argeo.jcr.ui.explorer.editors.GenericNodeEditor;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+public class OpenGenericNodeEditor extends AbstractHandler {
+       private final static Log log = LogFactory
+                       .getLog(OpenGenericNodeEditor.class);
+       public final static String ID = "org.argeo.jcr.ui.explorer.openGenericNodeEditor";
+       public final static String PARAM_PATH = "org.argeo.jcr.ui.explorer.nodePath";
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               String path = event.getParameter(PARAM_PATH);
+               try {
+                       NodeEditorInput nei = new NodeEditorInput(path);
+                       HandlerUtil.getActiveWorkbenchWindow(event).getActivePage()
+                                       .openEditor(nei, GenericNodeEditor.ID);
+               } catch (Exception e) {
+                       throw new ArgeoException("Cannot open editor", e);
+               }
+               return null;
+       }
+
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/dialogs/ChooseNameDialog.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/dialogs/ChooseNameDialog.java
new file mode 100644 (file)
index 0000000..01957f3
--- /dev/null
@@ -0,0 +1,52 @@
+package org.argeo.jcr.ui.explorer.dialogs;
+
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.dialogs.TitleAreaDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+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;
+import org.eclipse.swt.widgets.Text;
+
+/** Dialog to change the current user password */
+public class ChooseNameDialog extends TitleAreaDialog {
+       private Text nameT;
+
+       public ChooseNameDialog(Shell parentShell) {
+               super(parentShell);
+               setTitle("Choose name");
+       }
+
+       protected Point getInitialSize() {
+               return new Point(300, 250);
+       }
+
+       protected Control createDialogArea(Composite parent) {
+               Composite dialogarea = (Composite) super.createDialogArea(parent);
+               dialogarea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               Composite composite = new Composite(dialogarea, SWT.NONE);
+               composite.setLayout(new GridLayout(2, false));
+               composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+               nameT = createLT(composite, "Name");
+
+               setMessage("Choose name", IMessageProvider.INFORMATION);
+               parent.pack();
+               return composite;
+       }
+
+       /** Creates label and text. */
+       protected Text createLT(Composite parent, String label) {
+               new Label(parent, SWT.NONE).setText(label);
+               Text text = new Text(parent, SWT.SINGLE | SWT.LEAD | SWT.BORDER);
+               text.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+               return text;
+       }
+
+       public String getName() {
+               return nameT.getText();
+       }
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/editors/EmptyNodePage.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/editors/EmptyNodePage.java
new file mode 100644 (file)
index 0000000..b9f20b9
--- /dev/null
@@ -0,0 +1,36 @@
+package org.argeo.jcr.ui.explorer.editors;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormEditor;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+/**
+ * This page is only used at editor's creation time when current node has not
+ * yet been set
+ */
+public class EmptyNodePage extends FormPage {
+       private final static Log log = LogFactory.getLog(EmptyNodePage.class);
+
+       public EmptyNodePage(FormEditor editor, String title) {
+               super(editor, "Empty Page", title);
+       }
+
+       protected void createFormContent(IManagedForm managedForm) {
+               try {
+                       ScrolledForm form = managedForm.getForm();
+                       GridLayout twt = new GridLayout(1, false);
+                       twt.marginWidth = twt.marginHeight = 0;
+                       form.getBody().setLayout(twt);
+                       Label lbl = new Label(form.getBody(), SWT.NONE);
+                       lbl.setText("Empty page");
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
+       }
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/editors/GenericJcrQueryEditor.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/editors/GenericJcrQueryEditor.java
new file mode 100644 (file)
index 0000000..7dcb7c0
--- /dev/null
@@ -0,0 +1,44 @@
+package org.argeo.jcr.ui.explorer.editors;
+
+import org.argeo.eclipse.ui.jcr.editors.AbstractJcrQueryEditor;
+import org.eclipse.swt.SWT;
+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.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Text;
+
+/** Executes any JCR query. */
+public class GenericJcrQueryEditor extends AbstractJcrQueryEditor {
+       public final static String ID = "org.argeo.jcr.ui.explorer.genericJcrQueryEditor";
+
+       private Text queryField;
+
+       @Override
+       public void createQueryForm(Composite parent) {
+               parent.setLayout(new GridLayout(1, false));
+
+               queryField = new Text(parent, SWT.BORDER | SWT.MULTI | SWT.WRAP);
+               queryField.setText(initialQuery);
+               queryField.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+               Button execute = new Button(parent, SWT.PUSH);
+               execute.setText("Execute");
+
+               Listener executeListener = new Listener() {
+                       public void handleEvent(Event event) {
+                               executeQuery(queryField.getText());
+                       }
+               };
+
+               execute.addListener(SWT.Selection, executeListener);
+               // queryField.addListener(SWT.DefaultSelection, executeListener);
+       }
+
+       @Override
+       public void setFocus() {
+               queryField.setFocus();
+       }
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/editors/GenericNodeEditor.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/editors/GenericNodeEditor.java
new file mode 100644 (file)
index 0000000..4510ea3
--- /dev/null
@@ -0,0 +1,91 @@
+package org.argeo.jcr.ui.explorer.editors;
+
+import javax.jcr.Node;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.ArgeoException;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.jcr.ui.explorer.JcrExplorerPlugin;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorSite;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.forms.editor.FormEditor;
+
+/**
+ * Parent Abstract GR multitab editor. Insure the presence of a GrBackend
+ */
+public class GenericNodeEditor extends FormEditor {
+
+       private final static Log log = LogFactory.getLog(GenericNodeEditor.class);
+       public final static String ID = "org.argeo.jcr.ui.explorer.genericNodeEditor";
+
+       private Node currentNode;
+
+       private GenericNodePage networkDetailsPage;
+
+       public void init(IEditorSite site, IEditorInput input)
+                       throws PartInitException {
+               super.init(site, input);
+               GenericNodeEditorInput nei = (GenericNodeEditorInput) getEditorInput();
+               this.setPartName(JcrUtils.lastPathElement(nei.getPath()));
+       }
+
+       @Override
+       protected void addPages() {
+               EmptyNodePage enp = new EmptyNodePage(this, "Empty node page");
+               try {
+                       addPage(enp);
+               } catch (PartInitException e) {
+                       throw new ArgeoException("Not able to add an empty page ", e);
+               }
+       }
+
+       private void addPagesAfterNodeSet() {
+               try {
+                       networkDetailsPage = new GenericNodePage(this,
+                                       JcrExplorerPlugin.getMessage("genericNodePageTitle"),
+                                       currentNode);
+                       addPage(networkDetailsPage);
+                       this.setActivePage(networkDetailsPage.getIndex());
+               } catch (PartInitException e) {
+                       throw new ArgeoException("Not able to add page ", e);
+               }
+       }
+
+       @Override
+       public void doSaveAs() {
+               // unused compulsory method
+       }
+
+       @Override
+       public void doSave(IProgressMonitor monitor) {
+               try {
+                       // Automatically commit all pages of the editor
+                       commitPages(true);
+                       firePropertyChange(PROP_DIRTY);
+               } catch (Exception e) {
+                       throw new ArgeoException("Error while saving node", e);
+               }
+
+       }
+
+       @Override
+       public boolean isSaveAsAllowed() {
+               return true;
+       }
+
+       Node getCurrentNode() {
+               return currentNode;
+       }
+
+       public void setCurrentNode(Node currentNode) {
+               boolean nodeWasNull = this.currentNode == null;
+               this.currentNode = currentNode;
+               if (nodeWasNull) {
+                       this.removePage(0);
+                       addPagesAfterNodeSet();
+               }
+       }
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/editors/GenericNodeEditorInput.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/editors/GenericNodeEditorInput.java
new file mode 100644 (file)
index 0000000..9eceb62
--- /dev/null
@@ -0,0 +1,98 @@
+package org.argeo.jcr.ui.explorer.editors;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IPersistableElement;
+
+/**
+ * An editor input based on a path to a node plus workspace name and repository
+ * alias. In a multirepository environment, path can be enriched with Repository
+ * Alias and workspace
+ */
+
+public class GenericNodeEditorInput implements IEditorInput {
+       private final String path;
+       private final String repositoryAlias;
+       private final String workspaceName;
+
+       /**
+        * In order to implement a generic explorer that supports remote and multi
+        * workspaces repositories, node path can be detailed by these strings.
+        * 
+        * @param repositoryAlias
+        *            : can be null
+        * @param workspaceName
+        *            : can be null
+        * @param path
+        */
+       public GenericNodeEditorInput(String repositoryAlias, String workspaceName,
+                       String path) {
+               this.path = path;
+               this.repositoryAlias = repositoryAlias;
+               this.workspaceName = workspaceName;
+       }
+
+       public Object getAdapter(@SuppressWarnings("rawtypes") Class adapter) {
+               return null;
+       }
+
+       public boolean exists() {
+               return true;
+       }
+
+       public ImageDescriptor getImageDescriptor() {
+               return null;
+       }
+
+       public String getName() {
+               return path;
+       }
+
+       public String getRepositoryAlias() {
+               return repositoryAlias;
+       }
+
+       public String getWorkspaceName() {
+               return workspaceName;
+       }
+
+       public IPersistableElement getPersistable() {
+               return null;
+       }
+
+       public String getToolTipText() {
+               return path;
+       }
+
+       public String getPath() {
+               return path;
+       }
+
+       public boolean equals(Object obj) {
+               if (this == obj)
+                       return true;
+               if (obj == null)
+                       return false;
+               if (getClass() != obj.getClass())
+                       return false;
+
+               GenericNodeEditorInput other = (GenericNodeEditorInput) obj;
+
+               if (!path.equals(other.getPath()))
+                       return false;
+
+               String own = other.getWorkspaceName();
+               if ((workspaceName == null && own != null)
+                               || (workspaceName != null && (own == null || !workspaceName
+                                               .equals(own))))
+                       return false;
+
+               String ora = other.getRepositoryAlias();
+               if ((repositoryAlias == null && ora != null)
+                               || (repositoryAlias != null && (ora == null || !repositoryAlias
+                                               .equals(ora))))
+                       return false;
+
+               return true;
+       }
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/editors/GenericNodePage.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/editors/GenericNodePage.java
new file mode 100644 (file)
index 0000000..0be0f19
--- /dev/null
@@ -0,0 +1,199 @@
+package org.argeo.jcr.ui.explorer.editors;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.ListIterator;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.ArgeoException;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.jcr.ui.explorer.JcrExplorerConstants;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+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.Text;
+import org.eclipse.ui.forms.AbstractFormPart;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormEditor;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+public class GenericNodePage extends FormPage implements JcrExplorerConstants {
+       private final static Log log = LogFactory.getLog(GenericNodePage.class);
+
+       // local constants
+       private final static String JCR_PROPERTY_NAME = "jcr:name";
+
+       // Utils
+       protected DateFormat timeFormatter = new SimpleDateFormat(DATE_TIME_FORMAT);
+
+       // Main business Objects
+       private Node currentNode;
+
+       // This page widgets
+       private FormToolkit tk;
+       private List<Control> modifyableProperties = new ArrayList<Control>();
+
+       public GenericNodePage(FormEditor editor, String title, Node currentNode) {
+               super(editor, "id", title);
+               this.currentNode = currentNode;
+       }
+
+       protected void createFormContent(IManagedForm managedForm) {
+               try {
+                       tk = managedForm.getToolkit();
+                       ScrolledForm form = managedForm.getForm();
+                       GridLayout twt = new GridLayout(3, false);
+                       twt.marginWidth = twt.marginHeight = 0;
+
+                       form.getBody().setLayout(twt);
+
+                       createPropertiesPart(form.getBody());
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
+       }
+
+       private void createPropertiesPart(Composite parent) {
+               try {
+
+                       PropertyIterator pi = currentNode.getProperties();
+
+                       // Initializes form part
+                       AbstractFormPart part = new AbstractFormPart() {
+                               public void commit(boolean onSave) {
+                                       try {
+                                               if (onSave) {
+                                                       ListIterator<Control> it = modifyableProperties
+                                                                       .listIterator();
+                                                       while (it.hasNext()) {
+                                                               // we only support Text controls for the time
+                                                               // being
+                                                               Text curControl = (Text) it.next();
+                                                               String value = curControl.getText();
+                                                               currentNode.setProperty((String) curControl
+                                                                               .getData(JCR_PROPERTY_NAME), value);
+                                                       }
+
+                                                       // We only commit when onSave = true,
+                                                       // thus it is still possible to save after a tab
+                                                       // change.
+                                                       super.commit(onSave);
+                                               }
+                                       } catch (RepositoryException re) {
+                                               throw new ArgeoException(
+                                                               "Unexpected error while saving properties", re);
+                                       }
+                               }
+                       };
+
+                       while (pi.hasNext()) {
+                               Property prop = pi.nextProperty();
+                               addPropertyLine(parent, part, prop);
+                       }
+
+                       getManagedForm().addPart(part);
+               } catch (RepositoryException re) {
+                       throw new ArgeoException(
+                                       "Error during creation of network details section", re);
+               }
+
+       }
+
+       private void addPropertyLine(Composite parent, AbstractFormPart part,
+                       Property prop) {
+               try {
+                       Label lbl = tk.createLabel(parent, prop.getName());
+                       lbl = tk.createLabel(parent,
+                                       "[" + JcrUtils.getPropertyDefinitionAsString(prop) + "]");
+
+                       if (prop.getDefinition().isProtected()) {
+                               lbl = tk.createLabel(parent, formatReadOnlyPropertyValue(prop));
+                       } else
+                               addModifyableValueWidget(parent, part, prop);
+               } catch (RepositoryException re) {
+                       throw new ArgeoException("Cannot get property " + prop, re);
+               }
+       }
+
+       private String formatReadOnlyPropertyValue(Property prop) {
+               try {
+                       String strValue;
+
+                       if (prop.getType() == PropertyType.BINARY)
+                               strValue = "<binary>";
+                       else if (prop.isMultiple())
+                               strValue = Arrays.asList(prop.getValues()).toString();
+                       else if (prop.getType() == PropertyType.DATE)
+                               strValue = timeFormatter.format(prop.getValue().getDate()
+                                               .getTime());
+                       else
+                               strValue = prop.getValue().getString();
+
+                       return strValue;
+               } catch (RepositoryException re) {
+                       throw new ArgeoException(
+                                       "Unexpected error while formatting read only property value",
+                                       re);
+               }
+       }
+
+       private Control addModifyableValueWidget(Composite parent,
+                       AbstractFormPart part, Property prop) {
+               GridData gd;
+               try {
+                       if (prop.getType() == PropertyType.STRING) {
+                               Text txt = tk.createText(parent, prop.getString());
+                               gd = new GridData(GridData.FILL_HORIZONTAL);
+                               txt.setLayoutData(gd);
+                               txt.addModifyListener(new ModifiedFieldListener(part));
+                               txt.setData(JCR_PROPERTY_NAME, prop.getName());
+                               modifyableProperties.add(txt);
+                       } else {
+                               // unsupported property type for editing, we create a read only
+                               // label.
+                               return tk
+                                               .createLabel(parent, formatReadOnlyPropertyValue(prop));
+                       }
+                       return null;
+               } catch (RepositoryException re) {
+                       throw new ArgeoException(
+                                       "Unexpected error while formatting read only property value",
+                                       re);
+               }
+
+       }
+
+       //
+       // LISTENERS
+       //
+
+       private class ModifiedFieldListener implements ModifyListener {
+
+               private AbstractFormPart formPart;
+
+               public ModifiedFieldListener(AbstractFormPart generalPart) {
+                       this.formPart = generalPart;
+               }
+
+               public void modifyText(ModifyEvent e) {
+                       formPart.markDirty();
+               }
+       }
+
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/utils/GenericNodeDoubleClickListener.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/utils/GenericNodeDoubleClickListener.java
new file mode 100644 (file)
index 0000000..708c024
--- /dev/null
@@ -0,0 +1,111 @@
+package org.argeo.jcr.ui.explorer.utils;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.ArgeoException;
+import org.argeo.eclipse.ui.specific.FileHandler;
+import org.argeo.jcr.ui.explorer.JcrExplorerPlugin;
+import org.argeo.jcr.ui.explorer.browser.NodeContentProvider;
+import org.argeo.jcr.ui.explorer.browser.RepositoryNode;
+import org.argeo.jcr.ui.explorer.browser.WorkspaceNode;
+import org.argeo.jcr.ui.explorer.editors.GenericNodeEditor;
+import org.argeo.jcr.ui.explorer.editors.GenericNodeEditorInput;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.ui.PartInitException;
+
+/**
+ * 
+ * Centralizes the management of double click on a NodeTreeViewer
+ * 
+ */
+public class GenericNodeDoubleClickListener implements IDoubleClickListener {
+
+       private final static Log log = LogFactory
+                       .getLog(GenericNodeDoubleClickListener.class);
+
+       private TreeViewer nodeViewer;
+       private JcrFileProvider jfp;
+       private FileHandler fileHandler;
+
+       public GenericNodeDoubleClickListener(TreeViewer nodeViewer) {
+               this.nodeViewer = nodeViewer;
+               jfp = new JcrFileProvider();
+               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 RepositoryNode) {
+                       RepositoryNode rpNode = (RepositoryNode) obj;
+                       rpNode.login();
+                       nodeViewer.refresh(obj);
+               } else if (obj instanceof WorkspaceNode) {
+                       ((WorkspaceNode) obj).login();
+                       nodeViewer.refresh(obj);
+               } else if (obj instanceof Node) {
+                       Node node = (Node) obj;
+                       try {
+                               if (node.isNodeType(NodeType.NT_FILE)) {
+                                       // double click on a file node triggers its opening
+                                       String name = node.getName();
+                                       String id = node.getIdentifier();
+
+                                       // For the file provider to be able to browse the
+                                       // various
+                                       // repository.
+                                       // TODO : enhanced that.
+                                       ITreeContentProvider itcp = (ITreeContentProvider) nodeViewer
+                                                       .getContentProvider();
+                                       jfp.setRootNodes((Object[]) itcp.getElements(null));
+                                       fileHandler.openFile(name, id);
+                               }
+                               // File or not, we always open the corresponding node Editor.
+                               String repositoryAlias = getRepositoryAlias(obj);
+                               String workspaceName = node.getSession().getWorkspace()
+                                               .getName();
+                               String path = node.getPath();
+
+                               if (log.isDebugEnabled()) {
+                                       log.debug("RepoAlias: " + repositoryAlias + " - WS Name: "
+                                                       + workspaceName + " - path:" + path);
+                               }
+                               GenericNodeEditorInput gnei = new GenericNodeEditorInput(
+                                               repositoryAlias, workspaceName, path);
+
+                               GenericNodeEditor gne = (GenericNodeEditor) JcrExplorerPlugin
+                                               .getDefault().getWorkbench().getActiveWorkbenchWindow()
+                                               .getActivePage().openEditor(gnei, GenericNodeEditor.ID);
+                               gne.setCurrentNode(node);
+
+                       } catch (RepositoryException re) {
+                               throw new ArgeoException(
+                                               "Repository error while getting node info", re);
+                       } catch (PartInitException pie) {
+                               throw new ArgeoException(
+                                               "Unexepected exception while opening node editor", pie);
+                       }
+               }
+       }
+
+       // Enhance this method
+       private String getRepositoryAlias(Object element) {
+               NodeContentProvider ncp = (NodeContentProvider) nodeViewer
+                               .getContentProvider();
+               Object parent = element;
+               while (!(ncp.getParent(parent) instanceof RepositoryNode)
+                               && parent != null)
+                       parent = ncp.getParent(parent);
+               return parent == null ? null : ((RepositoryNode) parent).getName();
+       }
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/utils/JcrFileProvider.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/utils/JcrFileProvider.java
new file mode 100644 (file)
index 0000000..01fd634
--- /dev/null
@@ -0,0 +1,150 @@
+package org.argeo.jcr.ui.explorer.utils;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.ArgeoException;
+import org.argeo.eclipse.ui.specific.FileProvider;
+import org.argeo.jcr.RepositoryRegister;
+import org.argeo.jcr.ui.explorer.browser.RepositoryNode;
+
+/**
+ * Implements a FileProvider for UI purposes. Note that it might not be very
+ * reliable as long as we have not fixed login & 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.
+ * 
+ */
+
+public class JcrFileProvider implements FileProvider {
+
+       private Object[] rootNodes;
+
+       /**
+        * Must be set in order for the provider to be able to search the repository
+        * Provided object might be either JCR Nodes or UI RepositoryNode for the
+        * time being.
+        * 
+        * @param repositoryNode
+        */
+       public void setRootNodes(Object[] rootNodes) {
+               List<Object> tmpNodes = new ArrayList<Object>();
+               for (int i = 0; i < rootNodes.length; i++) {
+                       Object obj = rootNodes[i];
+                       if (obj instanceof Node) {
+                               tmpNodes.add(obj);
+                       } else if (obj instanceof RepositoryRegister) {
+                               RepositoryRegister repositoryRegister = (RepositoryRegister) obj;
+                               Map<String, Repository> repositories = repositoryRegister
+                                               .getRepositories();
+                               for (String name : repositories.keySet()) {
+                                       tmpNodes.add(new RepositoryNode(name, repositories
+                                                       .get(name)));
+                               }
+
+                       }
+               }
+               this.rootNodes = tmpNodes.toArray();
+       }
+
+       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 ArgeoException("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 ArgeoException("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 = null;
+
+                       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];
+                                       result = curNode.getSession().getNodeByIdentifier(fileId);
+                                       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 ArgeoException("File node not found for ID" + fileId);
+
+                       // Ensure that the node have the correct type.
+                       if (!result.isNodeType(NodeType.NT_FILE))
+                               throw new ArgeoException(
+                                               "Cannot open file children Node that are not of '"
+                                                               + NodeType.NT_RESOURCE + "' type.");
+
+                       // Get the usefull part of the Node
+                       Node child = result.getNodes().nextNode();
+                       if (child == null || !child.isNodeType(NodeType.NT_RESOURCE))
+                               throw new ArgeoException(
+                                               "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 ArgeoException("Erreur while getting file node of ID "
+                                       + fileId, re);
+               }
+       }
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/utils/JcrUiUtils.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/utils/JcrUiUtils.java
new file mode 100644 (file)
index 0000000..72bd9ab
--- /dev/null
@@ -0,0 +1,5 @@
+package org.argeo.jcr.ui.explorer.utils;
+
+public class JcrUiUtils {
+
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/views/GenericJcrBrowser.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/views/GenericJcrBrowser.java
new file mode 100644 (file)
index 0000000..d009a85
--- /dev/null
@@ -0,0 +1,300 @@
+package org.argeo.jcr.ui.explorer.views;
+
+import java.util.Arrays;
+import java.util.List;
+
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.observation.Event;
+import javax.jcr.observation.EventListener;
+import javax.jcr.observation.ObservationManager;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.ArgeoException;
+import org.argeo.eclipse.ui.jcr.AsyncUiEventListener;
+import org.argeo.eclipse.ui.jcr.utils.NodeViewerComparer;
+import org.argeo.eclipse.ui.jcr.views.AbstractJcrBrowser;
+import org.argeo.eclipse.ui.specific.FileHandler;
+import org.argeo.jcr.ArgeoJcrConstants;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.jcr.RepositoryRegister;
+import org.argeo.jcr.ui.explorer.browser.NodeContentProvider;
+import org.argeo.jcr.ui.explorer.browser.NodeLabelProvider;
+import org.argeo.jcr.ui.explorer.browser.PropertiesContentProvider;
+import org.argeo.jcr.ui.explorer.utils.GenericNodeDoubleClickListener;
+import org.argeo.jcr.ui.explorer.utils.JcrFileProvider;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Menu;
+
+public class GenericJcrBrowser extends AbstractJcrBrowser {
+       private final static Log log = LogFactory.getLog(GenericJcrBrowser.class);
+
+       /* DEPENDENCY INJECTION */
+       private Session session;
+       private RepositoryRegister repositoryRegister;
+
+       // This page widgets
+       private TreeViewer nodesViewer;
+       private NodeContentProvider nodeContentProvider;
+       private TableViewer propertiesViewer;
+       private EventListener resultsObserver;
+
+       // Manage documents
+       private JcrFileProvider jcrFileProvider;
+       private FileHandler fileHandler;
+
+       @Override
+       public void createPartControl(Composite parent) {
+
+               // look for session
+               Session nodeSession = session;
+               if (nodeSession == null) {
+                       Repository nodeRepository = JcrUtils.getRepositoryByAlias(
+                                       repositoryRegister, ArgeoJcrConstants.ALIAS_NODE);
+                       if (nodeRepository != null)
+                               try {
+                                       nodeSession = nodeRepository.login();
+                                       // TODO : enhance that to enable multirepository listener.
+                                       session = nodeSession;
+                               } catch (RepositoryException e1) {
+                                       throw new ArgeoException("Cannot login to node repository");
+                               }
+               }
+
+               // Instantiate the generic object that fits for
+               // both RCP & RAP
+               // Note that in RAP, it registers a service handler that provide the
+               // access to the files.
+               jcrFileProvider = new JcrFileProvider();
+               fileHandler = new FileHandler(jcrFileProvider);
+
+               parent.setLayout(new FillLayout());
+               SashForm sashForm = new SashForm(parent, SWT.VERTICAL);
+               sashForm.setSashWidth(4);
+               sashForm.setLayout(new FillLayout());
+
+               // Create the tree on top of the view
+               Composite top = new Composite(sashForm, SWT.NONE);
+               GridLayout gl = new GridLayout(1, false);
+               top.setLayout(gl);
+
+               nodeContentProvider = new NodeContentProvider(nodeSession,
+                               repositoryRegister);
+
+               // nodes viewer
+               nodesViewer = createNodeViewer(top, nodeContentProvider);
+
+               // context menu
+               MenuManager menuManager = new MenuManager();
+               Menu menu = menuManager.createContextMenu(nodesViewer.getTree());
+               nodesViewer.getTree().setMenu(menu);
+               getSite().registerContextMenu(menuManager, nodesViewer);
+               getSite().setSelectionProvider(nodesViewer);
+               nodesViewer.setInput(getViewSite());
+
+               // Create the property viewer on the bottom
+               Composite bottom = new Composite(sashForm, SWT.NONE);
+               bottom.setLayout(new GridLayout(1, false));
+               propertiesViewer = new TableViewer(bottom);
+               propertiesViewer.getTable().setLayoutData(
+                               new GridData(SWT.FILL, SWT.FILL, true, true));
+               propertiesViewer.getTable().setHeaderVisible(true);
+               propertiesViewer.setContentProvider(new PropertiesContentProvider());
+               TableViewerColumn col = new TableViewerColumn(propertiesViewer,
+                               SWT.NONE);
+               col.getColumn().setText("Name");
+               col.getColumn().setWidth(200);
+               col.setLabelProvider(new ColumnLabelProvider() {
+                       public String getText(Object element) {
+                               try {
+                                       return ((Property) element).getName();
+                               } catch (RepositoryException e) {
+                                       throw new ArgeoException(
+                                                       "Unexpected exception in label provider", e);
+                               }
+                       }
+               });
+               col = new TableViewerColumn(propertiesViewer, SWT.NONE);
+               col.getColumn().setText("Value");
+               col.getColumn().setWidth(400);
+               col.setLabelProvider(new ColumnLabelProvider() {
+                       public String getText(Object element) {
+                               try {
+                                       Property property = (Property) element;
+                                       if (property.getType() == PropertyType.BINARY)
+                                               return "<binary>";
+                                       else if (property.isMultiple())
+                                               return Arrays.asList(property.getValues()).toString();
+                                       else
+                                               return property.getValue().getString();
+                               } catch (RepositoryException e) {
+                                       throw new ArgeoException(
+                                                       "Unexpected exception in label provider", e);
+                               }
+                       }
+               });
+               col = new TableViewerColumn(propertiesViewer, SWT.NONE);
+               col.getColumn().setText("Type");
+               col.getColumn().setWidth(200);
+               col.setLabelProvider(new ColumnLabelProvider() {
+                       public String getText(Object element) {
+                               try {
+                                       return PropertyType.nameFromValue(((Property) element)
+                                                       .getType());
+                               } catch (RepositoryException e) {
+                                       throw new ArgeoException(
+                                                       "Unexpected exception in label provider", e);
+                               }
+                       }
+               });
+               propertiesViewer.setInput(getViewSite());
+
+               sashForm.setWeights(getWeights());
+               nodesViewer.setComparer(new NodeViewerComparer());
+       }
+
+       /**
+        * To be overridden to adapt size of form and result frames.
+        */
+       protected int[] getWeights() {
+               return new int[] { 70, 30 };
+       }
+
+       // @Override
+       // public void setFocus() {
+       // nodesViewer.getTree().setFocus();
+       // }
+       //
+       // /*
+       // * NOTIFICATION
+       // */
+       // public void refresh(Object obj) {
+       // nodesViewer.refresh(obj);
+       // }
+       //
+       // public void nodeAdded(Node parentNode, Node newNode) {
+       // nodesViewer.refresh(parentNode);
+       // nodesViewer.expandToLevel(newNode, 0);
+       // }
+       //
+       // public void nodeRemoved(Node parentNode) {
+       //
+       // IStructuredSelection newSel = new StructuredSelection(parentNode);
+       // nodesViewer.setSelection(newSel, true);
+       // // Force refresh
+       // IStructuredSelection tmpSel = (IStructuredSelection) nodesViewer
+       // .getSelection();
+       // nodesViewer.refresh(tmpSel.getFirstElement());
+       // }
+
+       private JcrFileProvider getJcrFileProvider() {
+               return jcrFileProvider;
+       }
+
+       private FileHandler getFileHandler() {
+               return fileHandler;
+       }
+
+       protected TreeViewer createNodeViewer(Composite parent,
+                       final ITreeContentProvider nodeContentProvider) {
+
+               final TreeViewer tmpNodeViewer = new TreeViewer(parent, SWT.MULTI);
+
+               tmpNodeViewer.getTree().setLayoutData(
+                               new GridData(SWT.FILL, SWT.FILL, true, true));
+
+               tmpNodeViewer.setContentProvider(nodeContentProvider);
+               tmpNodeViewer.setLabelProvider(new NodeLabelProvider());
+               tmpNodeViewer
+                               .addSelectionChangedListener(new ISelectionChangedListener() {
+                                       public void selectionChanged(SelectionChangedEvent event) {
+                                               if (!event.getSelection().isEmpty()) {
+                                                       IStructuredSelection sel = (IStructuredSelection) event
+                                                                       .getSelection();
+                                                       propertiesViewer.setInput(sel.getFirstElement());
+                                               } else {
+                                                       propertiesViewer.setInput(getViewSite());
+                                               }
+                                       }
+                               });
+
+               resultsObserver = new TreeObserver(tmpNodeViewer.getTree().getDisplay());
+               try {
+                       ObservationManager observationManager = session.getWorkspace()
+                                       .getObservationManager();
+                       // FIXME Will not be notified if empty result is deleted
+                       observationManager.addEventListener(resultsObserver,
+                                       Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED, "/", true,
+                                       null, null, false);
+               } catch (RepositoryException e) {
+                       throw new ArgeoException("Cannot register listeners", e);
+               }
+
+               tmpNodeViewer
+                               .addDoubleClickListener(new GenericNodeDoubleClickListener(
+                                               tmpNodeViewer));
+               return tmpNodeViewer;
+       }
+
+       @Override
+       protected TreeViewer getNodeViewer() {
+               return nodesViewer;
+       }
+
+       class TreeObserver extends AsyncUiEventListener {
+
+               public TreeObserver(Display display) {
+                       super(display);
+               }
+
+               @Override
+               protected Boolean willProcessInUiThread(List<Event> events)
+                               throws RepositoryException {
+                       for (Event event : events) {
+                               getLog().debug("Received event " + event);
+                               String path = event.getPath();
+                               int index = path.lastIndexOf('/');
+                               String propertyName = path.substring(index + 1);
+                               getLog().debug("Concerned property " + propertyName);
+                       }
+                       return false;
+               }
+
+               protected void onEventInUiThread(List<Event> events)
+                               throws RepositoryException {
+                       if (getLog().isTraceEnabled())
+                               getLog().trace("Refresh result list");
+                       nodesViewer.refresh();
+               }
+
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setRepositoryRegister(RepositoryRegister repositoryRegister) {
+               this.repositoryRegister = repositoryRegister;
+       }
+
+       public void setSession(Session session) {
+               this.session = session;
+       }
+
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/wizards/ImportFileSystemWizard.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/wizards/ImportFileSystemWizard.java
new file mode 100644 (file)
index 0000000..9e9168e
--- /dev/null
@@ -0,0 +1,220 @@
+package org.argeo.jcr.ui.explorer.wizards;
+
+import java.io.File;
+import java.io.FileInputStream;
+
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.nodetype.NodeType;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.ArgeoException;
+import org.argeo.eclipse.ui.dialogs.Error;
+import org.argeo.eclipse.ui.specific.ImportToServerWizardPage;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.wizard.Wizard;
+
+public class ImportFileSystemWizard extends Wizard {
+       private final static Log log = LogFactory
+                       .getLog(ImportFileSystemWizard.class);
+
+       private ImportToServerWizardPage importPage;
+       private final Node folder;
+
+       public ImportFileSystemWizard(Node folder) {
+               this.folder = folder;
+               setWindowTitle("Import from file system");
+       }
+
+       @Override
+       public void addPages() {
+               importPage = new ImportToServerWizardPage();
+               addPage(importPage);
+               setNeedsProgressMonitor(importPage.getNeedsProgressMonitor());
+       }
+
+       /**
+        * Called when the user click on 'Finish' in the wizard. The real upload to
+        * the JCR repository is done here.
+        */
+       @Override
+       public boolean performFinish() {
+
+               // Initialization
+               final String objectType = importPage.getObjectType();
+               final String objectPath = importPage.getObjectPath();
+
+               // We do not display a progress bar for one file only
+               if (importPage.FILE_ITEM_TYPE.equals(objectType)) {
+                       // In Rap we must force the "real" upload of the file
+                       importPage.performFinish();
+                       try {
+                               Node fileNode = folder.addNode(importPage.getObjectName(),
+                                               NodeType.NT_FILE);
+                               Node resNode = fileNode.addNode(Property.JCR_CONTENT,
+                                               NodeType.NT_RESOURCE);
+                               Binary binary = null;
+                               try {
+                                       binary = folder.getSession().getValueFactory()
+                                                       .createBinary(importPage.getFileInputStream());
+                                       resNode.setProperty(Property.JCR_DATA, binary);
+                               } finally {
+                                       if (binary != null)
+                                               binary.dispose();
+                                       IOUtils.closeQuietly(importPage.getFileInputStream());
+                               }
+                               folder.getSession().save();
+                       } catch (Exception e) {
+                               e.printStackTrace();
+                               return false;
+                       }
+                       return true;
+               } else if (importPage.FOLDER_ITEM_TYPE.equals(objectType)) {
+                       if (objectPath == null || !new File(objectPath).exists()) {
+                               Error.show("Directory " + objectPath + " does not exist");
+                               return false;
+                       }
+
+                       Boolean failed = false;
+                       final File dir = new File(objectPath).getAbsoluteFile();
+                       final Long sizeB = directorySize(dir, 0l);
+                       final Stats stats = new Stats();
+                       Long begin = System.currentTimeMillis();
+                       try {
+                               getContainer().run(true, true, new IRunnableWithProgress() {
+                                       public void run(IProgressMonitor monitor) {
+                                               try {
+                                                       Integer sizeKB = (int) (sizeB / FileUtils.ONE_KB);
+                                                       monitor.beginTask("", sizeKB);
+                                                       importDirectory(folder, dir, monitor, stats);
+                                                       monitor.done();
+                                               } catch (Exception e) {
+                                                       if (e instanceof RuntimeException)
+                                                               throw (RuntimeException) e;
+                                                       else
+                                                               throw new ArgeoException("Cannot import "
+                                                                               + objectPath, e);
+                                               }
+                                       }
+                               });
+                       } catch (Exception e) {
+                               Error.show("Cannot import " + objectPath, e);
+                               failed = true;
+                       }
+
+                       Long duration = System.currentTimeMillis() - begin;
+                       Long durationS = duration / 1000l;
+                       String durationStr = (durationS / 60) + " min " + (durationS % 60)
+                                       + " s";
+                       StringBuffer message = new StringBuffer("Imported\n");
+                       message.append(stats.fileCount).append(" files\n");
+                       message.append(stats.dirCount).append(" directories\n");
+                       message.append(FileUtils.byteCountToDisplaySize(stats.sizeB));
+                       if (failed)
+                               message.append(" of planned ").append(
+                                               FileUtils.byteCountToDisplaySize(sizeB));
+                       message.append("\n");
+                       message.append("in ").append(durationStr).append("\n");
+                       if (failed)
+                               MessageDialog.openError(getShell(), "Import failed",
+                                               message.toString());
+                       else
+                               MessageDialog.openInformation(getShell(), "Import successful",
+                                               message.toString());
+
+                       return true;
+               }
+               return false;
+
+       }
+
+       /** Recursively computes the size of the directory in bytes. */
+       protected Long directorySize(File dir, Long currentSize) {
+               Long size = currentSize;
+               File[] files = dir.listFiles();
+               for (File file : files) {
+                       if (file.isDirectory()) {
+                               size = directorySize(file, size);
+                       } else {
+                               size = size + file.length();
+                       }
+               }
+               return size;
+       }
+
+       /**
+        * Import recursively a directory and its content to the repository.
+        */
+       protected void importDirectory(Node folder, File dir,
+                       IProgressMonitor monitor, Stats stats) {
+               try {
+                       File[] files = dir.listFiles();
+                       for (File file : files) {
+                               if (file.isDirectory()) {
+                                       Node childFolder = folder.addNode(file.getName(),
+                                                       NodeType.NT_FOLDER);
+                                       importDirectory(childFolder, file, monitor, stats);
+                                       folder.getSession().save();
+                                       stats.dirCount++;
+                               } else {
+                                       Long fileSize = file.length();
+
+                                       // we skip tempory files that are created by apps when a
+                                       // file is being edited.
+                                       // TODO : make this configurable.
+                                       if (file.getName().lastIndexOf('~') != file.getName()
+                                                       .length() - 1) {
+
+                                               monitor.subTask(file.getName() + " ("
+                                                               + FileUtils.byteCountToDisplaySize(fileSize)
+                                                               + ") " + file.getCanonicalPath());
+                                               try {
+                                                       Node fileNode = folder.addNode(file.getName(),
+                                                                       NodeType.NT_FILE);
+                                                       Node resNode = fileNode.addNode(
+                                                                       Property.JCR_CONTENT, NodeType.NT_RESOURCE);
+                                                       Binary binary = null;
+                                                       try {
+                                                               binary = folder
+                                                                               .getSession()
+                                                                               .getValueFactory()
+                                                                               .createBinary(new FileInputStream(file));
+                                                               resNode.setProperty(Property.JCR_DATA, binary);
+                                                       } finally {
+                                                               if (binary != null)
+                                                                       binary.dispose();
+                                                       }
+                                                       folder.getSession().save();
+                                                       stats.fileCount++;
+                                                       stats.sizeB = stats.sizeB + fileSize;
+                                               } catch (Exception e) {
+                                                       log.warn("Import of "
+                                                                       + file
+                                                                       + " ("
+                                                                       + FileUtils
+                                                                                       .byteCountToDisplaySize(fileSize)
+                                                                       + ") failed: " + e);
+                                                       folder.getSession().refresh(false);
+                                               }
+                                               monitor.worked((int) (fileSize / FileUtils.ONE_KB));
+                                       }
+                               }
+                       }
+               } catch (Exception e) {
+                       throw new ArgeoException("Cannot import " + dir + " to " + folder,
+                                       e);
+               }
+       }
+
+       static class Stats {
+               public Long fileCount = 0l;
+               public Long dirCount = 0l;
+               public Long sizeB = 0l;
+       }
+}
diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/resources/org/argeo/jcr/ui/explorer/messages.properties b/server/plugins/org.argeo.jcr.ui.explorer/src/main/resources/org/argeo/jcr/ui/explorer/messages.properties
new file mode 100644 (file)
index 0000000..2902b1e
--- /dev/null
@@ -0,0 +1,8 @@
+## English labels for Agreo JCR UI application 
+
+## Generic labels 
+nodeEditorLbl=Generic node editor
+genericNodePageTitle=Edit Node properties
+
+## Dummy ones 
+testLbl=Internationalizations of messages seems to work properly.