001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    package org.apache.hadoop.hdfs.server.namenode;
019    
020    import java.io.FileNotFoundException;
021    import java.io.PrintWriter;
022    import java.util.ArrayList;
023    import java.util.Collections;
024    import java.util.Iterator;
025    import java.util.List;
026    import java.util.Map;
027    
028    import org.apache.hadoop.fs.PathIsNotDirectoryException;
029    import org.apache.hadoop.fs.UnresolvedLinkException;
030    import org.apache.hadoop.fs.permission.PermissionStatus;
031    import org.apache.hadoop.hdfs.DFSUtil;
032    import org.apache.hadoop.hdfs.protocol.QuotaExceededException;
033    import org.apache.hadoop.hdfs.protocol.SnapshotAccessControlException;
034    import org.apache.hadoop.hdfs.server.namenode.INodeReference.WithCount;
035    import org.apache.hadoop.hdfs.server.namenode.snapshot.INodeDirectorySnapshottable;
036    import org.apache.hadoop.hdfs.server.namenode.snapshot.INodeDirectoryWithSnapshot;
037    import org.apache.hadoop.hdfs.server.namenode.snapshot.INodeFileUnderConstructionWithSnapshot;
038    import org.apache.hadoop.hdfs.server.namenode.snapshot.INodeFileWithSnapshot;
039    import org.apache.hadoop.hdfs.server.namenode.snapshot.Snapshot;
040    import org.apache.hadoop.hdfs.util.ReadOnlyList;
041    
042    import com.google.common.annotations.VisibleForTesting;
043    import com.google.common.base.Preconditions;
044    
045    /**
046     * Directory INode class.
047     */
048    public class INodeDirectory extends INodeWithAdditionalFields
049        implements INodeDirectoryAttributes {
050      /** Cast INode to INodeDirectory. */
051      public static INodeDirectory valueOf(INode inode, Object path
052          ) throws FileNotFoundException, PathIsNotDirectoryException {
053        if (inode == null) {
054          throw new FileNotFoundException("Directory does not exist: "
055              + DFSUtil.path2String(path));
056        }
057        if (!inode.isDirectory()) {
058          throw new PathIsNotDirectoryException(DFSUtil.path2String(path));
059        }
060        return inode.asDirectory(); 
061      }
062    
063      protected static final int DEFAULT_FILES_PER_DIRECTORY = 5;
064      final static byte[] ROOT_NAME = DFSUtil.string2Bytes("");
065    
066      private List<INode> children = null;
067    
068      /** constructor */
069      public INodeDirectory(long id, byte[] name, PermissionStatus permissions,
070          long mtime) {
071        super(id, name, permissions, mtime, 0L);
072      }
073      
074      /**
075       * Copy constructor
076       * @param other The INodeDirectory to be copied
077       * @param adopt Indicate whether or not need to set the parent field of child
078       *              INodes to the new node
079       */
080      public INodeDirectory(INodeDirectory other, boolean adopt) {
081        super(other);
082        this.children = other.children;
083        if (adopt && this.children != null) {
084          for (INode child : children) {
085            child.setParent(this);
086          }
087        }
088      }
089    
090      /** @return true unconditionally. */
091      @Override
092      public final boolean isDirectory() {
093        return true;
094      }
095    
096      /** @return this object. */
097      @Override
098      public final INodeDirectory asDirectory() {
099        return this;
100      }
101    
102      /** Is this a snapshottable directory? */
103      public boolean isSnapshottable() {
104        return false;
105      }
106    
107      private int searchChildren(byte[] name) {
108        return children == null? -1: Collections.binarySearch(children, name);
109      }
110    
111      /**
112       * Remove the specified child from this directory.
113       * 
114       * @param child the child inode to be removed
115       * @param latest See {@link INode#recordModification(Snapshot, INodeMap)}.
116       */
117      public boolean removeChild(INode child, Snapshot latest,
118          final INodeMap inodeMap) throws QuotaExceededException {
119        if (isInLatestSnapshot(latest)) {
120          return replaceSelf4INodeDirectoryWithSnapshot(inodeMap)
121              .removeChild(child, latest, inodeMap);
122        }
123    
124        return removeChild(child);
125      }
126    
127      /** 
128       * Remove the specified child from this directory.
129       * The basic remove method which actually calls children.remove(..).
130       *
131       * @param child the child inode to be removed
132       * 
133       * @return true if the child is removed; false if the child is not found.
134       */
135      protected final boolean removeChild(final INode child) {
136        final int i = searchChildren(child.getLocalNameBytes());
137        if (i < 0) {
138          return false;
139        }
140    
141        final INode removed = children.remove(i);
142        Preconditions.checkState(removed == child);
143        return true;
144      }
145    
146      /**
147       * Replace itself with {@link INodeDirectoryWithQuota} or
148       * {@link INodeDirectoryWithSnapshot} depending on the latest snapshot.
149       */
150      INodeDirectoryWithQuota replaceSelf4Quota(final Snapshot latest,
151          final long nsQuota, final long dsQuota, final INodeMap inodeMap)
152          throws QuotaExceededException {
153        Preconditions.checkState(!(this instanceof INodeDirectoryWithQuota),
154            "this is already an INodeDirectoryWithQuota, this=%s", this);
155    
156        if (!this.isInLatestSnapshot(latest)) {
157          final INodeDirectoryWithQuota q = new INodeDirectoryWithQuota(
158              this, true, nsQuota, dsQuota);
159          replaceSelf(q, inodeMap);
160          return q;
161        } else {
162          final INodeDirectoryWithSnapshot s = new INodeDirectoryWithSnapshot(this);
163          s.setQuota(nsQuota, dsQuota);
164          return replaceSelf(s, inodeMap).saveSelf2Snapshot(latest, this);
165        }
166      }
167      /** Replace itself with an {@link INodeDirectorySnapshottable}. */
168      public INodeDirectorySnapshottable replaceSelf4INodeDirectorySnapshottable(
169          Snapshot latest, final INodeMap inodeMap) throws QuotaExceededException {
170        Preconditions.checkState(!(this instanceof INodeDirectorySnapshottable),
171            "this is already an INodeDirectorySnapshottable, this=%s", this);
172        final INodeDirectorySnapshottable s = new INodeDirectorySnapshottable(this);
173        replaceSelf(s, inodeMap).saveSelf2Snapshot(latest, this);
174        return s;
175      }
176    
177      /** Replace itself with an {@link INodeDirectoryWithSnapshot}. */
178      public INodeDirectoryWithSnapshot replaceSelf4INodeDirectoryWithSnapshot(
179          final INodeMap inodeMap) {
180        return replaceSelf(new INodeDirectoryWithSnapshot(this), inodeMap);
181      }
182    
183      /** Replace itself with {@link INodeDirectory}. */
184      public INodeDirectory replaceSelf4INodeDirectory(final INodeMap inodeMap) {
185        Preconditions.checkState(getClass() != INodeDirectory.class,
186            "the class is already INodeDirectory, this=%s", this);
187        return replaceSelf(new INodeDirectory(this, true), inodeMap);
188      }
189    
190      /** Replace itself with the given directory. */
191      private final <N extends INodeDirectory> N replaceSelf(final N newDir,
192          final INodeMap inodeMap) {
193        final INodeReference ref = getParentReference();
194        if (ref != null) {
195          ref.setReferredINode(newDir);
196          if (inodeMap != null) {
197            inodeMap.put(newDir);
198          }
199        } else {
200          final INodeDirectory parent = getParent();
201          Preconditions.checkArgument(parent != null, "parent is null, this=%s", this);
202          parent.replaceChild(this, newDir, inodeMap);
203        }
204        clear();
205        return newDir;
206      }
207    
208      /** Replace the given child with a new child. */
209      public void replaceChild(INode oldChild, final INode newChild,
210          final INodeMap inodeMap) {
211        Preconditions.checkNotNull(children);
212        final int i = searchChildren(newChild.getLocalNameBytes());
213        Preconditions.checkState(i >= 0);
214        Preconditions.checkState(oldChild == children.get(i)
215            || oldChild == children.get(i).asReference().getReferredINode()
216                .asReference().getReferredINode());
217        oldChild = children.get(i);
218        
219        if (oldChild.isReference() && !newChild.isReference()) {
220          // replace the referred inode, e.g., 
221          // INodeFileWithSnapshot -> INodeFileUnderConstructionWithSnapshot
222          final INode withCount = oldChild.asReference().getReferredINode();
223          withCount.asReference().setReferredINode(newChild);
224        } else {
225          if (oldChild.isReference()) {
226            // both are reference nodes, e.g., DstReference -> WithName
227            final INodeReference.WithCount withCount = 
228                (WithCount) oldChild.asReference().getReferredINode();
229            withCount.removeReference(oldChild.asReference());
230          }
231          children.set(i, newChild);
232        }
233        // update the inodeMap
234        if (inodeMap != null) {
235          inodeMap.put(newChild);
236        }
237      }
238    
239      INodeReference.WithName replaceChild4ReferenceWithName(INode oldChild,
240          Snapshot latest) {
241        Preconditions.checkArgument(latest != null);
242        if (oldChild instanceof INodeReference.WithName) {
243          return (INodeReference.WithName)oldChild;
244        }
245    
246        final INodeReference.WithCount withCount;
247        if (oldChild.isReference()) {
248          Preconditions.checkState(oldChild instanceof INodeReference.DstReference);
249          withCount = (INodeReference.WithCount) oldChild.asReference()
250              .getReferredINode();
251        } else {
252          withCount = new INodeReference.WithCount(null, oldChild);
253        }
254        final INodeReference.WithName ref = new INodeReference.WithName(this,
255            withCount, oldChild.getLocalNameBytes(), latest.getId());
256        replaceChild(oldChild, ref, null);
257        return ref;
258      }
259      
260      private void replaceChildFile(final INodeFile oldChild,
261          final INodeFile newChild, final INodeMap inodeMap) {
262        replaceChild(oldChild, newChild, inodeMap);
263        oldChild.clear();
264        newChild.updateBlockCollection();
265      }
266    
267      /** Replace a child {@link INodeFile} with an {@link INodeFileWithSnapshot}. */
268      INodeFileWithSnapshot replaceChild4INodeFileWithSnapshot(
269          final INodeFile child, final INodeMap inodeMap) {
270        Preconditions.checkArgument(!(child instanceof INodeFileWithSnapshot),
271            "Child file is already an INodeFileWithSnapshot, child=" + child);
272        final INodeFileWithSnapshot newChild = new INodeFileWithSnapshot(child);
273        replaceChildFile(child, newChild, inodeMap);
274        return newChild;
275      }
276    
277      /** Replace a child {@link INodeFile} with an {@link INodeFileUnderConstructionWithSnapshot}. */
278      INodeFileUnderConstructionWithSnapshot replaceChild4INodeFileUcWithSnapshot(
279          final INodeFileUnderConstruction child, final INodeMap inodeMap) {
280        Preconditions.checkArgument(!(child instanceof INodeFileUnderConstructionWithSnapshot),
281            "Child file is already an INodeFileUnderConstructionWithSnapshot, child=" + child);
282        final INodeFileUnderConstructionWithSnapshot newChild
283            = new INodeFileUnderConstructionWithSnapshot(child, null);
284        replaceChildFile(child, newChild, inodeMap);
285        return newChild;
286      }
287    
288      @Override
289      public INodeDirectory recordModification(Snapshot latest,
290          final INodeMap inodeMap) throws QuotaExceededException {
291        if (isInLatestSnapshot(latest)) {
292          return replaceSelf4INodeDirectoryWithSnapshot(inodeMap)
293              .recordModification(latest, inodeMap);
294        } else {
295          return this;
296        }
297      }
298    
299      /**
300       * Save the child to the latest snapshot.
301       * 
302       * @return the child inode, which may be replaced.
303       */
304      public INode saveChild2Snapshot(final INode child, final Snapshot latest,
305          final INode snapshotCopy, final INodeMap inodeMap)
306          throws QuotaExceededException {
307        if (latest == null) {
308          return child;
309        }
310        return replaceSelf4INodeDirectoryWithSnapshot(inodeMap)
311            .saveChild2Snapshot(child, latest, snapshotCopy, inodeMap);
312      }
313    
314      /**
315       * @param name the name of the child
316       * @param snapshot
317       *          if it is not null, get the result from the given snapshot;
318       *          otherwise, get the result from the current directory.
319       * @return the child inode.
320       */
321      public INode getChild(byte[] name, Snapshot snapshot) {
322        final ReadOnlyList<INode> c = getChildrenList(snapshot);
323        final int i = ReadOnlyList.Util.binarySearch(c, name);
324        return i < 0? null: c.get(i);
325      }
326    
327      /** @return the {@link INodesInPath} containing only the last inode. */
328      INodesInPath getLastINodeInPath(String path, boolean resolveLink
329          ) throws UnresolvedLinkException {
330        return INodesInPath.resolve(this, getPathComponents(path), 1, resolveLink);
331      }
332    
333      /** @return the {@link INodesInPath} containing all inodes in the path. */
334      INodesInPath getINodesInPath(String path, boolean resolveLink
335          ) throws UnresolvedLinkException {
336        final byte[][] components = getPathComponents(path);
337        return INodesInPath.resolve(this, components, components.length, resolveLink);
338      }
339    
340      /** @return the last inode in the path. */
341      INode getNode(String path, boolean resolveLink) 
342        throws UnresolvedLinkException {
343        return getLastINodeInPath(path, resolveLink).getINode(0);
344      }
345    
346      /**
347       * @return the INode of the last component in src, or null if the last
348       * component does not exist.
349       * @throws UnresolvedLinkException if symlink can't be resolved
350       * @throws SnapshotAccessControlException if path is in RO snapshot
351       */
352      INode getINode4Write(String src, boolean resolveLink)
353          throws UnresolvedLinkException, SnapshotAccessControlException {
354        return getINodesInPath4Write(src, resolveLink).getLastINode();
355      }
356    
357      /**
358       * @return the INodesInPath of the components in src
359       * @throws UnresolvedLinkException if symlink can't be resolved
360       * @throws SnapshotAccessControlException if path is in RO snapshot
361       */
362      INodesInPath getINodesInPath4Write(String src, boolean resolveLink)
363          throws UnresolvedLinkException, SnapshotAccessControlException {
364        final byte[][] components = INode.getPathComponents(src);
365        INodesInPath inodesInPath = INodesInPath.resolve(this, components,
366            components.length, resolveLink);
367        if (inodesInPath.isSnapshot()) {
368          throw new SnapshotAccessControlException(
369              "Modification on a read-only snapshot is disallowed");
370        }
371        return inodesInPath;
372      }
373    
374      /**
375       * Given a child's name, return the index of the next child
376       *
377       * @param name a child's name
378       * @return the index of the next child
379       */
380      static int nextChild(ReadOnlyList<INode> children, byte[] name) {
381        if (name.length == 0) { // empty name
382          return 0;
383        }
384        int nextPos = ReadOnlyList.Util.binarySearch(children, name) + 1;
385        if (nextPos >= 0) {
386          return nextPos;
387        }
388        return -nextPos;
389      }
390    
391      /**
392       * Add a child inode to the directory.
393       * 
394       * @param node INode to insert
395       * @param setModTime set modification time for the parent node
396       *                   not needed when replaying the addition and 
397       *                   the parent already has the proper mod time
398       * @param inodeMap update the inodeMap if the directory node gets replaced
399       * @return false if the child with this name already exists; 
400       *         otherwise, return true;
401       */
402      public boolean addChild(INode node, final boolean setModTime,
403          final Snapshot latest, final INodeMap inodeMap)
404          throws QuotaExceededException {
405        final int low = searchChildren(node.getLocalNameBytes());
406        if (low >= 0) {
407          return false;
408        }
409    
410        if (isInLatestSnapshot(latest)) {
411          INodeDirectoryWithSnapshot sdir = 
412              replaceSelf4INodeDirectoryWithSnapshot(inodeMap);
413          boolean added = sdir.addChild(node, setModTime, latest, inodeMap);
414          return added;
415        }
416        addChild(node, low);
417        if (setModTime) {
418          // update modification time of the parent directory
419          updateModificationTime(node.getModificationTime(), latest, inodeMap);
420        }
421        return true;
422      }
423    
424    
425      /** The same as addChild(node, false, null, false) */
426      public boolean addChild(INode node) {
427        final int low = searchChildren(node.getLocalNameBytes());
428        if (low >= 0) {
429          return false;
430        }
431        addChild(node, low);
432        return true;
433      }
434    
435      /**
436       * Add the node to the children list at the given insertion point.
437       * The basic add method which actually calls children.add(..).
438       */
439      private void addChild(final INode node, final int insertionPoint) {
440        if (children == null) {
441          children = new ArrayList<INode>(DEFAULT_FILES_PER_DIRECTORY);
442        }
443        node.setParent(this);
444        children.add(-insertionPoint - 1, node);
445    
446        if (node.getGroupName() == null) {
447          node.setGroup(getGroupName());
448        }
449      }
450    
451      @Override
452      public Quota.Counts computeQuotaUsage(Quota.Counts counts, boolean useCache,
453          int lastSnapshotId) {
454        if (children != null) {
455          for (INode child : children) {
456            child.computeQuotaUsage(counts, useCache, lastSnapshotId);
457          }
458        }
459        return computeQuotaUsage4CurrentDirectory(counts);
460      }
461      
462      /** Add quota usage for this inode excluding children. */
463      public Quota.Counts computeQuotaUsage4CurrentDirectory(Quota.Counts counts) {
464        counts.add(Quota.NAMESPACE, 1);
465        return counts;
466      }
467    
468      @Override
469      public Content.Counts computeContentSummary(final Content.Counts counts) {
470        for (INode child : getChildrenList(null)) {
471          child.computeContentSummary(counts);
472        }
473        counts.add(Content.DIRECTORY, 1);
474        return counts;
475      }
476    
477      /**
478       * @param snapshot
479       *          if it is not null, get the result from the given snapshot;
480       *          otherwise, get the result from the current directory.
481       * @return the current children list if the specified snapshot is null;
482       *         otherwise, return the children list corresponding to the snapshot.
483       *         Note that the returned list is never null.
484       */
485      public ReadOnlyList<INode> getChildrenList(final Snapshot snapshot) {
486        return children == null ? ReadOnlyList.Util.<INode>emptyList()
487            : ReadOnlyList.Util.asReadOnlyList(children);
488      }
489    
490      /** Set the children list to null. */
491      public void clearChildren() {
492        this.children = null;
493      }
494    
495      @Override
496      public void clear() {
497        super.clear();
498        clearChildren();
499      }
500    
501      /** Call cleanSubtree(..) recursively down the subtree. */
502      public Quota.Counts cleanSubtreeRecursively(final Snapshot snapshot,
503          Snapshot prior, final BlocksMapUpdateInfo collectedBlocks,
504          final List<INode> removedINodes, final Map<INode, INode> excludedNodes, 
505          final boolean countDiffChange) throws QuotaExceededException {
506        Quota.Counts counts = Quota.Counts.newInstance();
507        // in case of deletion snapshot, since this call happens after we modify
508        // the diff list, the snapshot to be deleted has been combined or renamed
509        // to its latest previous snapshot. (besides, we also need to consider nodes
510        // created after prior but before snapshot. this will be done in 
511        // INodeDirectoryWithSnapshot#cleanSubtree)
512        Snapshot s = snapshot != null && prior != null ? prior : snapshot;
513        for (INode child : getChildrenList(s)) {
514          if (snapshot != null && excludedNodes != null
515              && excludedNodes.containsKey(child)) {
516            continue;
517          } else {
518            Quota.Counts childCounts = child.cleanSubtree(snapshot, prior,
519                collectedBlocks, removedINodes, countDiffChange);
520            counts.add(childCounts);
521          }
522        }
523        return counts;
524      }
525    
526      @Override
527      public void destroyAndCollectBlocks(final BlocksMapUpdateInfo collectedBlocks,
528          final List<INode> removedINodes) {
529        for (INode child : getChildrenList(null)) {
530          child.destroyAndCollectBlocks(collectedBlocks, removedINodes);
531        }
532        clear();
533        removedINodes.add(this);
534      }
535      
536      @Override
537      public Quota.Counts cleanSubtree(final Snapshot snapshot, Snapshot prior,
538          final BlocksMapUpdateInfo collectedBlocks,
539          final List<INode> removedINodes, final boolean countDiffChange)
540          throws QuotaExceededException {
541        if (prior == null && snapshot == null) {
542          // destroy the whole subtree and collect blocks that should be deleted
543          Quota.Counts counts = Quota.Counts.newInstance();
544          this.computeQuotaUsage(counts, true);
545          destroyAndCollectBlocks(collectedBlocks, removedINodes);
546          return counts; 
547        } else {
548          // process recursively down the subtree
549          Quota.Counts counts = cleanSubtreeRecursively(snapshot, prior,
550              collectedBlocks, removedINodes, null, countDiffChange);
551          if (isQuotaSet()) {
552            ((INodeDirectoryWithQuota) this).addSpaceConsumed2Cache(
553                -counts.get(Quota.NAMESPACE), -counts.get(Quota.DISKSPACE));
554          }
555          return counts;
556        }
557      }
558      
559      /**
560       * Compare the metadata with another INodeDirectory
561       */
562      @Override
563      public boolean metadataEquals(INodeDirectoryAttributes other) {
564        return other != null
565            && getNsQuota() == other.getNsQuota()
566            && getDsQuota() == other.getDsQuota()
567            && getPermissionLong() == other.getPermissionLong();
568      }
569      
570      /*
571       * The following code is to dump the tree recursively for testing.
572       * 
573       *      \- foo   (INodeDirectory@33dd2717)
574       *        \- sub1   (INodeDirectory@442172)
575       *          +- file1   (INodeFile@78392d4)
576       *          +- file2   (INodeFile@78392d5)
577       *          +- sub11   (INodeDirectory@8400cff)
578       *            \- file3   (INodeFile@78392d6)
579       *          \- z_file4   (INodeFile@45848712)
580       */
581      static final String DUMPTREE_EXCEPT_LAST_ITEM = "+-"; 
582      static final String DUMPTREE_LAST_ITEM = "\\-";
583      @VisibleForTesting
584      @Override
585      public void dumpTreeRecursively(PrintWriter out, StringBuilder prefix,
586          final Snapshot snapshot) {
587        super.dumpTreeRecursively(out, prefix, snapshot);
588        out.print(", childrenSize=" + getChildrenList(snapshot).size());
589        if (this instanceof INodeDirectoryWithQuota) {
590          out.print(((INodeDirectoryWithQuota)this).quotaString());
591        }
592        if (this instanceof Snapshot.Root) {
593          out.print(", snapshotId=" + snapshot.getId());
594        }
595        out.println();
596    
597        if (prefix.length() >= 2) {
598          prefix.setLength(prefix.length() - 2);
599          prefix.append("  ");
600        }
601        dumpTreeRecursively(out, prefix, new Iterable<SnapshotAndINode>() {
602          final Iterator<INode> i = getChildrenList(snapshot).iterator();
603          
604          @Override
605          public Iterator<SnapshotAndINode> iterator() {
606            return new Iterator<SnapshotAndINode>() {
607              @Override
608              public boolean hasNext() {
609                return i.hasNext();
610              }
611    
612              @Override
613              public SnapshotAndINode next() {
614                return new SnapshotAndINode(snapshot, i.next());
615              }
616    
617              @Override
618              public void remove() {
619                throw new UnsupportedOperationException();
620              }
621            };
622          }
623        });
624      }
625    
626      /**
627       * Dump the given subtrees.
628       * @param prefix The prefix string that each line should print.
629       * @param subs The subtrees.
630       */
631      @VisibleForTesting
632      protected static void dumpTreeRecursively(PrintWriter out,
633          StringBuilder prefix, Iterable<SnapshotAndINode> subs) {
634        if (subs != null) {
635          for(final Iterator<SnapshotAndINode> i = subs.iterator(); i.hasNext();) {
636            final SnapshotAndINode pair = i.next();
637            prefix.append(i.hasNext()? DUMPTREE_EXCEPT_LAST_ITEM: DUMPTREE_LAST_ITEM);
638            pair.inode.dumpTreeRecursively(out, prefix, pair.snapshot);
639            prefix.setLength(prefix.length() - 2);
640          }
641        }
642      }
643    
644      /** A pair of Snapshot and INode objects. */
645      protected static class SnapshotAndINode {
646        public final Snapshot snapshot;
647        public final INode inode;
648    
649        public SnapshotAndINode(Snapshot snapshot, INode inode) {
650          this.snapshot = snapshot;
651          this.inode = inode;
652        }
653    
654        public SnapshotAndINode(Snapshot snapshot) {
655          this(snapshot, snapshot.getRoot());
656        }
657      }
658    
659      public final int getChildrenNum(final Snapshot snapshot) {
660        return getChildrenList(snapshot).size();
661      }
662    }