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