View Javadoc

1   /**
2    * This file Copyright (c) 2003-2010 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.HierarchyManager;
38  import info.magnolia.cms.core.ItemType;
39  import info.magnolia.cms.core.NodeData;
40  import info.magnolia.cms.security.Permission;
41  import info.magnolia.cms.security.PermissionImpl;
42  import info.magnolia.cms.util.ExclusiveWrite;
43  import info.magnolia.cms.util.Rule;
44  import info.magnolia.cms.util.RuleBasedContentFilter;
45  import info.magnolia.cms.util.UrlPattern;
46  import info.magnolia.context.MgnlContext;
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.Collections;
56  import java.util.List;
57  
58  import javax.jcr.ItemNotFoundException;
59  import javax.jcr.Node;
60  import javax.jcr.NodeIterator;
61  import javax.jcr.PathNotFoundException;
62  import javax.jcr.RepositoryException;
63  import javax.jcr.UnsupportedRepositoryOperationException;
64  import javax.jcr.version.Version;
65  import javax.jcr.version.VersionException;
66  import javax.jcr.version.VersionHistory;
67  import javax.jcr.version.VersionIterator;
68  
69  import org.apache.commons.codec.binary.Base64;
70  import org.apache.commons.io.IOUtils;
71  import org.slf4j.Logger;
72  import org.slf4j.LoggerFactory;
73  
74  /**
75   * @author Sameer Charles
76   * $Id$
77   */
78  public abstract class BaseVersionManager {
79  
80      /**
81        * version data base
82        */
83       public static final String VERSION_WORKSPACE = "mgnlVersion";
84  
85       /**
86        * version workspace system path
87        */
88       public static final String TMP_REFERENCED_NODES = "mgnl:tmpReferencedNodes";
89  
90       /**
91        * version system node, holds this node version specific data
92        */
93       protected static final String SYSTEM_NODE = "mgnl:versionMetaData";
94  
95       /**
96        * property name for collection rule
97        */
98       public static final String PROPERTY_RULE = "Rule";
99  
100      /**
101       * jcr root version
102       */
103      protected static final String ROOT_VERSION = "jcr:rootVersion";
104 
105      /**
106       * Logger.
107       */
108      private static Logger log = LoggerFactory.getLogger(BaseVersionManager.class);
109 
110      /**
111       * create structure needed for version store workspace
112       * @throws RepositoryException if unable to create magnolia system structure
113       */
114      protected void createInitialStructure() throws RepositoryException {
115          HierarchyManager hm = MgnlContext.getSystemContext().getHierarchyManager(VERSION_WORKSPACE);
116          try {
117              Content tmp = hm.getContent("/" + VersionManager.TMP_REFERENCED_NODES);
118              // remove nodes if they are no longer referenced within this workspace
119              NodeIterator children = tmp.getJCRNode().getNodes();
120              while (children.hasNext()) {
121                  Node child = children.nextNode();
122                  if (child.getReferences().getSize() < 1) {
123                      child.remove();
124                  }
125              }
126          }
127          catch (PathNotFoundException e) {
128              hm.createContent("", VersionManager.TMP_REFERENCED_NODES, ItemType.SYSTEM.getSystemName());
129          }
130          hm.save();
131      }
132 
133      /**
134       * add version of the specified node and all child nodes while ignoring the same node type
135       * @param node to be versioned
136       * @return newly created version node
137       * @throws UnsupportedOperationException if repository implementation does not support Versions API
138       * @throws javax.jcr.RepositoryException if any repository error occurs
139       */
140      public synchronized Version addVersion(Content node) throws UnsupportedRepositoryOperationException,
141          RepositoryException {
142          // Rule rule = new Rule(new String[] {node.getNodeType().getName(), ItemType.SYSTEM.getSystemName()});
143          Rule rule = new Rule(node.getNodeTypeName() + "," + ItemType.SYSTEM.getSystemName(), ",");
144          rule.reverse();
145          return this.addVersion(node, rule);
146      }
147 
148      /**
149       * add version of the specified node and all child nodes while ignoring the same node type
150       * @param node to be versioned
151       * @return newly created version node
152       * @throws UnsupportedOperationException if repository implementation does not support Versions API
153       * @throws javax.jcr.RepositoryException if any repository error occurs
154       */
155      public synchronized Version addVersion(Content node, Rule rule) throws UnsupportedRepositoryOperationException,
156          RepositoryException {
157          List permissions = this.getAccessManagerPermissions();
158          this.impersonateAccessManager(null);
159          try {
160              return this.createVersion(node, rule);
161          }
162          catch (RepositoryException re) {
163              // since add version is synchronized on a singleton object, its safe to revert all changes made in
164              // the session attached to workspace - mgnlVersion
165              log.error("failed to copy versionable node to version store, reverting all changes made in this session");
166              getHierarchyManager().refresh(false);
167              throw re;
168          }
169          finally {
170              this.revertAccessManager(permissions);
171          }
172      }
173 
174      /**
175       * create version of the specified node and all child nodes based on the given <code>Rule</code>
176       * @param node to be versioned
177       * @param rule
178       * @return newly created version node
179       * @throws UnsupportedOperationException if repository implementation does not support Versions API
180       * @throws javax.jcr.RepositoryException if any repository error occurs
181       */
182      protected Version createVersion(Content node, Rule rule) throws UnsupportedRepositoryOperationException,
183          RepositoryException {
184          if (isInvalidMaxVersions()) {
185              log.debug("Ignore create version, MaxVersionIndex < 1");
186              log.debug("Returning root version of the source node");
187              return node.getJCRNode().getVersionHistory().getRootVersion();
188          }
189          CopyUtil.getInstance().copyToversion(node, new RuleBasedContentFilter(rule));
190          Content versionedNode = this.getVersionedNode(node);
191          checkAndAddMixin(versionedNode);
192          Content systemInfo = this.getSystemNode(versionedNode);
193          // add serialized rule which was used to create this version
194          ByteArrayOutputStream out = new ByteArrayOutputStream();
195          try {
196              ObjectOutput objectOut = new ObjectOutputStream(out);
197              objectOut.writeObject(rule);
198              objectOut.flush();
199              objectOut.close();
200              NodeData nodeData;
201              // PROPERTY_RULE is not a part of MetaData to allow versioning of node types which does NOT support MetaData
202              if (!systemInfo.hasNodeData(PROPERTY_RULE)) {
203                  nodeData = systemInfo.createNodeData(PROPERTY_RULE);
204              }
205              else {
206                  nodeData = systemInfo.getNodeData(PROPERTY_RULE);
207              }
208              nodeData.setValue(new String(Base64.encodeBase64(out.toByteArray())));
209          }
210          catch (IOException e) {
211              throw new RepositoryException("Unable to add serialized Rule to the versioned content");
212          }
213          // temp fix, MgnlContext should always have user either logged-in or anonymous
214          String userName = "";
215          if (MgnlContext.getUser() != null) {
216              userName = MgnlContext.getUser().getName();
217          }
218          // add all system properties for this version
219          if (!systemInfo.hasNodeData(ContentVersion.VERSION_USER)) {
220              systemInfo.createNodeData(ContentVersion.VERSION_USER).setValue(userName);
221          }
222          else {
223              systemInfo.getNodeData(ContentVersion.VERSION_USER).setValue(userName);
224          }
225          if (!systemInfo.hasNodeData(ContentVersion.NAME)) {
226              systemInfo.createNodeData(ContentVersion.NAME).setValue(node.getName());
227          }
228          else {
229              systemInfo.getNodeData(ContentVersion.NAME).setValue(node.getName());
230          }
231 
232          versionedNode.save();
233          // add version
234          Version newVersion = versionedNode.getJCRNode().checkin();
235          versionedNode.getJCRNode().checkout();
236          try {
237              this.setMaxVersionHistory(versionedNode);
238          }
239          catch (RepositoryException re) {
240              log.error("Failed to limit version history to the maximum configured", re);
241              log.error("New version has already been created");
242          }
243 
244          return newVersion;
245      }
246 
247      /**
248       * check if version index is set to negative number
249       * */
250      public abstract boolean isInvalidMaxVersions();
251 
252      /**
253       * get node from version store
254       * @param node
255       */
256      public synchronized Content getVersionedNode(Content node) throws RepositoryException {
257          return getVersionedNode(node.getUUID());
258      }
259 
260      /**
261       * get node from version store
262       * @param uuid
263       */
264      protected synchronized Content getVersionedNode(String uuid) throws RepositoryException {
265          List permissions = this.getAccessManagerPermissions();
266          this.impersonateAccessManager(null);
267          try {
268              return getHierarchyManager().getContentByUUID(uuid);
269          }
270          catch (ItemNotFoundException e) {
271              return null;
272          }
273          catch (RepositoryException re) {
274              throw re;
275          }
276          finally {
277              this.revertAccessManager(permissions);
278          }
279      }
280 
281      /**
282       * set version history to max version possible
283       * @param node
284       * @throws RepositoryException if failed to get VersionHistory or fail to remove
285       */
286      public abstract void setMaxVersionHistory(Content node) throws RepositoryException;
287 
288      /**
289       * get history of this node as recorded in the version store
290       * @param node
291       * @return version history of the given node
292       * @throws UnsupportedOperationException if repository implementation does not support Versions API
293       * @throws javax.jcr.RepositoryException if any repository error occurs
294       */
295      public synchronized VersionHistory getVersionHistory(Content node) throws UnsupportedRepositoryOperationException,
296          RepositoryException {
297          Content versionedNode = this.getVersionedNode(node);
298          if (versionedNode == null) {
299              // node does not exist in version store so no version history
300              log.info("No VersionHistory found for this node");
301              return null;
302          }
303          return versionedNode.getJCRNode().getVersionHistory();
304      }
305 
306      /**
307       * get named version
308       * @param node
309       * @param name
310       * @return version node
311       * @throws UnsupportedOperationException if repository implementation does not support Versions API
312       * @throws javax.jcr.RepositoryException if any repository error occurs
313       */
314      public synchronized Version getVersion(Content node, String name) throws UnsupportedRepositoryOperationException,
315          RepositoryException {
316          VersionHistory history = this.getVersionHistory(node);
317          if (history != null) {
318              return history.getVersion(name);
319          }
320          log.error("Node " + node.getHandle() + " was never versioned");
321          return null;
322      }
323 
324      /**
325       * Returns the current base version of given node
326       * @throws UnsupportedRepositoryOperationException
327       * @throws RepositoryException
328       */
329      public Version getBaseVersion(Content node) throws UnsupportedOperationException, RepositoryException {
330          Content versionedNode = this.getVersionedNode(node);
331          if (versionedNode != null) {
332              return versionedNode.getJCRNode().getBaseVersion();
333          }
334 
335          throw new RepositoryException("Node " + node.getHandle() + " was never versioned");
336      }
337 
338      /**
339       * get all versions
340       * @param node
341       * @return Version iterator retreived from version history
342       * @throws UnsupportedOperationException if repository implementation does not support Versions API
343       * @throws javax.jcr.RepositoryException if any repository error occurs
344       */
345      public synchronized VersionIterator getAllVersions(Content node) throws UnsupportedRepositoryOperationException,
346          RepositoryException {
347          Content versionedNode = this.getVersionedNode(node);
348          if (versionedNode == null) {
349              // node does not exist in version store so no versions
350              return null;
351          }
352          return versionedNode.getJCRNode().getVersionHistory().getAllVersions();
353      }
354 
355      /**
356       * restore specified version
357       * @param node to be restored
358       * @param version to be used
359       * @param removeExisting
360       * @throws javax.jcr.version.VersionException if the specified <code>versionName</code> does not exist in this
361       * node's version history
362       * @throws javax.jcr.RepositoryException if an error occurs
363       * @throws javax.jcr.version.VersionException
364       */
365      public synchronized void restore(Content node, Version version, boolean removeExisting) throws VersionException,
366          UnsupportedRepositoryOperationException, RepositoryException {
367          // get the cloned node from version store
368          Content versionedNode = this.getVersionedNode(node);
369          versionedNode.getJCRNode().restore(version, removeExisting);
370          versionedNode.getJCRNode().checkout();
371          List permissions = this.getAccessManagerPermissions();
372          this.impersonateAccessManager(null);
373          try {
374              // if restored, update original node with the restored node and its subtree
375              Rule rule = this.getUsedFilter(versionedNode);
376              try {
377                  synchronized (ExclusiveWrite.getInstance()) {
378                      CopyUtil.getInstance().copyFromVersion(versionedNode, node, new RuleBasedContentFilter(rule));
379                      node.save();
380                  }
381              }
382              catch (RepositoryException re) {
383                  log.error("failed to restore versioned node, reverting all changes make to this node");
384                  node.refresh(false);
385                  throw re;
386              }
387          }
388          catch (IOException e) {
389              throw new RepositoryException(e);
390          }
391          catch (ClassNotFoundException e) {
392              throw new RepositoryException(e);
393          }
394          catch (RepositoryException e) {
395              throw e;
396          }
397          finally {
398              this.revertAccessManager(permissions);
399          }
400      }
401 
402      /**
403       * Removes all versions of the node associated with given UUID
404       * @param uuid
405       * @throws RepositoryException if fails to remove versioned node from the version store
406       */
407      public synchronized void removeVersionHistory(String uuid) throws RepositoryException {
408          List permissions = this.getAccessManagerPermissions();
409          this.impersonateAccessManager(null);
410          try {
411              Content node = this.getVersionedNode(uuid);
412              if (node != null) {
413                  if (node.getJCRNode().getReferences().getSize() < 1) {
414                      // remove node from the version store only if its not referenced
415                      node.delete();
416                  } else { // remove all associated versions
417                      VersionHistory history = node.getVersionHistory();
418                      VersionIterator versions = node.getAllVersions();
419                      if (versions != null) {
420                          // skip root version
421                          versions.nextVersion();
422                          while (versions.hasNext()) {
423                              history.removeVersion(versions.nextVersion().getName());
424                          }
425                      }
426                  }
427              }
428          }
429          catch (RepositoryException re) {
430              throw re;
431          }
432          finally {
433              this.revertAccessManager(permissions);
434          }
435          getHierarchyManager().save();
436      }
437 
438      /**
439       * Veryfies the existence of the mix:versionable and adds it if not.
440       */
441      protected void checkAndAddMixin(Content node) throws RepositoryException {
442          if(!node.getJCRNode().isNodeType("mix:versionable")){
443              log.debug("Add mixin");
444              node.addMixin("mix:versionable");
445          }
446      }
447 
448      /**
449       * get Rule used for this version
450       * @param versionedNode
451       * @throws IOException
452       * @throws ClassNotFoundException
453       * @throws RepositoryException
454       */
455      protected Rule getUsedFilter(Content versionedNode) throws IOException, ClassNotFoundException, RepositoryException {
456          // if restored, update original node with the restored node and its subtree
457          ByteArrayInputStream inStream = null;
458          try {
459              String ruleString = this.getSystemNode(versionedNode).getNodeData(PROPERTY_RULE).getString();
460              inStream = new ByteArrayInputStream(Base64.decodeBase64(ruleString.getBytes()));
461              ObjectInput objectInput = new ObjectInputStream(inStream);
462              return (Rule) objectInput.readObject();
463          }
464          catch (IOException e) {
465              throw e;
466          }
467          catch (ClassNotFoundException e) {
468              throw e;
469          }
470              finally {
471              IOUtils.closeQuietly(inStream);
472          }
473      }
474 
475      /**
476       * get magnolia system node created under the given node
477       * @param node
478       * @throws RepositoryException if failed to create system node
479       */
480      protected synchronized Content getSystemNode(Content node) throws RepositoryException {
481          try {
482              return node.getContent(SYSTEM_NODE);
483          }
484          catch (PathNotFoundException e) {
485              return node.createContent(SYSTEM_NODE, ItemType.SYSTEM);
486          }
487      }
488 
489      /**
490       * impersonate to be access manager with system rights
491       * @param permissions
492       */
493      protected void impersonateAccessManager(List permissions) {
494          // FIXME: this is a very ugly hack but it needs the least change in the code
495          // see MAGNOLIA-1753
496          if(permissions == null){
497              Permission permission = new PermissionImpl();
498              permission.setPermissions(Permission.ALL);
499              permission.setPattern(UrlPattern.MATCH_ALL);
500              permissions = Collections.singletonList(permission);
501          }
502          this.getHierarchyManager().getAccessManager().setPermissionList(permissions);
503      }
504 
505      /**
506       * revert access manager permissions
507       * @param permissions
508       */
509      protected void revertAccessManager(List permissions) {
510          this.getHierarchyManager().getAccessManager().setPermissionList(permissions);
511      }
512 
513      /**
514       * get access manager permission list
515       */
516      protected List getAccessManagerPermissions() {
517          return this.getHierarchyManager().getAccessManager().getPermissionList();
518      }
519 
520      /**
521       * get version store hierarchy manager
522       */
523      protected HierarchyManager getHierarchyManager() {
524          return MgnlContext.getHierarchyManager(VersionManager.VERSION_WORKSPACE);
525      }
526 
527 
528 }