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      /**
209       * Used when load fileUC from fsimage. The file to be replaced is actually 
210       * only in snapshot, thus may not be contained in the children list. 
211       * See HDFS-5428 for details.
212       */
213      public void replaceChildFileInSnapshot(INodeFile oldChild,
214          final INodeFile newChild) {
215        if (children != null) {
216          final int i = searchChildren(newChild.getLocalNameBytes());
217          if (i >= 0 && children.get(i).getId() == oldChild.getId()) {
218            // no need to consider reference node here, since we already do the 
219            // replacement in FSImageFormat.Loader#loadFilesUnderConstruction
220            children.set(i, newChild);
221          }
222        }
223      }
224      
225      /** Replace the given child with a new child. */
226      public void replaceChild(INode oldChild, final INode newChild,
227          final INodeMap inodeMap) {
228        Preconditions.checkNotNull(children);
229        final int i = searchChildren(newChild.getLocalNameBytes());
230        Preconditions.checkState(i >= 0);
231        Preconditions.checkState(oldChild == children.get(i)
232            || oldChild == children.get(i).asReference().getReferredINode()
233                .asReference().getReferredINode());
234        oldChild = children.get(i);
235        
236        if (oldChild.isReference() && !newChild.isReference()) {
237          // replace the referred inode, e.g., 
238          // INodeFileWithSnapshot -> INodeFileUnderConstructionWithSnapshot
239          final INode withCount = oldChild.asReference().getReferredINode();
240          withCount.asReference().setReferredINode(newChild);
241        } else {
242          if (oldChild.isReference()) {
243            // both are reference nodes, e.g., DstReference -> WithName
244            final INodeReference.WithCount withCount = 
245                (WithCount) oldChild.asReference().getReferredINode();
246            withCount.removeReference(oldChild.asReference());
247          }
248          children.set(i, newChild);
249        }
250        // update the inodeMap
251        if (inodeMap != null) {
252          inodeMap.put(newChild);
253        }
254      }
255    
256      INodeReference.WithName replaceChild4ReferenceWithName(INode oldChild,
257          Snapshot latest) {
258        Preconditions.checkArgument(latest != null);
259        if (oldChild instanceof INodeReference.WithName) {
260          return (INodeReference.WithName)oldChild;
261        }
262    
263        final INodeReference.WithCount withCount;
264        if (oldChild.isReference()) {
265          Preconditions.checkState(oldChild instanceof INodeReference.DstReference);
266          withCount = (INodeReference.WithCount) oldChild.asReference()
267              .getReferredINode();
268        } else {
269          withCount = new INodeReference.WithCount(null, oldChild);
270        }
271        final INodeReference.WithName ref = new INodeReference.WithName(this,
272            withCount, oldChild.getLocalNameBytes(), latest.getId());
273        replaceChild(oldChild, ref, null);
274        return ref;
275      }
276      
277      private void replaceChildFile(final INodeFile oldChild,
278          final INodeFile newChild, final INodeMap inodeMap) {
279        replaceChild(oldChild, newChild, inodeMap);
280        oldChild.clear();
281        newChild.updateBlockCollection();
282      }
283    
284      /** Replace a child {@link INodeFile} with an {@link INodeFileWithSnapshot}. */
285      INodeFileWithSnapshot replaceChild4INodeFileWithSnapshot(
286          final INodeFile child, final INodeMap inodeMap) {
287        Preconditions.checkArgument(!(child instanceof INodeFileWithSnapshot),
288            "Child file is already an INodeFileWithSnapshot, child=" + child);
289        final INodeFileWithSnapshot newChild = new INodeFileWithSnapshot(child);
290        replaceChildFile(child, newChild, inodeMap);
291        return newChild;
292      }
293    
294      /** Replace a child {@link INodeFile} with an {@link INodeFileUnderConstructionWithSnapshot}. */
295      INodeFileUnderConstructionWithSnapshot replaceChild4INodeFileUcWithSnapshot(
296          final INodeFileUnderConstruction child, final INodeMap inodeMap) {
297        Preconditions.checkArgument(!(child instanceof INodeFileUnderConstructionWithSnapshot),
298            "Child file is already an INodeFileUnderConstructionWithSnapshot, child=" + child);
299        final INodeFileUnderConstructionWithSnapshot newChild
300            = new INodeFileUnderConstructionWithSnapshot(child, null);
301        replaceChildFile(child, newChild, inodeMap);
302        return newChild;
303      }
304    
305      @Override
306      public INodeDirectory recordModification(Snapshot latest,
307          final INodeMap inodeMap) throws QuotaExceededException {
308        if (isInLatestSnapshot(latest)) {
309          return replaceSelf4INodeDirectoryWithSnapshot(inodeMap)
310              .recordModification(latest, inodeMap);
311        } else {
312          return this;
313        }
314      }
315    
316      /**
317       * Save the child to the latest snapshot.
318       * 
319       * @return the child inode, which may be replaced.
320       */
321      public INode saveChild2Snapshot(final INode child, final Snapshot latest,
322          final INode snapshotCopy, final INodeMap inodeMap)
323          throws QuotaExceededException {
324        if (latest == null) {
325          return child;
326        }
327        return replaceSelf4INodeDirectoryWithSnapshot(inodeMap)
328            .saveChild2Snapshot(child, latest, snapshotCopy, inodeMap);
329      }
330    
331      /**
332       * @param name the name of the child
333       * @param snapshot
334       *          if it is not null, get the result from the given snapshot;
335       *          otherwise, get the result from the current directory.
336       * @return the child inode.
337       */
338      public INode getChild(byte[] name, Snapshot snapshot) {
339        final ReadOnlyList<INode> c = getChildrenList(snapshot);
340        final int i = ReadOnlyList.Util.binarySearch(c, name);
341        return i < 0? null: c.get(i);
342      }
343    
344      /** @return the {@link INodesInPath} containing only the last inode. */
345      INodesInPath getLastINodeInPath(String path, boolean resolveLink
346          ) throws UnresolvedLinkException {
347        return INodesInPath.resolve(this, getPathComponents(path), 1, resolveLink);
348      }
349    
350      /** @return the {@link INodesInPath} containing all inodes in the path. */
351      INodesInPath getINodesInPath(String path, boolean resolveLink
352          ) throws UnresolvedLinkException {
353        final byte[][] components = getPathComponents(path);
354        return INodesInPath.resolve(this, components, components.length, resolveLink);
355      }
356    
357      /** @return the last inode in the path. */
358      INode getNode(String path, boolean resolveLink) 
359        throws UnresolvedLinkException {
360        return getLastINodeInPath(path, resolveLink).getINode(0);
361      }
362    
363      /**
364       * @return the INode of the last component in src, or null if the last
365       * component does not exist.
366       * @throws UnresolvedLinkException if symlink can't be resolved
367       * @throws SnapshotAccessControlException if path is in RO snapshot
368       */
369      INode getINode4Write(String src, boolean resolveLink)
370          throws UnresolvedLinkException, SnapshotAccessControlException {
371        return getINodesInPath4Write(src, resolveLink).getLastINode();
372      }
373    
374      /**
375       * @return the INodesInPath of the components in src
376       * @throws UnresolvedLinkException if symlink can't be resolved
377       * @throws SnapshotAccessControlException if path is in RO snapshot
378       */
379      INodesInPath getINodesInPath4Write(String src, boolean resolveLink)
380          throws UnresolvedLinkException, SnapshotAccessControlException {
381        final byte[][] components = INode.getPathComponents(src);
382        INodesInPath inodesInPath = INodesInPath.resolve(this, components,
383            components.length, resolveLink);
384        if (inodesInPath.isSnapshot()) {
385          throw new SnapshotAccessControlException(
386              "Modification on a read-only snapshot is disallowed");
387        }
388        return inodesInPath;
389      }
390    
391      /**
392       * Given a child's name, return the index of the next child
393       *
394       * @param name a child's name
395       * @return the index of the next child
396       */
397      static int nextChild(ReadOnlyList<INode> children, byte[] name) {
398        if (name.length == 0) { // empty name
399          return 0;
400        }
401        int nextPos = ReadOnlyList.Util.binarySearch(children, name) + 1;
402        if (nextPos >= 0) {
403          return nextPos;
404        }
405        return -nextPos;
406      }
407    
408      /**
409       * Add a child inode to the directory.
410       * 
411       * @param node INode to insert
412       * @param setModTime set modification time for the parent node
413       *                   not needed when replaying the addition and 
414       *                   the parent already has the proper mod time
415       * @param inodeMap update the inodeMap if the directory node gets replaced
416       * @return false if the child with this name already exists; 
417       *         otherwise, return true;
418       */
419      public boolean addChild(INode node, final boolean setModTime,
420          final Snapshot latest, final INodeMap inodeMap)
421          throws QuotaExceededException {
422        final int low = searchChildren(node.getLocalNameBytes());
423        if (low >= 0) {
424          return false;
425        }
426    
427        if (isInLatestSnapshot(latest)) {
428          INodeDirectoryWithSnapshot sdir = 
429              replaceSelf4INodeDirectoryWithSnapshot(inodeMap);
430          boolean added = sdir.addChild(node, setModTime, latest, inodeMap);
431          return added;
432        }
433        addChild(node, low);
434        if (setModTime) {
435          // update modification time of the parent directory
436          updateModificationTime(node.getModificationTime(), latest, inodeMap);
437        }
438        return true;
439      }
440    
441    
442      /** The same as addChild(node, false, null, false) */
443      public boolean addChild(INode node) {
444        final int low = searchChildren(node.getLocalNameBytes());
445        if (low >= 0) {
446          return false;
447        }
448        addChild(node, low);
449        return true;
450      }
451    
452      /**
453       * Add the node to the children list at the given insertion point.
454       * The basic add method which actually calls children.add(..).
455       */
456      private void addChild(final INode node, final int insertionPoint) {
457        if (children == null) {
458          children = new ArrayList<INode>(DEFAULT_FILES_PER_DIRECTORY);
459        }
460        node.setParent(this);
461        children.add(-insertionPoint - 1, node);
462    
463        if (node.getGroupName() == null) {
464          node.setGroup(getGroupName());
465        }
466      }
467    
468      @Override
469      public Quota.Counts computeQuotaUsage(Quota.Counts counts, boolean useCache,
470          int lastSnapshotId) {
471        if (children != null) {
472          for (INode child : children) {
473            child.computeQuotaUsage(counts, useCache, lastSnapshotId);
474          }
475        }
476        return computeQuotaUsage4CurrentDirectory(counts);
477      }
478      
479      /** Add quota usage for this inode excluding children. */
480      public Quota.Counts computeQuotaUsage4CurrentDirectory(Quota.Counts counts) {
481        counts.add(Quota.NAMESPACE, 1);
482        return counts;
483      }
484    
485      @Override
486      public Content.Counts computeContentSummary(final Content.Counts counts) {
487        for (INode child : getChildrenList(null)) {
488          child.computeContentSummary(counts);
489        }
490        counts.add(Content.DIRECTORY, 1);
491        return counts;
492      }
493    
494      /**
495       * @param snapshot
496       *          if it is not null, get the result from the given snapshot;
497       *          otherwise, get the result from the current directory.
498       * @return the current children list if the specified snapshot is null;
499       *         otherwise, return the children list corresponding to the snapshot.
500       *         Note that the returned list is never null.
501       */
502      public ReadOnlyList<INode> getChildrenList(final Snapshot snapshot) {
503        return children == null ? ReadOnlyList.Util.<INode>emptyList()
504            : ReadOnlyList.Util.asReadOnlyList(children);
505      }
506    
507      /** Set the children list to null. */
508      public void clearChildren() {
509        this.children = null;
510      }
511    
512      @Override
513      public void clear() {
514        super.clear();
515        clearChildren();
516      }
517    
518      /** Call cleanSubtree(..) recursively down the subtree. */
519      public Quota.Counts cleanSubtreeRecursively(final Snapshot snapshot,
520          Snapshot prior, final BlocksMapUpdateInfo collectedBlocks,
521          final List<INode> removedINodes, final Map<INode, INode> excludedNodes, 
522          final boolean countDiffChange) throws QuotaExceededException {
523        Quota.Counts counts = Quota.Counts.newInstance();
524        // in case of deletion snapshot, since this call happens after we modify
525        // the diff list, the snapshot to be deleted has been combined or renamed
526        // to its latest previous snapshot. (besides, we also need to consider nodes
527        // created after prior but before snapshot. this will be done in 
528        // INodeDirectoryWithSnapshot#cleanSubtree)
529        Snapshot s = snapshot != null && prior != null ? prior : snapshot;
530        for (INode child : getChildrenList(s)) {
531          if (snapshot != null && excludedNodes != null
532              && excludedNodes.containsKey(child)) {
533            continue;
534          } else {
535            Quota.Counts childCounts = child.cleanSubtree(snapshot, prior,
536                collectedBlocks, removedINodes, countDiffChange);
537            counts.add(childCounts);
538          }
539        }
540        return counts;
541      }
542    
543      @Override
544      public void destroyAndCollectBlocks(final BlocksMapUpdateInfo collectedBlocks,
545          final List<INode> removedINodes) {
546        for (INode child : getChildrenList(null)) {
547          child.destroyAndCollectBlocks(collectedBlocks, removedINodes);
548        }
549        clear();
550        removedINodes.add(this);
551      }
552      
553      @Override
554      public Quota.Counts cleanSubtree(final Snapshot snapshot, Snapshot prior,
555          final BlocksMapUpdateInfo collectedBlocks,
556          final List<INode> removedINodes, final boolean countDiffChange)
557          throws QuotaExceededException {
558        if (prior == null && snapshot == null) {
559          // destroy the whole subtree and collect blocks that should be deleted
560          Quota.Counts counts = Quota.Counts.newInstance();
561          this.computeQuotaUsage(counts, true);
562          destroyAndCollectBlocks(collectedBlocks, removedINodes);
563          return counts; 
564        } else {
565          // process recursively down the subtree
566          Quota.Counts counts = cleanSubtreeRecursively(snapshot, prior,
567              collectedBlocks, removedINodes, null, countDiffChange);
568          if (isQuotaSet()) {
569            ((INodeDirectoryWithQuota) this).addSpaceConsumed2Cache(
570                -counts.get(Quota.NAMESPACE), -counts.get(Quota.DISKSPACE));
571          }
572          return counts;
573        }
574      }
575      
576      /**
577       * Compare the metadata with another INodeDirectory
578       */
579      @Override
580      public boolean metadataEquals(INodeDirectoryAttributes other) {
581        return other != null
582            && getNsQuota() == other.getNsQuota()
583            && getDsQuota() == other.getDsQuota()
584            && getPermissionLong() == other.getPermissionLong();
585      }
586      
587      /*
588       * The following code is to dump the tree recursively for testing.
589       * 
590       *      \- foo   (INodeDirectory@33dd2717)
591       *        \- sub1   (INodeDirectory@442172)
592       *          +- file1   (INodeFile@78392d4)
593       *          +- file2   (INodeFile@78392d5)
594       *          +- sub11   (INodeDirectory@8400cff)
595       *            \- file3   (INodeFile@78392d6)
596       *          \- z_file4   (INodeFile@45848712)
597       */
598      static final String DUMPTREE_EXCEPT_LAST_ITEM = "+-"; 
599      static final String DUMPTREE_LAST_ITEM = "\\-";
600      @VisibleForTesting
601      @Override
602      public void dumpTreeRecursively(PrintWriter out, StringBuilder prefix,
603          final Snapshot snapshot) {
604        super.dumpTreeRecursively(out, prefix, snapshot);
605        out.print(", childrenSize=" + getChildrenList(snapshot).size());
606        if (this instanceof INodeDirectoryWithQuota) {
607          out.print(((INodeDirectoryWithQuota)this).quotaString());
608        }
609        if (this instanceof Snapshot.Root) {
610          out.print(", snapshotId=" + snapshot.getId());
611        }
612        out.println();
613    
614        if (prefix.length() >= 2) {
615          prefix.setLength(prefix.length() - 2);
616          prefix.append("  ");
617        }
618        dumpTreeRecursively(out, prefix, new Iterable<SnapshotAndINode>() {
619          final Iterator<INode> i = getChildrenList(snapshot).iterator();
620          
621          @Override
622          public Iterator<SnapshotAndINode> iterator() {
623            return new Iterator<SnapshotAndINode>() {
624              @Override
625              public boolean hasNext() {
626                return i.hasNext();
627              }
628    
629              @Override
630              public SnapshotAndINode next() {
631                return new SnapshotAndINode(snapshot, i.next());
632              }
633    
634              @Override
635              public void remove() {
636                throw new UnsupportedOperationException();
637              }
638            };
639          }
640        });
641      }
642    
643      /**
644       * Dump the given subtrees.
645       * @param prefix The prefix string that each line should print.
646       * @param subs The subtrees.
647       */
648      @VisibleForTesting
649      protected static void dumpTreeRecursively(PrintWriter out,
650          StringBuilder prefix, Iterable<SnapshotAndINode> subs) {
651        if (subs != null) {
652          for(final Iterator<SnapshotAndINode> i = subs.iterator(); i.hasNext();) {
653            final SnapshotAndINode pair = i.next();
654            prefix.append(i.hasNext()? DUMPTREE_EXCEPT_LAST_ITEM: DUMPTREE_LAST_ITEM);
655            pair.inode.dumpTreeRecursively(out, prefix, pair.snapshot);
656            prefix.setLength(prefix.length() - 2);
657          }
658        }
659      }
660    
661      /** A pair of Snapshot and INode objects. */
662      protected static class SnapshotAndINode {
663        public final Snapshot snapshot;
664        public final INode inode;
665    
666        public SnapshotAndINode(Snapshot snapshot, INode inode) {
667          this.snapshot = snapshot;
668          this.inode = inode;
669        }
670    
671        public SnapshotAndINode(Snapshot snapshot) {
672          this(snapshot, snapshot.getRoot());
673        }
674      }
675    
676      public final int getChildrenNum(final Snapshot snapshot) {
677        return getChildrenList(snapshot).size();
678      }
679    }