View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors.
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   * 
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.jcr;
25  
26  import java.util.Collection;
27  import java.util.Iterator;
28  import java.util.LinkedList;
29  import java.util.List;
30  import java.util.NoSuchElementException;
31  import javax.jcr.AccessDeniedException;
32  import javax.jcr.Node;
33  import javax.jcr.NodeIterator;
34  import javax.jcr.PathNotFoundException;
35  import javax.jcr.Property;
36  import javax.jcr.PropertyIterator;
37  import javax.jcr.PropertyType;
38  import javax.jcr.ReferentialIntegrityException;
39  import javax.jcr.RepositoryException;
40  import javax.jcr.UnsupportedRepositoryOperationException;
41  import javax.jcr.Value;
42  import javax.jcr.version.Version;
43  import javax.jcr.version.VersionException;
44  import javax.jcr.version.VersionHistory;
45  import javax.jcr.version.VersionIterator;
46  import org.modeshape.graph.Graph;
47  import org.modeshape.graph.connector.RepositorySourceException;
48  import org.modeshape.graph.property.Name;
49  import org.modeshape.graph.property.Path;
50  import org.modeshape.graph.property.Reference;
51  import org.modeshape.graph.property.Path.Segment;
52  
53  /**
54   * Convenience wrapper around a version history {@link JcrNode node}.
55   */
56  class JcrVersionHistoryNode extends JcrNode implements VersionHistory {
57  
58      private static final String[] EMPTY_STRING_ARRAY = new String[0];
59  
60      public JcrVersionHistoryNode( AbstractJcrNode node ) {
61          super(node.cache, node.nodeId, node.location);
62  
63          assert !node.isRoot() : "Version histories should always be located in the /jcr:system/jcr:versionStorage subgraph";
64      }
65  
66      /**
67       * @return a reference to the {@code jcr:versionLabels} child node of this history node.
68       * @throws RepositoryException if an error occurs accessing this node
69       */
70      private AbstractJcrNode versionLabels() throws RepositoryException {
71          Segment segment = segmentFrom(JcrLexicon.VERSION_LABELS);
72          return nodeInfo().getChild(segment).getPayload().getJcrNode();
73      }
74  
75      /**
76       * @{inheritDoc
77       */
78      @Override
79      public VersionIterator getAllVersions() throws RepositoryException {
80          return new JcrVersionIterator(getNodes());
81      }
82  
83      /**
84       * @{inheritDoc
85       */
86      @Override
87      public Version getRootVersion() throws RepositoryException {
88          // Copied from AbstractJcrNode.getNode(String) to avoid double conversion. Needs to be refactored.
89          Segment segment = context().getValueFactories().getPathFactory().createSegment(JcrLexicon.ROOT_VERSION);
90          try {
91              return (JcrVersionNode)nodeInfo().getChild(segment).getPayload().getJcrNode();
92          } catch (org.modeshape.graph.property.PathNotFoundException e) {
93              String msg = JcrI18n.childNotFoundUnderNode.text(segment, getPath(), cache.workspaceName());
94              throw new PathNotFoundException(msg);
95          } catch (RepositorySourceException e) {
96              throw new RepositoryException(e.getLocalizedMessage(), e);
97          }
98      }
99  
100     /**
101      * @{inheritDoc
102      */
103     @Override
104     public JcrVersionNode getVersion( String versionName ) throws VersionException, RepositoryException {
105         try {
106             AbstractJcrNode version = getNode(versionName);
107             return (JcrVersionNode)version;
108         } catch (PathNotFoundException pnfe) {
109             throw new VersionException(JcrI18n.invalidVersionName.text(versionName, getPath()));
110         }
111     }
112 
113     /**
114      * @{inheritDoc
115      */
116     @Override
117     public JcrVersionNode getVersionByLabel( String label ) throws VersionException, RepositoryException {
118         Property prop = versionLabels().getProperty(label);
119         if (prop == null) throw new VersionException(JcrI18n.invalidVersionLabel.text(label, getPath()));
120 
121         AbstractJcrNode version = session().getNodeByUUID(prop.getString());
122 
123         assert version != null;
124 
125         return (JcrVersionNode)version;
126     }
127 
128     /**
129      * @{inheritDoc
130      */
131     @Override
132     public String[] getVersionLabels() throws RepositoryException {
133         PropertyIterator iter = versionLabels().getProperties();
134 
135         String[] labels = new String[(int)iter.getSize()];
136         for (int i = 0; iter.hasNext(); i++) {
137             labels[i] = iter.nextProperty().getName();
138         }
139 
140         return labels;
141     }
142 
143     /**
144      * Returns the version labels that point to the given version
145      * 
146      * @param version the version for which the labels should be retrieved
147      * @return the version labels for that version
148      * @throws RepositoryException if an error occurs accessing the repository
149      */
150     @SuppressWarnings( "deprecation" )
151     private Collection<String> versionLabelsFor( Version version ) throws RepositoryException {
152         if (!version.getParent().equals(this)) {
153             throw new VersionException(JcrI18n.invalidVersion.text(version.getPath(), getPath()));
154         }
155 
156         String versionUuid = version.getUUID();
157 
158         PropertyIterator iter = versionLabels().getProperties();
159 
160         List<String> labels = new LinkedList<String>();
161         for (int i = 0; iter.hasNext(); i++) {
162             Property prop = iter.nextProperty();
163 
164             if (versionUuid.equals(prop.getString())) {
165                 labels.add(prop.getName());
166             }
167         }
168 
169         return labels;
170     }
171 
172     /**
173      * @{inheritDoc
174      */
175     @Override
176     public String[] getVersionLabels( Version version ) throws RepositoryException {
177         return versionLabelsFor(version).toArray(EMPTY_STRING_ARRAY);
178     }
179 
180     /**
181      * @{inheritDoc
182      */
183     @Override
184     public String getVersionableUUID() throws RepositoryException {
185         return getProperty(JcrLexicon.VERSIONABLE_UUID).getString();
186     }
187 
188     /**
189      * @{inheritDoc
190      */
191     @Override
192     public boolean hasVersionLabel( String label ) throws RepositoryException {
193         return versionLabels().hasProperty(label);
194     }
195 
196     /**
197      * @{inheritDoc
198      */
199     @Override
200     public boolean hasVersionLabel( Version version,
201                                     String label ) throws RepositoryException {
202         Collection<String> labels = versionLabelsFor(version);
203 
204         return labels.contains(label);
205     }
206 
207     /**
208      * @{inheritDoc
209      */
210     @SuppressWarnings( "deprecation" )
211     @Override
212     public void removeVersion( String versionName )
213         throws ReferentialIntegrityException, AccessDeniedException, UnsupportedRepositoryOperationException, VersionException,
214         RepositoryException {
215 
216         JcrVersionNode version = getVersion(versionName);
217 
218         /*
219          * Verify that the only references to this version are from its predecessors and successors in the version history.s
220          */
221         Path versionHistoryPath = version.path().getParent();
222         for (PropertyIterator iter = version.getReferences(); iter.hasNext();) {
223             AbstractJcrProperty prop = (AbstractJcrProperty)iter.next();
224 
225             Path nodePath = prop.path().getParent();
226 
227             // If the property's parent is the root node, fail.
228             if (nodePath.isRoot()) {
229                 throw new ReferentialIntegrityException(JcrI18n.cannotRemoveVersion.text(prop.getPath()));
230             }
231 
232             if (!versionHistoryPath.equals(nodePath.getParent())) {
233                 throw new ReferentialIntegrityException(JcrI18n.cannotRemoveVersion.text(prop.getPath()));
234             }
235 
236         }
237 
238         String versionUuid = version.getUUID();
239         Value[] values;
240 
241         // Remove the reference to the dead version from the successors property of all the predecessors
242         Property predecessors = version.getProperty(JcrLexicon.PREDECESSORS);
243         values = predecessors.getValues();
244         for (int i = 0; i < values.length; i++) {
245             AbstractJcrNode predecessor = session().getNodeByUUID(values[i].getString());
246             Value[] nodeSuccessors = predecessor.getProperty(JcrLexicon.SUCCESSORS).getValues();
247             Value[] newNodeSuccessors = new Value[nodeSuccessors.length - 1];
248 
249             int idx = 0;
250             for (int j = 0; j < nodeSuccessors.length; j++) {
251                 if (!versionUuid.equals(nodeSuccessors[j].getString())) {
252                     newNodeSuccessors[idx++] = nodeSuccessors[j];
253                 }
254             }
255 
256             predecessor.editor().setProperty(JcrLexicon.SUCCESSORS, newNodeSuccessors, PropertyType.REFERENCE, false);
257         }
258 
259         // Remove the reference to the dead version from the predecessors property of all the successors
260         Property successors = version.getProperty(JcrLexicon.SUCCESSORS);
261         values = successors.getValues();
262         for (int i = 0; i < values.length; i++) {
263             AbstractJcrNode successor = session().getNodeByUUID(values[i].getString());
264             Value[] nodePredecessors = successor.getProperty(JcrLexicon.PREDECESSORS).getValues();
265             Value[] newNodePredecessors = new Value[nodePredecessors.length - 1];
266 
267             int idx = 0;
268             for (int j = 0; j < nodePredecessors.length; j++) {
269                 if (!versionUuid.equals(nodePredecessors[j].getString())) {
270                     newNodePredecessors[idx++] = nodePredecessors[j];
271                 }
272             }
273 
274             successor.editor().setProperty(JcrLexicon.PREDECESSORS, newNodePredecessors, PropertyType.REFERENCE, false);
275         }
276 
277         session().recordRemoval(version.location); // do this first before we destroy the node!
278         version.editor().destroy();
279     }
280 
281     /**
282      * @{inheritDoc
283      */
284     @SuppressWarnings( "deprecation" )
285     @Override
286     public void addVersionLabel( String versionName,
287                                  String label,
288                                  boolean moveLabel ) throws VersionException, RepositoryException {
289         AbstractJcrNode versionLabels = versionLabels();
290         Version version = getVersion(versionName);
291 
292         try {
293             // This throws a PNFE if the named property doesn't already exist
294             versionLabels.getProperty(label);
295             if (!moveLabel) throw new VersionException(JcrI18n.versionLabelAlreadyExists.text(label));
296 
297         } catch (PathNotFoundException pnfe) {
298             // This gets thrown if the label doesn't already exist
299         }
300 
301         Graph graph = cache.session().repository().createSystemGraph(context());
302         Reference ref = context().getValueFactories().getReferenceFactory().create(version.getUUID());
303         graph.set(label).on(versionLabels.location).to(ref);
304 
305         versionLabels.refresh(false);
306 
307     }
308 
309     /**
310      * @{inheritDoc
311      */
312     @Override
313     public void removeVersionLabel( String label ) throws VersionException, RepositoryException {
314         AbstractJcrNode versionLabels = versionLabels();
315 
316         try {
317             // This throws a PNFE if the named property doesn't already exist
318             versionLabels.getProperty(label);
319         } catch (PathNotFoundException pnfe) {
320             // This gets thrown if the label doesn't already exist
321             throw new VersionException(JcrI18n.invalidVersionLabel.text(label, getPath()));
322         }
323 
324         Graph graph = cache.session().repository().createSystemGraph(context());
325         graph.remove(label).on(versionLabels.location).and();
326 
327         versionLabels.refresh(false);
328     }
329 
330     @Override
331     public NodeIterator getAllFrozenNodes() throws RepositoryException {
332         return new FrozenNodeIterator(getAllVersions());
333     }
334 
335     @Override
336     public NodeIterator getAllLinearFrozenNodes() throws RepositoryException {
337         return new FrozenNodeIterator(getAllLinearVersions());
338     }
339 
340     @Override
341     public VersionIterator getAllLinearVersions() throws RepositoryException {
342         AbstractJcrNode existingNode = session().getNodeByIdentifier(getVersionableIdentifier());
343         if (existingNode == null) return getAllVersions();
344 
345         assert existingNode.isNodeType(JcrMixLexicon.VERSIONABLE);
346 
347         LinkedList<JcrVersionNode> versions = new LinkedList<JcrVersionNode>();
348         JcrVersionNode baseVersion = existingNode.getBaseVersion();
349 
350         while (baseVersion != null) {
351             versions.addFirst(baseVersion);
352             baseVersion = baseVersion.getLinearPredecessor();
353         }
354 
355         return new LinearVersionIterator(versions, versions.size());
356     }
357 
358     @Override
359     public String getVersionableIdentifier() throws RepositoryException {
360         // ModeShape uses a node's UUID as it's identifier
361         return getVersionableUUID();
362     }
363 
364     /**
365      * Iterator over the versions within a version history. This class wraps the {@link JcrChildNodeIterator node iterator} for
366      * all nodes of the {@link JcrVersionHistoryNode version history}, silently ignoring the {@code jcr:rootVersion} and {@code
367      * jcr:versionLabels} children.
368      */
369     class JcrVersionIterator implements VersionIterator {
370 
371         private final NodeIterator nodeIterator;
372         private Version next;
373         private int position = 0;
374 
375         public JcrVersionIterator( NodeIterator nodeIterator ) {
376             super();
377             this.nodeIterator = nodeIterator;
378         }
379 
380         /**
381          * @{inheritDoc
382          */
383         @Override
384         public Version nextVersion() {
385             Version next = this.next;
386 
387             if (next != null) {
388                 this.next = null;
389                 return next;
390             }
391 
392             next = nextVersionIfPossible();
393 
394             if (next == null) {
395                 throw new NoSuchElementException();
396             }
397 
398             position++;
399             return next;
400         }
401 
402         private JcrVersionNode nextVersionIfPossible() {
403             while (nodeIterator.hasNext()) {
404                 AbstractJcrNode node = (AbstractJcrNode)nodeIterator.nextNode();
405 
406                 Name nodeName;
407                 try {
408                     nodeName = node.segment().getName();
409                 } catch (RepositoryException re) {
410                     throw new IllegalStateException(re);
411                 }
412 
413                 if (!JcrLexicon.VERSION_LABELS.equals(nodeName)) {
414                     return (JcrVersionNode)node;
415                 }
416             }
417 
418             return null;
419         }
420 
421         /**
422          * @{inheritDoc
423          */
424         @Override
425         public long getPosition() {
426             return position;
427         }
428 
429         /**
430          * @{inheritDoc
431          */
432         @Override
433         public long getSize() {
434             // The number of version nodes is the number of child nodes of the version history - 1
435             // (the jcr:versionLabels node)
436             return nodeIterator.getSize() - 1;
437         }
438 
439         /**
440          * @{inheritDoc
441          */
442         @Override
443         public void skip( long count ) {
444             // Walk through the list to make sure that we don't accidentally count jcr:rootVersion or jcr:versionLabels as a
445             // skipped node
446             while (count-- > 0) {
447                 nextVersion();
448             }
449         }
450 
451         /**
452          * @{inheritDoc
453          */
454         @Override
455         public boolean hasNext() {
456             if (this.next != null) return true;
457 
458             this.next = nextVersionIfPossible();
459 
460             return this.next != null;
461         }
462 
463         /**
464          * @{inheritDoc
465          */
466         @Override
467         public Object next() {
468             return nextVersion();
469         }
470 
471         /**
472          * @{inheritDoc
473          */
474         @Override
475         public void remove() {
476             throw new UnsupportedOperationException();
477         }
478     }
479 
480     /**
481      * An implementation of {@link VersionIterator} that iterates over a given set of versions. This differs from
482      * {@link JcrVersionIterator} in that it expects an exact list of versions to iterate over whereas {@code JcrVersionIterator}
483      * expects list of children for a {@code nt:versionHistory} node and filters out the label child.
484      */
485     class LinearVersionIterator implements VersionIterator {
486 
487         private final Iterator<? extends Version> versions;
488         private final int size;
489         private int pos;
490 
491         protected LinearVersionIterator( Iterable<? extends Version> versions,
492                                          int size ) {
493             this.versions = versions.iterator();
494             this.size = size;
495             this.pos = 0;
496         }
497 
498         @Override
499         public long getPosition() {
500             return pos;
501         }
502 
503         @Override
504         public long getSize() {
505             return this.size;
506         }
507 
508         @Override
509         public void skip( long skipNum ) {
510             while (skipNum-- > 0 && versions.hasNext()) {
511                 versions.next();
512                 pos++;
513             }
514 
515         }
516 
517         @Override
518         public Version nextVersion() {
519             return versions.next();
520         }
521 
522         @Override
523         public boolean hasNext() {
524             return versions.hasNext();
525         }
526 
527         @Override
528         public Object next() {
529             return nextVersion();
530         }
531 
532         @Override
533         public void remove() {
534             throw new UnsupportedOperationException();
535         }
536     }
537 
538     class FrozenNodeIterator implements NodeIterator {
539         private final VersionIterator versions;
540 
541         FrozenNodeIterator( VersionIterator versionIter ) {
542             this.versions = versionIter;
543         }
544 
545         @Override
546         public boolean hasNext() {
547             return versions.hasNext();
548         }
549 
550         @Override
551         public Object next() {
552             return nextNode();
553         }
554 
555         @Override
556         public void remove() {
557             throw new UnsupportedOperationException();
558         }
559 
560         @Override
561         public Node nextNode() {
562             try {
563                 return versions.nextVersion().getFrozenNode();
564             } catch (RepositoryException re) {
565                 // ModeShape doesn't throw a RepositoryException on getFrozenNode() from a valid version node
566                 throw new IllegalStateException(re);
567             }
568         }
569 
570         @Override
571         public long getPosition() {
572             return versions.getPosition();
573         }
574 
575         @Override
576         public long getSize() {
577             return versions.getSize();
578         }
579 
580         @Override
581         public void skip( long skipNum ) {
582             versions.skip(skipNum);
583         }
584     }
585 }