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