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         Version version = MgnlContext.doInSystemContext(new JCRSessionOp<Version>(node.getSession().getWorkspace().getName()) {
172 
173             @Override
174             public Version exec(Session session) throws RepositoryException {
175                 try {
176                     return createVersion(session.getNodeByIdentifier(node.getIdentifier()), rule);
177                 }
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");
182                     getSession().refresh(false);
183                     throw re;
184                 }
185 
186             }});
187         return version;
188     }
189 
190     /**
191      * Create version of the specified node and all child nodes based on the given <code>Rule</code>.
192      * @param node to be versioned
193      * @param rule
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      * @deprecated since 4.5 use {@link #createVersion(Node, Rule)} instead
198      */
199     @Deprecated
200     protected Version createVersion(Content node, Rule rule) throws UnsupportedRepositoryOperationException, RepositoryException {
201         return createVersion(node.getJCRNode(), rule);
202     }
203 
204     /**
205      * Create version of the specified node and all child nodes based on the given <code>Rule</code>.
206      * @param node to be versioned
207      * @param rule
208      * @return newly created version node
209      * @throws UnsupportedOperationException if repository implementation does not support Versions API
210      * @throws javax.jcr.RepositoryException if any repository error occurs
211      */
212     protected Version createVersion(Node node, Rule rule) throws UnsupportedRepositoryOperationException,
213     RepositoryException {
214         if (isInvalidMaxVersions()) {
215             log.debug("Ignore create version, MaxVersionIndex < 1");
216             log.debug("Returning root version of the source node");
217             return node.getVersionHistory().getRootVersion();
218         }
219 
220         CopyUtil.getInstance().copyToversion(node, new RuleBasedNodePredicate(rule));
221         Node versionedNode = this.getVersionedNode(node);
222 
223         checkAndAddMixin(versionedNode);
224         Node systemInfo = this.getSystemNode(versionedNode);
225         // add serialized rule which was used to create this version
226         ByteArrayOutputStream out = new ByteArrayOutputStream();
227         try {
228             ObjectOutput objectOut = new ObjectOutputStream(out);
229             objectOut.writeObject(rule);
230             objectOut.flush();
231             objectOut.close();
232             // PROPERTY_RULE is not a part of MetaData to allow versioning of node types which does NOT support MetaData
233             systemInfo.setProperty(PROPERTY_RULE, new String(Base64.encodeBase64(out.toByteArray())));
234         }
235         catch (IOException e) {
236             throw new RepositoryException("Unable to add serialized Rule to the versioned content");
237         }
238         // temp fix, MgnlContext should always have user either logged-in or anonymous
239         String userName = "";
240         if (MgnlContext.getUser() != null) {
241             userName = MgnlContext.getUser().getName();
242         }
243         // add all system properties for this version
244         systemInfo.setProperty(ContentVersion.VERSION_USER, userName);
245         systemInfo.setProperty(ContentVersion.NAME, node.getName());
246 
247         versionedNode.save();
248         // add version
249         Version newVersion = versionedNode.checkin();
250         versionedNode.checkout();
251 
252         try {
253             this.setMaxVersionHistory(versionedNode);
254         }
255         catch (RepositoryException re) {
256             log.error("Failed to limit version history to the maximum configured", re);
257             log.error("New version has already been created");
258         }
259 
260         return newVersion;
261     }
262 
263     /**
264      * Check if version index is set to negative number.
265      */
266     public abstract boolean isInvalidMaxVersions();
267 
268     /**
269      * Get node from version store.
270      */
271     public synchronized Node getVersionedNode(Node node) throws RepositoryException {
272         return getVersionedNode(node.getIdentifier());
273     }
274 
275     /**
276      * Get node from version store.
277      */
278     protected Node getVersionedNode(String uuid) throws RepositoryException {
279         try {
280             return getSession().getNodeByIdentifier(uuid);
281         } catch (ItemNotFoundException e) {
282             // node is not versioned yet
283             return null;
284         }
285     }
286 
287     /**
288      * Set version history to max version possible.
289      * @throws RepositoryException if failed to get VersionHistory or fail to remove
290      */
291     public abstract void setMaxVersionHistory(Node node) throws RepositoryException;
292 
293     /**
294      * Get history of this node as recorded in the version store.
295      * @param node
296      * @return version history of the given node
297      * @throws UnsupportedOperationException if repository implementation does not support Versions API
298      * @throws javax.jcr.RepositoryException if any repository error occurs
299      */
300     public synchronized VersionHistory getVersionHistory(Node node) throws UnsupportedRepositoryOperationException,
301     RepositoryException {
302         try {
303             Node versionedNode = this.getVersionedNode(node);
304             if (versionedNode == null) {
305                 // node does not exist in version store so no version history
306                 log.debug("No VersionHistory found for {} node.", node);
307                 return null;
308             }
309             return versionedNode.getVersionHistory();
310         } catch (UnsupportedRepositoryOperationException e) {
311             log.debug("Node {} is not versionable.", node);
312             // node is not versionable or underlying repo doesn't support versioning.
313             return null;
314         }
315     }
316 
317     /**
318      * /** Get named version.
319      * 
320      * @throws UnsupportedOperationException
321      *             if repository implementation does not support Versions API
322      * @throws javax.jcr.RepositoryException
323      *             if any repository error occurs
324      */
325     public synchronized Version getVersion(Node node, String name) throws UnsupportedRepositoryOperationException,
326     RepositoryException {
327         VersionHistory history = this.getVersionHistory(node);
328         if (history != null) {
329             return new VersionedNode(history.getVersion(name), node);
330         }
331         log.error("Node " + node.getPath() + " was never versioned");
332         return null;
333     }
334 
335     /**
336      * Returns the current base version of given node.
337      * @throws UnsupportedRepositoryOperationException
338      * @throws RepositoryException
339      */
340     public Version getBaseVersion(Node node) throws UnsupportedOperationException, RepositoryException {
341         Node versionedNode = this.getVersionedNode(node);
342         if (versionedNode != null) {
343             return versionedNode.getBaseVersion();
344         }
345 
346         throw new RepositoryException("Node " + node.getPath() + " was never versioned");
347     }
348 
349     /**
350      * Get all versions.
351      * @param node
352      * @return Version iterator retrieved from version history
353      * @throws UnsupportedOperationException if repository implementation does not support Versions API
354      * @throws javax.jcr.RepositoryException if any repository error occurs
355      */
356     public synchronized VersionIterator getAllVersions(Node node) throws UnsupportedRepositoryOperationException, RepositoryException {
357         Node versionedNode = this.getVersionedNode(node);
358         if (versionedNode == null) {
359             // node does not exist in version store so no versions
360             return null;
361         }
362         return versionedNode.getVersionHistory().getAllVersions();
363     }
364 
365     /**
366      * Restore specified version.
367      * @param node to be restored
368      * @param version to be used
369      * @param removeExisting
370      * @throws javax.jcr.version.VersionException if the specified <code>versionName</code> does not exist in this
371      * node's version history
372      * @throws javax.jcr.RepositoryException if an error occurs
373      * @throws javax.jcr.version.VersionException
374      * @deprecated since 4.5 use {@link #restore(Node, Version, boolean)} instead
375      */
376     @Deprecated
377     public synchronized void restore(Content node, Version version, boolean removeExisting) throws VersionException, UnsupportedRepositoryOperationException, RepositoryException {
378         restore(node.getJCRNode(), version, removeExisting);
379     }
380     /**
381      * Restore specified version.
382      * @param node to be restored
383      * @param version to be used
384      * @param removeExisting
385      * @throws javax.jcr.version.VersionException if the specified <code>versionName</code> does not exist in this
386      * node's version history
387      * @throws javax.jcr.RepositoryException if an error occurs
388      * @throws javax.jcr.version.VersionException
389      */
390     public synchronized void restore(final Node node, Version version, boolean removeExisting) throws VersionException, UnsupportedRepositoryOperationException, RepositoryException {
391         // FYI: restore is done in SC!!! Check permissions manually
392         PermissionUtil.verifyIsGrantedOrThrowException(node.getSession(), node.getPath(), Session.ACTION_SET_PROPERTY + "," + Session.ACTION_REMOVE + "," + Session.ACTION_ADD_NODE);
393 
394         // get the cloned node from version store
395         final Node versionedNode = this.getVersionedNode(node);
396 
397         final Version unwrappedVersion;
398         if (version instanceof VersionedNode) {
399             unwrappedVersion = ((VersionedNode) version).unwrap();
400         } else {
401             unwrappedVersion = version;
402         }
403 
404         versionedNode.restore(unwrappedVersion, removeExisting);
405         versionedNode.checkout();
406         MgnlContext.doInSystemContext(new JCRSessionOp<Void>(versionedNode.getSession().getWorkspace().getName()) {
407 
408             @Override
409             public Void exec(Session session) throws RepositoryException {
410                 //mixins are NOT restored automatically
411                 List<String> mixins = new ArrayList<String>();
412                 for (Value v: unwrappedVersion.getNode(ItemType.JCR_FROZENNODE).getProperty("jcr:frozenMixinTypes").getValues()) {
413                     mixins.add(v.getString());
414                 }
415 
416                 final Node systemVersionedNode = session.getNodeByIdentifier(versionedNode.getUUID());
417                 for (NodeType nt : versionedNode.getMixinNodeTypes()) {
418                     if (!mixins.remove(nt.getName())) {
419                         systemVersionedNode.removeMixin(nt.getName());
420                     }
421                 }
422                 for (String mix : mixins) {
423                     systemVersionedNode.addMixin(mix);
424                 }
425                 systemVersionedNode.save();
426 
427                 try {
428                     // if restored, update original node with the restored node and its subtree
429                     Rule rule = getUsedFilter(versionedNode);
430                     try {
431                         synchronized (ExclusiveWrite.getInstance()) {
432                             CopyUtil.getInstance().copyFromVersion(versionedNode, node, new RuleBasedNodePredicate(rule));
433                             if (NodeUtil.hasMixin(node, ItemType.DELETED_NODE_MIXIN)) {
434                                 node.removeMixin(ItemType.DELETED_NODE_MIXIN);
435                             }
436                             node.save();
437                         }
438                     }
439                     catch (RepositoryException re) {
440                         log.error("failed to restore versioned node, reverting all changes make to this node");
441                         node.refresh(false);
442                         throw re;
443                     }
444                 }
445                 catch (IOException e) {
446                     throw new RepositoryException(e);
447                 }
448                 catch (ClassNotFoundException e) {
449                     throw new RepositoryException(e);
450                 }
451                 return null;
452             }
453         });
454     }
455 
456     /**
457      * Removes all versions of the node associated with given UUID.
458      * @throws RepositoryException if fails to remove versioned node from the version store
459      */
460     public synchronized void removeVersionHistory(final Node node) throws RepositoryException {
461         PermissionUtil.verifyIsGrantedOrThrowException(node.getSession(), Path.getAbsolutePath(node.getPath()), Session.ACTION_ADD_NODE + "," + Session.ACTION_REMOVE + "," + Session.ACTION_SET_PROPERTY);
462         final String uuid = node.getIdentifier();
463         MgnlContext.doInSystemContext(new JCRSessionOp<Void>(VersionManager.VERSION_WORKSPACE) {
464 
465             @Override
466             public Void exec(Session session) throws RepositoryException {
467                 Node node = getVersionedNode(uuid);
468                 if (node != null) {
469                     if (node.getReferences().getSize() < 1) {
470                         // remove node from the version store only if its not referenced
471                         node.remove();
472                     } else { // remove all associated versions
473                         VersionHistory history = node.getVersionHistory();
474                         VersionIterator versions = history.getAllVersions();
475                         if (versions != null) {
476                             // skip root version
477                             versions.nextVersion();
478                             while (versions.hasNext()) {
479                                 history.removeVersion(versions.nextVersion().getName());
480                             }
481                         }
482                     }
483                 }
484                 session.save();
485                 return null;
486             }
487         });
488     }
489 
490     /**
491      * Verifies the existence of the mix:versionable and adds it if not.
492      */
493     protected void checkAndAddMixin(Node node) throws RepositoryException {
494         if(!node.isNodeType("mix:versionable")){
495             log.debug("Add mix:versionable");
496             node.addMixin("mix:versionable");
497         }
498     }
499 
500     /**
501      * Get Rule used for this version.
502      * @param versionedNode
503      * @throws IOException
504      * @throws ClassNotFoundException
505      * @throws RepositoryException
506      */
507     protected Rule getUsedFilter(Node versionedNode) throws IOException, ClassNotFoundException, RepositoryException {
508         // if restored, update original node with the restored node and its subtree
509         ByteArrayInputStream inStream = null;
510         try {
511             String ruleString = this.getSystemNode(versionedNode).getProperty(PROPERTY_RULE).getString();
512             inStream = new ByteArrayInputStream(Base64.decodeBase64(ruleString.getBytes()));
513             ObjectInput objectInput = new ObjectInputStream(inStream);
514             return (Rule) objectInput.readObject();
515         }
516         catch (IOException e) {
517             throw e;
518         }
519         catch (ClassNotFoundException e) {
520             throw e;
521         }
522         finally {
523             IOUtils.closeQuietly(inStream);
524         }
525     }
526 
527     /**
528      * Get the Magnolia system node created under the given node.
529      * @param node
530      * @throws RepositoryException if failed to create system node
531      */
532     protected synchronized Node getSystemNode(Node node) throws RepositoryException {
533         if (node.hasNode(SYSTEM_NODE)) {
534             return node.getNode(SYSTEM_NODE);
535         }
536         return node.addNode(SYSTEM_NODE, ItemType.SYSTEM.getSystemName());
537     }
538 
539     /**
540      * Get version store hierarchy manager.
541      * @throws RepositoryException
542      * @throws LoginException
543      */
544     protected Session getSession() throws LoginException, RepositoryException {
545         return MgnlContext.getSystemContext().getJCRSession(VersionManager.VERSION_WORKSPACE);
546     }
547 }