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.util.Arrays;
021
022import org.apache.commons.logging.Log;
023import org.apache.commons.logging.LogFactory;
024import org.apache.hadoop.fs.Path;
025import org.apache.hadoop.fs.UnresolvedLinkException;
026import org.apache.hadoop.hdfs.DFSUtil;
027import org.apache.hadoop.hdfs.protocol.HdfsConstants;
028import org.apache.hadoop.hdfs.protocol.UnresolvedPathException;
029import org.apache.hadoop.hdfs.server.namenode.snapshot.INodeDirectorySnapshottable;
030import org.apache.hadoop.hdfs.server.namenode.snapshot.INodeDirectoryWithSnapshot;
031import org.apache.hadoop.hdfs.server.namenode.snapshot.Snapshot;
032
033import com.google.common.base.Preconditions;
034
035/**
036 * Contains INodes information resolved from a given path.
037 */
038public class INodesInPath {
039  public static final Log LOG = LogFactory.getLog(INodesInPath.class);
040
041  /**
042   * @return true if path component is {@link HdfsConstants#DOT_SNAPSHOT_DIR}
043   */
044  private static boolean isDotSnapshotDir(byte[] pathComponent) {
045    return pathComponent == null ? false
046        : Arrays.equals(HdfsConstants.DOT_SNAPSHOT_DIR_BYTES, pathComponent);
047  }
048
049  /**
050   * Given some components, create a path name.
051   * @param components The path components
052   * @param start index
053   * @param end index
054   * @return concatenated path
055   */
056  private static String constructPath(byte[][] components, int start, int end) {
057    StringBuilder buf = new StringBuilder();
058    for (int i = start; i < end; i++) {
059      buf.append(DFSUtil.bytes2String(components[i]));
060      if (i < end - 1) {
061        buf.append(Path.SEPARATOR);
062      }
063    }
064    return buf.toString();
065  }
066
067  static INodesInPath resolve(final INodeDirectory startingDir,
068      final byte[][] components) throws UnresolvedLinkException {
069    return resolve(startingDir, components, components.length, false);
070  }
071
072  /**
073   * Retrieve existing INodes from a path. If existing is big enough to store
074   * all path components (existing and non-existing), then existing INodes
075   * will be stored starting from the root INode into existing[0]; if
076   * existing is not big enough to store all path components, then only the
077   * last existing and non existing INodes will be stored so that
078   * existing[existing.length-1] refers to the INode of the final component.
079   * 
080   * An UnresolvedPathException is always thrown when an intermediate path 
081   * component refers to a symbolic link. If the final path component refers 
082   * to a symbolic link then an UnresolvedPathException is only thrown if
083   * resolveLink is true.  
084   * 
085   * <p>
086   * Example: <br>
087   * Given the path /c1/c2/c3 where only /c1/c2 exists, resulting in the
088   * following path components: ["","c1","c2","c3"],
089   * 
090   * <p>
091   * <code>getExistingPathINodes(["","c1","c2"], [?])</code> should fill the
092   * array with [c2] <br>
093   * <code>getExistingPathINodes(["","c1","c2","c3"], [?])</code> should fill the
094   * array with [null]
095   * 
096   * <p>
097   * <code>getExistingPathINodes(["","c1","c2"], [?,?])</code> should fill the
098   * array with [c1,c2] <br>
099   * <code>getExistingPathINodes(["","c1","c2","c3"], [?,?])</code> should fill
100   * the array with [c2,null]
101   * 
102   * <p>
103   * <code>getExistingPathINodes(["","c1","c2"], [?,?,?,?])</code> should fill
104   * the array with [rootINode,c1,c2,null], <br>
105   * <code>getExistingPathINodes(["","c1","c2","c3"], [?,?,?,?])</code> should
106   * fill the array with [rootINode,c1,c2,null]
107   * 
108   * @param startingDir the starting directory
109   * @param components array of path component name
110   * @param numOfINodes number of INodes to return
111   * @param resolveLink indicates whether UnresolvedLinkException should
112   *        be thrown when the path refers to a symbolic link.
113   * @return the specified number of existing INodes in the path
114   */
115  static INodesInPath resolve(final INodeDirectory startingDir,
116      final byte[][] components, final int numOfINodes, 
117      final boolean resolveLink) throws UnresolvedLinkException {
118    Preconditions.checkArgument(startingDir.compareTo(components[0]) == 0);
119
120    INode curNode = startingDir;
121    final INodesInPath existing = new INodesInPath(components, numOfINodes);
122    int count = 0;
123    int index = numOfINodes - components.length;
124    if (index > 0) {
125      index = 0;
126    }
127    while (count < components.length && curNode != null) {
128      final boolean lastComp = (count == components.length - 1);      
129      if (index >= 0) {
130        existing.addNode(curNode);
131      }
132      final boolean isRef = curNode.isReference();
133      final boolean isDir = curNode.isDirectory();
134      final INodeDirectory dir = isDir? curNode.asDirectory(): null;  
135      if (!isRef && isDir && dir instanceof INodeDirectoryWithSnapshot) {
136        //if the path is a non-snapshot path, update the latest snapshot.
137        if (!existing.isSnapshot()) {
138          existing.updateLatestSnapshot(
139              ((INodeDirectoryWithSnapshot)dir).getLastSnapshot());
140        }
141      } else if (isRef && isDir && !lastComp) {
142        // If the curNode is a reference node, need to check its dstSnapshot:
143        // 1. if the existing snapshot is no later than the dstSnapshot (which
144        // is the latest snapshot in dst before the rename), the changes 
145        // should be recorded in previous snapshots (belonging to src).
146        // 2. however, if the ref node is already the last component, we still 
147        // need to know the latest snapshot among the ref node's ancestors, 
148        // in case of processing a deletion operation. Thus we do not overwrite
149        // the latest snapshot if lastComp is true. In case of the operation is
150        // a modification operation, we do a similar check in corresponding 
151        // recordModification method.
152        if (!existing.isSnapshot()) {
153          int dstSnapshotId = curNode.asReference().getDstSnapshotId();
154          Snapshot latest = existing.getLatestSnapshot();
155          if (latest == null ||  // no snapshot in dst tree of rename
156              dstSnapshotId >= latest.getId()) { // the above scenario 
157            Snapshot lastSnapshot = null;
158            if (curNode.isDirectory()
159                && curNode.asDirectory() instanceof INodeDirectoryWithSnapshot) {
160              lastSnapshot = ((INodeDirectoryWithSnapshot) curNode
161                  .asDirectory()).getLastSnapshot();
162            }
163            existing.setSnapshot(lastSnapshot);
164          }
165        }
166      }
167      if (curNode.isSymlink() && (!lastComp || (lastComp && resolveLink))) {
168        final String path = constructPath(components, 0, components.length);
169        final String preceding = constructPath(components, 0, count);
170        final String remainder =
171          constructPath(components, count + 1, components.length);
172        final String link = DFSUtil.bytes2String(components[count]);
173        final String target = curNode.asSymlink().getSymlinkString();
174        if (LOG.isDebugEnabled()) {
175          LOG.debug("UnresolvedPathException " +
176            " path: " + path + " preceding: " + preceding +
177            " count: " + count + " link: " + link + " target: " + target +
178            " remainder: " + remainder);
179        }
180        throw new UnresolvedPathException(path, preceding, remainder, target);
181      }
182      if (lastComp || !isDir) {
183        break;
184      }
185      final byte[] childName = components[count + 1];
186      
187      // check if the next byte[] in components is for ".snapshot"
188      if (isDotSnapshotDir(childName)
189          && isDir && dir instanceof INodeDirectorySnapshottable) {
190        // skip the ".snapshot" in components
191        count++;
192        index++;
193        existing.isSnapshot = true;
194        if (index >= 0) { // decrease the capacity by 1 to account for .snapshot
195          existing.capacity--;
196        }
197        // check if ".snapshot" is the last element of components
198        if (count == components.length - 1) {
199          break;
200        }
201        // Resolve snapshot root
202        final Snapshot s = ((INodeDirectorySnapshottable)dir).getSnapshot(
203            components[count + 1]);
204        if (s == null) {
205          //snapshot not found
206          curNode = null;
207        } else {
208          curNode = s.getRoot();
209          existing.setSnapshot(s);
210        }
211        if (index >= -1) {
212          existing.snapshotRootIndex = existing.numNonNull;
213        }
214      } else {
215        // normal case, and also for resolving file/dir under snapshot root
216        curNode = dir.getChild(childName, existing.getPathSnapshot());
217      }
218      count++;
219      index++;
220    }
221    return existing;
222  }
223
224  private final byte[][] path;
225  /**
226   * Array with the specified number of INodes resolved for a given path.
227   */
228  private INode[] inodes;
229  /**
230   * Indicate the number of non-null elements in {@link #inodes}
231   */
232  private int numNonNull;
233  /**
234   * The path for a snapshot file/dir contains the .snapshot thus makes the
235   * length of the path components larger the number of inodes. We use
236   * the capacity to control this special case.
237   */
238  private int capacity;
239  /**
240   * true if this path corresponds to a snapshot
241   */
242  private boolean isSnapshot;
243  /**
244   * Index of {@link INodeDirectoryWithSnapshot} for snapshot path, else -1
245   */
246  private int snapshotRootIndex;
247  /**
248   * For snapshot paths, it is the reference to the snapshot; or null if the
249   * snapshot does not exist. For non-snapshot paths, it is the reference to
250   * the latest snapshot found in the path; or null if no snapshot is found.
251   */
252  private Snapshot snapshot = null; 
253
254  private INodesInPath(byte[][] path, int number) {
255    this.path = path;
256    assert (number >= 0);
257    inodes = new INode[number];
258    capacity = number;
259    numNonNull = 0;
260    isSnapshot = false;
261    snapshotRootIndex = -1;
262  }
263
264  /**
265   * For non-snapshot paths, return the latest snapshot found in the path.
266   * For snapshot paths, return null.
267   */
268  public Snapshot getLatestSnapshot() {
269    return isSnapshot? null: snapshot;
270  }
271  
272  /**
273   * For snapshot paths, return the snapshot specified in the path.
274   * For non-snapshot paths, return null.
275   */
276  public Snapshot getPathSnapshot() {
277    return isSnapshot? snapshot: null;
278  }
279
280  private void setSnapshot(Snapshot s) {
281    snapshot = s;
282  }
283  
284  private void updateLatestSnapshot(Snapshot s) {
285    if (snapshot == null
286        || (s != null && Snapshot.ID_COMPARATOR.compare(snapshot, s) < 0)) {
287      snapshot = s;
288    }
289  }
290
291  /**
292   * @return the whole inodes array including the null elements.
293   */
294  INode[] getINodes() {
295    if (capacity < inodes.length) {
296      INode[] newNodes = new INode[capacity];
297      System.arraycopy(inodes, 0, newNodes, 0, capacity);
298      inodes = newNodes;
299    }
300    return inodes;
301  }
302  
303  /**
304   * @return the i-th inode if i >= 0;
305   *         otherwise, i < 0, return the (length + i)-th inode.
306   */
307  public INode getINode(int i) {
308    return inodes[i >= 0? i: inodes.length + i];
309  }
310  
311  /** @return the last inode. */
312  public INode getLastINode() {
313    return inodes[inodes.length - 1];
314  }
315
316  byte[] getLastLocalName() {
317    return path[path.length - 1];
318  }
319  
320  /**
321   * @return index of the {@link INodeDirectoryWithSnapshot} in
322   *         {@link #inodes} for snapshot path, else -1.
323   */
324  int getSnapshotRootIndex() {
325    return this.snapshotRootIndex;
326  }
327  
328  /**
329   * @return isSnapshot true for a snapshot path
330   */
331  boolean isSnapshot() {
332    return this.isSnapshot;
333  }
334  
335  /**
336   * Add an INode at the end of the array
337   */
338  private void addNode(INode node) {
339    inodes[numNonNull++] = node;
340  }
341  
342  void setINode(int i, INode inode) {
343    inodes[i >= 0? i: inodes.length + i] = inode;
344  }
345  
346  void setLastINode(INode last) {
347    inodes[inodes.length - 1] = last;
348  }
349  
350  /**
351   * @return The number of non-null elements
352   */
353  int getNumNonNull() {
354    return numNonNull;
355  }
356  
357  private static String toString(INode inode) {
358    return inode == null? null: inode.getLocalName();
359  }
360
361  @Override
362  public String toString() {
363    return toString(true);
364  }
365
366  private String toString(boolean vaildateObject) {
367    if (vaildateObject) {
368      vaildate();
369    }
370
371    final StringBuilder b = new StringBuilder(getClass().getSimpleName())
372        .append(": path = ").append(DFSUtil.byteArray2PathString(path))
373        .append("\n  inodes = ");
374    if (inodes == null) {
375      b.append("null");
376    } else if (inodes.length == 0) {
377      b.append("[]");
378    } else {
379      b.append("[").append(toString(inodes[0]));
380      for(int i = 1; i < inodes.length; i++) {
381        b.append(", ").append(toString(inodes[i]));
382      }
383      b.append("], length=").append(inodes.length);
384    }
385    b.append("\n  numNonNull = ").append(numNonNull)
386     .append("\n  capacity   = ").append(capacity)
387     .append("\n  isSnapshot        = ").append(isSnapshot)
388     .append("\n  snapshotRootIndex = ").append(snapshotRootIndex)
389     .append("\n  snapshot          = ").append(snapshot);
390    return b.toString();
391  }
392
393  void vaildate() {
394    // check parent up to snapshotRootIndex or numNonNull
395    final int n = snapshotRootIndex >= 0? snapshotRootIndex + 1: numNonNull;  
396    int i = 0;
397    if (inodes[i] != null) {
398      for(i++; i < n && inodes[i] != null; i++) {
399        final INodeDirectory parent_i = inodes[i].getParent();
400        final INodeDirectory parent_i_1 = inodes[i-1].getParent();
401        if (parent_i != inodes[i-1] &&
402            (parent_i_1 == null || !parent_i_1.isSnapshottable()
403                || parent_i != parent_i_1)) {
404          throw new AssertionError(
405              "inodes[" + i + "].getParent() != inodes[" + (i-1)
406              + "]\n  inodes[" + i + "]=" + inodes[i].toDetailString()
407              + "\n  inodes[" + (i-1) + "]=" + inodes[i-1].toDetailString()
408              + "\n this=" + toString(false));
409        }
410      }
411    }
412    if (i != n) {
413      throw new AssertionError("i = " + i + " != " + n
414          + ", this=" + toString(false));
415    }
416  }
417}