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.Path;
39  import info.magnolia.cms.security.JCRSessionOp;
40  import info.magnolia.cms.security.PermissionUtil;
41  import info.magnolia.cms.util.Rule;
42  import info.magnolia.context.MgnlContext;
43  import info.magnolia.jcr.predicate.RuleBasedNodePredicate;
44  import info.magnolia.jcr.util.NodeTypes;
45  import info.magnolia.jcr.util.NodeUtil;
46  import info.magnolia.jcr.util.VersionUtil;
47  
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  public abstract class BaseVersionManager {
86  
87      /**
88       * Name of the workspace.
89       */
90      public static final String VERSION_WORKSPACE = "mgnlVersion";
91  
92      /**
93       * Node which contains stubs for referenced nodes. We have to copy them to the workspace as well.
94       */
95      public static final String TMP_REFERENCED_NODES = "mgnl:tmpReferencedNodes";
96  
97      /**
98       * Sub-node containing the data used for the version/restore process.
99       */
100     protected static final String SYSTEM_NODE = "mgnl:versionMetaData";
101 
102     /**
103      * Property name for collection rule. The rule defines which sub-nodes belong to a node: page and paragraphs.
104      */
105     public static final String PROPERTY_RULE = "Rule";
106 
107     /**
108      * JCR version store root.
109      */
110     protected static final String ROOT_VERSION = "jcr:rootVersion";
111 
112     private static Logger log = LoggerFactory.getLogger(BaseVersionManager.class);
113 
114     /**
115      * Create structure needed for version store workspace.
116      * @throws RepositoryException if unable to create magnolia system structure
117      */
118     protected void createInitialStructure() throws RepositoryException {
119         MgnlContext.doInSystemContext(new JCRSessionOp<Void>(VERSION_WORKSPACE) {
120 
121             @Override
122             public Void exec(Session session) throws RepositoryException {
123                 try {
124                     Node tmp = session.getNode("/" + VersionManager.TMP_REFERENCED_NODES);
125                     // remove nodes if they are no longer referenced within this workspace
126                     NodeIterator children = tmp.getNodes();
127                     while (children.hasNext()) {
128                         Node child = children.nextNode();
129                         if (child.getReferences().getSize() < 1) {
130                             child.remove();
131                         }
132                     }
133                 }
134                 catch (PathNotFoundException e) {
135                     session.getRootNode().addNode(VersionManager.TMP_REFERENCED_NODES, ItemType.SYSTEM.getSystemName());
136                 }
137                 session.save();
138 
139                 return null;
140             }
141         });
142 
143     }
144 
145     /**
146      * Add version of the specified node and all child nodes while ignoring the same node type.
147      * @param node to be versioned
148      * @return newly created version node
149      * @throws UnsupportedOperationException if repository implementation does not support Versions API
150      * @throws javax.jcr.RepositoryException if any repository error occurs
151      */
152     public synchronized Version addVersion(Node node) throws UnsupportedRepositoryOperationException,
153     RepositoryException {
154         // Rule rule = new Rule(new String[] {node.getNodeType().getName(), ItemType.SYSTEM.getSystemName()});
155         Rule rule = new Rule(VersionUtil.getNodeTypeName(node) + "," + ItemType.SYSTEM.getSystemName(), ",");
156         rule.reverse();
157         return this.addVersion(node, rule);
158     }
159 
160     /**
161      * Add version of the specified node and all child nodes while ignoring the same node type.
162      * @param node to be versioned
163      * @return newly created version node
164      * @throws UnsupportedOperationException if repository implementation does not support Versions API
165      * @throws javax.jcr.RepositoryException if any repository error occurs
166      */
167     public synchronized Version addVersion(final Node node, final Rule rule) throws UnsupportedRepositoryOperationException,
168     RepositoryException {
169         final String userName = getSafelyUsersNameFromMgnlContenxt();
170         Version version = MgnlContext.doInSystemContext(new JCRSessionOp<Version>(node.getSession().getWorkspace().getName()) {
171 
172             @Override
173             public Version exec(Session session) throws RepositoryException {
174                 try {
175                     return createVersion(session.getNodeByIdentifier(node.getIdentifier()), rule, userName);
176                 }
177                 catch (RepositoryException re) {
178                     // since add version is synchronized on a singleton object, its safe to revert all changes made in
179                     // the session attached to workspace - mgnlVersion
180                     log.error("failed to copy versionable node to version store, reverting all changes made in this session");
181                     getSession().refresh(false);
182                     throw re;
183                 }
184 
185             }});
186         return version;
187     }
188 
189     /**
190      * Create version of the specified node and all child nodes based on the given <code>Rule</code>.
191      * @param node to be versioned
192      *
193      * @return newly created version node
194      * @throws UnsupportedOperationException if repository implementation does not support Versions API
195      * @throws javax.jcr.RepositoryException if any repository error occurs
196      * @deprecated since 4.5 use {@link #createVersion(Node, Rule, String)} instead
197      */
198     @Deprecated
199     protected Version createVersion(Content node, Rule rule) throws UnsupportedRepositoryOperationException, RepositoryException {
200         return createVersion(node.getJCRNode(), rule, getSafelyUsersNameFromMgnlContenxt());
201     }
202 
203     private String getSafelyUsersNameFromMgnlContenxt(){
204         String userName = "";
205         if (MgnlContext.getUser() != null) {
206             userName = MgnlContext.getUser().getName();
207         }
208         return userName;
209     }
210 
211     /**
212      * Create version of the specified node and all child nodes based on the given <code>Rule</code>.
213      * This will return null if can't create new version in case when max version index is lower then one
214      * or when content is marked for deletion.
215      * @param node to be versioned
216      *
217      * @return newly created version node
218      * @throws UnsupportedOperationException if repository implementation does not support Versions API
219      * @throws javax.jcr.RepositoryException if any repository error occurs
220      */
221     protected Version createVersion(Node node, Rule rule, String userName) throws UnsupportedRepositoryOperationException,
222     RepositoryException {
223         if (isInvalidMaxVersions()) {
224             log.debug("Ignore create version, MaxVersionIndex < 1");
225             return null;
226         }
227         if(node.isNodeType(NodeTypes.Deleted.NAME)){
228             log.debug("Don't create version for content marked as deleted");
229             return null;
230         }
231 
232         CopyUtil.getInstance().copyToversion(node, new RuleBasedNodePredicate(rule));
233         Node versionedNode = this.getVersionedNode(node);
234 
235         checkAndAddMixin(versionedNode);
236         Node systemInfo = this.getSystemNode(versionedNode);
237         // add serialized rule which was used to create this version
238         ByteArrayOutputStream out = new ByteArrayOutputStream();
239         try {
240             ObjectOutput objectOut = new ObjectOutputStream(out);
241             objectOut.writeObject(rule);
242             objectOut.flush();
243             objectOut.close();
244             // PROPERTY_RULE is not a part of MetaData to allow versioning of node types which does NOT support MetaData
245             systemInfo.setProperty(PROPERTY_RULE, new String(Base64.encodeBase64(out.toByteArray())));
246         }
247         catch (IOException e) {
248             throw new RepositoryException("Unable to add serialized Rule to the versioned content");
249         }
250         // add all system properties for this version
251         systemInfo.setProperty(ContentVersion.VERSION_USER, userName);
252         systemInfo.setProperty(ContentVersion.NAME, node.getName());
253 
254         versionedNode.save();
255         // add version
256         Version newVersion = versionedNode.checkin();
257         versionedNode.checkout();
258 
259         try {
260             this.setMaxVersionHistory(versionedNode);
261         }
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      * @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      *
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     /**
326      * /** Get named version.
327      *
328      * @throws UnsupportedOperationException if repository implementation does not support Versions API
329      * @throws javax.jcr.RepositoryException if any repository error occurs
330      */
331     public synchronized Version getVersion(Node node, String name) throws UnsupportedRepositoryOperationException,
332     RepositoryException {
333         VersionHistory history = this.getVersionHistory(node);
334         if (history != null) {
335             return new VersionedNode(history.getVersion(name), node);
336         }
337         log.error("Node " + node.getPath() + " was never versioned");
338         return null;
339     }
340 
341     /**
342      * Returns the current base version of given node.
343      */
344     public Version getBaseVersion(Node node) throws UnsupportedOperationException, RepositoryException {
345         Node versionedNode = this.getVersionedNode(node);
346         if (versionedNode != null) {
347             return versionedNode.getBaseVersion();
348         }
349 
350         throw new RepositoryException("Node " + node.getPath() + " was never versioned");
351     }
352 
353     /**
354      * Get all versions.
355      *
356      * @return Version iterator retrieved from version history
357      * @throws UnsupportedOperationException if repository implementation does not support Versions API
358      * @throws javax.jcr.RepositoryException if any repository error occurs
359      */
360     public synchronized VersionIterator getAllVersions(Node node) throws UnsupportedRepositoryOperationException, RepositoryException {
361         Node versionedNode = this.getVersionedNode(node);
362         if (versionedNode == null) {
363             // node does not exist in version store so no versions
364             return null;
365         }
366         return versionedNode.getVersionHistory().getAllVersions();
367     }
368 
369     /**
370      * Restore specified version.
371      * @param node to be restored
372      * @param version to be used
373      *
374      * @throws javax.jcr.version.VersionException if the specified <code>versionName</code> does not exist in this
375      * node's version history
376      * @throws javax.jcr.RepositoryException if an error occurs
377      * @throws javax.jcr.version.VersionException
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      * Restore specified version.
386      * @param node to be restored
387      * @param version to be used
388      *
389      * @throws javax.jcr.version.VersionException if the specified <code>versionName</code> does not exist in this
390      * node's version history
391      * @throws javax.jcr.RepositoryException if an error occurs
392      * @throws javax.jcr.version.VersionException
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(ItemType.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, ItemType.DELETED_NODE_MIXIN)) {
441                             destinationNode.removeMixin(ItemType.DELETED_NODE_MIXIN);
442                         }
443                         destinationNode.save();
444                         // node was updated in system context and we should make sure it is notified of the changes
445                         node.refresh(false);
446                     }
447                     catch (RepositoryException re) {
448                         if (log.isDebugEnabled()) {
449                             log.debug("error during restore: " + re.getMessage(), re);
450                         } else {
451                             log.error("failed to restore versioned node, reverting all changes make to this node");
452                         }
453                         destinationNode.refresh(false);
454                         throw re;
455                     }
456                 }
457                 catch (IOException e) {
458                     throw new RepositoryException(e);
459                 }
460                 catch (ClassNotFoundException e) {
461                     throw new RepositoryException(e);
462                 }
463                 return null;
464             }
465         });
466     }
467 
468     /**
469      * Removes all versions of the node associated with given UUID.
470      * @throws RepositoryException if fails to remove versioned node from the version store
471      */
472     public synchronized void removeVersionHistory(final Node node) throws RepositoryException {
473         PermissionUtil.verifyIsGrantedOrThrowException(node.getSession(), Path.getAbsolutePath(node.getPath()), Session.ACTION_ADD_NODE + "," + Session.ACTION_REMOVE + "," + Session.ACTION_SET_PROPERTY);
474         final String uuid = node.getIdentifier();
475         MgnlContext.doInSystemContext(new JCRSessionOp<Void>(VersionManager.VERSION_WORKSPACE) {
476 
477             @Override
478             public Void exec(Session session) throws RepositoryException {
479                 Node node = getVersionedNode(uuid);
480                 if (node != null) {
481                     if (node.getReferences().getSize() < 1) {
482                         // remove node from the version store only if its not referenced
483                         node.remove();
484                     } else { // remove all associated versions
485                         VersionHistory history = node.getVersionHistory();
486                         VersionIterator versions = history.getAllVersions();
487                         if (versions != null) {
488                             // skip root version
489                             versions.nextVersion();
490                             while (versions.hasNext()) {
491                                 history.removeVersion(versions.nextVersion().getName());
492                             }
493                         }
494                     }
495                 }
496                 session.save();
497                 return null;
498             }
499         });
500     }
501 
502     /**
503      * Verifies the existence of the mix:versionable and adds it if not.
504      */
505     protected void checkAndAddMixin(Node node) throws RepositoryException {
506         if(!node.isNodeType("mix:versionable")){
507             log.debug("Add mix:versionable");
508             node.addMixin("mix:versionable");
509         }
510     }
511 
512     /**
513      * Get Rule used for this version.
514      */
515     protected Rule getUsedFilter(Node versionedNode) throws IOException, ClassNotFoundException, RepositoryException {
516         // if restored, update original node with the restored node and its subtree
517         ByteArrayInputStream inStream = null;
518         try {
519             String ruleString = this.getSystemNode(versionedNode).getProperty(PROPERTY_RULE).getString();
520             inStream = new ByteArrayInputStream(Base64.decodeBase64(ruleString.getBytes()));
521             ObjectInput objectInput = new ObjectInputStream(inStream);
522             return (Rule) objectInput.readObject();
523         }
524         catch (IOException e) {
525             throw e;
526         }
527         catch (ClassNotFoundException e) {
528             throw e;
529         }
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, ItemType.SYSTEM.getSystemName());
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 }