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