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