001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.hadoop.hdfs.web;
020
021import java.io.BufferedOutputStream;
022import java.io.FileNotFoundException;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.InputStreamReader;
026import java.net.HttpURLConnection;
027import java.net.InetSocketAddress;
028import java.net.MalformedURLException;
029import java.net.URI;
030import java.net.URL;
031import java.security.PrivilegedExceptionAction;
032import java.util.List;
033import java.util.Map;
034import java.util.StringTokenizer;
035
036import javax.ws.rs.core.MediaType;
037
038import org.apache.commons.logging.Log;
039import org.apache.commons.logging.LogFactory;
040import org.apache.hadoop.conf.Configuration;
041import org.apache.hadoop.fs.BlockLocation;
042import org.apache.hadoop.fs.ContentSummary;
043import org.apache.hadoop.fs.DelegationTokenRenewer;
044import org.apache.hadoop.fs.FSDataInputStream;
045import org.apache.hadoop.fs.FSDataOutputStream;
046import org.apache.hadoop.fs.FileStatus;
047import org.apache.hadoop.fs.FileSystem;
048import org.apache.hadoop.fs.MD5MD5CRC32FileChecksum;
049import org.apache.hadoop.fs.Options;
050import org.apache.hadoop.fs.Path;
051import org.apache.hadoop.fs.permission.AclEntry;
052import org.apache.hadoop.fs.permission.AclStatus;
053import org.apache.hadoop.fs.permission.FsPermission;
054import org.apache.hadoop.hdfs.DFSConfigKeys;
055import org.apache.hadoop.hdfs.DFSUtil;
056import org.apache.hadoop.hdfs.HAUtil;
057import org.apache.hadoop.hdfs.protocol.HdfsFileStatus;
058import org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenIdentifier;
059import org.apache.hadoop.hdfs.server.namenode.SafeModeException;
060import org.apache.hadoop.hdfs.web.resources.AccessTimeParam;
061import org.apache.hadoop.hdfs.web.resources.AclPermissionParam;
062import org.apache.hadoop.hdfs.web.resources.BlockSizeParam;
063import org.apache.hadoop.hdfs.web.resources.BufferSizeParam;
064import org.apache.hadoop.hdfs.web.resources.ConcatSourcesParam;
065import org.apache.hadoop.hdfs.web.resources.CreateParentParam;
066import org.apache.hadoop.hdfs.web.resources.DelegationParam;
067import org.apache.hadoop.hdfs.web.resources.DeleteOpParam;
068import org.apache.hadoop.hdfs.web.resources.DestinationParam;
069import org.apache.hadoop.hdfs.web.resources.DoAsParam;
070import org.apache.hadoop.hdfs.web.resources.GetOpParam;
071import org.apache.hadoop.hdfs.web.resources.GroupParam;
072import org.apache.hadoop.hdfs.web.resources.HttpOpParam;
073import org.apache.hadoop.hdfs.web.resources.LengthParam;
074import org.apache.hadoop.hdfs.web.resources.ModificationTimeParam;
075import org.apache.hadoop.hdfs.web.resources.OffsetParam;
076import org.apache.hadoop.hdfs.web.resources.OverwriteParam;
077import org.apache.hadoop.hdfs.web.resources.OwnerParam;
078import org.apache.hadoop.hdfs.web.resources.Param;
079import org.apache.hadoop.hdfs.web.resources.PermissionParam;
080import org.apache.hadoop.hdfs.web.resources.PostOpParam;
081import org.apache.hadoop.hdfs.web.resources.PutOpParam;
082import org.apache.hadoop.hdfs.web.resources.RecursiveParam;
083import org.apache.hadoop.hdfs.web.resources.RenameOptionSetParam;
084import org.apache.hadoop.hdfs.web.resources.RenewerParam;
085import org.apache.hadoop.hdfs.web.resources.ReplicationParam;
086import org.apache.hadoop.hdfs.web.resources.TokenArgumentParam;
087import org.apache.hadoop.hdfs.web.resources.UserParam;
088import org.apache.hadoop.io.Text;
089import org.apache.hadoop.io.retry.RetryPolicies;
090import org.apache.hadoop.io.retry.RetryPolicy;
091import org.apache.hadoop.io.retry.RetryUtils;
092import org.apache.hadoop.ipc.RemoteException;
093import org.apache.hadoop.net.NetUtils;
094import org.apache.hadoop.security.SecurityUtil;
095import org.apache.hadoop.security.UserGroupInformation;
096import org.apache.hadoop.security.authentication.client.AuthenticationException;
097import org.apache.hadoop.security.token.SecretManager.InvalidToken;
098import org.apache.hadoop.security.token.Token;
099import org.apache.hadoop.security.token.TokenIdentifier;
100import org.apache.hadoop.util.Progressable;
101import org.mortbay.util.ajax.JSON;
102
103import com.google.common.annotations.VisibleForTesting;
104import com.google.common.base.Charsets;
105import com.google.common.collect.Lists;
106
107/** A FileSystem for HDFS over the web. */
108public class WebHdfsFileSystem extends FileSystem
109    implements DelegationTokenRenewer.Renewable, TokenAspect.TokenManagementDelegator {
110  public static final Log LOG = LogFactory.getLog(WebHdfsFileSystem.class);
111  /** File System URI: {SCHEME}://namenode:port/path/to/file */
112  public static final String SCHEME = "webhdfs";
113  /** WebHdfs version. */
114  public static final int VERSION = 1;
115  /** Http URI: http://namenode:port/{PATH_PREFIX}/path/to/file */
116  public static final String PATH_PREFIX = "/" + SCHEME + "/v" + VERSION;
117
118  /** Default connection factory may be overridden in tests to use smaller timeout values */
119  protected URLConnectionFactory connectionFactory;
120
121  /** Delegation token kind */
122  public static final Text TOKEN_KIND = new Text("WEBHDFS delegation");
123  protected TokenAspect<? extends WebHdfsFileSystem> tokenAspect;
124
125  private UserGroupInformation ugi;
126  private URI uri;
127  private Token<?> delegationToken;
128  protected Text tokenServiceName;
129  private RetryPolicy retryPolicy = null;
130  private Path workingDir;
131  private InetSocketAddress nnAddrs[];
132  private int currentNNAddrIndex;
133
134  /**
135   * Return the protocol scheme for the FileSystem.
136   * <p/>
137   *
138   * @return <code>webhdfs</code>
139   */
140  @Override
141  public String getScheme() {
142    return SCHEME;
143  }
144
145  /**
146   * return the underlying transport protocol (http / https).
147   */
148  protected String getTransportScheme() {
149    return "http";
150  }
151
152  /**
153   * Initialize tokenAspect. This function is intended to
154   * be overridden by SWebHdfsFileSystem.
155   */
156  protected synchronized void initializeTokenAspect() {
157    tokenAspect = new TokenAspect<WebHdfsFileSystem>(this, tokenServiceName,
158        TOKEN_KIND);
159  }
160
161  @Override
162  public synchronized void initialize(URI uri, Configuration conf
163      ) throws IOException {
164    super.initialize(uri, conf);
165    setConf(conf);
166    /** set user pattern based on configuration file */
167    UserParam.setUserPattern(conf.get(
168        DFSConfigKeys.DFS_WEBHDFS_USER_PATTERN_KEY,
169        DFSConfigKeys.DFS_WEBHDFS_USER_PATTERN_DEFAULT));
170
171    connectionFactory = URLConnectionFactory
172        .newDefaultURLConnectionFactory(conf);
173
174    ugi = UserGroupInformation.getCurrentUser();
175    this.uri = URI.create(uri.getScheme() + "://" + uri.getAuthority());
176    this.nnAddrs = DFSUtil.resolveWebHdfsUri(this.uri, conf);
177
178    boolean isHA = HAUtil.isLogicalUri(conf, this.uri);
179    // In non-HA case, the code needs to call getCanonicalUri() in order to
180    // handle the case where no port is specified in the URI
181    this.tokenServiceName = isHA ? HAUtil.buildTokenServiceForLogicalUri(uri)
182        : SecurityUtil.buildTokenService(getCanonicalUri());
183    initializeTokenAspect();
184
185    if (!isHA) {
186      this.retryPolicy =
187          RetryUtils.getDefaultRetryPolicy(
188              conf,
189              DFSConfigKeys.DFS_HTTP_CLIENT_RETRY_POLICY_ENABLED_KEY,
190              DFSConfigKeys.DFS_HTTP_CLIENT_RETRY_POLICY_ENABLED_DEFAULT,
191              DFSConfigKeys.DFS_HTTP_CLIENT_RETRY_POLICY_SPEC_KEY,
192              DFSConfigKeys.DFS_HTTP_CLIENT_RETRY_POLICY_SPEC_DEFAULT,
193              SafeModeException.class);
194    } else {
195
196      int maxFailoverAttempts = conf.getInt(
197          DFSConfigKeys.DFS_HTTP_CLIENT_FAILOVER_MAX_ATTEMPTS_KEY,
198          DFSConfigKeys.DFS_HTTP_CLIENT_FAILOVER_MAX_ATTEMPTS_DEFAULT);
199      int maxRetryAttempts = conf.getInt(
200          DFSConfigKeys.DFS_HTTP_CLIENT_RETRY_MAX_ATTEMPTS_KEY,
201          DFSConfigKeys.DFS_HTTP_CLIENT_RETRY_MAX_ATTEMPTS_DEFAULT);
202      int failoverSleepBaseMillis = conf.getInt(
203          DFSConfigKeys.DFS_HTTP_CLIENT_FAILOVER_SLEEPTIME_BASE_KEY,
204          DFSConfigKeys.DFS_HTTP_CLIENT_FAILOVER_SLEEPTIME_BASE_DEFAULT);
205      int failoverSleepMaxMillis = conf.getInt(
206          DFSConfigKeys.DFS_HTTP_CLIENT_FAILOVER_SLEEPTIME_MAX_KEY,
207          DFSConfigKeys.DFS_HTTP_CLIENT_FAILOVER_SLEEPTIME_MAX_DEFAULT);
208
209      this.retryPolicy = RetryPolicies
210          .failoverOnNetworkException(RetryPolicies.TRY_ONCE_THEN_FAIL,
211              maxFailoverAttempts, maxRetryAttempts, failoverSleepBaseMillis,
212              failoverSleepMaxMillis);
213    }
214
215    this.workingDir = getHomeDirectory();
216
217    if (UserGroupInformation.isSecurityEnabled()) {
218      tokenAspect.initDelegationToken(ugi);
219    }
220  }
221
222  @Override
223  public URI getCanonicalUri() {
224    return super.getCanonicalUri();
225  }
226
227  /** Is WebHDFS enabled in conf? */
228  public static boolean isEnabled(final Configuration conf, final Log log) {
229    final boolean b = conf.getBoolean(DFSConfigKeys.DFS_WEBHDFS_ENABLED_KEY,
230        DFSConfigKeys.DFS_WEBHDFS_ENABLED_DEFAULT);
231    return b;
232  }
233
234  protected synchronized Token<?> getDelegationToken() throws IOException {
235    try {
236      tokenAspect.ensureTokenInitialized();
237      return delegationToken;
238    } catch (IOException e) {
239      LOG.warn(e.getMessage());
240      LOG.debug(e.getMessage(), e);
241    }
242
243    return null;
244  }
245
246  @Override
247  protected int getDefaultPort() {
248    return getConf().getInt(DFSConfigKeys.DFS_NAMENODE_HTTP_PORT_KEY,
249        DFSConfigKeys.DFS_NAMENODE_HTTP_PORT_DEFAULT);
250  }
251
252  @Override
253  public URI getUri() {
254    return this.uri;
255  }
256  
257  @Override
258  protected URI canonicalizeUri(URI uri) {
259    return NetUtils.getCanonicalUri(uri, getDefaultPort());
260  }
261
262  /** @return the home directory. */
263  public static String getHomeDirectoryString(final UserGroupInformation ugi) {
264    return "/user/" + ugi.getShortUserName();
265  }
266
267  @Override
268  public Path getHomeDirectory() {
269    return makeQualified(new Path(getHomeDirectoryString(ugi)));
270  }
271
272  @Override
273  public synchronized Path getWorkingDirectory() {
274    return workingDir;
275  }
276
277  @Override
278  public synchronized void setWorkingDirectory(final Path dir) {
279    String result = makeAbsolute(dir).toUri().getPath();
280    if (!DFSUtil.isValidName(result)) {
281      throw new IllegalArgumentException("Invalid DFS directory name " + 
282                                         result);
283    }
284    workingDir = makeAbsolute(dir);
285  }
286
287  private Path makeAbsolute(Path f) {
288    return f.isAbsolute()? f: new Path(workingDir, f);
289  }
290
291  static Map<?, ?> jsonParse(final HttpURLConnection c, final boolean useErrorStream
292      ) throws IOException {
293    if (c.getContentLength() == 0) {
294      return null;
295    }
296    final InputStream in = useErrorStream? c.getErrorStream(): c.getInputStream();
297    if (in == null) {
298      throw new IOException("The " + (useErrorStream? "error": "input") + " stream is null.");
299    }
300    final String contentType = c.getContentType();
301    if (contentType != null) {
302      final MediaType parsed = MediaType.valueOf(contentType);
303      if (!MediaType.APPLICATION_JSON_TYPE.isCompatible(parsed)) {
304        throw new IOException("Content-Type \"" + contentType
305            + "\" is incompatible with \"" + MediaType.APPLICATION_JSON
306            + "\" (parsed=\"" + parsed + "\")");
307      }
308    }
309    return (Map<?, ?>)JSON.parse(new InputStreamReader(in, Charsets.UTF_8));
310  }
311
312  private static Map<?, ?> validateResponse(final HttpOpParam.Op op,
313      final HttpURLConnection conn, boolean unwrapException) throws IOException {
314    final int code = conn.getResponseCode();
315    // server is demanding an authentication we don't support
316    if (code == HttpURLConnection.HTTP_UNAUTHORIZED) {
317      throw new IOException(
318          new AuthenticationException(conn.getResponseMessage()));
319    }
320    if (code != op.getExpectedHttpResponseCode()) {
321      final Map<?, ?> m;
322      try {
323        m = jsonParse(conn, true);
324      } catch(Exception e) {
325        throw new IOException("Unexpected HTTP response: code=" + code + " != "
326            + op.getExpectedHttpResponseCode() + ", " + op.toQueryString()
327            + ", message=" + conn.getResponseMessage(), e);
328      }
329
330      if (m == null) {
331        throw new IOException("Unexpected HTTP response: code=" + code + " != "
332            + op.getExpectedHttpResponseCode() + ", " + op.toQueryString()
333            + ", message=" + conn.getResponseMessage());
334      } else if (m.get(RemoteException.class.getSimpleName()) == null) {
335        return m;
336      }
337
338      final RemoteException re = JsonUtil.toRemoteException(m);
339      throw unwrapException? toIOException(re): re;
340    }
341    return null;
342  }
343
344  /**
345   * Covert an exception to an IOException.
346   * 
347   * For a non-IOException, wrap it with IOException.
348   * For a RemoteException, unwrap it.
349   * For an IOException which is not a RemoteException, return it. 
350   */
351  private static IOException toIOException(Exception e) {
352    if (!(e instanceof IOException)) {
353      return new IOException(e);
354    }
355
356    final IOException ioe = (IOException)e;
357    if (!(ioe instanceof RemoteException)) {
358      return ioe;
359    }
360
361    return ((RemoteException)ioe).unwrapRemoteException();
362  }
363
364  private synchronized InetSocketAddress getCurrentNNAddr() {
365    return nnAddrs[currentNNAddrIndex];
366  }
367
368  /**
369   * Reset the appropriate state to gracefully fail over to another name node
370   */
371  private synchronized void resetStateToFailOver() {
372    currentNNAddrIndex = (currentNNAddrIndex + 1) % nnAddrs.length;
373    delegationToken = null;
374    tokenAspect.reset();
375  }
376
377  /**
378   * Return a URL pointing to given path on the namenode.
379   *
380   * @param path to obtain the URL for
381   * @param query string to append to the path
382   * @return namenode URL referring to the given path
383   * @throws IOException on error constructing the URL
384   */
385  private URL getNamenodeURL(String path, String query) throws IOException {
386    InetSocketAddress nnAddr = getCurrentNNAddr();
387    final URL url = new URL(getTransportScheme(), nnAddr.getHostName(),
388          nnAddr.getPort(), path + '?' + query);
389    if (LOG.isTraceEnabled()) {
390      LOG.trace("url=" + url);
391    }
392    return url;
393  }
394  
395  Param<?,?>[] getAuthParameters(final HttpOpParam.Op op) throws IOException {
396    List<Param<?,?>> authParams = Lists.newArrayList();    
397    // Skip adding delegation token for token operations because these
398    // operations require authentication.
399    Token<?> token = null;
400    if (UserGroupInformation.isSecurityEnabled() && !op.getRequireAuth()) {
401      token = getDelegationToken();
402    }
403    if (token != null) {
404      authParams.add(new DelegationParam(token.encodeToUrlString()));
405    } else {
406      UserGroupInformation userUgi = ugi;
407      UserGroupInformation realUgi = userUgi.getRealUser();
408      if (realUgi != null) { // proxy user
409        authParams.add(new DoAsParam(userUgi.getShortUserName()));
410        userUgi = realUgi;
411      }
412      authParams.add(new UserParam(userUgi.getShortUserName()));
413    }
414    return authParams.toArray(new Param<?,?>[0]);
415  }
416
417  URL toUrl(final HttpOpParam.Op op, final Path fspath,
418      final Param<?,?>... parameters) throws IOException {
419    //initialize URI path and query
420    final String path = PATH_PREFIX
421        + (fspath == null? "/": makeQualified(fspath).toUri().getRawPath());
422    final String query = op.toQueryString()
423        + Param.toSortedString("&", getAuthParameters(op))
424        + Param.toSortedString("&", parameters);
425    final URL url = getNamenodeURL(path, query);
426    if (LOG.isTraceEnabled()) {
427      LOG.trace("url=" + url);
428    }
429    return url;
430  }
431
432  /**
433   * Run a http operation.
434   * Connect to the http server, validate response, and obtain the JSON output.
435   * 
436   * @param op http operation
437   * @param fspath file system path
438   * @param parameters parameters for the operation
439   * @return a JSON object, e.g. Object[], Map<?, ?>, etc.
440   * @throws IOException
441   */
442  private Map<?, ?> run(final HttpOpParam.Op op, final Path fspath,
443      final Param<?,?>... parameters) throws IOException {
444    return new FsPathRunner(op, fspath, parameters).run().json;
445  }
446
447  /**
448   * This class is for initialing a HTTP connection, connecting to server,
449   * obtaining a response, and also handling retry on failures.
450   */
451  abstract class AbstractRunner {
452    abstract protected URL getUrl() throws IOException;
453
454    protected final HttpOpParam.Op op;
455    private final boolean redirected;
456
457    private boolean checkRetry;
458    protected HttpURLConnection conn = null;
459    private Map<?, ?> json = null;
460
461    protected AbstractRunner(final HttpOpParam.Op op, boolean redirected) {
462      this.op = op;
463      this.redirected = redirected;
464    }
465
466    AbstractRunner run() throws IOException {
467      UserGroupInformation connectUgi = ugi.getRealUser();
468      if (connectUgi == null) {
469        connectUgi = ugi;
470      }
471      if (op.getRequireAuth()) {
472        connectUgi.checkTGTAndReloginFromKeytab();
473      }
474      try {
475        // the entire lifecycle of the connection must be run inside the
476        // doAs to ensure authentication is performed correctly
477        return connectUgi.doAs(
478            new PrivilegedExceptionAction<AbstractRunner>() {
479              @Override
480              public AbstractRunner run() throws IOException {
481                return runWithRetry();
482              }
483            });
484      } catch (InterruptedException e) {
485        throw new IOException(e);
486      }
487    }
488    
489    private void init() throws IOException {
490      checkRetry = !redirected;
491      URL url = getUrl();
492      conn = (HttpURLConnection) connectionFactory.openConnection(url);
493    }
494    
495    private void connect() throws IOException {
496      connect(op.getDoOutput());
497    }
498
499    private void connect(boolean doOutput) throws IOException {
500      conn.setRequestMethod(op.getType().toString());
501      conn.setDoOutput(doOutput);
502      conn.setInstanceFollowRedirects(false);
503      conn.connect();
504    }
505
506    private void disconnect() {
507      if (conn != null) {
508        conn.disconnect();
509        conn = null;
510      }
511    }
512
513    private AbstractRunner runWithRetry() throws IOException {
514      /**
515       * Do the real work.
516       *
517       * There are three cases that the code inside the loop can throw an
518       * IOException:
519       *
520       * <ul>
521       * <li>The connection has failed (e.g., ConnectException,
522       * @see FailoverOnNetworkExceptionRetry for more details)</li>
523       * <li>The namenode enters the standby state (i.e., StandbyException).</li>
524       * <li>The server returns errors for the command (i.e., RemoteException)</li>
525       * </ul>
526       *
527       * The call to shouldRetry() will conduct the retry policy. The policy
528       * examines the exception and swallows it if it decides to rerun the work.
529       */
530      for(int retry = 0; ; retry++) {
531        try {
532          init();
533          if (op.getDoOutput()) {
534            twoStepWrite();
535          } else {
536            getResponse(op != GetOpParam.Op.OPEN);
537          }
538          return this;
539        } catch(IOException ioe) {
540          Throwable cause = ioe.getCause();
541          if (cause != null && cause instanceof AuthenticationException) {
542            throw ioe; // no retries for auth failures
543          }
544          shouldRetry(ioe, retry);
545        }
546      }
547    }
548
549    private void shouldRetry(final IOException ioe, final int retry
550        ) throws IOException {
551      InetSocketAddress nnAddr = getCurrentNNAddr();
552      if (checkRetry) {
553        try {
554          final RetryPolicy.RetryAction a = retryPolicy.shouldRetry(
555              ioe, retry, 0, true);
556
557          boolean isRetry = a.action == RetryPolicy.RetryAction.RetryDecision.RETRY;
558          boolean isFailoverAndRetry =
559              a.action == RetryPolicy.RetryAction.RetryDecision.FAILOVER_AND_RETRY;
560
561          if (isRetry || isFailoverAndRetry) {
562            LOG.info("Retrying connect to namenode: " + nnAddr
563                + ". Already tried " + retry + " time(s); retry policy is "
564                + retryPolicy + ", delay " + a.delayMillis + "ms.");
565
566            if (isFailoverAndRetry) {
567              resetStateToFailOver();
568            }
569
570            Thread.sleep(a.delayMillis);
571            return;
572          }
573        } catch(Exception e) {
574          LOG.warn("Original exception is ", ioe);
575          throw toIOException(e);
576        }
577      }
578      throw toIOException(ioe);
579    }
580
581    /**
582     * Two-step Create/Append:
583     * Step 1) Submit a Http request with neither auto-redirect nor data. 
584     * Step 2) Submit another Http request with the URL from the Location header with data.
585     * 
586     * The reason of having two-step create/append is for preventing clients to
587     * send out the data before the redirect. This issue is addressed by the
588     * "Expect: 100-continue" header in HTTP/1.1; see RFC 2616, Section 8.2.3.
589     * Unfortunately, there are software library bugs (e.g. Jetty 6 http server
590     * and Java 6 http client), which do not correctly implement "Expect:
591     * 100-continue". The two-step create/append is a temporary workaround for
592     * the software library bugs.
593     */
594    HttpURLConnection twoStepWrite() throws IOException {
595      //Step 1) Submit a Http request with neither auto-redirect nor data. 
596      connect(false);
597      validateResponse(HttpOpParam.TemporaryRedirectOp.valueOf(op), conn, false);
598      final String redirect = conn.getHeaderField("Location");
599      disconnect();
600      checkRetry = false;
601      
602      //Step 2) Submit another Http request with the URL from the Location header with data.
603      conn = (HttpURLConnection) connectionFactory.openConnection(new URL(
604          redirect));
605      conn.setRequestProperty("Content-Type",
606          MediaType.APPLICATION_OCTET_STREAM);
607      conn.setChunkedStreamingMode(32 << 10); //32kB-chunk
608      connect();
609      return conn;
610    }
611
612    FSDataOutputStream write(final int bufferSize) throws IOException {
613      return WebHdfsFileSystem.this.write(op, conn, bufferSize);
614    }
615
616    void getResponse(boolean getJsonAndDisconnect) throws IOException {
617      try {
618        connect();
619        final int code = conn.getResponseCode();
620        if (!redirected && op.getRedirect()
621            && code != op.getExpectedHttpResponseCode()) {
622          final String redirect = conn.getHeaderField("Location");
623          json = validateResponse(HttpOpParam.TemporaryRedirectOp.valueOf(op),
624              conn, false);
625          disconnect();
626  
627          checkRetry = false;
628          conn = (HttpURLConnection) connectionFactory.openConnection(new URL(
629              redirect));
630          connect();
631        }
632
633        json = validateResponse(op, conn, false);
634        if (json == null && getJsonAndDisconnect) {
635          json = jsonParse(conn, false);
636        }
637      } finally {
638        if (getJsonAndDisconnect) {
639          disconnect();
640        }
641      }
642    }
643  }
644
645  final class FsPathRunner extends AbstractRunner {
646    private final Path fspath;
647    private final Param<?, ?>[] parameters;
648
649    FsPathRunner(final HttpOpParam.Op op, final Path fspath, final Param<?,?>... parameters) {
650      super(op, false);
651      this.fspath = fspath;
652      this.parameters = parameters;
653    }
654
655    @Override
656    protected URL getUrl() throws IOException {
657      return toUrl(op, fspath, parameters);
658    }
659  }
660
661  final class URLRunner extends AbstractRunner {
662    private final URL url;
663    @Override
664    protected URL getUrl() {
665      return url;
666    }
667
668    protected URLRunner(final HttpOpParam.Op op, final URL url, boolean redirected) {
669      super(op, redirected);
670      this.url = url;
671    }
672  }
673
674  private FsPermission applyUMask(FsPermission permission) {
675    if (permission == null) {
676      permission = FsPermission.getDefault();
677    }
678    return permission.applyUMask(FsPermission.getUMask(getConf()));
679  }
680
681  private HdfsFileStatus getHdfsFileStatus(Path f) throws IOException {
682    final HttpOpParam.Op op = GetOpParam.Op.GETFILESTATUS;
683    final Map<?, ?> json = run(op, f);
684    final HdfsFileStatus status = JsonUtil.toFileStatus(json, true);
685    if (status == null) {
686      throw new FileNotFoundException("File does not exist: " + f);
687    }
688    return status;
689  }
690
691  @Override
692  public FileStatus getFileStatus(Path f) throws IOException {
693    statistics.incrementReadOps(1);
694    return makeQualified(getHdfsFileStatus(f), f);
695  }
696
697  private FileStatus makeQualified(HdfsFileStatus f, Path parent) {
698    return new FileStatus(f.getLen(), f.isDir(), f.getReplication(),
699        f.getBlockSize(), f.getModificationTime(), f.getAccessTime(),
700        f.getPermission(), f.getOwner(), f.getGroup(),
701        f.isSymlink() ? new Path(f.getSymlink()) : null,
702        f.getFullPath(parent).makeQualified(getUri(), getWorkingDirectory()));
703  }
704
705  @Override
706  public AclStatus getAclStatus(Path f) throws IOException {
707    final HttpOpParam.Op op = GetOpParam.Op.GETACLSTATUS;
708    final Map<?, ?> json = run(op, f);
709    AclStatus status = JsonUtil.toAclStatus(json);
710    if (status == null) {
711      throw new FileNotFoundException("File does not exist: " + f);
712    }
713    return status;
714  }
715
716  @Override
717  public boolean mkdirs(Path f, FsPermission permission) throws IOException {
718    statistics.incrementWriteOps(1);
719    final HttpOpParam.Op op = PutOpParam.Op.MKDIRS;
720    final Map<?, ?> json = run(op, f,
721        new PermissionParam(applyUMask(permission)));
722    return (Boolean)json.get("boolean");
723  }
724
725  /**
726   * Create a symlink pointing to the destination path.
727   * @see org.apache.hadoop.fs.Hdfs#createSymlink(Path, Path, boolean) 
728   */
729  public void createSymlink(Path destination, Path f, boolean createParent
730      ) throws IOException {
731    statistics.incrementWriteOps(1);
732    final HttpOpParam.Op op = PutOpParam.Op.CREATESYMLINK;
733    run(op, f, new DestinationParam(makeQualified(destination).toUri().getPath()),
734        new CreateParentParam(createParent));
735  }
736
737  @Override
738  public boolean rename(final Path src, final Path dst) throws IOException {
739    statistics.incrementWriteOps(1);
740    final HttpOpParam.Op op = PutOpParam.Op.RENAME;
741    final Map<?, ?> json = run(op, src,
742        new DestinationParam(makeQualified(dst).toUri().getPath()));
743    return (Boolean)json.get("boolean");
744  }
745
746  @SuppressWarnings("deprecation")
747  @Override
748  public void rename(final Path src, final Path dst,
749      final Options.Rename... options) throws IOException {
750    statistics.incrementWriteOps(1);
751    final HttpOpParam.Op op = PutOpParam.Op.RENAME;
752    run(op, src, new DestinationParam(makeQualified(dst).toUri().getPath()),
753        new RenameOptionSetParam(options));
754  }
755
756  @Override
757  public void setOwner(final Path p, final String owner, final String group
758      ) throws IOException {
759    if (owner == null && group == null) {
760      throw new IOException("owner == null && group == null");
761    }
762
763    statistics.incrementWriteOps(1);
764    final HttpOpParam.Op op = PutOpParam.Op.SETOWNER;
765    run(op, p, new OwnerParam(owner), new GroupParam(group));
766  }
767
768  @Override
769  public void setPermission(final Path p, final FsPermission permission
770      ) throws IOException {
771    statistics.incrementWriteOps(1);
772    final HttpOpParam.Op op = PutOpParam.Op.SETPERMISSION;
773    run(op, p, new PermissionParam(permission));
774  }
775
776  @Override
777  public void modifyAclEntries(Path path, List<AclEntry> aclSpec)
778      throws IOException {
779    statistics.incrementWriteOps(1);
780    final HttpOpParam.Op op = PutOpParam.Op.MODIFYACLENTRIES;
781    run(op, path, new AclPermissionParam(aclSpec));
782  }
783
784  @Override
785  public void removeAclEntries(Path path, List<AclEntry> aclSpec)
786      throws IOException {
787    statistics.incrementWriteOps(1);
788    final HttpOpParam.Op op = PutOpParam.Op.REMOVEACLENTRIES;
789    run(op, path, new AclPermissionParam(aclSpec));
790  }
791
792  @Override
793  public void removeDefaultAcl(Path path) throws IOException {
794    statistics.incrementWriteOps(1);
795    final HttpOpParam.Op op = PutOpParam.Op.REMOVEDEFAULTACL;
796    run(op, path);
797  }
798
799  @Override
800  public void removeAcl(Path path) throws IOException {
801    statistics.incrementWriteOps(1);
802    final HttpOpParam.Op op = PutOpParam.Op.REMOVEACL;
803    run(op, path);
804  }
805
806  @Override
807  public void setAcl(final Path p, final List<AclEntry> aclSpec)
808      throws IOException {
809    statistics.incrementWriteOps(1);
810    final HttpOpParam.Op op = PutOpParam.Op.SETACL;
811    run(op, p, new AclPermissionParam(aclSpec));
812  }
813
814  @Override
815  public boolean setReplication(final Path p, final short replication
816     ) throws IOException {
817    statistics.incrementWriteOps(1);
818    final HttpOpParam.Op op = PutOpParam.Op.SETREPLICATION;
819    final Map<?, ?> json = run(op, p, new ReplicationParam(replication));
820    return (Boolean)json.get("boolean");
821  }
822
823  @Override
824  public void setTimes(final Path p, final long mtime, final long atime
825      ) throws IOException {
826    statistics.incrementWriteOps(1);
827    final HttpOpParam.Op op = PutOpParam.Op.SETTIMES;
828    run(op, p, new ModificationTimeParam(mtime), new AccessTimeParam(atime));
829  }
830
831  @Override
832  public long getDefaultBlockSize() {
833    return getConf().getLongBytes(DFSConfigKeys.DFS_BLOCK_SIZE_KEY,
834        DFSConfigKeys.DFS_BLOCK_SIZE_DEFAULT);
835  }
836
837  @Override
838  public short getDefaultReplication() {
839    return (short)getConf().getInt(DFSConfigKeys.DFS_REPLICATION_KEY,
840        DFSConfigKeys.DFS_REPLICATION_DEFAULT);
841  }
842
843  FSDataOutputStream write(final HttpOpParam.Op op,
844      final HttpURLConnection conn, final int bufferSize) throws IOException {
845    return new FSDataOutputStream(new BufferedOutputStream(
846        conn.getOutputStream(), bufferSize), statistics) {
847      @Override
848      public void close() throws IOException {
849        try {
850          super.close();
851        } finally {
852          try {
853            validateResponse(op, conn, true);
854          } finally {
855            conn.disconnect();
856          }
857        }
858      }
859    };
860  }
861
862  @Override
863  public void concat(final Path trg, final Path [] srcs) throws IOException {
864    statistics.incrementWriteOps(1);
865    final HttpOpParam.Op op = PostOpParam.Op.CONCAT;
866
867    ConcatSourcesParam param = new ConcatSourcesParam(srcs);
868    run(op, trg, param);
869  }
870
871  @Override
872  public FSDataOutputStream create(final Path f, final FsPermission permission,
873      final boolean overwrite, final int bufferSize, final short replication,
874      final long blockSize, final Progressable progress) throws IOException {
875    statistics.incrementWriteOps(1);
876
877    final HttpOpParam.Op op = PutOpParam.Op.CREATE;
878    return new FsPathRunner(op, f,
879        new PermissionParam(applyUMask(permission)),
880        new OverwriteParam(overwrite),
881        new BufferSizeParam(bufferSize),
882        new ReplicationParam(replication),
883        new BlockSizeParam(blockSize))
884      .run()
885      .write(bufferSize);
886  }
887
888  @Override
889  public FSDataOutputStream append(final Path f, final int bufferSize,
890      final Progressable progress) throws IOException {
891    statistics.incrementWriteOps(1);
892
893    final HttpOpParam.Op op = PostOpParam.Op.APPEND;
894    return new FsPathRunner(op, f, new BufferSizeParam(bufferSize))
895      .run()
896      .write(bufferSize);
897  }
898
899  @Override
900  public boolean delete(Path f, boolean recursive) throws IOException {
901    final HttpOpParam.Op op = DeleteOpParam.Op.DELETE;
902    final Map<?, ?> json = run(op, f, new RecursiveParam(recursive));
903    return (Boolean)json.get("boolean");
904  }
905
906  @Override
907  public FSDataInputStream open(final Path f, final int buffersize
908      ) throws IOException {
909    statistics.incrementReadOps(1);
910    final HttpOpParam.Op op = GetOpParam.Op.OPEN;
911    final URL url = toUrl(op, f, new BufferSizeParam(buffersize));
912    return new FSDataInputStream(new OffsetUrlInputStream(
913        new OffsetUrlOpener(url), new OffsetUrlOpener(null)));
914  }
915
916  @Override
917  public void close() throws IOException {
918    super.close();
919    synchronized (this) {
920      tokenAspect.removeRenewAction();
921    }
922  }
923
924  class OffsetUrlOpener extends ByteRangeInputStream.URLOpener {
925    OffsetUrlOpener(final URL url) {
926      super(url);
927    }
928
929    /** Setup offset url and connect. */
930    @Override
931    protected HttpURLConnection connect(final long offset,
932        final boolean resolved) throws IOException {
933      final URL offsetUrl = offset == 0L? url
934          : new URL(url + "&" + new OffsetParam(offset));
935      return new URLRunner(GetOpParam.Op.OPEN, offsetUrl, resolved).run().conn;
936    }  
937  }
938
939  private static final String OFFSET_PARAM_PREFIX = OffsetParam.NAME + "=";
940
941  /** Remove offset parameter, if there is any, from the url */
942  static URL removeOffsetParam(final URL url) throws MalformedURLException {
943    String query = url.getQuery();
944    if (query == null) {
945      return url;
946    }
947    final String lower = query.toLowerCase();
948    if (!lower.startsWith(OFFSET_PARAM_PREFIX)
949        && !lower.contains("&" + OFFSET_PARAM_PREFIX)) {
950      return url;
951    }
952
953    //rebuild query
954    StringBuilder b = null;
955    for(final StringTokenizer st = new StringTokenizer(query, "&");
956        st.hasMoreTokens();) {
957      final String token = st.nextToken();
958      if (!token.toLowerCase().startsWith(OFFSET_PARAM_PREFIX)) {
959        if (b == null) {
960          b = new StringBuilder("?").append(token);
961        } else {
962          b.append('&').append(token);
963        }
964      }
965    }
966    query = b == null? "": b.toString();
967
968    final String urlStr = url.toString();
969    return new URL(urlStr.substring(0, urlStr.indexOf('?')) + query);
970  }
971
972  static class OffsetUrlInputStream extends ByteRangeInputStream {
973    OffsetUrlInputStream(OffsetUrlOpener o, OffsetUrlOpener r) {
974      super(o, r);
975    }
976
977    /** Remove offset parameter before returning the resolved url. */
978    @Override
979    protected URL getResolvedUrl(final HttpURLConnection connection
980        ) throws MalformedURLException {
981      return removeOffsetParam(connection.getURL());
982    }
983  }
984
985  @Override
986  public FileStatus[] listStatus(final Path f) throws IOException {
987    statistics.incrementReadOps(1);
988
989    final HttpOpParam.Op op = GetOpParam.Op.LISTSTATUS;
990    final Map<?, ?> json  = run(op, f);
991    final Map<?, ?> rootmap = (Map<?, ?>)json.get(FileStatus.class.getSimpleName() + "es");
992    final Object[] array = (Object[])rootmap.get(FileStatus.class.getSimpleName());
993
994    //convert FileStatus
995    final FileStatus[] statuses = new FileStatus[array.length];
996    for(int i = 0; i < array.length; i++) {
997      final Map<?, ?> m = (Map<?, ?>)array[i];
998      statuses[i] = makeQualified(JsonUtil.toFileStatus(m, false), f);
999    }
1000    return statuses;
1001  }
1002
1003  @Override
1004  public Token<DelegationTokenIdentifier> getDelegationToken(
1005      final String renewer) throws IOException {
1006    try {
1007      final HttpOpParam.Op op = GetOpParam.Op.GETDELEGATIONTOKEN;
1008      final Map<?, ?> m = run(op, null, new RenewerParam(renewer));
1009      final Token<DelegationTokenIdentifier> token = JsonUtil.toDelegationToken(m);
1010      token.setService(tokenServiceName);
1011      return token;
1012    } catch (IOException e) {
1013      LOG.warn(e.getMessage());
1014      LOG.debug(e.getMessage(), e);
1015    }
1016
1017    return null;
1018  }
1019
1020  @Override
1021  public synchronized Token<?> getRenewToken() {
1022    return delegationToken;
1023  }
1024
1025  @Override
1026  public <T extends TokenIdentifier> void setDelegationToken(
1027      final Token<T> token) {
1028    synchronized (this) {
1029      delegationToken = token;
1030    }
1031  }
1032
1033  @Override
1034  public synchronized long renewDelegationToken(final Token<?> token
1035      ) throws IOException {
1036    final HttpOpParam.Op op = PutOpParam.Op.RENEWDELEGATIONTOKEN;
1037    TokenArgumentParam dtargParam = new TokenArgumentParam(
1038        token.encodeToUrlString());
1039    final Map<?, ?> m = run(op, null, dtargParam);
1040    return (Long) m.get("long");
1041  }
1042
1043  @Override
1044  public synchronized void cancelDelegationToken(final Token<?> token
1045      ) throws IOException {
1046    final HttpOpParam.Op op = PutOpParam.Op.CANCELDELEGATIONTOKEN;
1047    TokenArgumentParam dtargParam = new TokenArgumentParam(
1048        token.encodeToUrlString());
1049    run(op, null, dtargParam);
1050  }
1051  
1052  @Override
1053  public BlockLocation[] getFileBlockLocations(final FileStatus status,
1054      final long offset, final long length) throws IOException {
1055    if (status == null) {
1056      return null;
1057    }
1058    return getFileBlockLocations(status.getPath(), offset, length);
1059  }
1060
1061  @Override
1062  public BlockLocation[] getFileBlockLocations(final Path p, 
1063      final long offset, final long length) throws IOException {
1064    statistics.incrementReadOps(1);
1065
1066    final HttpOpParam.Op op = GetOpParam.Op.GET_BLOCK_LOCATIONS;
1067    final Map<?, ?> m = run(op, p, new OffsetParam(offset),
1068        new LengthParam(length));
1069    return DFSUtil.locatedBlocks2Locations(JsonUtil.toLocatedBlocks(m));
1070  }
1071
1072  @Override
1073  public ContentSummary getContentSummary(final Path p) throws IOException {
1074    statistics.incrementReadOps(1);
1075
1076    final HttpOpParam.Op op = GetOpParam.Op.GETCONTENTSUMMARY;
1077    final Map<?, ?> m = run(op, p);
1078    return JsonUtil.toContentSummary(m);
1079  }
1080
1081  @Override
1082  public MD5MD5CRC32FileChecksum getFileChecksum(final Path p
1083      ) throws IOException {
1084    statistics.incrementReadOps(1);
1085  
1086    final HttpOpParam.Op op = GetOpParam.Op.GETFILECHECKSUM;
1087    final Map<?, ?> m = run(op, p);
1088    return JsonUtil.toMD5MD5CRC32FileChecksum(m);
1089  }
1090
1091  @Override
1092  public String getCanonicalServiceName() {
1093    return tokenServiceName == null ? super.getCanonicalServiceName()
1094        : tokenServiceName.toString();
1095  }
1096
1097  @VisibleForTesting
1098  InetSocketAddress[] getResolvedNNAddr() {
1099    return nnAddrs;
1100  }
1101}