Introduce JCR subtree export to simple xml.
[lgpl/argeo-commons.git] / org.argeo.jcr / src / org / argeo / jcr / JcrUtils.java
index e304649e23ac386c718895b532b0b276e52e92ca..3be8be184b25f269d581d09f2bf541980883143d 100644 (file)
@@ -6,13 +6,19 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.security.Principal;
 import java.text.DateFormat;
 import java.text.ParseException;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Collections;
@@ -25,17 +31,20 @@ import java.util.TreeMap;
 
 import javax.jcr.Binary;
 import javax.jcr.Credentials;
+import javax.jcr.ImportUUIDBehavior;
 import javax.jcr.NamespaceRegistry;
 import javax.jcr.NoSuchWorkspaceException;
 import javax.jcr.Node;
 import javax.jcr.NodeIterator;
 import javax.jcr.Property;
 import javax.jcr.PropertyIterator;
+import javax.jcr.PropertyType;
 import javax.jcr.Repository;
 import javax.jcr.RepositoryException;
 import javax.jcr.Session;
 import javax.jcr.Value;
 import javax.jcr.Workspace;
+import javax.jcr.nodetype.NoSuchNodeTypeException;
 import javax.jcr.nodetype.NodeType;
 import javax.jcr.observation.EventListener;
 import javax.jcr.query.Query;
@@ -305,20 +314,46 @@ public class JcrUtils {
                }
        }
 
+//     /**
+//      * Routine that get the child with this name, adding it if it does not already
+//      * exist
+//      */
+//     public static Node getOrAdd(Node parent, String name, String primaryNodeType) throws RepositoryException {
+//             return parent.hasNode(name) ? parent.getNode(name) : parent.addNode(name, primaryNodeType);
+//     }
+
        /**
-        * Routine that get the child with this name, adding id it does not already
+        * Routine that get the child with this name, adding it if it does not already
         * exist
         */
