View Javadoc
1   /**
2    * This file Copyright (c) 2003-2015 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.AbstractContent;
37  import info.magnolia.cms.core.Content;
38  import info.magnolia.cms.core.DefaultContent;
39  import info.magnolia.cms.core.HierarchyManager;
40  import info.magnolia.cms.core.ItemType;
41  import info.magnolia.cms.core.NodeData;
42  import info.magnolia.cms.security.AccessDeniedException;
43  import info.magnolia.cms.security.AccessManager;
44  import info.magnolia.cms.security.Permission;
45  import info.magnolia.cms.util.ContentWrapper;
46  import info.magnolia.cms.util.NodeDataWrapper;
47  import info.magnolia.cms.util.Rule;
48  
49  import java.util.ArrayList;
50  import java.util.Calendar;
51  import java.util.Collection;
52  import java.util.Comparator;
53  import java.util.List;
54  
55  import javax.jcr.Node;
56  import javax.jcr.PathNotFoundException;
57  import javax.jcr.PropertyType;
58  import javax.jcr.RepositoryException;
59  import javax.jcr.Value;
60  import javax.jcr.Workspace;
61  import javax.jcr.lock.Lock;
62  import javax.jcr.lock.LockException;
63  import javax.jcr.nodetype.NodeType;
64  import javax.jcr.nodetype.NodeTypeManager;
65  import javax.jcr.version.Version;
66  import javax.jcr.version.VersionHistory;
67  import javax.jcr.version.VersionIterator;
68  
69  import org.apache.commons.lang.StringUtils;
70  import org.slf4j.Logger;
71  import org.slf4j.LoggerFactory;
72  
73  
74  /**
75   * Wraps a versioned node (frozen node) and allows traversing the hierarchy as if the node where in the original place.
76   */
77  public class ContentVersion extends DefaultContent {
78  
79      /**
80       * Makes sure that the handle hides the fact that the nodes live in the version store.
81       */
82      private final class ContentVersionChildWrapper extends ContentWrapper {
83          private final Content parent;
84  
85          private ContentVersionChildWrapper(Content wrappedContent, Content parent) {
86              super(wrappedContent);
87              this.parent = parent;
88          }
89  
90          @Override
91          public Content getParent() throws RepositoryException {
92              return parent;
93          }
94  
95          /**
96           * Show the original path not the one from the version store.
97           */
98          @Override
99          public String getHandle() {
100             try {
101                 return getParent().getHandle() + "/" + getName();
102             } catch (RepositoryException e) {
103                 throw new RuntimeException("Can't create handle for versioned node.", e);
104             }
105         }
106 
107         /**
108          * We have to wrap the node data to make sure that the handle is correct.
109          */
110         @Override
111         public NodeData newNodeDataInstance(String name, int type, boolean createIfNotExisting) throws AccessDeniedException, RepositoryException {
112             return new NodeDataWrapper(super.newNodeDataInstance(name, type, createIfNotExisting)) {
113                 @Override
114                 public String getHandle() {
115                     return ContentVersionChildWrapper.this.getHandle() + "/" + getName();
116                 }
117             };
118         }
119 
120         @Override
121         protected Content wrap(Content node) {
122             return new ContentVersionChildWrapper(node, this);
123         }
124     }
125 
126     private static Logger log = LoggerFactory.getLogger(ContentVersion.class);
127 
128     /**
129      * User who created this version.
130      */
131     public static final String VERSION_USER = "versionUser";
132 
133     /**
134      * Name of the base node.
135      */
136     public static final String NAME = "name";
137 
138     /**
139      * Version node (nt:version).
140      */
141     private final Version state;
142 
143     /**
144      * The node as existing in the workspace. Not the version node.
145      */
146     private final Content base;
147 
148     /**
149      * Rule used to create this version.
150      */
151     private Rule rule;
152 
153     private final Version versionedNode;
154 
155     public ContentVersion(Version thisVersion, Content base) throws RepositoryException {
156         if (thisVersion == null) {
157             throw new RepositoryException("Failed to get ContentVersion, version does not exist");
158         }
159         this.versionedNode = thisVersion;
160         if (thisVersion instanceof VersionedNode) {
161             this.state = ((VersionedNode) thisVersion).unwrap();
162         } else {
163             this.state = thisVersion;
164         }
165         this.base = base;
166 
167         //setting permissions should be no longer necessary as it is handled by jcr itself
168 
169         //        this.hierarchyManager = new HierarchyManagerWrapper(base.getHierarchyManager()) {
170         //            private AccessManagerImpl accessManager;
171         //
172         //            {
173         //                // child nodes (and metaData if nothing else) depends on this to have access when root access is restricted for given user
174         //                List<Permission> permissions = new ArrayList<Permission>(getWrappedHierarchyManager().getAccessManager().getPermissionList());
175         //                PermissionImpl p = new PermissionImpl();
176         //                p.setPattern(new SimpleUrlPattern("/jcr:system/jcr:versionStorage/*"));
177         //                // read only
178         //                p.setPermissions(8);
179         //                permissions.add(p);
180         //                // use dedicated AM and not the one base share with its parent
181         //                accessManager = new AccessManagerImpl();
182         //                accessManager.setPermissionList(permissions);
183         //            }
184         //
185         //            @Override
186         //            public AccessManager getAccessManager() {
187         //                return accessManager;
188         //            }
189         //        };
190         this.init();
191     }
192 
193     /**
194      * Set frozen node of this version as working node.
195      */
196     private void init() throws RepositoryException {
197         this.setNode(this.state.getNode(ItemType.JCR_FROZENNODE));
198         try {
199             if (!StringUtils.equalsIgnoreCase(this.state.getName(), VersionManager.ROOT_VERSION)) {
200                 this.rule = VersionManager.getInstance().getUsedFilter(this.getJCRNode());
201             }
202         } catch (Exception e) {
203             log.error(e.getMessage(), e);
204         }
205         if (this.rule == null) {
206             log.info("failed to get filter used for creating this version, use open filter");
207             this.rule = new Rule();
208         }
209     }
210 
211     /**
212      * Get creation date of this version.
213      *
214      * @return creation date as calendar
215      */
216     public Calendar getCreated() throws RepositoryException {
217         return this.state.getCreated();
218     }
219 
220     /**
221      * Return the name of the version represented by this object.
222      *
223      * @return the versions name
224      */
225     public String getVersionLabel() throws RepositoryException {
226         return this.state.getName();
227     }
228 
229     /**
230      * Get containing version history.
231      *
232      * @return version history associated to this version
233      */
234     public VersionHistory getContainingHistory() throws RepositoryException {
235         return this.state.getContainingHistory();
236     }
237 
238     /**
239      * The original name of the node.
240      */
241     @Override
242     public String getName() {
243         try {
244             return VersionManager.getInstance().getSystemNode(this.getJCRNode()).getProperty(NAME).getString();
245         } catch (RepositoryException re) {
246             log.error("Failed to retrieve name from version system node", re);
247             return "";
248         }
249     }
250 
251     /**
252      * The name of the user who created this version.
253      */
254     public String getUserName() {
255         try {
256             return VersionManager.getInstance().getSystemNode(this.getJCRNode()).getProperty(VERSION_USER).getString();
257         } catch (RepositoryException re) {
258             log.error("Failed to retrieve user from version system node", re);
259             return "";
260         }
261     }
262 
263     /**
264      * Get original path of this versioned content.
265      */
266     @Override
267     public String getHandle() {
268         return this.base.getHandle();
269     }
270 
271     /**
272      * Returns a direct child if it was included in the version. Otherwise it tries to get the child from the original place.
273      * The versioning rule is respected.
274      */
275     @Override
276     public Content getContent(String name) throws PathNotFoundException, RepositoryException, AccessDeniedException {
277         //first we have to check if this is a direct child
278         if (super.hasContent(name)) {
279             return new ContentVersionChildWrapper(super.getContent(name), this);
280         }
281         Content content = base.getContent(name);
282         // only return the node if it was excluded from the versioning, otherwise the node is new
283         if (!rule.isAllowed(content.getJCRNode())) {
284             return content;
285         }
286         throw new PathNotFoundException(base.getHandle() + "/" + name);
287     }
288 
289     /**
290      * Uses the same approach as {@link #getContent(String)}.
291      */
292     @Override
293     public boolean hasContent(String name) throws RepositoryException {
294         if (super.hasContent(name)) {
295             return true;
296         } else if (base.hasContent(name)) {
297             Content content = base.getContent(name);
298             // only return the node if it was excluded from the versioning, otherwise the node is new
299             if (!rule.isAllowed(content.getJCRNode())) {
300                 return true;
301             }
302         }
303         return false;
304     }
305 
306     /**
307      * Throws an {@link AccessDeniedException} as versions are read only.
308      */
309     @Override
310     public Content createContent(String name) throws AccessDeniedException {
311         throw new AccessDeniedException("Not allowed to write on version preview");
312     }
313 
314     /**
315      * Throws an {@link AccessDeniedException} as versions are read only.
316      */
317     @Override
318     public Content createContent(String name, String contentType) throws AccessDeniedException {
319         throw new AccessDeniedException("Not allowed to write on version preview");
320     }
321 
322     /**
323      * Throws an {@link AccessDeniedException} as versions are read only.
324      */
325     @Override
326     public Content createContent(String name, ItemType contentType) throws AccessDeniedException {
327         throw new AccessDeniedException("Not allowed to write on version preview");
328     }
329 
330     /**
331      * Throws an {@link AccessDeniedException} as versions are read only.
332      */
333     @Override
334     public NodeData createNodeData(String name) throws AccessDeniedException {
335         throw new AccessDeniedException("Not allowed to write on version preview");
336     }
337 
338     /**
339      * Throws an {@link AccessDeniedException} as versions are read only.
340      */
341     public NodeData createNodeData(String name, Value value, int type) throws AccessDeniedException {
342         throw new AccessDeniedException("Not allowed to write on version preview");
343     }
344 
345     /**
346      * Throws an {@link AccessDeniedException} as versions are read only.
347      */
348     @Override
349     public NodeData createNodeData(String name, Value value) throws AccessDeniedException {
350         throw new AccessDeniedException("Not allowed to write on version preview");
351     }
352 
353     /**
354      * Throws an {@link AccessDeniedException} as versions are read only.
355      */
356     @Override
357     public NodeData createNodeData(String name, int type) throws AccessDeniedException {
358         throw new AccessDeniedException("Not allowed to write on version preview");
359     }
360 
361     /**
362      * Throws an {@link AccessDeniedException} as versions are read only.
363      */
364     @Override
365     public void deleteNodeData(String name) throws RepositoryException {
366         throw new AccessDeniedException("Not allowed to write on version preview");
367     }
368 
369     /**
370      * Throws an {@link AccessDeniedException} as versions are read only.
371      */
372     @Override
373     public void updateMetaData() throws AccessDeniedException {
374         throw new AccessDeniedException("Not allowed to write on version preview");
375     }
376 
377     /**
378      * All {@link #getChildren()} methods delegate to this method. We combine the direct children and children
379      * from the current node which were not included by the version rule.
380      */
381     @Override
382     public Collection<Content> getChildren(ContentFilter filter, String namePattern, Comparator<Content> orderCriteria) {
383         ArrayList<Content> result = new ArrayList<Content>();
384         result.addAll(wrap(super.getChildren(filter, namePattern, orderCriteria)));
385 
386         Collection<Content> transientChildren = ((AbstractContent) this.base).getChildren(filter, namePattern, orderCriteria);
387         for (Content transientChild : transientChildren) {
388             try {
389                 if (!rule.isAllowed(transientChild.getJCRNode())) {
390                     result.add(transientChild);
391                 }
392             } catch (RepositoryException e) {
393                 throw new RuntimeException("Can't determine node type of " + transientChild, e);
394             }
395         }
396 
397         return result;
398     }
399 
400     private Collection<Content> wrap(Collection<Content> children) {
401         List<Content> transformed = new ArrayList<Content>();
402         for (Content child : children) {
403             transformed.add(new ContentVersionChildWrapper(child, this));
404         }
405         return transformed;
406     }
407 
408     /**
409      * @return Boolean, if sub node(s) exists
410      */
411     @Override
412     public boolean hasChildren() {
413         return (this.getChildren().size() > 0);
414     }
415 
416     /**
417      * @param contentType JCR node type as configured
418      * @return Boolean, if sub <code>collectionType</code> exists
419      */
420     @Override
421     public boolean hasChildren(String contentType) {
422         return (this.getChildren(contentType).size() > 0);
423     }
424 
425     /**
426      * Returns the parent of the base node.
427      */
428     @Override
429     public Content getParent() throws PathNotFoundException, RepositoryException, AccessDeniedException {
430         return this.base.getParent();
431     }
432 
433     @Override
434     public Content getAncestor(int level) throws PathNotFoundException, RepositoryException, AccessDeniedException {
435         return this.base.getAncestor(level);
436     }
437 
438     /**
439      * Convenience method for taglib.
440      *
441      * @return Content representing node on level 0
442      * @throws javax.jcr.RepositoryException if an error occurs
443      */
444     @Override
445     public Collection<Content> getAncestors() throws PathNotFoundException, RepositoryException {
446         return this.base.getAncestors();
447     }
448 
449     /**
450      * Get node level from the ROOT node : FIXME implement getDepth in javax.jcr.
451      *
452      * @return level at which current node exist, relative to the ROOT node
453      * @throws javax.jcr.RepositoryException if an error occurs
454      */
455     @Override
456     public int getLevel() throws PathNotFoundException, RepositoryException {
457         return this.base.getLevel();
458     }
459 
460     /**
461      * Throws an {@link AccessDeniedException} as versions are read only.
462      */
463     @Override
464     public void orderBefore(String srcName, String beforeName) throws RepositoryException {
465         throw new AccessDeniedException("Not allowed to write on version preview");
466     }
467 
468     /**
469      * This method returns the index of this node within the ordered set of its same-name sibling nodes. This index is
470      * the one used to address same-name siblings using the square-bracket notation, e.g., /a[3]/b[4]. Note that the
471      * index always starts at 1 (not 0), for compatibility with XPath. As a result, for nodes that do not have
472      * same-name-siblings, this method will always return 1.
473      *
474      * @return The index of this node within the ordered set of its same-name sibling nodes.
475      * @throws javax.jcr.RepositoryException if an error occurs
476      */
477     @Override
478     public int getIndex() throws RepositoryException {
479         return this.base.getIndex();
480     }
481 
482     /**
483      * Returns primary node type definition of the associated Node of this object.
484      *
485      * @throws RepositoryException if an error occurs
486      */
487     @Override
488     public NodeType getNodeType() throws RepositoryException {
489         log.warn("This is a Version node, it will always return NT_FROZEN as node type.");
490         log.warn("Use getNodeTypeName to retrieve base node primary type");
491         return super.getNodeType();
492     }
493 
494     /**
495      * Throws an {@link AccessDeniedException} as versions are read only.
496      */
497     @Override
498     public void restore(String versionName, boolean removeExisting) throws RepositoryException {
499         throw new AccessDeniedException("Not allowed to write on version preview");
500     }
501 
502     /**
503      * Throws an {@link AccessDeniedException} as versions are read only.
504      */
505     @Override
506     public void restore(Version version, boolean removeExisting) throws RepositoryException {
507         throw new AccessDeniedException("Not allowed to write on version preview");
508     }
509 
510     /**
511      * Throws an {@link AccessDeniedException} as versions are read only.
512      */
513     @Override
514     public void restore(Version version, String relPath, boolean removeExisting) throws RepositoryException {
515         throw new AccessDeniedException("Not allowed to write on version preview");
516     }
517 
518     /**
519      * Throws an {@link AccessDeniedException} as versions are read only.
520      */
521     @Override
522     public void restoreByLabel(String versionLabel, boolean removeExisting) throws RepositoryException {
523         throw new AccessDeniedException("Not allowed to write on version preview");
524     }
525 
526     /**
527      * Throws an {@link AccessDeniedException} as versions are read only.
528      */
529     @Override
530     public Version addVersion() throws RepositoryException {
531         throw new AccessDeniedException("Not allowed to add version on version preview");
532     }
533 
534     /**
535      * Throws an {@link AccessDeniedException} as versions are read only.
536      */
537     @Override
538     public Version addVersion(Rule rule) throws RepositoryException {
539         throw new AccessDeniedException("Not allowed to add version on version preview");
540     }
541 
542     /**
543      * Returns always false as verions are read only.
544      */
545     @Override
546     public boolean isModified() {
547         log.error("Not valid for version");
548         return false;
549     }
550 
551     /**
552      * Throws an {@link AccessDeniedException} as versions are read only.
553      */
554     @Override
555     public VersionHistory getVersionHistory() throws RepositoryException {
556         throw new AccessDeniedException("Not allowed to read VersionHistory of Version");
557     }
558 
559     /**
560      * Throws an {@link AccessDeniedException} as versions are read only.
561      */
562     @Override
563     public VersionIterator getAllVersions() throws RepositoryException {
564         throw new AccessDeniedException("Not allowed to get VersionIterator of Version");
565     }
566 
567     /**
568      * Throws an {@link AccessDeniedException} as versions are read only.
569      */
570     @Override
571     public ContentVersion getBaseVersion() throws RepositoryException {
572         throw new AccessDeniedException("Not allowed to get base version of Version");
573     }
574 
575     /**
576      * Throws an {@link AccessDeniedException} as versions are read only.
577      */
578     @Override
579     public ContentVersion getVersionedContent(Version version) throws RepositoryException {
580         throw new AccessDeniedException("Not allowed to get preview of Version itself");
581     }
582 
583     /**
584      * Throws an {@link AccessDeniedException} as versions are read only.
585      */
586     @Override
587     public ContentVersion getVersionedContent(String versionName) throws RepositoryException {
588         throw new AccessDeniedException("Not allowed to get preview of Version itself");
589     }
590 
591     /**
592      * Throws an {@link AccessDeniedException} as versions are read only.
593      */
594     @Override
595     public void save() throws RepositoryException {
596         throw new AccessDeniedException("Not allowed to write on version preview");
597     }
598 
599     /**
600      * Checks for the allowed access rights.
601      *
602      * @param permissions as defined in javax.jcr.Permission
603      * @return true is the current user has specified access on this node.
604      */
605     @Override
606     public boolean isGranted(long permissions) {
607         return (permissions & Permission.READ) == permissions;
608     }
609 
610     /**
611      * Throws an {@link AccessDeniedException} as versions are read only.
612      */
613     @Override
614     public void delete() throws RepositoryException {
615         throw new AccessDeniedException("Not allowed to write on version preview");
616     }
617 
618     /**
619      * Throws an {@link AccessDeniedException} as versions are read only.
620      */
621     @Override
622     public void delete(String path) throws RepositoryException {
623         throw new AccessDeniedException("Not allowed to write on version preview");
624     }
625 
626     /**
627      * UUID of the node referenced by this object.
628      *
629      * @return the uuid
630      */
631     @Override
632     public String getUUID() {
633         return this.base.getUUID();
634     }
635 
636     /**
637      * Throws an {@link AccessDeniedException} as versions are read only.
638      */
639     @Override
640     public void addMixin(String type) throws RepositoryException {
641         throw new AccessDeniedException("Not allowed to write on version preview");
642     }
643 
644     /**
645      * Throws an {@link AccessDeniedException} as versions are read only.
646      */
647     @Override
648     public void removeMixin(String type) throws RepositoryException {
649         throw new AccessDeniedException("Not allowed to write on version preview");
650     }
651 
652     /**
653      * Throws an {@link AccessDeniedException} as versions are read only.
654      */
655     @Override
656     public Lock lock(boolean isDeep, boolean isSessionScoped) throws LockException, RepositoryException {
657         throw new AccessDeniedException("Lock not supported on version preview");
658     }
659 
660     /**
661      * Throws an {@link AccessDeniedException} as versions are read only.
662      */
663     @Override
664     public Lock lock(boolean isDeep, boolean isSessionScoped, long yieldFor) throws LockException, RepositoryException {
665         throw new AccessDeniedException("Lock not supported on version preview");
666     }
667 
668     /**
669      * Throws an {@link AccessDeniedException} as versions are read only.
670      */
671     @Override
672     public Lock getLock() throws LockException, RepositoryException {
673         throw new AccessDeniedException("Lock not supported on version preview");
674     }
675 
676     /**
677      * Throws an {@link AccessDeniedException} as versions are read only.
678      */
679     @Override
680     public void unlock() throws LockException, RepositoryException {
681         throw new AccessDeniedException("Lock not supported on version preview");
682     }
683 
684     /**
685      * Throws an {@link AccessDeniedException} as versions are read only.
686      */
687     @Override
688     public boolean holdsLock() throws RepositoryException {
689         throw new AccessDeniedException("Lock not supported on version preview");
690     }
691 
692     /**
693      * Throws an {@link AccessDeniedException} as versions are read only.
694      */
695     @Override
696     public boolean isLocked() throws RepositoryException {
697         throw new AccessDeniedException("Lock not supported on version preview");
698     }
699 
700     /**
701      * Get hierarchy manager if previously set for this object.
702      */
703     @Override
704     public HierarchyManager getHierarchyManager() {
705         return this.base.getHierarchyManager();
706     }
707 
708     /**
709      * Get access manager if previously set for this object.
710      *
711      * @deprecated use getHierarchyManager instead
712      */
713     @Deprecated
714     @Override
715     public AccessManager getAccessManager() {
716         return this.base.getAccessManager();
717     }
718 
719     @Override
720     public Workspace getWorkspace() throws RepositoryException {
721         return this.base.getWorkspace();
722     }
723 
724     @Override
725     public boolean hasNodeData(String name) throws RepositoryException {
726         if (this.node.hasProperty(name)) {
727             return true;
728         }
729         if (this.node.hasNode(name) && this.node.getNode(name).getProperty("jcr:frozenPrimaryType").getValue().getString().equals(ItemType.NT_RESOURCE)) {
730             return true;
731         }
732         return false;
733     }
734 
735     @Override
736     protected int determineNodeDataType(String name) {
737         // FIXME: maybe delegate to NodeDataImplementations?
738         try {
739             if (this.node.hasProperty(name)) {
740                 return this.node.getProperty(name).getType();
741             }
742             if (this.node.hasNode(name) && this.node.getNode(name).getProperty("jcr:frozenPrimaryType").getValue().getString().equals(ItemType.NT_RESOURCE)) {
743                 return PropertyType.BINARY;
744             }
745         } catch (RepositoryException e) {
746             throw new IllegalStateException("Can't determine property type of [" + getHandle() + "/" + name + "]", e);
747         }
748         return PropertyType.UNDEFINED;
749     }
750 
751     @Override
752     public NodeType[] getMixinNodeTypes() throws RepositoryException {
753         Value[] vals = this.node.getProperty("jcr:frozenMixinTypes").getValues();
754         NodeTypeManager typeMan = getJCRNode().getSession().getWorkspace().getNodeTypeManager();
755         NodeType[] types = new NodeType[vals.length];
756         int i = 0;
757         for (Value val : vals) {
758             types[i++] = typeMan.getNodeType(val.getString());
759         }
760         return types;
761     }
762 
763     //    public List<ContentVersion> getPredecessors() throws RepositoryException {
764     //        List<ContentVersion> list = new ArrayList<ContentVersion>();
765     //        for (Version v : this.state.getPredecessors()) {
766     //            list.add(new ContentVersion(v, this.base));
767     //        }
768     //        return list;
769     //    }
770     public Version[] getPredecessors() throws RepositoryException {
771         // would prefer to return the above version (List of ContentVersions), but since our other APIs return jcr Version as well ...
772         return this.state.getPredecessors();
773     }
774 
775     @Override
776     public Node getJCRNode() {
777         // we seem to build content version from 2 different types of nodes - from Version and from jcr:frozenNode
778         try {
779             if (versionedNode.hasNode("jcr:frozenNode")) {
780                 return versionedNode.getFrozenNode();
781             }
782         } catch (RepositoryException e) {
783             log.error("Failed to retrieve frozen node from version " + versionedNode, e);
784         }
785 
786         return versionedNode;
787     }
788 }