001/**
002res * 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
019package org.apache.hadoop.hdfs.web;
020
021import java.io.BufferedOutputStream;
022import java.io.FileNotFoundException;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.InputStreamReader;
026import java.net.HttpURLConnection;
027import java.net.InetSocketAddress;
028import java.net.MalformedURLException;
029import java.net.URI;
030import java.net.URL;
031import java.security.PrivilegedExceptionAction;
032import java.util.ArrayList;
033import java.util.EnumSet;
034import java.util.List;
035import java.util.Map;
036import java.util.StringTokenizer;
037
038import javax.ws.rs.core.MediaType;
039
040import org.apache.commons.logging.Log;
041import org.apache.commons.logging.LogFactory;
042import org.apache.hadoop.conf.Configuration;
043import org.apache.hadoop.fs.BlockLocation;
044import org.apache.hadoop.fs.ContentSummary;
045import org.apache.hadoop.fs.DelegationTokenRenewer;
046import org.apache.hadoop.fs.FSDataInputStream;
047import org.apache.hadoop.fs.FSDataOutputStream;
048import org.apache.hadoop.fs.FileStatus;
049import org.apache.hadoop.fs.FileSystem;
050import org.apache.hadoop.fs.MD5MD5CRC32FileChecksum;
051import org.apache.hadoop.fs.Options;
052import org.apache.hadoop.fs.Path;
053import org.apache.hadoop.fs.XAttrCodec;
054import org.apache.hadoop.fs.XAttrSetFlag;
055import org.apache.hadoop.fs.permission.AclEntry;
056import org.apache.hadoop.fs.permission.AclStatus;
057import org.apache.hadoop.fs.permission.FsPermission;
058import org.apache.hadoop.hdfs.DFSConfigKeys;
059import org.apache.hadoop.hdfs.DFSUtil;
060import org.apache.hadoop.hdfs.HAUtil;
061import org.apache.hadoop.hdfs.protocol.HdfsFileStatus;
062import org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenIdentifier;
063import org.apache.hadoop.hdfs.server.namenode.SafeModeException;
064import org.apache.hadoop.hdfs.web.resources.*;
065import org.apache.hadoop.hdfs.web.resources.HttpOpParam.Op;
066import org.apache.hadoop.io.Text;
067import org.apache.hadoop.io.retry.RetryPolicies;
068import org.apache.hadoop.io.retry.RetryPolicy;
069import org.apache.hadoop.io.retry.RetryUtils;
070import org.apache.hadoop.ipc.RemoteException;
071import org.apache.hadoop.net.NetUtils;
072import org.apache.hadoop.security.AccessControlException;
073import org.apache.hadoop.security.SecurityUtil;
074import org.apache.hadoop.security.UserGroupInformation;
075import org.apache.hadoop.security.token.SecretManager.InvalidToken;
076import org.apache.hadoop.security.token.Token;
077import org.apache.hadoop.security.token.TokenIdentifier;
078import org.apache.hadoop.security.token.TokenSelector;
079import org.apache.hadoop.security.token.delegation.AbstractDelegationTokenSelector;
080import org.apache.hadoop.util.Progressable;
081import org.mortbay.util.ajax.JSON;
082
083import com.google.common.annotations.VisibleForTesting;
084import com.google.common.base.Charsets;
085import com.google.common.base.Preconditions;
086import com.google.common.collect.Lists;
087
088/** A FileSystem for HDFS over the web. */
089public class WebHdfsFileSystem extends FileSystem
090    implements DelegationTokenRenewer.Renewable, TokenAspect.TokenManagementDelegator {
091  public static final Log LOG = LogFactory.getLog(WebHdfsFileSystem.class);
092  /** File System URI: {SCHEME}://namenode:port/path/to/file */
093  public static final String SCHEME = "webhdfs";
094  /** WebHdfs version. */
095  public static final int VERSION = 1;
096  /** Http URI: http://namenode:port/{PATH_PREFIX}/path/to/file */
097  public static final String PATH_PREFIX = "/" + SCHEME + "/v" + VERSION;
098
099  /** Default connection factory may be overridden in tests to use smaller timeout values */
100  protected URLConnectionFactory connectionFactory;
101
102  /** Delegation token kind */
103  public static final Text TOKEN_KIND = new Text("WEBHDFS delegation");
104  private boolean canRefreshDelegationToken;
105
106  private UserGroupInformation ugi;
107  private URI uri;
108  private Token<?> delegationToken;
109  protected Text tokenServiceName;
110  private RetryPolicy retryPolicy = null;
111  private Path workingDir;
112  private InetSocketAddress nnAddrs[];
113  private int currentNNAddrIndex;
114
115  /**
116   * Return the protocol scheme for the FileSystem.
117   * <p/>
118   *
119   * @return <code>webhdfs</code>
120   */
121  @Override
122  public String getScheme() {
123    return SCHEME;
124  }
125
126  /**
127   * return the underlying transport protocol (http / https).
128   */
129  protected String getTransportScheme() {
130    return "http";
131  }
132
133  protected Text getTokenKind() {
134    return TOKEN_KIND;
135  }
136
137  @Override
138  public synchronized void initialize(URI uri, Configuration conf
139      ) throws IOException {
140    super.initialize(uri, conf);
141    setConf(conf);
142    /** set user pattern based on configuration file */
143    UserParam.setUserPattern(conf.get(
144        DFSConfigKeys.DFS_WEBHDFS_USER_PATTERN_KEY,
145        DFSConfigKeys.DFS_WEBHDFS_USER_PATTERN_DEFAULT));
146
147    connectionFactory = URLConnectionFactory
148        .newDefaultURLConnectionFactory(conf);
149
150    ugi = UserGroupInformation.getCurrentUser();
151    this.uri = URI.create(uri.getScheme() + "://" + uri.getAuthority());
152    this.nnAddrs = resolveNNAddr();
153
154    boolean isHA = HAUtil.isClientFailoverConfigured(conf, this.uri);
155    boolean isLogicalUri = isHA && HAUtil.isLogicalUri(conf, this.uri);
156    // In non-HA or non-logical URI case, the code needs to call
157    // getCanonicalUri() in order to handle the case where no port is
158    // specified in the URI
159    this.tokenServiceName = isLogicalUri ?
160        HAUtil.buildTokenServiceForLogicalUri(uri)
161        : SecurityUtil.buildTokenService(getCanonicalUri());
162
163    if (!isHA) {
164      this.retryPolicy =
165          RetryUtils.getDefaultRetryPolicy(
166              conf,
167              DFSConfigKeys.DFS_HTTP_CLIENT_RETRY_POLICY_ENABLED_KEY,
168              DFSConfigKeys.DFS_HTTP_CLIENT_RETRY_POLICY_ENABLED_DEFAULT,
169              DFSConfigKeys.DFS_HTTP_CLIENT_RETRY_POLICY_SPEC_KEY,
170              DFSConfigKeys.DFS_HTTP_CLIENT_RETRY_POLICY_SPEC_DEFAULT,
171              SafeModeException.class);
172    } else {
173
174      int maxFailoverAttempts = conf.getInt(
175          DFSConfigKeys.DFS_HTTP_CLIENT_FAILOVER_MAX_ATTEMPTS_KEY,
176          DFSConfigKeys.DFS_HTTP_CLIENT_FAILOVER_MAX_ATTEMPTS_DEFAULT);
177      int maxRetryAttempts = conf.getInt(
178          DFSConfigKeys.DFS_HTTP_CLIENT_RETRY_MAX_ATTEMPTS_KEY,
179          DFSConfigKeys.DFS_HTTP_CLIENT_RETRY_MAX_ATTEMPTS_DEFAULT);
180      int failoverSleepBaseMillis = conf.getInt(
181          DFSConfigKeys.DFS_HTTP_CLIENT_FAILOVER_SLEEPTIME_BASE_KEY,
182          DFSConfigKeys.DFS_HTTP_CLIENT_FAILOVER_SLEEPTIME_BASE_DEFAULT);
183      int failoverSleepMaxMillis = conf.getInt(
184          DFSConfigKeys.DFS_HTTP_CLIENT_FAILOVER_SLEEPTIME_MAX_KEY,
185          DFSConfigKeys.DFS_HTTP_CLIENT_FAILOVER_SLEEPTIME_MAX_DEFAULT);
186
187      this.retryPolicy = RetryPolicies
188          .failoverOnNetworkException(RetryPolicies.TRY_ONCE_THEN_FAIL,
189              maxFailoverAttempts, maxRetryAttempts, failoverSleepBaseMillis,
190              failoverSleepMaxMillis);
191    }
192
193    this.workingDir = getHomeDirectory();
194    this.canRefreshDelegationToken = UserGroupInformation.isSecurityEnabled();
195    this.delegationToken = null;
196  }
197
198  @Override
199  public URI getCanonicalUri() {
200    return super.getCanonicalUri();
201  }
202
203  /** Is WebHDFS enabled in conf? */
204  public static boolean isEnabled(final Configuration conf, final Log log) {
205    final boolean b = conf.getBoolean(DFSConfigKeys.DFS_WEBHDFS_ENABLED_KEY,
206        DFSConfigKeys.DFS_WEBHDFS_ENABLED_DEFAULT);
207    return b;
208  }
209
210  TokenSelector<DelegationTokenIdentifier> tokenSelector =
211      new AbstractDelegationTokenSelector<DelegationTokenIdentifier>(getTokenKind()){};
212
213  // the first getAuthParams() for a non-token op will either get the
214  // internal token from the ugi or lazy fetch one
215  protected synchronized Token<?> getDelegationToken() throws IOException {
216    try {
217      if (canRefreshDelegationToken && delegationToken == null) {
218         Token<?> token = tokenSelector.selectToken(
219            new Text(getCanonicalServiceName()), ugi.getTokens());
220        // ugi tokens are usually indicative of a task which can't
221        // refetch tokens.  even if ugi has credentials, don't attempt
222        // to get another token to match hdfs/rpc behavior
223        if (token != null) {
224          LOG.debug("Using UGI token: " + token);
225          canRefreshDelegationToken = false; 
226        } else {
227          token = getDelegationToken(null);
228          if (token != null) {
229            LOG.debug("Fetched new token: " + token);
230          } else { // security is disabled
231            canRefreshDelegationToken = false;
232          }
233        }
234        setDelegationToken(token);
235      }
236      return delegationToken;
237    } catch (IOException e) {
238      LOG.warn(e.getMessage());
239      LOG.debug(e.getMessage(), e);
240    }
241
242    return null;
243  }
244
245  @VisibleForTesting
246  synchronized boolean replaceExpiredDelegationToken() throws IOException {
247    boolean replaced = false;
248    if (canRefreshDelegationToken) {
249      Token<?> token = getDelegationToken(null);
250      LOG.debug("Replaced expired token: " + token);
251      setDelegationToken(token);
252      replaced = (token != null);
253    }
254    return replaced;
255  }
256
257  @Override
258  @VisibleForTesting
259  public int getDefaultPort() {
260    return getConf().getInt(DFSConfigKeys.DFS_NAMENODE_HTTP_PORT_KEY,
261        DFSConfigKeys.DFS_NAMENODE_HTTP_PORT_DEFAULT);
262  }
263
264  @Override
265  public URI getUri() {
266    return this.uri;
267  }
268  
269  @Override
270  protected URI canonicalizeUri(URI uri) {
271    return NetUtils.getCanonicalUri(uri, getDefaultPort());
272  }
273
274  /** @return the home directory. */
275  public static String getHomeDirectoryString(final UserGroupInformation ugi) {
276    return "/user/" + ugi.getShortUserName();
277  }
278
279  @Override
280  public Path getHomeDirectory() {
281    return makeQualified(new Path(getHomeDirectoryString(ugi)));
282  }
283
284  @Override
285  public synchronized Path getWorkingDirectory() {
286    return workingDir;
287  }
288
289  @Override
290  public synchronized void setWorkingDirectory(final Path dir) {
291    String result = makeAbsolute(dir).toUri().getPath();
292    if (!DFSUtil.isValidName(result)) {
293      throw new IllegalArgumentException("Invalid DFS directory name " + 
294                                         result);
295    }
296    workingDir = makeAbsolute(dir);
297  }
298
299  private Path makeAbsolute(Path f) {
300    return f.isAbsolute()? f: new Path(workingDir, f);
301  }
302
303  static Map<?, ?> jsonParse(final HttpURLConnection c, final boolean useErrorStream
304      ) throws IOException {
305    if (c.getContentLength() == 0) {
306      return null;
307    }
308    final InputStream in = useErrorStream? c.getErrorStream(): c.getInputStream();
309    if (in == null) {
310      throw new IOException("The " + (useErrorStream? "error": "input") + " stream is null.");
311    }
312    final String contentType = c.getContentType();
313    if (contentType != null) {
314      final MediaType parsed = MediaType.valueOf(contentType);
315      if (!MediaType.APPLICATION_JSON_TYPE.isCompatible(parsed)) {
316        throw new IOException("Content-Type \"" + contentType
317            + "\" is incompatible with \"" + MediaType.APPLICATION_JSON
318            + "\" (parsed=\"" + parsed + "\")");
319      }
320    }
321    return (Map<?, ?>)JSON.parse(new InputStreamReader(in, Charsets.UTF_8));
322  }
323
324  private static Map<?, ?> validateResponse(final HttpOpParam.Op op,
325      final HttpURLConnection conn, boolean unwrapException) throws IOException {
326    final int code = conn.getResponseCode();
327    // server is demanding an authentication we don't support
328    if (code == HttpURLConnection.HTTP_UNAUTHORIZED) {
329      // match hdfs/rpc exception
330      throw new AccessControlException(conn.getResponseMessage());
331    }
332    if (code != op.getExpectedHttpResponseCode()) {
333      final Map<?, ?> m;
334      try {
335        m = jsonParse(conn, true);
336      } catch(Exception e) {
337        throw new IOException("Unexpected HTTP response: code=" + code + " != "
338            + op.getExpectedHttpResponseCode() + ", " + op.toQueryString()
339            + ", message=" + conn.getResponseMessage(), e);
340      }
341
342      if (m == null) {
343        throw new IOException("Unexpected HTTP response: code=" + code + " != "
344            + op.getExpectedHttpResponseCode() + ", " + op.toQueryString()
345            + ", message=" + conn.getResponseMessage());
346      } else if (m.get(RemoteException.class.getSimpleName()) == null) {
347        return m;
348      }
349
350      IOException re = JsonUtil.toRemoteException(m);
351      // extract UGI-related exceptions and unwrap InvalidToken
352      // the NN mangles these exceptions but the DN does not and may need
353      // to re-fetch a token if either report the token is expired
354      if (re.getMessage().startsWith("Failed to obtain user group information:")) {
355        String[] parts = re.getMessage().split(":\\s+", 3);
356        re = new RemoteException(parts[1], parts[2]);
357        re = ((RemoteException)re).unwrapRemoteException(InvalidToken.class);
358      }
359      throw unwrapException? toIOException(re): re;
360    }
361    return null;
362  }
363
364  /**
365   * Covert an exception to an IOException.
366   * 
367   * For a non-IOException, wrap it with IOException.
368   * For a RemoteException, unwrap it.
369   * For an IOException which is not a RemoteException, return it. 
370   */
371  private static IOException toIOException(Exception e) {
372    if (!(e instanceof IOException)) {
373      return new IOException(e);
374    }
375
376    final IOException ioe = (IOException)e;
377    if (!(ioe instanceof RemoteException)) {
378      return ioe;
379    }
380
381    return ((RemoteException)ioe).unwrapRemoteException();
382  }
383
384  private synchronized InetSocketAddress getCurrentNNAddr() {
385    return nnAddrs[currentNNAddrIndex];
386  }
387
388  /**
389   * Reset the appropriate state to gracefully fail over to another name node
390   */
391  private synchronized void resetStateToFailOver() {
392    currentNNAddrIndex = (currentNNAddrIndex + 1) % nnAddrs.length;
393  }
394
395  /**
396   * Return a URL pointing to given path on the namenode.
397   *
398   * @param path to obtain the URL for
399   * @param query string to append to the path
400   * @return namenode URL referring to the given path
401   * @throws IOException on error constructing the URL
402   */
403  private URL getNamenodeURL(String path, String query) throws IOException {
404    InetSocketAddress nnAddr = getCurrentNNAddr();
405    final URL url = new URL(getTransportScheme(), nnAddr.getHostName(),
406          nnAddr.getPort(), path + '?' + query);
407    if (LOG.isTraceEnabled()) {
408      LOG.trace("url=" + url);
409    }
410    return url;
411  }
412  
413  Param<?,?>[] getAuthParameters(final HttpOpParam.Op op) throws IOException {
414    List<Param<?,?>> authParams = Lists.newArrayList();    
415    // Skip adding delegation token for token operations because these
416    // operations require authentication.
417    Token<?> token = null;
418    if (!op.getRequireAuth()) {
419      token = getDelegationToken();
420    }
421    if (token != null) {
422      authParams.add(new DelegationParam(token.encodeToUrlString()));
423    } else {
424      UserGroupInformation userUgi = ugi;
425      UserGroupInformation realUgi = userUgi.getRealUser();
426      if (realUgi != null) { // proxy user
427        authParams.add(new DoAsParam(userUgi.getShortUserName()));
428        userUgi = realUgi;
429      }
430      authParams.add(new UserParam(userUgi.getShortUserName()));
431    }
432    return authParams.toArray(new Param<?,?>[0]);
433  }
434
435  URL toUrl(final HttpOpParam.Op op, final Path fspath,
436      final Param<?,?>... parameters) throws IOException {
437    //initialize URI path and query
438    final String path = PATH_PREFIX
439        + (fspath == null? "/": makeQualified(fspath).toUri().getRawPath());
440    final String query = op.toQueryString()
441        + Param.toSortedString("&", getAuthParameters(op))
442        + Param.toSortedString("&", parameters);
443    final URL url = getNamenodeURL(path, query);
444    if (LOG.isTraceEnabled()) {
445      LOG.trace("url=" + url);
446    }
447    return url;
448  }
449
450  /**
451   * This class is for initialing a HTTP connection, connecting to server,
452   * obtaining a response, and also handling retry on failures.
453   */
454  abstract class AbstractRunner<T> {
455    abstract protected URL getUrl() throws IOException;
456
457    protected final HttpOpParam.Op op;
458    private final boolean redirected;
459
460    private boolean checkRetry;
461
462    protected AbstractRunner(final HttpOpParam.Op op, boolean redirected) {
463      this.op = op;
464      this.redirected = redirected;
465    }
466
467    T run() throws IOException {
468      UserGroupInformation connectUgi = ugi.getRealUser();
469      if (connectUgi == null) {
470        connectUgi = ugi;
471      }
472      if (op.getRequireAuth()) {
473        connectUgi.checkTGTAndReloginFromKeytab();
474      }
475      try {
476        // the entire lifecycle of the connection must be run inside the
477        // doAs to ensure authentication is performed correctly
478        return connectUgi.doAs(
479            new PrivilegedExceptionAction<T>() {
480              @Override
481              public T run() throws IOException {
482                return runWithRetry();
483              }
484            });
485      } catch (InterruptedException e) {
486        throw new IOException(e);
487      }
488    }
489
490    /**
491     * Two-step requests redirected to a DN
492     * 
493     * Create/Append:
494     * Step 1) Submit a Http request with neither auto-redirect nor data. 
495     * Step 2) Submit another Http request with the URL from the Location header with data.
496     * 
497     * The reason of having two-step create/append is for preventing clients to
498     * send out the data before the redirect. This issue is addressed by the
499     * "Expect: 100-continue" header in HTTP/1.1; see RFC 2616, Section 8.2.3.
500     * Unfortunately, there are software library bugs (e.g. Jetty 6 http server
501     * and Java 6 http client), which do not correctly implement "Expect:
502     * 100-continue". The two-step create/append is a temporary workaround for
503     * the software library bugs.
504     * 
505     * Open/Checksum
506     * Also implements two-step connects for other operations redirected to
507     * a DN such as open and checksum
508     */
509    private HttpURLConnection connect(URL url) throws IOException {
510      // resolve redirects for a DN operation unless already resolved
511      if (op.getRedirect() && !redirected) {
512        final HttpOpParam.Op redirectOp =
513            HttpOpParam.TemporaryRedirectOp.valueOf(op);
514        final HttpURLConnection conn = connect(redirectOp, url);
515        // application level proxy like httpfs might not issue a redirect
516        if (conn.getResponseCode() == op.getExpectedHttpResponseCode()) {
517          return conn;
518        }
519        try {
520          validateResponse(redirectOp, conn, false);
521          url = new URL(conn.getHeaderField("Location"));
522        } finally {
523          conn.disconnect();
524        }
525      }
526      return connect(op, url);
527    }
528
529    private HttpURLConnection connect(final HttpOpParam.Op op, final URL url)
530        throws IOException {
531      final HttpURLConnection conn =
532          (HttpURLConnection)connectionFactory.openConnection(url);
533      final boolean doOutput = op.getDoOutput();
534      conn.setRequestMethod(op.getType().toString());
535      conn.setInstanceFollowRedirects(false);
536      switch (op.getType()) {
537        // if not sending a message body for a POST or PUT operation, need
538        // to ensure the server/proxy knows this 
539        case POST:
540        case PUT: {
541          conn.setDoOutput(true);
542          if (!doOutput) {
543            // explicitly setting content-length to 0 won't do spnego!!
544            // opening and closing the stream will send "Content-Length: 0"
545            conn.getOutputStream().close();
546          } else {
547            conn.setRequestProperty("Content-Type",
548                MediaType.APPLICATION_OCTET_STREAM);
549            conn.setChunkedStreamingMode(32 << 10); //32kB-chunk
550          }
551          break;
552        }
553        default: {
554          conn.setDoOutput(doOutput);
555          break;
556        }
557      }
558      conn.connect();
559      return conn;
560    }
561
562    private T runWithRetry() throws IOException {
563      /**
564       * Do the real work.
565       *
566       * There are three cases that the code inside the loop can throw an
567       * IOException:
568       *
569       * <ul>
570       * <li>The connection has failed (e.g., ConnectException,
571       * @see FailoverOnNetworkExceptionRetry for more details)</li>
572       * <li>The namenode enters the standby state (i.e., StandbyException).</li>
573       * <li>The server returns errors for the command (i.e., RemoteException)</li>
574       * </ul>
575       *
576       * The call to shouldRetry() will conduct the retry policy. The policy
577       * examines the exception and swallows it if it decides to rerun the work.
578       */
579      for(int retry = 0; ; retry++) {
580        checkRetry = !redirected;
581        final URL url = getUrl();
582        try {
583          final HttpURLConnection conn = connect(url);
584          // output streams will validate on close
585          if (!op.getDoOutput()) {
586            validateResponse(op, conn, false);
587          }
588          return getResponse(conn);
589        } catch (AccessControlException ace) {
590          // no retries for auth failures
591          throw ace;
592        } catch (InvalidToken it) {
593          // try to replace the expired token with a new one.  the attempt
594          // to acquire a new token must be outside this operation's retry
595          // so if it fails after its own retries, this operation fails too.
596          if (op.getRequireAuth() || !replaceExpiredDelegationToken()) {
597            throw it;
598          }
599        } catch (IOException ioe) {
600          shouldRetry(ioe, retry);
601        }
602      }
603    }
604
605    private void shouldRetry(final IOException ioe, final int retry
606        ) throws IOException {
607      InetSocketAddress nnAddr = getCurrentNNAddr();
608      if (checkRetry) {
609        try {
610          final RetryPolicy.RetryAction a = retryPolicy.shouldRetry(
611              ioe, retry, 0, true);
612
613          boolean isRetry = a.action == RetryPolicy.RetryAction.RetryDecision.RETRY;
614          boolean isFailoverAndRetry =
615              a.action == RetryPolicy.RetryAction.RetryDecision.FAILOVER_AND_RETRY;
616
617          if (isRetry || isFailoverAndRetry) {
618            LOG.info("Retrying connect to namenode: " + nnAddr
619                + ". Already tried " + retry + " time(s); retry policy is "
620                + retryPolicy + ", delay " + a.delayMillis + "ms.");
621
622            if (isFailoverAndRetry) {
623              resetStateToFailOver();
624            }
625
626            Thread.sleep(a.delayMillis);
627            return;
628          }
629        } catch(Exception e) {
630          LOG.warn("Original exception is ", ioe);
631          throw toIOException(e);
632        }
633      }
634      throw toIOException(ioe);
635    }
636
637    abstract T getResponse(HttpURLConnection conn) throws IOException;
638  }
639
640  /**
641   * Abstract base class to handle path-based operations with params
642   */
643  abstract class AbstractFsPathRunner<T> extends AbstractRunner<T> {
644    private final Path fspath;
645    private final Param<?,?>[] parameters;
646    
647    AbstractFsPathRunner(final HttpOpParam.Op op, final Path fspath,
648        Param<?,?>... parameters) {
649      super(op, false);
650      this.fspath = fspath;
651      this.parameters = parameters;
652    }
653    
654    AbstractFsPathRunner(final HttpOpParam.Op op, Param<?,?>[] parameters,
655        final Path fspath) {
656      super(op, false);
657      this.fspath = fspath;
658      this.parameters = parameters;
659    }
660    
661    @Override
662    protected URL getUrl() throws IOException {
663      return toUrl(op, fspath, parameters);
664    }
665  }
666
667  /**
668   * Default path-based implementation expects no json response
669   */
670  class FsPathRunner extends AbstractFsPathRunner<Void> {
671    FsPathRunner(Op op, Path fspath, Param<?,?>... parameters) {
672      super(op, fspath, parameters);
673    }
674    
675    @Override
676    Void getResponse(HttpURLConnection conn) throws IOException {
677      return null;
678    }
679  }
680
681  /**
682   * Handle path-based operations with a json response
683   */
684  abstract class FsPathResponseRunner<T> extends AbstractFsPathRunner<T> {
685    FsPathResponseRunner(final HttpOpParam.Op op, final Path fspath,
686        Param<?,?>... parameters) {
687      super(op, fspath, parameters);
688    }
689    
690    FsPathResponseRunner(final HttpOpParam.Op op, Param<?,?>[] parameters,
691        final Path fspath) {
692      super(op, parameters, fspath);
693    }
694    
695    @Override
696    final T getResponse(HttpURLConnection conn) throws IOException {
697      try {
698        final Map<?,?> json = jsonParse(conn, false);
699        if (json == null) {
700          // match exception class thrown by parser
701          throw new IllegalStateException("Missing response");
702        }
703        return decodeResponse(json);
704      } catch (IOException ioe) {
705        throw ioe;
706      } catch (Exception e) { // catch json parser errors
707        final IOException ioe =
708            new IOException("Response decoding failure: "+e.toString(), e);
709        if (LOG.isDebugEnabled()) {
710          LOG.debug(ioe);
711        }
712        throw ioe;
713      } finally {
714        conn.disconnect();
715      }
716    }
717    
718    abstract T decodeResponse(Map<?,?> json) throws IOException;
719  }
720
721  /**
722   * Handle path-based operations with json boolean response
723   */
724  class FsPathBooleanRunner extends FsPathResponseRunner<Boolean> {
725    FsPathBooleanRunner(Op op, Path fspath, Param<?,?>... parameters) {
726      super(op, fspath, parameters);
727    }
728    
729    @Override
730    Boolean decodeResponse(Map<?,?> json) throws IOException {
731      return (Boolean)json.get("boolean");
732    }
733  }
734
735  /**
736   * Handle create/append output streams
737   */
738  class FsPathOutputStreamRunner extends AbstractFsPathRunner<FSDataOutputStream> {
739    private final int bufferSize;
740    
741    FsPathOutputStreamRunner(Op op, Path fspath, int bufferSize,
742        Param<?,?>... parameters) {
743      super(op, fspath, parameters);
744      this.bufferSize = bufferSize;
745    }
746    
747    @Override
748    FSDataOutputStream getResponse(final HttpURLConnection conn)
749        throws IOException {
750      return new FSDataOutputStream(new BufferedOutputStream(
751          conn.getOutputStream(), bufferSize), statistics) {
752        @Override
753        public void close() throws IOException {
754          try {
755            super.close();
756          } finally {
757            try {
758              validateResponse(op, conn, true);
759            } finally {
760              conn.disconnect();
761            }
762          }
763        }
764      };
765    }
766  }
767
768  class FsPathConnectionRunner extends AbstractFsPathRunner<HttpURLConnection> {
769    FsPathConnectionRunner(Op op, Path fspath, Param<?,?>... parameters) {
770      super(op, fspath, parameters);
771    }
772    @Override
773    HttpURLConnection getResponse(final HttpURLConnection conn)
774        throws IOException {
775      return conn;
776    }
777  }
778  
779  /**
780   * Used by open() which tracks the resolved url itself
781   */
782  final class URLRunner extends AbstractRunner<HttpURLConnection> {
783    private final URL url;
784    @Override
785    protected URL getUrl() {
786      return url;
787    }
788
789    protected URLRunner(final HttpOpParam.Op op, final URL url, boolean redirected) {
790      super(op, redirected);
791      this.url = url;
792    }
793
794    @Override
795    HttpURLConnection getResponse(HttpURLConnection conn) throws IOException {
796      return conn;
797    }
798  }
799
800  private FsPermission applyUMask(FsPermission permission) {
801    if (permission == null) {
802      permission = FsPermission.getDefault();
803    }
804    return permission.applyUMask(FsPermission.getUMask(getConf()));
805  }
806
807  private HdfsFileStatus getHdfsFileStatus(Path f) throws IOException {
808    final HttpOpParam.Op op = GetOpParam.Op.GETFILESTATUS;
809    HdfsFileStatus status = new FsPathResponseRunner<HdfsFileStatus>(op, f) {
810      @Override
811      HdfsFileStatus decodeResponse(Map<?,?> json) {
812        return JsonUtil.toFileStatus(json, true);
813      }
814    }.run();
815    if (status == null) {
816      throw new FileNotFoundException("File does not exist: " + f);
817    }
818    return status;
819  }
820
821  @Override
822  public FileStatus getFileStatus(Path f) throws IOException {
823    statistics.incrementReadOps(1);
824    return makeQualified(getHdfsFileStatus(f), f);
825  }
826
827  private FileStatus makeQualified(HdfsFileStatus f, Path parent) {
828    return new FileStatus(f.getLen(), f.isDir(), f.getReplication(),
829        f.getBlockSize(), f.getModificationTime(), f.getAccessTime(),
830        f.getPermission(), f.getOwner(), f.getGroup(),
831        f.isSymlink() ? new Path(f.getSymlink()) : null,
832        f.getFullPath(parent).makeQualified(getUri(), getWorkingDirectory()));
833  }
834
835  @Override
836  public AclStatus getAclStatus(Path f) throws IOException {
837    final HttpOpParam.Op op = GetOpParam.Op.GETACLSTATUS;
838    AclStatus status = new FsPathResponseRunner<AclStatus>(op, f) {
839      @Override
840      AclStatus decodeResponse(Map<?,?> json) {
841        return JsonUtil.toAclStatus(json);
842      }
843    }.run();
844    if (status == null) {
845      throw new FileNotFoundException("File does not exist: " + f);
846    }
847    return status;
848  }
849
850  @Override
851  public boolean mkdirs(Path f, FsPermission permission) throws IOException {
852    statistics.incrementWriteOps(1);
853    final HttpOpParam.Op op = PutOpParam.Op.MKDIRS;
854    return new FsPathBooleanRunner(op, f,
855        new PermissionParam(applyUMask(permission))
856    ).run();
857  }
858
859  /**
860   * Create a symlink pointing to the destination path.
861   * @see org.apache.hadoop.fs.Hdfs#createSymlink(Path, Path, boolean) 
862   */
863  public void createSymlink(Path destination, Path f, boolean createParent
864      ) throws IOException {
865    statistics.incrementWriteOps(1);
866    final HttpOpParam.Op op = PutOpParam.Op.CREATESYMLINK;
867    new FsPathRunner(op, f,
868        new DestinationParam(makeQualified(destination).toUri().getPath()),
869        new CreateParentParam(createParent)
870    ).run();
871  }
872
873  @Override
874  public boolean rename(final Path src, final Path dst) throws IOException {
875    statistics.incrementWriteOps(1);
876    final HttpOpParam.Op op = PutOpParam.Op.RENAME;
877    return new FsPathBooleanRunner(op, src,
878        new DestinationParam(makeQualified(dst).toUri().getPath())
879    ).run();
880  }
881
882  @SuppressWarnings("deprecation")
883  @Override
884  public void rename(final Path src, final Path dst,
885      final Options.Rename... options) throws IOException {
886    statistics.incrementWriteOps(1);
887    final HttpOpParam.Op op = PutOpParam.Op.RENAME;
888    new FsPathRunner(op, src,
889        new DestinationParam(makeQualified(dst).toUri().getPath()),
890        new RenameOptionSetParam(options)
891    ).run();
892  }
893  
894  @Override
895  public void setXAttr(Path p, String name, byte[] value, 
896      EnumSet<XAttrSetFlag> flag) throws IOException {
897    statistics.incrementWriteOps(1);
898    final HttpOpParam.Op op = PutOpParam.Op.SETXATTR;
899    if (value != null) {
900      new FsPathRunner(op, p, new XAttrNameParam(name), new XAttrValueParam(
901          XAttrCodec.encodeValue(value, XAttrCodec.HEX)), 
902          new XAttrSetFlagParam(flag)).run();
903    } else {
904      new FsPathRunner(op, p, new XAttrNameParam(name), 
905          new XAttrSetFlagParam(flag)).run();
906    }
907  }
908  
909  @Override
910  public byte[] getXAttr(Path p, final String name) throws IOException {
911    final HttpOpParam.Op op = GetOpParam.Op.GETXATTRS;
912    return new FsPathResponseRunner<byte[]>(op, p, new XAttrNameParam(name), 
913        new XAttrEncodingParam(XAttrCodec.HEX)) {
914      @Override
915      byte[] decodeResponse(Map<?, ?> json) throws IOException {
916        return JsonUtil.getXAttr(json, name);
917      }
918    }.run();
919  }
920  
921  @Override
922  public Map<String, byte[]> getXAttrs(Path p) throws IOException {
923    final HttpOpParam.Op op = GetOpParam.Op.GETXATTRS;
924    return new FsPathResponseRunner<Map<String, byte[]>>(op, p, 
925        new XAttrEncodingParam(XAttrCodec.HEX)) {
926      @Override
927      Map<String, byte[]> decodeResponse(Map<?, ?> json) throws IOException {
928        return JsonUtil.toXAttrs(json);
929      }
930    }.run();
931  }
932  
933  @Override
934  public Map<String, byte[]> getXAttrs(Path p, final List<String> names) 
935      throws IOException {
936    Preconditions.checkArgument(names != null && !names.isEmpty(), 
937        "XAttr names cannot be null or empty.");
938    Param<?,?>[] parameters = new Param<?,?>[names.size() + 1];
939    for (int i = 0; i < parameters.length - 1; i++) {
940      parameters[i] = new XAttrNameParam(names.get(i));
941    }
942    parameters[parameters.length - 1] = new XAttrEncodingParam(XAttrCodec.HEX);
943    
944    final HttpOpParam.Op op = GetOpParam.Op.GETXATTRS;
945    return new FsPathResponseRunner<Map<String, byte[]>>(op, parameters, p) {
946      @Override
947      Map<String, byte[]> decodeResponse(Map<?, ?> json) throws IOException {
948        return JsonUtil.toXAttrs(json);
949      }
950    }.run();
951  }
952  
953  @Override
954  public List<String> listXAttrs(Path p) throws IOException {
955    final HttpOpParam.Op op = GetOpParam.Op.LISTXATTRS;
956    return new FsPathResponseRunner<List<String>>(op, p) {
957      @Override
958      List<String> decodeResponse(Map<?, ?> json) throws IOException {
959        return JsonUtil.toXAttrNames(json);
960      }
961    }.run();
962  }
963
964  @Override
965  public void removeXAttr(Path p, String name) throws IOException {
966    statistics.incrementWriteOps(1);
967    final HttpOpParam.Op op = PutOpParam.Op.REMOVEXATTR;
968    new FsPathRunner(op, p, new XAttrNameParam(name)).run();
969  }
970
971  @Override
972  public void setOwner(final Path p, final String owner, final String group
973      ) throws IOException {
974    if (owner == null && group == null) {
975      throw new IOException("owner == null && group == null");
976    }
977
978    statistics.incrementWriteOps(1);
979    final HttpOpParam.Op op = PutOpParam.Op.SETOWNER;
980    new FsPathRunner(op, p,
981        new OwnerParam(owner), new GroupParam(group)
982    ).run();
983  }
984
985  @Override
986  public void setPermission(final Path p, final FsPermission permission
987      ) throws IOException {
988    statistics.incrementWriteOps(1);
989    final HttpOpParam.Op op = PutOpParam.Op.SETPERMISSION;
990    new FsPathRunner(op, p,new PermissionParam(permission)).run();
991  }
992
993  @Override
994  public void modifyAclEntries(Path path, List<AclEntry> aclSpec)
995      throws IOException {
996    statistics.incrementWriteOps(1);
997    final HttpOpParam.Op op = PutOpParam.Op.MODIFYACLENTRIES;
998    new FsPathRunner(op, path, new AclPermissionParam(aclSpec)).run();
999  }
1000
1001  @Override
1002  public void removeAclEntries(Path path, List<AclEntry> aclSpec)
1003      throws IOException {
1004    statistics.incrementWriteOps(1);
1005    final HttpOpParam.Op op = PutOpParam.Op.REMOVEACLENTRIES;
1006    new FsPathRunner(op, path, new AclPermissionParam(aclSpec)).run();
1007  }
1008
1009  @Override
1010  public void removeDefaultAcl(Path path) throws IOException {
1011    statistics.incrementWriteOps(1);
1012    final HttpOpParam.Op op = PutOpParam.Op.REMOVEDEFAULTACL;
1013    new FsPathRunner(op, path).run();
1014  }
1015
1016  @Override
1017  public void removeAcl(Path path) throws IOException {
1018    statistics.incrementWriteOps(1);
1019    final HttpOpParam.Op op = PutOpParam.Op.REMOVEACL;
1020    new FsPathRunner(op, path).run();
1021  }
1022
1023  @Override
1024  public void setAcl(final Path p, final List<AclEntry> aclSpec)
1025      throws IOException {
1026    statistics.incrementWriteOps(1);
1027    final HttpOpParam.Op op = PutOpParam.Op.SETACL;
1028    new FsPathRunner(op, p, new AclPermissionParam(aclSpec)).run();
1029  }
1030
1031  @Override
1032  public Path createSnapshot(final Path path, final String snapshotName) 
1033      throws IOException {
1034    statistics.incrementWriteOps(1);
1035    final HttpOpParam.Op op = PutOpParam.Op.CREATESNAPSHOT;
1036    Path spath = new FsPathResponseRunner<Path>(op, path,
1037        new SnapshotNameParam(snapshotName)) {
1038      @Override
1039      Path decodeResponse(Map<?,?> json) {
1040        return new Path((String) json.get(Path.class.getSimpleName()));
1041      }
1042    }.run();
1043    return spath;
1044  }
1045
1046  @Override
1047  public void deleteSnapshot(final Path path, final String snapshotName)
1048      throws IOException {
1049    statistics.incrementWriteOps(1);
1050    final HttpOpParam.Op op = DeleteOpParam.Op.DELETESNAPSHOT;
1051    new FsPathRunner(op, path, new SnapshotNameParam(snapshotName)).run();
1052  }
1053
1054  @Override
1055  public void renameSnapshot(final Path path, final String snapshotOldName,
1056      final String snapshotNewName) throws IOException {
1057    statistics.incrementWriteOps(1);
1058    final HttpOpParam.Op op = PutOpParam.Op.RENAMESNAPSHOT;
1059    new FsPathRunner(op, path, new OldSnapshotNameParam(snapshotOldName),
1060        new SnapshotNameParam(snapshotNewName)).run();
1061  }
1062
1063  @Override
1064  public boolean setReplication(final Path p, final short replication
1065     ) throws IOException {
1066    statistics.incrementWriteOps(1);
1067    final HttpOpParam.Op op = PutOpParam.Op.SETREPLICATION;
1068    return new FsPathBooleanRunner(op, p,
1069        new ReplicationParam(replication)
1070    ).run();
1071  }
1072
1073  @Override
1074  public void setTimes(final Path p, final long mtime, final long atime
1075      ) throws IOException {
1076    statistics.incrementWriteOps(1);
1077    final HttpOpParam.Op op = PutOpParam.Op.SETTIMES;
1078    new FsPathRunner(op, p,
1079        new ModificationTimeParam(mtime),
1080        new AccessTimeParam(atime)
1081    ).run();
1082  }
1083
1084  @Override
1085  public long getDefaultBlockSize() {
1086    return getConf().getLongBytes(DFSConfigKeys.DFS_BLOCK_SIZE_KEY,
1087        DFSConfigKeys.DFS_BLOCK_SIZE_DEFAULT);
1088  }
1089
1090  @Override
1091  public short getDefaultReplication() {
1092    return (short)getConf().getInt(DFSConfigKeys.DFS_REPLICATION_KEY,
1093        DFSConfigKeys.DFS_REPLICATION_DEFAULT);
1094  }
1095
1096  @Override
1097  public void concat(final Path trg, final Path [] srcs) throws IOException {
1098    statistics.incrementWriteOps(1);
1099    final HttpOpParam.Op op = PostOpParam.Op.CONCAT;
1100    new FsPathRunner(op, trg, new ConcatSourcesParam(srcs)).run();
1101  }
1102
1103  @Override
1104  public FSDataOutputStream create(final Path f, final FsPermission permission,
1105      final boolean overwrite, final int bufferSize, final short replication,
1106      final long blockSize, final Progressable progress) throws IOException {
1107    statistics.incrementWriteOps(1);
1108
1109    final HttpOpParam.Op op = PutOpParam.Op.CREATE;
1110    return new FsPathOutputStreamRunner(op, f, bufferSize,
1111        new PermissionParam(applyUMask(permission)),
1112        new OverwriteParam(overwrite),
1113        new BufferSizeParam(bufferSize),
1114        new ReplicationParam(replication),
1115        new BlockSizeParam(blockSize)
1116    ).run();
1117  }
1118
1119  @Override
1120  public FSDataOutputStream append(final Path f, final int bufferSize,
1121      final Progressable progress) throws IOException {
1122    statistics.incrementWriteOps(1);
1123
1124    final HttpOpParam.Op op = PostOpParam.Op.APPEND;
1125    return new FsPathOutputStreamRunner(op, f, bufferSize,
1126        new BufferSizeParam(bufferSize)
1127    ).run();
1128  }
1129
1130  @Override
1131  public boolean delete(Path f, boolean recursive) throws IOException {
1132    final HttpOpParam.Op op = DeleteOpParam.Op.DELETE;
1133    return new FsPathBooleanRunner(op, f,
1134        new RecursiveParam(recursive)
1135    ).run();
1136  }
1137
1138  @Override
1139  public FSDataInputStream open(final Path f, final int buffersize
1140      ) throws IOException {
1141    statistics.incrementReadOps(1);
1142    final HttpOpParam.Op op = GetOpParam.Op.OPEN;
1143    // use a runner so the open can recover from an invalid token
1144    FsPathConnectionRunner runner =
1145        new FsPathConnectionRunner(op, f, new BufferSizeParam(buffersize));
1146    return new FSDataInputStream(new OffsetUrlInputStream(
1147        new UnresolvedUrlOpener(runner), new OffsetUrlOpener(null)));
1148  }
1149
1150  @Override
1151  public synchronized void close() throws IOException {
1152    try {
1153      if (canRefreshDelegationToken && delegationToken != null) {
1154        cancelDelegationToken(delegationToken);
1155      }
1156    } catch (IOException ioe) {
1157      LOG.debug("Token cancel failed: "+ioe);
1158    } finally {
1159      super.close();
1160    }
1161  }
1162
1163  // use FsPathConnectionRunner to ensure retries for InvalidTokens
1164  class UnresolvedUrlOpener extends ByteRangeInputStream.URLOpener {
1165    private final FsPathConnectionRunner runner;
1166    UnresolvedUrlOpener(FsPathConnectionRunner runner) {
1167      super(null);
1168      this.runner = runner;
1169    }
1170
1171    @Override
1172    protected HttpURLConnection connect(long offset, boolean resolved)
1173        throws IOException {
1174      assert offset == 0;
1175      HttpURLConnection conn = runner.run();
1176      setURL(conn.getURL());
1177      return conn;
1178    }
1179  }
1180
1181  class OffsetUrlOpener extends ByteRangeInputStream.URLOpener {
1182    OffsetUrlOpener(final URL url) {
1183      super(url);
1184    }
1185
1186    /** Setup offset url and connect. */
1187    @Override
1188    protected HttpURLConnection connect(final long offset,
1189        final boolean resolved) throws IOException {
1190      final URL offsetUrl = offset == 0L? url
1191          : new URL(url + "&" + new OffsetParam(offset));
1192      return new URLRunner(GetOpParam.Op.OPEN, offsetUrl, resolved).run();
1193    }  
1194  }
1195
1196  private static final String OFFSET_PARAM_PREFIX = OffsetParam.NAME + "=";
1197
1198  /** Remove offset parameter, if there is any, from the url */
1199  static URL removeOffsetParam(final URL url) throws MalformedURLException {
1200    String query = url.getQuery();
1201    if (query == null) {
1202      return url;
1203    }
1204    final String lower = query.toLowerCase();
1205    if (!lower.startsWith(OFFSET_PARAM_PREFIX)
1206        && !lower.contains("&" + OFFSET_PARAM_PREFIX)) {
1207      return url;
1208    }
1209
1210    //rebuild query
1211    StringBuilder b = null;
1212    for(final StringTokenizer st = new StringTokenizer(query, "&");
1213        st.hasMoreTokens();) {
1214      final String token = st.nextToken();
1215      if (!token.toLowerCase().startsWith(OFFSET_PARAM_PREFIX)) {
1216        if (b == null) {
1217          b = new StringBuilder("?").append(token);
1218        } else {
1219          b.append('&').append(token);
1220        }
1221      }
1222    }
1223    query = b == null? "": b.toString();
1224
1225    final String urlStr = url.toString();
1226    return new URL(urlStr.substring(0, urlStr.indexOf('?')) + query);
1227  }
1228
1229  static class OffsetUrlInputStream extends ByteRangeInputStream {
1230    OffsetUrlInputStream(UnresolvedUrlOpener o, OffsetUrlOpener r)
1231        throws IOException {
1232      super(o, r);
1233    }
1234
1235    /** Remove offset parameter before returning the resolved url. */
1236    @Override
1237    protected URL getResolvedUrl(final HttpURLConnection connection
1238        ) throws MalformedURLException {
1239      return removeOffsetParam(connection.getURL());
1240    }
1241  }
1242
1243  @Override
1244  public FileStatus[] listStatus(final Path f) throws IOException {
1245    statistics.incrementReadOps(1);
1246
1247    final HttpOpParam.Op op = GetOpParam.Op.LISTSTATUS;
1248    return new FsPathResponseRunner<FileStatus[]>(op, f) {
1249      @Override
1250      FileStatus[] decodeResponse(Map<?,?> json) {
1251        final Map<?, ?> rootmap = (Map<?, ?>)json.get(FileStatus.class.getSimpleName() + "es");
1252        final Object[] array = (Object[])rootmap.get(FileStatus.class.getSimpleName());
1253
1254        //convert FileStatus
1255        final FileStatus[] statuses = new FileStatus[array.length];
1256        for (int i = 0; i < array.length; i++) {
1257          final Map<?, ?> m = (Map<?, ?>)array[i];
1258          statuses[i] = makeQualified(JsonUtil.toFileStatus(m, false), f);
1259        }
1260        return statuses;
1261      }
1262    }.run();
1263  }
1264
1265  @Override
1266  public Token<DelegationTokenIdentifier> getDelegationToken(
1267      final String renewer) throws IOException {
1268    try {
1269      final HttpOpParam.Op op = GetOpParam.Op.GETDELEGATIONTOKEN;
1270      Token<DelegationTokenIdentifier> token =
1271          new FsPathResponseRunner<Token<DelegationTokenIdentifier>>(
1272              op, null, new RenewerParam(renewer)) {
1273        @Override
1274        Token<DelegationTokenIdentifier> decodeResponse(Map<?,?> json)
1275            throws IOException {
1276          return JsonUtil.toDelegationToken(json);
1277        }
1278      }.run();
1279      token.setService(tokenServiceName);
1280      return token;
1281    } catch (IOException e) {
1282      LOG.warn(e.getMessage());
1283      LOG.debug(e.getMessage(), e);
1284    }
1285
1286    return null;
1287  }
1288
1289  @Override
1290  public synchronized Token<?> getRenewToken() {
1291    return delegationToken;
1292  }
1293
1294  @Override
1295  public <T extends TokenIdentifier> void setDelegationToken(
1296      final Token<T> token) {
1297    synchronized (this) {
1298      delegationToken = token;
1299    }
1300  }
1301
1302  @Override
1303  public synchronized long renewDelegationToken(final Token<?> token
1304      ) throws IOException {
1305    final HttpOpParam.Op op = PutOpParam.Op.RENEWDELEGATIONTOKEN;
1306    return new FsPathResponseRunner<Long>(op, null,
1307        new TokenArgumentParam(token.encodeToUrlString())) {
1308      @Override
1309      Long decodeResponse(Map<?,?> json) throws IOException {
1310        return (Long) json.get("long");
1311      }
1312    }.run();
1313  }
1314
1315  @Override
1316  public synchronized void cancelDelegationToken(final Token<?> token
1317      ) throws IOException {
1318    final HttpOpParam.Op op = PutOpParam.Op.CANCELDELEGATIONTOKEN;
1319    new FsPathRunner(op, null,
1320        new TokenArgumentParam(token.encodeToUrlString())
1321    ).run();
1322  }
1323  
1324  @Override
1325  public BlockLocation[] getFileBlockLocations(final FileStatus status,
1326      final long offset, final long length) throws IOException {
1327    if (status == null) {
1328      return null;
1329    }
1330    return getFileBlockLocations(status.getPath(), offset, length);
1331  }
1332
1333  @Override
1334  public BlockLocation[] getFileBlockLocations(final Path p, 
1335      final long offset, final long length) throws IOException {
1336    statistics.incrementReadOps(1);
1337
1338    final HttpOpParam.Op op = GetOpParam.Op.GET_BLOCK_LOCATIONS;
1339    return new FsPathResponseRunner<BlockLocation[]>(op, p,
1340        new OffsetParam(offset), new LengthParam(length)) {
1341      @Override
1342      BlockLocation[] decodeResponse(Map<?,?> json) throws IOException {
1343        return DFSUtil.locatedBlocks2Locations(
1344            JsonUtil.toLocatedBlocks(json));
1345      }
1346    }.run();
1347  }
1348
1349  @Override
1350  public ContentSummary getContentSummary(final Path p) throws IOException {
1351    statistics.incrementReadOps(1);
1352
1353    final HttpOpParam.Op op = GetOpParam.Op.GETCONTENTSUMMARY;
1354    return new FsPathResponseRunner<ContentSummary>(op, p) {
1355      @Override
1356      ContentSummary decodeResponse(Map<?,?> json) {
1357        return JsonUtil.toContentSummary(json);        
1358      }
1359    }.run();
1360  }
1361
1362  @Override
1363  public MD5MD5CRC32FileChecksum getFileChecksum(final Path p
1364      ) throws IOException {
1365    statistics.incrementReadOps(1);
1366  
1367    final HttpOpParam.Op op = GetOpParam.Op.GETFILECHECKSUM;
1368    return new FsPathResponseRunner<MD5MD5CRC32FileChecksum>(op, p) {
1369      @Override
1370      MD5MD5CRC32FileChecksum decodeResponse(Map<?,?> json) throws IOException {
1371        return JsonUtil.toMD5MD5CRC32FileChecksum(json);
1372      }
1373    }.run();
1374  }
1375
1376  /**
1377   * Resolve an HDFS URL into real INetSocketAddress. It works like a DNS
1378   * resolver when the URL points to an non-HA cluster. When the URL points to
1379   * an HA cluster with its logical name, the resolver further resolves the
1380   * logical name(i.e., the authority in the URL) into real namenode addresses.
1381   */
1382  private InetSocketAddress[] resolveNNAddr() throws IOException {
1383    Configuration conf = getConf();
1384    final String scheme = uri.getScheme();
1385
1386    ArrayList<InetSocketAddress> ret = new ArrayList<InetSocketAddress>();
1387
1388    if (!HAUtil.isLogicalUri(conf, uri)) {
1389      InetSocketAddress addr = NetUtils.createSocketAddr(uri.getAuthority(),
1390          getDefaultPort());
1391      ret.add(addr);
1392
1393    } else {
1394      Map<String, Map<String, InetSocketAddress>> addresses = DFSUtil
1395          .getHaNnWebHdfsAddresses(conf, scheme);
1396
1397      // Extract the entry corresponding to the logical name.
1398      Map<String, InetSocketAddress> addrs = addresses.get(uri.getHost());
1399      for (InetSocketAddress addr : addrs.values()) {
1400        ret.add(addr);
1401      }
1402    }
1403
1404    InetSocketAddress[] r = new InetSocketAddress[ret.size()];
1405    return ret.toArray(r);
1406  }
1407
1408  @Override
1409  public String getCanonicalServiceName() {
1410    return tokenServiceName == null ? super.getCanonicalServiceName()
1411        : tokenServiceName.toString();
1412  }
1413
1414  @VisibleForTesting
1415  InetSocketAddress[] getResolvedNNAddr() {
1416    return nnAddrs;
1417  }
1418}