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 }