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