View Javadoc
1   /**
2    * This file Copyright (c) 2003-2017 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.cms.core.version;
35  
36  import info.magnolia.cms.core.Path;
37  import info.magnolia.context.SystemContext;
38  import info.magnolia.jcr.util.NodeTypes;
39  import info.magnolia.jcr.util.NodeUtil;
40  import info.magnolia.jcr.util.VersionUtil;
41  import info.magnolia.repository.RepositoryConstants;
42  import info.magnolia.repository.RepositoryManager;
43  
44  import java.io.File;
45  import java.io.FileInputStream;
46  import java.io.FileOutputStream;
47  import java.io.IOException;
48  import java.util.ArrayList;
49  import java.util.List;
50  
51  import javax.inject.Inject;
52  import javax.inject.Singleton;
53  import javax.jcr.ImportUUIDBehavior;
54  import javax.jcr.ItemNotFoundException;
55  import javax.jcr.Node;
56  import javax.jcr.NodeIterator;
57  import javax.jcr.Property;
58  import javax.jcr.PropertyIterator;
59  import javax.jcr.PropertyType;
60  import javax.jcr.RepositoryException;
61  import javax.jcr.Session;
62  import javax.jcr.Workspace;
63  import javax.jcr.nodetype.ConstraintViolationException;
64  import javax.jcr.nodetype.NodeType;
65  
66  import org.apache.commons.io.IOUtils;
67  import org.apache.commons.lang3.RandomStringUtils;
68  import org.apache.commons.lang3.StringUtils;
69  import org.apache.jackrabbit.JcrConstants;
70  import org.apache.jackrabbit.commons.iterator.FilteringNodeIterator;
71  import org.apache.jackrabbit.commons.predicate.Predicate;
72  import org.slf4j.Logger;
73  import org.slf4j.LoggerFactory;
74  
75  
76  /**
77   * <p>Util to copy nodes and hierarchies between workspaces. A {@link Node, Predicate} defines what such a copy process includes.
78   * This is used to copy pages to the version workspace. While the paragraph nodes have to be copied the sub-pages should not.</p>
79   * <p>Storage structure is generated from node's uuid, level 1 node - first two letters from the first part, level 2 node - two letters from the second part and level 3 node - two letters from third part.</p>
80   * <p>Example: for uuid<pre>93b0b5d8-8a10-11e6-ae22-56b6b6499611</pre> is generated following structure: <pre>/93/8a/11/[nodeName]</pre></p>.
81   */
82  @Singleton
83  public final class CopyUtil {
84  
85      private static Logger log = LoggerFactory.getLogger(CopyUtil.class);
86  
87      private final SystemContext systemContext;
88      private final RepositoryManager repositoryManager;
89  
90      @Inject
91      public CopyUtil(SystemContext systemContext, RepositoryManager repositoryManager) {
92          this.systemContext = systemContext;
93          this.repositoryManager = repositoryManager;
94      }
95  
96      /**
97       * @deprecated since 5.3.1 - use {CopyUtil#copyToVersion} instead.
98       */
99      void copyToversion(Node source, Predicate filter) throws RepositoryException {
100         copyToVersion(source, filter);
101     }
102 
103     /**
104      * Copy given node to the version store using specified filter.
105      */
106     void copyToVersion(Node source, Predicate filter) throws RepositoryException {
107         // first check if the node already exist
108         Node root;
109         try {
110             root = getVersionSessionFor(source).getNodeByIdentifier(source.getUUID());
111             if (root.getParent().getName().equalsIgnoreCase(VersionManager.TMP_REFERENCED_NODES)) {
112                 root.getSession().move(root.getPath(), "/" + root.getName());
113             } else if (root.getDepth() > 3) {
114                 this.removeProperties(root);
115                 // copy root properties
116                 this.updateProperties(source, root);
117 
118                 this.updateNodeTypes(source, root);
119                 root.getSession().save();
120             } else {
121                 root = doImport(source);
122             }
123         } catch (ItemNotFoundException e) {
124             // create root for this versionable node
125             root = doImport(source);
126         }
127         // copy all child nodes
128         NodeIterator children = new FilteringNodeIterator(source.getNodes(), filter);
129         while (children.hasNext()) {
130             Node child = children.nextNode();
131             this.clone(child, root, filter, true);
132         }
133         this.removeNonExistingChildNodes(source, root, filter);
134     }
135 
136     private Node doImport(Node source) throws RepositoryException {
137         try {
138             this.importNode(getVersionSessionFor(source).getRootNode(), source);
139         } catch (IOException ioe) {
140             throw new RepositoryException("Failed to import node in magnolia version store : " + ioe.getMessage(), ioe);
141         }
142         Node root = getVersionSessionFor(source).getNodeByIdentifier(source.getIdentifier());
143         // save parent node since this node is newly created
144         getVersionSessionFor(source).save();
145         return root;
146     }
147 
148     private void updateNodeTypes(Node source, Node root) throws RepositoryException {
149         String sourcePrimaryNodeType = source.getPrimaryNodeType().getName();
150         if (!root.getPrimaryNodeType().getName().equals(sourcePrimaryNodeType)) {
151             root.setPrimaryType(sourcePrimaryNodeType);
152         }
153 
154         List<String> targetNodeTypes = new ArrayList<String>();
155         for (NodeType t : root.getMixinNodeTypes()) {
156             targetNodeTypes.add(t.getName());
157         }
158         NodeType[] nodeTypes = source.getMixinNodeTypes();
159         for (NodeType type : nodeTypes) {
160             root.addMixin(type.getName());
161             targetNodeTypes.remove(type.getName());
162         }
163         // remove all mixins not found in the original except MIX_VERSIONABLE
164         for (String nodeType : targetNodeTypes) {
165             if (JcrConstants.MIX_VERSIONABLE.equals(nodeType)) {
166                 continue;
167             }
168             root.removeMixin(nodeType);
169         }
170     }
171 
172     /**
173      * Copy source to destination using the provided filter.
174      *
175      * @param source node in version store
176      * @param destination which needs to be restored
177      * @param filter this must be the same filter as used while creating this version
178      */
179     void copyFromVersion(Node source, Node destination, Predicate filter) throws RepositoryException {
180         // copy all nodes from version store
181         this.copyAllChildNodes(source, destination, filter);
182         // remove all non existing nodes
183         this.removeNonExistingChildNodes(source, destination, filter);
184 
185         // merge top node properties
186         this.removeProperties(destination);
187         this.updateProperties(source, destination);
188 
189         this.removeNonExistingMixins(source, destination);
190     }
191 
192     private void removeNonExistingMixins(Node source, Node destination) throws RepositoryException {
193         List<String> destNodeTypes = new ArrayList<String>();
194         // has to match mixin names as mixin instances to not equal()
195         for (NodeType nt : destination.getMixinNodeTypes()) {
196             destNodeTypes.add(nt.getName());
197         }
198         // remove all that still exist in source
199         for (NodeType nt : source.getMixinNodeTypes()) {
200             destNodeTypes.remove(nt.getName());
201         }
202         // un-mix the rest
203         for (String type : destNodeTypes) {
204             destination.removeMixin(type);
205         }
206     }
207 
208     /**
209      * Recursively removes all child nodes from node using specified filter.
210      */
211     private void removeNonExistingChildNodes(Node source, Node destination, Predicate filter)
212             throws RepositoryException {
213         // collect all uuids from the source node hierarchy using the given filter
214         NodeIterator children = new FilteringNodeIterator(destination.getNodes(), filter);
215         while (children.hasNext()) {
216             Node child = children.nextNode();
217             // check if this child exist in source, if not remove it
218             if (child.getDefinition().isAutoCreated()) {
219                 continue;
220             }
221             try {
222                 source.getSession().getNodeByIdentifier(child.getIdentifier());
223                 // if exist its ok, recursively remove all sub nodes
224                 this.removeNonExistingChildNodes(source, child, filter);
225             } catch (ItemNotFoundException e) {
226                 PropertyIterator referencedProperties = child.getReferences();
227                 if (referencedProperties.getSize() > 0) {
228                     // remove all referenced properties, its safe since source workspace cannot have these
229                     // properties if node with this UUID does not exist
230                     while (referencedProperties.hasNext()) {
231                         referencedProperties.nextProperty().remove();
232                     }
233                 }
234                 child.remove();
235             }
236         }
237     }
238 
239     /**
240      * Copy all child nodes from node1 to node2.
241      */
242     private void copyAllChildNodes(Node node1, Node node2, Predicate filter)
243             throws RepositoryException {
244         NodeIterator children = new FilteringNodeIterator(node1.getNodes(), filter);
245         while (children.hasNext()) {
246             Node child = children.nextNode();
247             this.clone(child, node2, filter, true);
248         }
249     }
250 
251     public void clone(Node node, Node parent, Predicate filter, boolean removeExisting)
252             throws RepositoryException {
253         try {
254             // it seems to be a bug in jackrabbit - cloning does not work if the node with the same uuid
255             // exist, "removeExisting" has no effect
256             // if node exist with the same UUID, simply update non propected properties
257             String workspaceName = parent.getSession().getWorkspace().getName();
258             Session session = getSession(workspaceName);
259             Node existingNode = session.getNodeByIdentifier(node.getIdentifier());
260             if (removeExisting) {
261                 existingNode.remove();
262                 session.save();
263                 this.clone(node, parent);
264                 return;
265             }
266             this.removeProperties(existingNode);
267             this.updateProperties(node, existingNode);
268             NodeIterator children = new FilteringNodeIterator(node.getNodes(), filter);
269             while (children.hasNext()) {
270                 this.clone(children.nextNode(), existingNode, filter, removeExisting);
271             }
272         } catch (ItemNotFoundException e) {
273             // it's safe to clone if UUID does not exist in this workspace but we might have to remove same name sibling (different uuid) to avoid conflicts
274             if (parent.hasNode(node.getName())) {
275                 parent.getNode(node.getName()).remove();
276                 parent.getSession().save();
277             }
278             this.clone(node, parent);
279         }
280     }
281 
282     private void clone(Node node, Node parent) throws RepositoryException {
283         if (node.getDefinition().isAutoCreated()) {
284             Node destination = parent.getNode(node.getName());
285             this.removeProperties(destination);
286             this.updateProperties(node, destination);
287         } else {
288             final String parentPath = parent.getPath();
289             final String srcWorkspaceLogicalName = node.getSession().getWorkspace().getName();
290             final String srcWorkspacePhysicalName = repositoryManager.getWorkspaceMapping(srcWorkspaceLogicalName).getPhysicalWorkspaceName();
291             final Workspace targetWorkspace = parent.getSession().getWorkspace();
292             final String srcPath = node.getPath();
293             final String targetPath = parentPath + (parentPath != null && parentPath.endsWith("/") ? "" : "/") + node.getName();
294             log.debug("workspace level clone from {}:{} to {}:{}", srcWorkspaceLogicalName, srcPath, targetWorkspace.getName(), parentPath);
295             targetWorkspace.clone(srcWorkspacePhysicalName, srcPath, targetPath, true);
296         }
297     }
298 
299     /**
300      * Remove all properties under the given node.
301      */
302     private void removeProperties(Node node) throws RepositoryException {
303         PropertyIterator properties = node.getProperties();
304         while (properties.hasNext()) {
305             Property property = properties.nextProperty();
306             if (property.getDefinition().isProtected() || property.getDefinition().isMandatory()) {
307                 continue;
308             }
309             try {
310                 property.remove();
311             } catch (ConstraintViolationException e) {
312                 log.debug("Property {} is a reserved property", property.getName());
313             }
314         }
315     }
316 
317     /**
318      * Import while preserving UUID, parameters supplied must be from separate workspaces.
319      *
320      * @param parent under which the specified node will be imported
321      * @throws IOException if failed to import or export
322      */
323     private void importNode(Node parent, Node node) throws RepositoryException, IOException {
324         File file = File.createTempFile("mgnl", null, Path.getTempDirectory());
325         FileOutputStream outStream = new FileOutputStream(file);
326         try {
327             node.getSession().getWorkspace().getSession().exportSystemView(node.getPath(), outStream, false, true);
328             outStream.flush();
329         } finally {
330             IOUtils.closeQuietly(outStream);
331         }
332         FileInputStream inStream = new FileInputStream(file);
333         String tempPath = null;
334         Node maybeVersionNode;
335         try {
336             // check if uuid exists and if it's versioned node node or not ... if it's some of the structure nodes, we need to:
337             // - move children (versioned nodes) to some temporary location
338             // - copy node from source workspace
339             // - move back nodes from temporary location
340             maybeVersionNode = getVersionSessionFor(node).getNodeByIdentifier(node.getIdentifier());
341 
342             // rather check depth than folder node type - folders can be versioned too ...
343             if (maybeVersionNode.getDepth() < 4) {
344                 // we found structure node, lets move it's subnodes under temp node
345                 Iterable<Node> children = NodeUtil.getNodes(maybeVersionNode);
346                 tempPath = RandomStringUtils.randomAlphabetic(16);
347                 Node tempNode = parent.addNode(tempPath);
348                 for (Node child : children) {
349                     NodeUtil.createPath(parent, StringUtils.substringAfter(tempNode.getPath() + child.getParent().getPath(), "/"), NodeTypes.Folder.NAME);
350                     NodeUtil.copyInSession(child, tempNode.getPath() + child.getPath());
351                 }
352             }
353         } catch (ItemNotFoundException e) {
354             // this is fine, node is not versioned yet
355         }
356         // this will remove structure node if it has same uuid as the new versioned node
357         try {
358             parent.getSession().getWorkspace().getSession().importXML(
359                     NodeUtil.createPath(parent, getSavePath(node), NodeTypes.Folder.NAME, true).getPath(),
360                     inStream,
361                     ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING);
362         } finally {
363             IOUtils.closeQuietly(inStream);
364         }
365         // we need to check whether tempPath is not null and if not, move all nodes back where they should be
366         if (StringUtils.isNotBlank(tempPath)) {
367             moveChildren(parent, parent.getNode(tempPath), tempPath);
368             // finally remove temp node
369             parent.getNode(tempPath).remove();
370             parent.getSession().save();
371         }
372         file.delete();
373     }
374 
375     private void moveChildren(Node parent, Node node, String temPath) throws RepositoryException {
376         for (Node child : NodeUtil.getNodes(node)) {
377             if (parent.hasNode(StringUtils.substringAfter(child.getPath(), "/" + temPath + "/"))) {
378                 moveChildren(parent.getNode(StringUtils.substringAfter(child.getPath(), "/" + temPath + "/")), child, temPath);
379             } else {
380                 NodeUtil.moveNode(child, parent);
381             }
382         }
383     }
384 
385     /**
386      * Merge all non reserved properties.
387      */
388     private void updateProperties(Node source, Node destination) throws RepositoryException {
389         PropertyIterator properties = source.getProperties();
390         while (properties.hasNext()) {
391             Property property = properties.nextProperty();
392             // exclude system property Rule and Version specific properties which were created on version
393             String propertyName = property.getName();
394             if (propertyName.equalsIgnoreCase(VersionManager.PROPERTY_RULE)) {
395                 continue;
396             }
397             try {
398                 if (property.getDefinition().isProtected()) {
399                     continue;
400                 }
401                 if ("jcr:isCheckedOut".equals(propertyName)) {
402                     // do not attempt to restore isCheckedOut property as it makes no sense to restore versioned node with
403                     // checkedOut status and value for this property might not be set even though the property itself is set.
404                     // Since JCR-1272 attempt to restore the property with no value will end up with RepositoryException instead
405                     // of ConstraintViolationException and hence will not be caught by the catch{} block below.
406                     continue;
407                 }
408                 if (property.getType() == PropertyType.REFERENCE) {
409                     // first check for the referenced node existence
410                     String destWorkspaceName = destination.getSession().getWorkspace().getName();
411                     try {
412                         getSession(destWorkspaceName).getNodeByIdentifier(property.getString());
413                     } catch (ItemNotFoundException e) {
414                         String repositoryId = repositoryManager.getRepositoryNameForWorkspace(destWorkspaceName);
415                         if (!StringUtils.equalsIgnoreCase(destWorkspaceName, repositoryId + "-" + RepositoryConstants.VERSION_STORE)) {
416                             throw e;
417                         }
418                         // get referenced node under temporary store
419                         // use jcr import, there is no other way to get a node without sub hierarchy
420                         String srcWorkspaceName = source.getSession().getWorkspace().getName();
421                         Node referencedNode = getSession(srcWorkspaceName).getNodeByIdentifier(property.getString());
422                         try {
423                             this.importNode(getTemporaryPath(source), referencedNode);
424                             this.removeProperties(getVersionSessionFor(source).getNodeByIdentifier(property.getString()));
425                             getTemporaryPath(source).save();
426                         } catch (IOException ioe) {
427                             log.error("Failed to import referenced node", ioe);
428                         }
429                     }
430                 }
431                 if (property.getDefinition().isMultiple()) {
432                     destination.setProperty(propertyName, property.getValues());
433                 } else {
434                     destination.setProperty(propertyName, property.getValue());
435                 }
436             } catch (ConstraintViolationException e) {
437                 log.debug("Property {} is a reserved property", propertyName);
438             }
439         }
440     }
441 
442     /**
443      * Get session of the specified workspace. Always uses the System Context.
444      */
445     private Session getSession(String workspaceId) throws RepositoryException {
446         return systemContext.getJCRSession(workspaceId);
447     }
448 
449     private Session getVersionSessionFor(Node source) throws RepositoryException {
450         final String versionWorkspace = VersionUtil.getVersionWorkspaceForNode(repositoryManager, source);
451         return systemContext.getJCRSession(versionWorkspace);
452     }
453 
454     /**
455      * Get temporary node.
456      */
457     private Node getTemporaryPath(Node node) throws RepositoryException {
458         return getVersionSessionFor(node).getNode("/" + VersionManager.TMP_REFERENCED_NODES);
459     }
460 
461     static String getSavePath(Node node) throws RepositoryException {
462         String uuid = node.getIdentifier();
463         return String.format("%s/%s/%s", StringUtils.substring(uuid, 0, 2), StringUtils.substring(uuid, 9, 11), StringUtils.substring(uuid, 14, 16));
464     }
465 }