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 */
018package org.apache.hadoop.hdfs.server.namenode;
019
020import java.io.FileNotFoundException;
021import java.io.PrintWriter;
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.Iterator;
025import java.util.List;
026import java.util.Map;
027
028import org.apache.hadoop.fs.PathIsNotDirectoryException;
029import org.apache.hadoop.fs.UnresolvedLinkException;
030import org.apache.hadoop.fs.permission.PermissionStatus;
031import org.apache.hadoop.hdfs.DFSUtil;
032import org.apache.hadoop.hdfs.protocol.QuotaExceededException;
033import org.apache.hadoop.hdfs.protocol.SnapshotAccessControlException;
034import org.apache.hadoop.hdfs.server.namenode.INodeReference.WithCount;
035import org.apache.hadoop.hdfs.server.namenode.snapshot.INodeDirectorySnapshottable;
036import org.apache.hadoop.hdfs.server.namenode.snapshot.INodeDirectoryWithSnapshot;
037import org.apache.hadoop.hdfs.server.namenode.snapshot.INodeFileUnderConstructionWithSnapshot;
038import org.apache.hadoop.hdfs.server.namenode.snapshot.INodeFileWithSnapshot;
039import org.apache.hadoop.hdfs.server.namenode.snapshot.Snapshot;
040import org.apache.hadoop.hdfs.util.ReadOnlyList;
041
042import com.google.common.annotations.VisibleForTesting;
043import com.google.common.base.Preconditions;
044
045/**
046 * Directory INode class.
047 */
048public 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 ContentSummaryComputationContext computeContentSummary(
487      ContentSummaryComputationContext summary) {
488    ReadOnlyList<INode> childrenList = getChildrenList(null);
489    // Explicit traversing is done to enable repositioning after relinquishing
490    // and reacquiring locks.
491    for (int i = 0;  i < childrenList.size(); i++) {
492      INode child = childrenList.get(i);
493      byte[] childName = child.getLocalNameBytes();
494
495      long lastYieldCount = summary.getYieldCount();
496      child.computeContentSummary(summary);
497
498      // Check whether the computation was paused in the subtree.
499      // The counts may be off, but traversing the rest of children
500      // should be made safe.
501      if (lastYieldCount == summary.getYieldCount()) {
502        continue;
503      }
504
505      // The locks were released and reacquired. Check parent first.
506      if (getParent() == null) {
507        // Stop further counting and return whatever we have so far.
508        break;
509      }
510
511      // Obtain the children list again since it may have been modified.
512      childrenList = getChildrenList(null);
513      // Reposition in case the children list is changed. Decrement by 1
514      // since it will be incremented when loops.
515      i = nextChild(childrenList, childName) - 1;
516    }
517
518    // Increment the directory count for this directory.
519    summary.getCounts().add(Content.DIRECTORY, 1);
520
521    // Relinquish and reacquire locks if necessary.
522    summary.yield();
523
524    return summary;
525  }
526
527  /**
528   * @param snapshot
529   *          if it is not null, get the result from the given snapshot;
530   *          otherwise, get the result from the current directory.
531   * @return the current children list if the specified snapshot is null;
532   *         otherwise, return the children list corresponding to the snapshot.
533   *         Note that the returned list is never null.
534   */
535  public ReadOnlyList<INode> getChildrenList(final Snapshot snapshot) {
536    return children == null ? ReadOnlyList.Util.<INode>emptyList()
537        : ReadOnlyList.Util.asReadOnlyList(children);
538  }
539
540  /** Set the children list to null. */
541  public void clearChildren() {
542    this.children = null;
543  }
544
545  @Override
546  public void clear() {
547    super.clear();
548    clearChildren();
549  }
550
551  /** Call cleanSubtree(..) recursively down the subtree. */
552  public Quota.Counts cleanSubtreeRecursively(final Snapshot snapshot,
553      Snapshot prior, final BlocksMapUpdateInfo collectedBlocks,
554      final List<INode> removedINodes, final Map<INode, INode> excludedNodes, 
555      final boolean countDiffChange) throws QuotaExceededException {
556    Quota.Counts counts = Quota.Counts.newInstance();
557    // in case of deletion snapshot, since this call happens after we modify
558    // the diff list, the snapshot to be deleted has been combined or renamed
559    // to its latest previous snapshot. (besides, we also need to consider nodes
560    // created after prior but before snapshot. this will be done in 
561    // INodeDirectoryWithSnapshot#cleanSubtree)
562    Snapshot s = snapshot != null && prior != null ? prior : snapshot;
563    for (INode child : getChildrenList(s)) {
564      if (snapshot != null && excludedNodes != null
565          && excludedNodes.containsKey(child)) {
566        continue;
567      } else {
568        Quota.Counts childCounts = child.cleanSubtree(snapshot, prior,
569            collectedBlocks, removedINodes, countDiffChange);
570        counts.add(childCounts);
571      }
572    }
573    return counts;
574  }
575
576  @Override
577  public void destroyAndCollectBlocks(final BlocksMapUpdateInfo collectedBlocks,
578      final List<INode> removedINodes) {
579    for (INode child : getChildrenList(null)) {
580      child.destroyAndCollectBlocks(collectedBlocks, removedINodes);
581    }
582    clear();
583    removedINodes.add(this);
584  }
585  
586  @Override
587  public Quota.Counts cleanSubtree(final Snapshot snapshot, Snapshot prior,
588      final BlocksMapUpdateInfo collectedBlocks,
589      final List<INode> removedINodes, final boolean countDiffChange)
590      throws QuotaExceededException {
591    if (prior == null && snapshot == null) {
592      // destroy the whole subtree and collect blocks that should be deleted
593      Quota.Counts counts = Quota.Counts.newInstance();
594      this.computeQuotaUsage(counts, true);
595      destroyAndCollectBlocks(collectedBlocks, removedINodes);
596      return counts; 
597    } else {
598      // process recursively down the subtree
599      Quota.Counts counts = cleanSubtreeRecursively(snapshot, prior,
600          collectedBlocks, removedINodes, null, countDiffChange);
601      if (isQuotaSet()) {
602        ((INodeDirectoryWithQuota) this).addSpaceConsumed2Cache(
603            -counts.get(Quota.NAMESPACE), -counts.get(Quota.DISKSPACE));
604      }
605      return counts;
606    }
607  }
608  
609  /**
610   * Compare the metadata with another INodeDirectory
611   */
612  @Override
613  public boolean metadataEquals(INodeDirectoryAttributes other) {
614    return other != null
615        && getNsQuota() == other.getNsQuota()
616        && getDsQuota() == other.getDsQuota()
617        && getPermissionLong() == other.getPermissionLong();
618  }
619  
620  /*
621   * The following code is to dump the tree recursively for testing.
622   * 
623   *      \- foo   (INodeDirectory@33dd2717)
624   *        \- sub1   (INodeDirectory@442172)
625   *          +- file1   (INodeFile@78392d4)
626   *          +- file2   (INodeFile@78392d5)
627   *          +- sub11   (INodeDirectory@8400cff)
628   *            \- file3   (INodeFile@78392d6)
629   *          \- z_file4   (INodeFile@45848712)
630   */
631  static final String DUMPTREE_EXCEPT_LAST_ITEM = "+-"; 
632  static final String DUMPTREE_LAST_ITEM = "\\-";
633  @VisibleForTesting
634  @Override
635  public void dumpTreeRecursively(PrintWriter out, StringBuilder prefix,
636      final Snapshot snapshot) {
637    super.dumpTreeRecursively(out, prefix, snapshot);
638    out.print(", childrenSize=" + getChildrenList(snapshot).size());
639    if (this instanceof INodeDirectoryWithQuota) {
640      out.print(((INodeDirectoryWithQuota)this).quotaString());
641    }
642    if (this instanceof Snapshot.Root) {
643      out.print(", snapshotId=" + snapshot.getId());
644    }
645    out.println();
646
647    if (prefix.length() >= 2) {
648      prefix.setLength(prefix.length() - 2);
649      prefix.append("  ");
650    }
651    dumpTreeRecursively(out, prefix, new Iterable<SnapshotAndINode>() {
652      final Iterator<INode> i = getChildrenList(snapshot).iterator();
653      
654      @Override
655      public Iterator<SnapshotAndINode> iterator() {
656        return new Iterator<SnapshotAndINode>() {
657          @Override
658          public boolean hasNext() {
659            return i.hasNext();
660          }
661
662          @Override
663          public SnapshotAndINode next() {
664            return new SnapshotAndINode(snapshot, i.next());
665          }
666
667          @Override
668          public void remove() {
669            throw new UnsupportedOperationException();
670          }
671        };
672      }
673    });
674  }
675
676  /**
677   * Dump the given subtrees.
678   * @param prefix The prefix string that each line should print.
679   * @param subs The subtrees.
680   */
681  @VisibleForTesting
682  protected static void dumpTreeRecursively(PrintWriter out,
683      StringBuilder prefix, Iterable<SnapshotAndINode> subs) {
684    if (subs != null) {
685      for(final Iterator<SnapshotAndINode> i = subs.iterator(); i.hasNext();) {
686        final SnapshotAndINode pair = i.next();
687        prefix.append(i.hasNext()? DUMPTREE_EXCEPT_LAST_ITEM: DUMPTREE_LAST_ITEM);
688        pair.inode.dumpTreeRecursively(out, prefix, pair.snapshot);
689        prefix.setLength(prefix.length() - 2);
690      }
691    }
692  }
693
694  /** A pair of Snapshot and INode objects. */
695  protected static class SnapshotAndINode {
696    public final Snapshot snapshot;
697    public final INode inode;
698
699    public SnapshotAndINode(Snapshot snapshot, INode inode) {
700      this.snapshot = snapshot;
701      this.inode = inode;
702    }
703
704    public SnapshotAndINode(Snapshot snapshot) {
705      this(snapshot, snapshot.getRoot());
706    }
707  }
708
709  public final int getChildrenNum(final Snapshot snapshot) {
710    return getChildrenList(snapshot).size();
711  }
712}