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