View Javadoc

1   /**
2    * This file Copyright (c) 2003-2011 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.ItemType;
38  import info.magnolia.cms.core.Path;
39  import info.magnolia.cms.security.JCRSessionOp;
40  import info.magnolia.cms.security.PermissionUtil;
41  import info.magnolia.cms.util.ExclusiveWrite;
42  import info.magnolia.cms.util.Rule;
43  import info.magnolia.context.MgnlContext;
44  import info.magnolia.jcr.predicate.RuleBasedNodePredicate;
45  import info.magnolia.jcr.util.NodeUtil;
46  import info.magnolia.jcr.util.VersionUtil;
47  
48  import java.io.ByteArrayInputStream;
49  import java.io.ByteArrayOutputStream;
50  import java.io.IOException;
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.slf4j.Logger;
76  import org.slf4j.LoggerFactory;
77  
78  /**
79   * This version manager uses an extra workspace to manage the versions. The
80   * workspace maintains a flat hierarchy. The content is then finally versioned
81   * using JCR versioning which also copies the sub-nodes.
82   *
83   * The mix:versionable is only added on the top level nodes.
84   *
85   * @version $Id$
86   */
87  public abstract class BaseVersionManager {
88  
89      /**
90       * Name of the workspace.
91       */
92      public static final String VERSION_WORKSPACE = "mgnlVersion";
93  
94      /**
95       * Node which contains stubs for referenced nodes. We have to copy them to the workspace as well.
96       */
97      public static final String TMP_REFERENCED_NODES = "mgnl:tmpReferencedNodes";
98  
99      /**
100      * Sub-node containing the data used for the version/restore process.
101      */
102     protected static final String SYSTEM_NODE = "mgnl:versionMetaData";
103 
104     /**
105      * Property name for collection rule. The rule defines which sub-nodes belong to a node: page and paragraphs.
106      */
107     public static final String PROPERTY_RULE = "Rule";
108 
109     /**
110      * JCR version store root.
111      */
112     protected static final String ROOT_VERSION = "jcr:rootVersion";
113 
114     private static Logger log = LoggerFactory.getLogger(BaseVersionManager.class);
115 
116     /**
117      * Create structure needed for version store workspace.
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                 }
136                 catch (PathNotFoundException e) {
137                     session.getRootNode().addNode(VersionManager.TMP_REFERENCED_NODES, ItemType.SYSTEM.getSystemName());
138                 }
139                 session.save();
140 
141                 return null;
142             }
143         });
144 
145     }
146 
147     /**
148      * Add version of the specified node and all child nodes while ignoring the same node type.
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(new String[] {node.getNodeType().getName(), ItemType.SYSTEM.getSystemName()});
157         Rule rule = new Rule(VersionUtil.getNodeTypeName(node) + "," + ItemType.SYSTEM.getSystemName(), ",");
158         rule.reverse();
159         return this.addVersion(node, rule);
160     }
161 
162     /**
163      * Add version of the specified node and all child nodes while ignoring the same node type.
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                 }
179                 catch (RepositoryException re) {
180                     // since add version is synchronized on a singleton object, its safe to revert all changes made in
181                     // the session attached to workspace - mgnlVersion
182                     log.error("failed to copy versionable node to version store, reverting all changes made in this session");
183                     getSession().refresh(false);
184                     throw re;
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      * @param node to be versioned
194      * @param rule
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)} 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      * @param node to be versioned
216      * @param rule
217      * @param userName
218      * @return newly created version node
219      * @throws UnsupportedOperationException if repository implementation does not support Versions API
220      * @throws javax.jcr.RepositoryException if any repository error occurs
221      */
222     protected Version createVersion(Node node, Rule rule, String userName) throws UnsupportedRepositoryOperationException,
223     RepositoryException {
224         if (isInvalidMaxVersions()) {
225             log.debug("Ignore create version, MaxVersionIndex < 1");
226             log.debug("Returning root version of the source node");
227             return node.getVersionHistory().getRootVersion();
228         }
229 
230         CopyUtil.getInstance().copyToversion(node, new RuleBasedNodePredicate(rule));
231         Node versionedNode = this.getVersionedNode(node);
232 
233         checkAndAddMixin(versionedNode);
234         Node systemInfo = this.getSystemNode(versionedNode);
235         // add serialized rule which was used to create this version
236         ByteArrayOutputStream out = new ByteArrayOutputStream();
237         try {
238             ObjectOutput objectOut = new ObjectOutputStream(out);
239             objectOut.writeObject(rule);
240             objectOut.flush();
241             objectOut.close();
242             // PROPERTY_RULE is not a part of MetaData to allow versioning of node types which does NOT support MetaData
243             systemInfo.setProperty(PROPERTY_RULE, new String(Base64.encodeBase64(out.toByteArray())));
244         }
245         catch (IOException e) {
246             throw new RepositoryException("Unable to add serialized Rule to the versioned content");
247         }
248         // add all system properties for this version
249         systemInfo.setProperty(ContentVersion.VERSION_USER, userName);
250         systemInfo.setProperty(ContentVersion.NAME, node.getName());
251 
252         versionedNode.save();
253         // add version
254         Version newVersion = versionedNode.checkin();
255         versionedNode.checkout();
256 
257         try {
258             this.setMaxVersionHistory(versionedNode);
259         }
260         catch (RepositoryException re) {
261             log.error("Failed to limit version history to the maximum configured", re);
262             log.error("New version has already been created");
263         }
264 
265         return newVersion;
266     }
267 
268     /**
269      * Check if version index is set to negative number.
270      */
271     public abstract boolean isInvalidMaxVersions();
272 
273     /**
274      * Get node from version store.
275      */
276     public synchronized Node getVersionedNode(Node node) throws RepositoryException {
277         return getVersionedNode(node.getIdentifier());
278     }
279 
280     /**
281      * Get node from version store.
282      */
283     protected Node getVersionedNode(String uuid) throws RepositoryException {
284         try {
285             return getSession().getNodeByIdentifier(uuid);
286         } catch (ItemNotFoundException e) {
287             // node is not versioned yet
288             return null;
289         }
290     }
291 
292     /**
293      * Set version history to max version possible.
294      * @throws RepositoryException if failed to get VersionHistory or fail to remove
295      */
296     public abstract void setMaxVersionHistory(Node node) throws RepositoryException;
297 
298     /**
299      * Get history of this node as recorded in the version store.
300      * @param node
301      * @return version history of the given node
302      * @throws UnsupportedOperationException if repository implementation does not support Versions API
303      * @throws javax.jcr.RepositoryException if any repository error occurs
304      */
305     public synchronized VersionHistory getVersionHistory(Node node) throws UnsupportedRepositoryOperationException,
306     RepositoryException {
307         try {
308             Node versionedNode = this.getVersionedNode(node);
309             if (versionedNode == null) {
310                 // node does not exist in version store so no version history
311                 log.debug("No VersionHistory found for {} node.", node);
312                 return null;
313             }
314             return versionedNode.getVersionHistory();
315         } catch (UnsupportedRepositoryOperationException e) {
316             log.debug("Node {} is not versionable.", node);
317             // node is not versionable or underlying repo doesn't support versioning.
318             return null;
319         }
320     }
321 
322     /**
323      * /** Get named version.
324      * 
325      * @throws UnsupportedOperationException
326      *             if repository implementation does not support Versions API
327      * @throws javax.jcr.RepositoryException
328      *             if any repository error occurs
329      */
330     public synchronized Version getVersion(Node node, String name) throws UnsupportedRepositoryOperationException,
331     RepositoryException {
332         VersionHistory history = this.getVersionHistory(node);
333         if (history != null) {
334             return new VersionedNode(history.getVersion(name), node);
335         }
336         log.error("Node " + node.getPath() + " was never versioned");
337         return null;
338     }
339 
340     /**
341      * Returns the current base version of given node.
342      * @throws UnsupportedRepositoryOperationException
343      * @throws RepositoryException
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      * @param node
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      * @param node to be restored
373      * @param version to be used
374      * @param removeExisting
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      * @throws javax.jcr.version.VersionException
379      * @deprecated since 4.5 use {@link #restore(Node, Version, boolean)} instead
380      */
381     @Deprecated
382     public synchronized void restore(Content node, Version version, boolean removeExisting) throws VersionException, UnsupportedRepositoryOperationException, RepositoryException {
383         restore(node.getJCRNode(), version, removeExisting);
384     }
385     /**
386      * Restore specified version.
387      * @param node to be restored
388      * @param version to be used
389      * @param removeExisting
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      * @throws javax.jcr.version.VersionException
394      */
395     public synchronized void restore(final Node node, Version version, boolean removeExisting) throws VersionException, UnsupportedRepositoryOperationException, RepositoryException {
396         // FYI: restore is done in SC!!! Check permissions manually
397         PermissionUtil.verifyIsGrantedOrThrowException(node.getSession(), node.getPath(), Session.ACTION_SET_PROPERTY + "," + Session.ACTION_REMOVE + "," + Session.ACTION_ADD_NODE);
398 
399         // get the cloned node from version store
400         final Node versionedNode = this.getVersionedNode(node);
401 
402         final Version unwrappedVersion;
403         if (version instanceof VersionedNode) {
404             unwrappedVersion = ((VersionedNode) version).unwrap();
405         } else {
406             unwrappedVersion = version;
407         }
408 
409         versionedNode.restore(unwrappedVersion, removeExisting);
410         versionedNode.checkout();
411         MgnlContext.doInSystemContext(new JCRSessionOp<Void>(versionedNode.getSession().getWorkspace().getName()) {
412 
413             @Override
414             public Void exec(Session session) throws RepositoryException {
415                 //mixins are NOT restored automatically
416                 List<String> mixins = new ArrayList<String>();
417                 for (Value v: unwrappedVersion.getNode(ItemType.JCR_FROZENNODE).getProperty("jcr:frozenMixinTypes").getValues()) {
418                     mixins.add(v.getString());
419                 }
420 
421                 final Node systemVersionedNode = session.getNodeByIdentifier(versionedNode.getUUID());
422                 for (NodeType nt : versionedNode.getMixinNodeTypes()) {
423                     if (!mixins.remove(nt.getName())) {
424                         systemVersionedNode.removeMixin(nt.getName());
425                     }
426                 }
427                 for (String mix : mixins) {
428                     systemVersionedNode.addMixin(mix);
429                 }
430                 systemVersionedNode.save();
431 
432                 try {
433                     // if restored, update original node with the restored node and its subtree
434                     Rule rule = getUsedFilter(versionedNode);
435                     try {
436                         synchronized (ExclusiveWrite.getInstance()) {
437                             CopyUtil.getInstance().copyFromVersion(versionedNode, node, new RuleBasedNodePredicate(rule));
438                             if (NodeUtil.hasMixin(node, ItemType.DELETED_NODE_MIXIN)) {
439                                 node.removeMixin(ItemType.DELETED_NODE_MIXIN);
440                             }
441                             node.save();
442                         }
443                     }
444                     catch (RepositoryException re) {
445                         log.error("failed to restore versioned node, reverting all changes make to this node");
446                         node.refresh(false);
447                         throw re;
448                     }
449                 }
450                 catch (IOException e) {
451                     throw new RepositoryException(e);
452                 }
453                 catch (ClassNotFoundException e) {
454                     throw new RepositoryException(e);
455                 }
456                 return null;
457             }
458         });
459     }
460 
461     /**
462      * Removes all versions of the node associated with given UUID.
463      * @throws RepositoryException if fails to remove versioned node from the version store
464      */
465     public synchronized void removeVersionHistory(final Node node) throws RepositoryException {
466         PermissionUtil.verifyIsGrantedOrThrowException(node.getSession(), Path.getAbsolutePath(node.getPath()), Session.ACTION_ADD_NODE + "," + Session.ACTION_REMOVE + "," + Session.ACTION_SET_PROPERTY);
467         final String uuid = node.getIdentifier();
468         MgnlContext.doInSystemContext(new JCRSessionOp<Void>(VersionManager.VERSION_WORKSPACE) {
469 
470             @Override
471             public Void exec(Session session) throws RepositoryException {
472                 Node node = getVersionedNode(uuid);
473                 if (node != null) {
474                     if (node.getReferences().getSize() < 1) {
475                         // remove node from the version store only if its not referenced
476                         node.remove();
477                     } else { // remove all associated versions
478                         VersionHistory history = node.getVersionHistory();
479                         VersionIterator versions = history.getAllVersions();
480                         if (versions != null) {
481                             // skip root version
482                             versions.nextVersion();
483                             while (versions.hasNext()) {
484                                 history.removeVersion(versions.nextVersion().getName());
485                             }
486                         }
487                     }
488                 }
489                 session.save();
490                 return null;
491             }
492         });
493     }
494 
495     /**
496      * Verifies the existence of the mix:versionable and adds it if not.
497      */
498     protected void checkAndAddMixin(Node node) throws RepositoryException {
499         if(!node.isNodeType("mix:versionable")){
500             log.debug("Add mix:versionable");
501             node.addMixin("mix:versionable");
502         }
503     }
504 
505     /**
506      * Get Rule used for this version.
507      * @param versionedNode
508      * @throws IOException
509      * @throws ClassNotFoundException
510      * @throws RepositoryException
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         }
521         catch (IOException e) {
522             throw e;
523         }
524         catch (ClassNotFoundException e) {
525             throw e;
526         }
527         finally {
528             IOUtils.closeQuietly(inStream);
529         }
530     }
531 
532     /**
533      * Get the Magnolia system node created under the given node.
534      * @param node
535      * @throws RepositoryException if failed to create system node
536      */
537     protected synchronized Node getSystemNode(Node node) throws RepositoryException {
538         if (node.hasNode(SYSTEM_NODE)) {
539             return node.getNode(SYSTEM_NODE);
540         }
541         return node.addNode(SYSTEM_NODE, ItemType.SYSTEM.getSystemName());
542     }
543 
544     /**
545      * Get version store hierarchy manager.
546      * @throws RepositoryException
547      * @throws LoginException
548      */
549     protected Session getSession() throws LoginException, RepositoryException {
550         return MgnlContext.getSystemContext().getJCRSession(VersionManager.VERSION_WORKSPACE);
551     }
552 }