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.Content;
37  import info.magnolia.cms.core.Path;
38  import info.magnolia.cms.security.JCRSessionOp;
39  import info.magnolia.cms.security.PermissionUtil;
40  import info.magnolia.cms.util.Rule;
41  import info.magnolia.context.MgnlContext;
42  import info.magnolia.context.SystemContext;
43  import info.magnolia.jcr.predicate.RuleBasedNodePredicate;
44  import info.magnolia.jcr.util.NodeTypes;
45  import info.magnolia.jcr.util.NodeUtil;
46  import info.magnolia.jcr.util.VersionUtil;
47  import info.magnolia.repository.RepositoryConstants;
48  import info.magnolia.repository.RepositoryManager;
49  import info.magnolia.repository.definition.RepositoryDefinition;
50  
51  import java.io.ByteArrayInputStream;
52  import java.io.ByteArrayOutputStream;
53  import java.io.IOException;
54  import java.io.InvalidClassException;
55  import java.io.ObjectInput;
56  import java.io.ObjectInputStream;
57  import java.io.ObjectOutput;
58  import java.io.ObjectOutputStream;
59  import java.util.ArrayList;
60  import java.util.Collection;
61  import java.util.List;
62  
63  import javax.inject.Inject;
64  import javax.jcr.ItemNotFoundException;
65  import javax.jcr.Node;
66  import javax.jcr.NodeIterator;
67  import javax.jcr.PathNotFoundException;
68  import javax.jcr.RepositoryException;
69  import javax.jcr.Session;
70  import javax.jcr.UnsupportedRepositoryOperationException;
71  import javax.jcr.Value;
72  import javax.jcr.nodetype.NodeType;
73  import javax.jcr.version.Version;
74  import javax.jcr.version.VersionException;
75  import javax.jcr.version.VersionHistory;
76  import javax.jcr.version.VersionIterator;
77  import javax.jcr.version.VersionManager;
78  
79  import org.apache.commons.codec.binary.Base64;
80  import org.apache.commons.io.IOUtils;
81  import org.apache.jackrabbit.JcrConstants;
82  import org.slf4j.Logger;
83  import org.slf4j.LoggerFactory;
84  
85  /**
86   * This version manager uses an extra workspace to manage the versions. The
87   * workspace maintains a flat hierarchy. The content is then finally versioned
88   * using JCR versioning which also copies the sub-nodes.
89   *
90   * The mix:versionable is only added on the top level nodes.
91   */
92  public abstract class BaseVersionManager {
93  
94      private static final Logger log = LoggerFactory.getLogger(BaseVersionManager.class);
95  
96      /**
97       * Name of the workspace.
98       */
99      public static final String VERSION_WORKSPACE = "mgnlVersion";
100 
101     /**
102      * Node which contains stubs for referenced nodes. We have to copy them to the workspace as well.
103      */
104     public static final String TMP_REFERENCED_NODES = "mgnl:tmpReferencedNodes";
105 
106     /**
107      * Sub-node containing the data used for the version/restore process.
108      */
109     protected static final String SYSTEM_NODE = "mgnl:versionMetaData";
110 
111     /**
112      * Property name for collection rule. The rule defines which sub-nodes belong to a node: page and paragraphs.
113      */
114     public static final String PROPERTY_RULE = "Rule";
115 
116     /**
117      * JCR version store root.
118      */
119     protected static final String ROOT_VERSION = "jcr:rootVersion";
120 
121     private Collection<String> versionWorkspaces = new ArrayList<>();
122 
123     private final SystemContext systemContext;
124     private final RepositoryManager repositoryManager;
125     private final CopyUtil copyUtil;
126 
127     @Inject
128     public BaseVersionManager(SystemContext systemContext, RepositoryManager repositoryManager, CopyUtil copyUtil) {
129         this.systemContext = systemContext;
130         this.repositoryManager = repositoryManager;
131         this.copyUtil = copyUtil;
132         for (RepositoryDefinition repositoryDefinition : repositoryManager.getRepositoryDefinitions()) {
133             String repositoryId = repositoryDefinition.getName();
134             String workspaceName = repositoryId + "-" + RepositoryConstants.VERSION_STORE;
135             if (repositoryManager.getWorkspaceMapping(workspaceName) != null) {
136                 versionWorkspaces.add(workspaceName);
137             } else {
138                 throw new RuntimeException(String.format("Something went wrong, version workspace for repository %s does not exist.", repositoryId));
139             }
140         }
141     }
142 
143     /**
144      * Create structure needed for version store workspace.
145      *
146      * @throws RepositoryException if unable to create magnolia system structure
147      */
148     protected void createInitialStructure() throws RepositoryException {
149         for (String workspaceName : versionWorkspaces) {
150             MgnlContext.doInSystemContext(new JCRSessionOp<Void>(workspaceName) {
151 
152                 @Override
153                 public Void exec(Session session) throws RepositoryException {
154                     try {
155                         Node tmp = session.getNode("/" + TMP_REFERENCED_NODES);
156                         // remove nodes if they are no longer referenced within this workspace
157                         NodeIterator children = tmp.getNodes();
158                         while (children.hasNext()) {
159                             Node child = children.nextNode();
160                             if (child.getReferences().getSize() < 1) {
161                                 child.remove();
162                             }
163                         }
164                     } catch (PathNotFoundException e) {
165                         session.getRootNode().addNode(TMP_REFERENCED_NODES, NodeTypes.System.NAME);
166                     }
167                     session.save();
168 
169                     return null;
170                 }
171             });
172         }
173     }
174 
175     /**
176      * Add version of the specified node and all child nodes while ignoring the same node type.
177      *
178      * @param node to be versioned
179      * @return newly created version node
180      * @throws UnsupportedOperationException if repository implementation does not support Versions API
181      * @throws javax.jcr.RepositoryException if any repository error occurs
182      */
183     public synchronized Version addVersion(Node node) throws UnsupportedRepositoryOperationException,
184             RepositoryException {
185         Rule rule = new Rule(VersionUtil.getNodeTypeName(node) + "," + NodeTypes.System.NAME, ",");
186         rule.reverse();
187         return this.addVersion(node, rule);
188     }
189 
190     /**
191      * Add version of the specified node and all child nodes while ignoring the same node type.
192      *
193      * @param node to be versioned
194      * @return newly created version node
195      * @throws UnsupportedOperationException if repository implementation does not support Versions API
196      * @throws javax.jcr.RepositoryException if any repository error occurs
197      */
198     public synchronized Version addVersion(final Node node, final Rule rule) throws UnsupportedRepositoryOperationException,
199             RepositoryException {
200         final String userName = getSafelyUserNameFromMgnlContext();
201         final String workspaceName = node.getSession().getWorkspace().getName();
202         Version version = MgnlContext.doInSystemContext(new JCRSessionOp<Version>(workspaceName) {
203 
204             @Override
205             public Version exec(Session session) throws RepositoryException {
206                 try {
207                     return createVersion(session.getNodeByIdentifier(node.getIdentifier()), rule, userName);
208                 } catch (RepositoryException re) {
209                     // since add version is synchronized on a singleton object, its safe to revert all changes made in
210                     // the session attached to workspace - mgnlVersion
211                     log.error("failed to copy versionable node to version store, reverting all changes made in this session", re);
212                     String versionWorkspaceName = VersionUtil.getVersionWorkspaceForNode(repositoryManager, node);
213                     // We're in SystemContext
214                     MgnlContext.getJCRSession(versionWorkspaceName).refresh(false);
215                     throw re;
216                 }
217 
218             }
219         });
220         return version;
221     }
222 
223     /**
224      * Create version of the specified node and all child nodes based on the given <code>Rule</code>.
225      *
226      * @param node to be versioned
227      * @return newly created version node
228      * @throws UnsupportedOperationException if repository implementation does not support Versions API
229      * @throws javax.jcr.RepositoryException if any repository error occurs
230      * @deprecated since 4.5 use {@link #createVersion(Node, Rule, String)} instead
231      */
232     @Deprecated
233     protected Version createVersion(Content node, Rule rule) throws UnsupportedRepositoryOperationException, RepositoryException {
234         return createVersion(node.getJCRNode(), rule, getSafelyUserNameFromMgnlContext());
235     }
236 
237     private String getSafelyUserNameFromMgnlContext() {
238         String userName = "";
239         if (MgnlContext.getUser() != null) {
240             userName = MgnlContext.getUser().getName();
241         }
242         return userName;
243     }
244 
245     /**
246      * Create version of the specified node and all child nodes based on the given <code>Rule</code>.
247      * This will return null if can't create new version in case when max version index is lower then one
248      * or when content is marked for deletion.
249      *
250      * @param node to be versioned
251      * @return newly created version node
252      * @throws UnsupportedOperationException if repository implementation does not support Versions API
253      * @throws javax.jcr.RepositoryException if any repository error occurs
254      */
255     protected Version createVersion(Node node, Rule rule, String userName) throws UnsupportedRepositoryOperationException,
256             RepositoryException {
257         if (isInvalidMaxVersions()) {
258             log.debug("Ignore create version, MaxVersionIndex < 1");
259             return null;
260         }
261         if (node.isNodeType(NodeTypes.Deleted.NAME)) {
262             log.debug("Don't create version for content marked as deleted");
263             return null;
264         }
265 
266         copyUtil.copyToVersion(node, new RuleBasedNodePredicate(rule));
267         Node versionedNode = this.getVersionedNode(node);
268 
269         checkAndAddMixin(versionedNode);
270         Node systemInfo = this.getSystemNode(versionedNode);
271         // add serialized rule which was used to create this version
272         ByteArrayOutputStream out = new ByteArrayOutputStream();
273         try {
274             ObjectOutput objectOut = new ObjectOutputStream(out);
275             objectOut.writeObject(rule);
276             objectOut.flush();
277             objectOut.close();
278             // PROPERTY_RULE is not a part of MetaData to allow versioning of node types which does NOT support MetaData
279             systemInfo.setProperty(PROPERTY_RULE, new String(Base64.encodeBase64(out.toByteArray())));
280         } catch (IOException e) {
281             throw new RepositoryException("Unable to add serialized Rule to the versioned content");
282         }
283         // add all system properties for this version
284         systemInfo.setProperty(ContentVersion.VERSION_USER, userName);
285         systemInfo.setProperty(ContentVersion.NAME, node.getName());
286 
287         versionedNode.save();
288 
289         // add version
290         VersionManager versionManager = versionedNode.getSession().getWorkspace().getVersionManager();
291         Version newVersion = versionManager.checkin(versionedNode.getPath());
292         versionManager.checkout(versionedNode.getPath());
293 
294         try {
295             this.setMaxVersionHistory(versionedNode);
296         } catch (RepositoryException re) {
297             log.error("Failed to limit version history to the maximum configured", re);
298             log.error("New version has already been created");
299         }
300 
301 
302         return newVersion;
303     }
304 
305     /**
306      * Check if max version index is lower then one.
307      */
308     public abstract boolean isInvalidMaxVersions();
309 
310     /**
311      * Get node from version store.
312      */
313     public synchronized Node getVersionedNode(Node node) throws RepositoryException {
314         try {
315             final String versionWorkspace = VersionUtil.getVersionWorkspaceForNode(repositoryManager, node);
316             final Session versionSession = systemContext.getJCRSession(versionWorkspace);
317             return versionSession.getNodeByIdentifier(node.getUUID());
318         } catch (ItemNotFoundException e) {
319             // node is not versioned yet
320             return null;
321         }
322     }
323 
324     /**
325      * Get node from version store.
326      */
327     protected Node getVersionedNode(Session session, String uuid) throws RepositoryException {
328         try {
329             return session.getNodeByIdentifier(uuid);
330         } catch (ItemNotFoundException e) {
331             // node is not versioned yet
332             return null;
333         }
334     }
335 
336     /**
337      * Set version history to max version possible.
338      *
339      * @throws RepositoryException if failed to get VersionHistory or fail to remove
340      */
341     public abstract void setMaxVersionHistory(Node node) throws RepositoryException;
342 
343     /**
344      * Get history of this node as recorded in the version store.
345      *
346      * @return version history of the given node
347      * @throws UnsupportedOperationException if repository implementation does not support Versions API
348      * @throws javax.jcr.RepositoryException if any repository error occurs
349      */
350     public synchronized VersionHistory getVersionHistory(Node node) throws UnsupportedRepositoryOperationException,
351             RepositoryException {
352         try {
353             Node versionedNode = this.getVersionedNode(node);
354             if (versionedNode == null) {
355                 // node does not exist in version store so no version history
356                 log.debug("No VersionHistory found for {} node.", node);
357                 return null;
358             }
359             return versionedNode.getVersionHistory();
360         } catch (UnsupportedRepositoryOperationException e) {
361             log.debug("Node {} is not versionable.", node);
362             // node is not versionable or underlying repo doesn't support versioning.
363             return null;
364         }
365     }
366 
367     /**
368      * /** Get named version.
369      *
370      * @throws UnsupportedOperationException if repository implementation does not support Versions API
371      * @throws javax.jcr.RepositoryException if any repository error occurs
372      */
373     public synchronized Version getVersion(Node node, String name) throws UnsupportedRepositoryOperationException,
374             RepositoryException {
375         VersionHistory history = this.getVersionHistory(node);
376         if (history != null) {
377             return new VersionedNode(history.getVersion(name), node);
378         }
379         log.error("Node {} was never versioned", node.getPath());
380         return null;
381     }
382 
383     /**
384      * Returns the current base version of given node.
385      */
386     public Version getBaseVersion(Node node) throws UnsupportedOperationException, RepositoryException {
387         Node versionedNode = this.getVersionedNode(node);
388         if (versionedNode != null) {
389             VersionManager versionManager = versionedNode.getSession().getWorkspace().getVersionManager();
390             return versionManager.getBaseVersion(versionedNode.getPath());
391         }
392 
393         throw new RepositoryException("Node " + node.getPath() + " was never versioned");
394     }
395 
396     /**
397      * Get all versions.
398      *
399      * @return Version iterator retrieved from version history
400      * @throws UnsupportedOperationException if repository implementation does not support Versions API
401      * @throws javax.jcr.RepositoryException if any repository error occurs
402      */
403     public synchronized VersionIterator getAllVersions(Node node) throws UnsupportedRepositoryOperationException, RepositoryException {
404         Node versionedNode = this.getVersionedNode(node);
405         if (versionedNode == null) {
406             // node does not exist in version store so no versions
407             return null;
408         }
409         VersionManager versionManager = versionedNode.getSession().getWorkspace().getVersionManager();
410         return versionManager.getVersionHistory(versionedNode.getPath()).getAllVersions();
411     }
412 
413     /**
414      * Restore specified version.
415      *
416      * @param node to be restored
417      * @param version to be used
418      * @throws javax.jcr.version.VersionException if the specified <code>versionName</code> does not exist in this
419      * node's version history
420      * @throws javax.jcr.RepositoryException if an error occurs
421      * @deprecated since 4.5 use {@link #restore(Node, Version, boolean)} instead
422      */
423     @Deprecated
424     public synchronized void restore(Content node, Version version, boolean removeExisting) throws VersionException, UnsupportedRepositoryOperationException, RepositoryException {
425         restore(node.getJCRNode(), version, removeExisting);
426     }
427 
428     /**
429      * Restore specified version.
430      *
431      * @param node to be restored
432      * @param version to be used
433      * @throws javax.jcr.version.VersionException if the specified <code>versionName</code> does not exist in this
434      * node's version history
435      * @throws javax.jcr.RepositoryException if an error occurs
436      */
437     public synchronized void restore(final Node node, Version version, boolean removeExisting) throws VersionException, UnsupportedRepositoryOperationException, RepositoryException {
438         // FYI: restore is done in SC!!! Check permissions manually
439         PermissionUtil.verifyIsGrantedOrThrowException(node.getSession(), node.getPath(), Session.ACTION_SET_PROPERTY + "," + Session.ACTION_REMOVE + "," + Session.ACTION_ADD_NODE);
440 
441         // get the cloned node from version store
442         final Node versionedNode = this.getVersionedNode(node);
443 
444         final Version unwrappedVersion;
445         if (version instanceof VersionedNode) {
446             unwrappedVersion = VersionUtil.unwrap(((VersionedNode) version).unwrap());
447         } else {
448             unwrappedVersion = VersionUtil.unwrap(version);
449         }
450 
451         VersionManager versionManager = versionedNode.getSession().getWorkspace().getVersionManager();
452         versionManager.restore(unwrappedVersion, removeExisting);
453         versionManager.checkout(versionedNode.getPath());
454         MgnlContext.doInSystemContext(new JCRSessionOp<Void>(versionedNode.getSession().getWorkspace().getName()) {
455 
456             @Override
457             public Void exec(Session session) throws RepositoryException {
458                 //mixins are NOT restored automatically
459                 List<String> mixins = new ArrayList<String>();
460                 for (Value v : unwrappedVersion.getNode(JcrConstants.JCR_FROZENNODE).getProperty("jcr:frozenMixinTypes").getValues()) {
461                     mixins.add(v.getString());
462                 }
463 
464                 final Node systemVersionedNode = session.getNodeByIdentifier(versionedNode.getUUID());
465                 for (NodeType nt : versionedNode.getMixinNodeTypes()) {
466                     if (!mixins.remove(nt.getName())) {
467                         systemVersionedNode.removeMixin(nt.getName());
468                     }
469                 }
470                 for (String mix : mixins) {
471                     systemVersionedNode.addMixin(mix);
472                 }
473                 systemVersionedNode.save();
474 
475                 try {
476                     // using system context here is forced by the fact that JR will use same context to also check source (mgnlVersion) repo READ permission and ordinary user has no rights in this workspace
477                     Session sysDestinationSession = MgnlContext.getJCRSession(node.getSession().getWorkspace().getName());
478                     log.debug("restoring info:{}:{}", sysDestinationSession.getWorkspace().getName(), node.getPath());
479                     Node destinationNode = sysDestinationSession.getNode(node.getPath());
480                     // if restored, update original node with the restored node and its subtree
481                     Rule rule = getUsedFilter(versionedNode);
482                     try {
483                         copyUtil.copyFromVersion(versionedNode, destinationNode, new RuleBasedNodePredicate(rule));
484                         if (NodeUtil.hasMixin(destinationNode, NodeTypes.Deleted.NAME)) {
485                             destinationNode.removeMixin(NodeTypes.Deleted.NAME);
486                         }
487                         // next line is required because restore is done via clone op that preserves last modification date otherwise.
488                         NodeTypes.LastModified.update(destinationNode);
489                         destinationNode.save();
490                         // node was updated in system context and we should make sure it is notified of the changes
491                         node.refresh(false);
492                     } catch (RepositoryException re) {
493                         log.debug("error during restore: {}", re.getMessage(), re);
494                         log.error("failed to restore versioned node, reverting all changes make to this node");
495                         destinationNode.refresh(false);
496                         throw re;
497                     }
498                 } catch (IOException e) {
499                     throw new RepositoryException(e);
500                 } catch (ClassNotFoundException e) {
501                     throw new RepositoryException(e);
502                 }
503                 return null;
504             }
505         });
506     }
507 
508     /**
509      * Removes all versions of the node associated with given UUID.
510      *
511      * @throws RepositoryException if fails to remove versioned node from the version store
512      */
513     public synchronized void removeVersionHistory(final Node node) throws RepositoryException {
514         PermissionUtil.verifyIsGrantedOrThrowException(node.getSession(), Path.getAbsolutePath(node.getPath()), Session.ACTION_ADD_NODE + "," + Session.ACTION_REMOVE + "," + Session.ACTION_SET_PROPERTY);
515         final String uuid = node.getIdentifier();
516         String workspaceName = repositoryManager.getRepositoryNameForWorkspace(node.getSession().getWorkspace().getName()) + "-" + RepositoryConstants.VERSION_STORE;
517         MgnlContext.doInSystemContext(new JCRSessionOp<Void>(workspaceName) {
518 
519             @Override
520             public Void exec(Session session) throws RepositoryException {
521                 Node node = getVersionedNode(session, uuid);
522                 if (node != null) {
523                     if (node.getReferences().getSize() < 1) {
524                         // remove node from the version store only if its not referenced
525                         node.remove();
526                     } else { // remove all associated versions
527                         VersionManager versionManager = node.getSession().getWorkspace().getVersionManager();
528                         VersionHistory history = versionManager.getVersionHistory(node.getPath());
529                         VersionIterator versions = history.getAllVersions();
530                         if (versions != null) {
531                             // skip root version
532                             versions.nextVersion();
533                             while (versions.hasNext()) {
534                                 history.removeVersion(versions.nextVersion().getName());
535                             }
536                         }
537                     }
538                 }
539                 session.save();
540                 return null;
541             }
542         });
543     }
544 
545     /**
546      * Verifies the existence of the mix:versionable and adds it if not.
547      */
548     protected void checkAndAddMixin(Node node) throws RepositoryException {
549         if (!node.isNodeType("mix:versionable")) {
550             log.debug("Add mix:versionable");
551             node.addMixin("mix:versionable");
552         }
553     }
554 
555     /**
556      * Get Rule used for this version.
557      */
558     protected Rule getUsedFilter(Node versionedNode) throws IOException, ClassNotFoundException, RepositoryException {
559         // if restored, update original node with the restored node and its subtree
560         ByteArrayInputStream inStream = null;
561         try {
562             String ruleString = this.getSystemNode(versionedNode).getProperty(PROPERTY_RULE).getString();
563             inStream = new ByteArrayInputStream(Base64.decodeBase64(ruleString.getBytes()));
564             ObjectInput objectInput = new ObjectInputStream(inStream);
565             return (Rule) objectInput.readObject();
566         } catch (InvalidClassException e) {
567             log.debug(e.getMessage());
568             log.debug("Will return default rule -> all child nodes while ignoring versionedNode nodeType.");
569             Rule rule = new Rule(VersionUtil.getNodeTypeName(versionedNode) + "," + NodeTypes.System.NAME, ",");
570             rule.reverse();
571             return rule;
572         } catch (IOException e) {
573             throw e;
574         } catch (ClassNotFoundException e) {
575             throw e;
576         } finally {
577             IOUtils.closeQuietly(inStream);
578         }
579     }
580 
581     /**
582      * Get the Magnolia system node created under the given node.
583      *
584      * @throws RepositoryException if failed to create system node
585      */
586     protected synchronized Node getSystemNode(Node node) throws RepositoryException {
587         if (node.hasNode(SYSTEM_NODE)) {
588             return node.getNode(SYSTEM_NODE);
589         }
590         return node.addNode(SYSTEM_NODE, NodeTypes.System.NAME);
591     }
592 }