View Javadoc

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