-       public static Node getOrAdd(Node parent, String childName, String childPrimaryNodeType) throws RepositoryException {
-               return parent.hasNode(childName) ? parent.getNode(childName) : parent.addNode(childName, childPrimaryNodeType);
+       public static Node getOrAdd(Node parent, String name, String primaryNodeType, String... mixinNodeTypes)
+                       throws RepositoryException {
+               Node node;
+               if (parent.hasNode(name)) {
+                       node = parent.getNode(name);
+                       if (primaryNodeType != null && !node.isNodeType(primaryNodeType))
+                               throw new IllegalArgumentException("Node " + node + " exists but is of primary node type "
+                                               + node.getPrimaryNodeType().getName() + ", not " + primaryNodeType);
+                       for (String mixin : mixinNodeTypes) {
+                               if (!node.isNodeType(mixin))
+                                       node.addMixin(mixin);
+                       }
+                       return node;
+               } else {
+                       node = primaryNodeType != null ? parent.addNode(name, primaryNodeType) : parent.addNode(name);
+                       for (String mixin : mixinNodeTypes) {
+                               node.addMixin(mixin);
+                       }
+                       return node;
+               }
        }
 
        /**
-        * Routine that get the child with this name, adding id it does not already
+        * Routine that get the child with this name, adding it if it does not already
         * exist
         */
-       public static Node getOrAdd(Node parent, String childName) throws RepositoryException {
-               return parent.hasNode(childName) ? parent.getNode(childName) : parent.addNode(childName);
+       public static Node getOrAdd(Node parent, String name) throws RepositoryException {
+               return parent.hasNode(name) ? parent.getNode(name) : parent.addNode(name);
        }
 
        /** Convert a {@link NodeIterator} to a list of {@link Node} */
@@ -691,6 +726,33 @@ public class JcrUtils {
                }
        }
 
+       /** Copy the whole workspace via a system view XML. */
+       public static void copyWorkspaceXml(Session fromSession, Session toSession) {
+               Workspace fromWorkspace = fromSession.getWorkspace();
+               Workspace toWorkspace = toSession.getWorkspace();
+               String errorMsg = "Cannot copy workspace " + fromWorkspace + " to " + toWorkspace + " via XML.";
+
+               try (PipedInputStream in = new PipedInputStream(1024 * 1024);) {
+                       new Thread(() -> {
+                               try (PipedOutputStream out = new PipedOutputStream(in)) {
+                                       fromSession.exportSystemView("/", out, false, false);
+                                       out.flush();
+                               } catch (IOException e) {
+                                       throw new RuntimeException(errorMsg, e);
+                               } catch (RepositoryException e) {
+                                       throw new JcrException(errorMsg, e);
+                               }
+                       }, "Copy workspace" + fromWorkspace + " to " + toWorkspace).start();
+
+                       toSession.importXML("/", in, ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING);
+                       toSession.save();
+               } catch (IOException e) {
+                       throw new RuntimeException(errorMsg, e);
+               } catch (RepositoryException e) {
+                       throw new JcrException(errorMsg, e);
+               }
+       }
+
        /**
         * Copies recursively the content of a node to another one. Do NOT copy the
         * property values of {@link NodeType#MIX_CREATED} and
@@ -704,6 +766,16 @@ public class JcrUtils {
                        if (toNode.getDefinition().isProtected())
                                return;
 
+                       // add mixins
+                       for (NodeType mixinType : fromNode.getMixinNodeTypes()) {
+                               try {
+                                       toNode.addMixin(mixinType.getName());
+                               } catch (NoSuchNodeTypeException e) {
+                                       // ignore unknown mixins
+                                       // TODO log it
+                               }
+                       }
+
                        // process properties
                        PropertyIterator pit = fromNode.getProperties();
                        properties: while (pit.hasNext()) {
@@ -728,12 +800,7 @@ public class JcrUtils {
 
                        // update jcr:lastModified and jcr:lastModifiedBy in toNode in case
                        // they existed, before adding the mixins
-                       updateLastModified(toNode);
-
-                       // add mixins
-                       for (NodeType mixinType : fromNode.getMixinNodeTypes()) {
-                               toNode.addMixin(mixinType.getName());
-                       }
+                       updateLastModified(toNode, true);
 
                        // process children nodes
                        NodeIterator nit = fromNode.getNodes();
@@ -744,8 +811,15 @@ public class JcrUtils {
                                Node toChild;
                                if (toNode.hasNode(nodeRelPath))
                                        toChild = toNode.getNode(nodeRelPath);
-                               else
-                                       toChild = toNode.addNode(fromChild.getName(), fromChild.getPrimaryNodeType().getName());
+                               else {
+                                       try {
+                                               toChild = toNode.addNode(fromChild.getName(), fromChild.getPrimaryNodeType().getName());
+                                       } catch (NoSuchNodeTypeException e) {
+                                               // ignore unknown primary types
+                                               // TODO log it
+                                               return;
+                                       }
+                               }
                                copy(fromChild, toChild);
                        }
                } catch (RepositoryException e) {
@@ -1142,17 +1216,62 @@ public class JcrUtils {
        }
 
        /**
-        * If this node is has the {@link NodeType#MIX_LAST_MODIFIED} mixin, it updates
-        * the {@link Property#JCR_LAST_MODIFIED} property with the current time and the
-        * {@link Property#JCR_LAST_MODIFIED_BY} property with the underlying session
-        * user id. In Jackrabbit 2.x,
+        * Checks whether {@link Property#JCR_LAST_MODIFIED} or (afterwards)
+        * {@link Property#JCR_CREATED} are set and returns it as an {@link Instant}.
+        */
+       public static Instant getModified(Node node) {
+               Calendar calendar = null;
+               try {
+                       if (node.hasProperty(Property.JCR_LAST_MODIFIED))
+                               calendar = node.getProperty(Property.JCR_LAST_MODIFIED).getDate();
+                       else if (node.hasProperty(Property.JCR_CREATED))
+                               calendar = node.getProperty(Property.JCR_CREATED).getDate();
+                       else
+                               throw new IllegalArgumentException("No modification time found in " + node);
+                       return calendar.toInstant();
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot get modification time for " + node, e);
+               }
+
+       }
+
+       /**
+        * Get {@link Property#JCR_CREATED} as an {@link Instant}, if it is set.
+        */
+       public static Instant getCreated(Node node) {
+               Calendar calendar = null;
+               try {
+                       if (node.hasProperty(Property.JCR_CREATED))
+                               calendar = node.getProperty(Property.JCR_CREATED).getDate();
+                       else
+                               throw new IllegalArgumentException("No created time found in " + node);
+                       return calendar.toInstant();
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot get created time for " + node, e);
+               }
+
+       }
+
+       /**
+        * Updates the {@link Property#JCR_LAST_MODIFIED} property with the current time
+        * and the {@link Property#JCR_LAST_MODIFIED_BY} property with the underlying
+        * session user id.
+        */
+       public static void updateLastModified(Node node) {
+               updateLastModified(node, false);
+       }
+
+       /**
+        * Updates the {@link Property#JCR_LAST_MODIFIED} property with the current time
+        * and the {@link Property#JCR_LAST_MODIFIED_BY} property with the underlying
+        * session user id. In Jackrabbit 2.x,
         * <a href="https://issues.apache.org/jira/browse/JCR-2233">these properties are
         * not automatically updated</a>, hence the need for manual update. The session
         * is not saved.
         */
-       public static void updateLastModified(Node node) {
+       public static void updateLastModified(Node node, boolean addMixin) {
                try {
-                       if (!node.isNodeType(NodeType.MIX_LAST_MODIFIED))
+                       if (addMixin && !node.isNodeType(NodeType.MIX_LAST_MODIFIED))
                                node.addMixin(NodeType.MIX_LAST_MODIFIED);
                        node.setProperty(Property.JCR_LAST_MODIFIED, new GregorianCalendar());
                        node.setProperty(Property.JCR_LAST_MODIFIED_BY, node.getSession().getUserID());
@@ -1168,16 +1287,26 @@ public class JcrUtils {
         * @param untilPath the base path, null is equivalent to "/"
         */
        public static void updateLastModifiedAndParents(Node node, String untilPath) {
+               updateLastModifiedAndParents(node, untilPath, true);
+       }
+
+       /**
+        * Update lastModified recursively until this parent.
+        * 
+        * @param node      the node
+        * @param untilPath the base path, null is equivalent to "/"
+        */
+       public static void updateLastModifiedAndParents(Node node, String untilPath, boolean addMixin) {
                try {
                        if (untilPath != null && !node.getPath().startsWith(untilPath))
                                throw new IllegalArgumentException(node + " is not under " + untilPath);
-                       updateLastModified(node);
+                       updateLastModified(node, addMixin);
                        if (untilPath == null) {
                                if (!node.getPath().equals("/"))
-                                       updateLastModifiedAndParents(node.getParent(), untilPath);
+                                       updateLastModifiedAndParents(node.getParent(), untilPath, addMixin);
                        } else {
                                if (!node.getPath().equals(untilPath))
-                                       updateLastModifiedAndParents(node.getParent(), untilPath);
+                                       updateLastModifiedAndParents(node.getParent(), untilPath, addMixin);
                        }
                } catch (RepositoryException e) {
                        throw new JcrException("Cannot update lastModified from " + node + " until " + untilPath, e);
@@ -1343,6 +1472,8 @@ public class JcrUtils {
                // the new access control list must be applied otherwise this call:
                // acl.removeAccessControlEntry(ace); has no effect
                acm.setPolicy(path, acl);
+               session.refresh(true);
+               session.save();
        }
 
        /*
@@ -1450,6 +1581,7 @@ public class JcrUtils {
         * 
         * @return the created file node
         */
+       @Deprecated
        public static Node copyFile(Node folderNode, File file) {
                try (InputStream in = new FileInputStream(file)) {
                        return copyStreamAsFile(folderNode, file.getName(), in);
@@ -1494,6 +1626,7 @@ public class JcrUtils {
                        }
                        binary = contentNode.getSession().getValueFactory().createBinary(in);
                        contentNode.setProperty(Property.JCR_DATA, binary);
+                       updateLastModified(contentNode);
                        return fileNode;
                } catch (RepositoryException e) {
                        throw new JcrException("Cannot create file node " + fileName + " under " + folderNode, e);
@@ -1507,6 +1640,41 @@ public class JcrUtils {
                return fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary().getStream();
        }
 
+       /**
+        * Set the properties of {@link NodeType#MIX_MIMETYPE} on the content of this
+        * file node.
+        */
+       public static void setFileMimeType(Node fileNode, String mimeType, String encoding) throws RepositoryException {
+               Node contentNode = fileNode.getNode(Node.JCR_CONTENT);
+               if (mimeType != null)
+                       contentNode.setProperty(Property.JCR_MIMETYPE, mimeType);
+               if (encoding != null)
+                       contentNode.setProperty(Property.JCR_ENCODING, encoding);
+               // TODO remove properties if args are null?
+       }
+
+       public static void copyFilesToFs(Node baseNode, Path targetDir, boolean recursive) {
+               try {
+                       Files.createDirectories(targetDir);
+                       for (NodeIterator nit = baseNode.getNodes(); nit.hasNext();) {
+                               Node node = nit.nextNode();
+                               if (node.isNodeType(NodeType.NT_FILE)) {
+                                       Path filePath = targetDir.resolve(node.getName());
+                                       try (OutputStream out = Files.newOutputStream(filePath); InputStream in = getFileAsStream(node)) {
+                                               IOUtils.copy(in, out);
+                                       }
+                               } else if (recursive && node.isNodeType(NodeType.NT_FOLDER)) {
+                                       Path dirPath = targetDir.resolve(node.getName());
+                                       copyFilesToFs(node, dirPath, true);
+                               }
+                       }
+               } catch (RepositoryException e) {
+                       throw new JcrException("Cannot copy " + baseNode + " to " + targetDir, e);
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot copy " + baseNode + " to " + targetDir, e);
+               }
+       }
+
        /**
         * Computes the checksum of an nt:file.
         * 
@@ -1562,4 +1730,49 @@ public class JcrUtils {
                return new String(hexChars);
        }
 
+       /** Export a subtree as a compact XML without namespaces. */
+       public static void toSimpleXml(Node node, StringBuilder sb) throws RepositoryException {
+               sb.append('<');
+               String nodeName = node.getName();
+               int colIndex = nodeName.indexOf(':');
+               if (colIndex > 0) {
+                       nodeName = nodeName.substring(colIndex + 1);
+               }
+               sb.append(nodeName);
+               PropertyIterator pit = node.getProperties();
+               properties: while (pit.hasNext()) {
+                       Property p = pit.nextProperty();
+                       // skip multiple properties
+                       if (p.isMultiple())
+                               continue properties;
+                       String propertyName = p.getName();
+                       int pcolIndex = propertyName.indexOf(':');
+                       // skip properties with namespaces
+                       if (pcolIndex > 0)
+                               continue properties;
+                       // skip binaries
+                       if (p.getType() == PropertyType.BINARY) {
+                               continue properties;
+                               // TODO retrieve identifier?
+                       }
+                       sb.append(' ');
+                       sb.append(propertyName);
+                       sb.append('=');
+                       sb.append('\"').append(p.getString()).append('\"');
+               }
+
+               if (node.hasNodes()) {
+                       sb.append('>');
+                       NodeIterator children = node.getNodes();
+                       while (children.hasNext()) {
+                               toSimpleXml(children.nextNode(), sb);
+                       }
+                       sb.append("</");
+                       sb.append(nodeName);
+                       sb.append('>');
+               } else {
+                       sb.append("/>");
+               }
+       }
+
 }