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.security.PrivilegedExceptionAction;
022
023import java.io.BufferedOutputStream;
024import java.io.FileNotFoundException;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.InputStreamReader;
028import java.net.HttpURLConnection;
029import java.net.InetSocketAddress;
030import java.net.MalformedURLException;
031import java.net.URI;
032import java.net.URISyntaxException;
033import java.net.URL;
034import java.util.Collection;
035import java.util.List;
036import java.util.Map;
037import java.util.StringTokenizer;
038
039import javax.ws.rs.core.MediaType;
040
041import org.apache.commons.logging.Log;
042import org.apache.commons.logging.LogFactory;
043import org.apache.hadoop.conf.Configuration;
044import org.apache.hadoop.fs.BlockLocation;
045import org.apache.hadoop.fs.ContentSummary;
046import org.apache.hadoop.fs.FSDataInputStream;
047import org.apache.hadoop.fs.FSDataOutputStream;
048import org.apache.hadoop.fs.FileAlreadyExistsException;
049import org.apache.hadoop.fs.FileStatus;
050import org.apache.hadoop.fs.FileSystem;
051import org.apache.hadoop.fs.MD5MD5CRC32FileChecksum;
052import org.apache.hadoop.fs.Options;
053import org.apache.hadoop.fs.ParentNotDirectoryException;
054import org.apache.hadoop.fs.Path;
055import org.apache.hadoop.fs.permission.FsPermission;
056import org.apache.hadoop.hdfs.ByteRangeInputStream;
057import org.apache.hadoop.hdfs.DFSConfigKeys;
058import org.apache.hadoop.hdfs.DFSUtil;
059import org.apache.hadoop.hdfs.protocol.DSQuotaExceededException;
060import org.apache.hadoop.hdfs.protocol.HdfsFileStatus;
061import org.apache.hadoop.hdfs.protocol.NSQuotaExceededException;
062import org.apache.hadoop.hdfs.protocol.UnresolvedPathException;
063import org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenIdentifier;
064import org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenRenewer;
065import org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenSelector;
066import org.apache.hadoop.hdfs.server.common.JspHelper;
067import org.apache.hadoop.hdfs.server.namenode.SafeModeException;
068import org.apache.hadoop.hdfs.web.resources.*;
069import org.apache.hadoop.io.Text;
070import org.apache.hadoop.ipc.RemoteException;
071import org.apache.hadoop.net.NetUtils;
072import org.apache.hadoop.security.AccessControlException;
073import org.apache.hadoop.security.SecurityUtil;
074import org.apache.hadoop.security.UserGroupInformation;
075import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
076import org.apache.hadoop.security.authentication.client.AuthenticationException;
077import org.apache.hadoop.security.authorize.AuthorizationException;
078import org.apache.hadoop.security.token.SecretManager.InvalidToken;
079import org.apache.hadoop.security.token.Token;
080import org.apache.hadoop.security.token.TokenIdentifier;
081import org.apache.hadoop.security.token.TokenRenewer;
082import org.apache.hadoop.security.token.delegation.AbstractDelegationTokenSelector;
083import org.apache.hadoop.util.Progressable;
084import org.mortbay.util.ajax.JSON;
085
086import com.google.common.annotations.VisibleForTesting;
087import com.google.common.collect.Lists;
088
089/** A FileSystem for HDFS over the web. */
090public class WebHdfsFileSystem extends FileSystem
091    implements DelegationTokenRenewer.Renewable {
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  /** SPNEGO authenticator */
101  private static final KerberosUgiAuthenticator AUTH = new KerberosUgiAuthenticator();
102  /** Delegation token kind */
103  public static final Text TOKEN_KIND = new Text("WEBHDFS delegation");
104  /** Token selector */
105  public static final WebHdfsDelegationTokenSelector DT_SELECTOR
106      = new WebHdfsDelegationTokenSelector();
107
108  private  DelegationTokenRenewer dtRenewer = null;
109  @VisibleForTesting
110  DelegationTokenRenewer.RenewAction<?> action;
111
112  @VisibleForTesting
113  protected synchronized void addRenewAction(final WebHdfsFileSystem webhdfs) {
114    if (dtRenewer == null) {
115      dtRenewer = DelegationTokenRenewer.getInstance();
116    }
117
118    action = dtRenewer.addRenewAction(webhdfs);
119  }
120
121  /** Is WebHDFS enabled in conf? */
122  public static boolean isEnabled(final Configuration conf, final Log log) {
123    final boolean b = conf.getBoolean(DFSConfigKeys.DFS_WEBHDFS_ENABLED_KEY,
124        DFSConfigKeys.DFS_WEBHDFS_ENABLED_DEFAULT);
125    log.info(DFSConfigKeys.DFS_WEBHDFS_ENABLED_KEY + " = " + b);
126    return b;
127  }
128
129  private UserGroupInformation ugi;
130  private InetSocketAddress nnAddr;
131  private URI uri;
132  private boolean hasInitedToken;
133  private Token<?> delegationToken;
134  private Path workingDir;
135
136  @Override
137  public synchronized void initialize(URI uri, Configuration conf
138      ) throws IOException {
139    super.initialize(uri, conf);
140    setConf(conf);
141    ugi = UserGroupInformation.getCurrentUser();
142    try {
143      this.uri = new URI(uri.getScheme(), uri.getAuthority(), null, null, null);
144    } catch (URISyntaxException e) {
145      throw new IllegalArgumentException(e);
146    }
147    this.nnAddr = NetUtils.createSocketAddr(uri.getAuthority(), getDefaultPort());
148    this.workingDir = getHomeDirectory();
149
150    if (UserGroupInformation.isSecurityEnabled()) {
151      initDelegationToken();
152    }
153  }
154
155  protected void initDelegationToken() throws IOException {
156    // look for webhdfs token, then try hdfs
157    Token<?> token = selectDelegationToken(ugi);
158    if (token != null) {
159      LOG.debug("Found existing DT for " + token.getService());        
160      setDelegationToken(token);
161      hasInitedToken = true;
162    }
163  }
164
165  protected synchronized Token<?> getDelegationToken() throws IOException {
166    // we haven't inited yet, or we used to have a token but it expired
167    if (!hasInitedToken || (action != null && !action.isValid())) {
168      //since we don't already have a token, go get one
169      Token<?> token = getDelegationToken(null);
170      // security might be disabled
171      if (token != null) {
172        setDelegationToken(token);
173        addRenewAction(this);
174        LOG.debug("Created new DT for " + token.getService());
175      }
176      hasInitedToken = true;
177    }
178    return delegationToken;
179  }
180
181  protected Token<DelegationTokenIdentifier> selectDelegationToken(
182      UserGroupInformation ugi) {
183    return DT_SELECTOR.selectToken(getCanonicalUri(), ugi.getTokens(), getConf());
184  }
185
186  @Override
187  protected int getDefaultPort() {
188    return getConf().getInt(DFSConfigKeys.DFS_NAMENODE_HTTP_PORT_KEY,
189        DFSConfigKeys.DFS_NAMENODE_HTTP_PORT_DEFAULT);
190  }
191
192  @Override
193  public URI getUri() {
194    return this.uri;
195  }
196
197  /** @return the home directory. */
198  public static String getHomeDirectoryString(final UserGroupInformation ugi) {
199    return "/user/" + ugi.getShortUserName();
200  }
201
202  @Override
203  public Path getHomeDirectory() {
204    return makeQualified(new Path(getHomeDirectoryString(ugi)));
205  }
206
207  @Override
208  public synchronized Path getWorkingDirectory() {
209    return workingDir;
210  }
211
212  @Override
213  public synchronized void setWorkingDirectory(final Path dir) {
214    String result = makeAbsolute(dir).toUri().getPath();
215    if (!DFSUtil.isValidName(result)) {
216      throw new IllegalArgumentException("Invalid DFS directory name " + 
217                                         result);
218    }
219    workingDir = makeAbsolute(dir);
220  }
221
222  private Path makeAbsolute(Path f) {
223    return f.isAbsolute()? f: new Path(workingDir, f);
224  }
225
226  static Map<?, ?> jsonParse(final HttpURLConnection c, final boolean useErrorStream
227      ) throws IOException {
228    if (c.getContentLength() == 0) {
229      return null;
230    }
231    final InputStream in = useErrorStream? c.getErrorStream(): c.getInputStream();
232    if (in == null) {
233      throw new IOException("The " + (useErrorStream? "error": "input") + " stream is null.");
234    }
235    final String contentType = c.getContentType();
236    if (contentType != null) {
237      final MediaType parsed = MediaType.valueOf(contentType);
238      if (!MediaType.APPLICATION_JSON_TYPE.isCompatible(parsed)) {
239        throw new IOException("Content-Type \"" + contentType
240            + "\" is incompatible with \"" + MediaType.APPLICATION_JSON
241            + "\" (parsed=\"" + parsed + "\")");
242      }
243    }
244    return (Map<?, ?>)JSON.parse(new InputStreamReader(in));
245  }
246
247  private static Map<?, ?> validateResponse(final HttpOpParam.Op op,
248      final HttpURLConnection conn) throws IOException {
249    final int code = conn.getResponseCode();
250    if (code != op.getExpectedHttpResponseCode()) {
251      final Map<?, ?> m;
252      try {
253        m = jsonParse(conn, true);
254      } catch(IOException e) {
255        throw new IOException("Unexpected HTTP response: code=" + code + " != "
256            + op.getExpectedHttpResponseCode() + ", " + op.toQueryString()
257            + ", message=" + conn.getResponseMessage(), e);
258      }
259
260      if (m == null) {
261        throw new IOException("Unexpected HTTP response: code=" + code + " != "
262            + op.getExpectedHttpResponseCode() + ", " + op.toQueryString()
263            + ", message=" + conn.getResponseMessage());
264      } else if (m.get(RemoteException.class.getSimpleName()) == null) {
265        return m;
266      }
267
268      final RemoteException re = JsonUtil.toRemoteException(m);
269      throw re.unwrapRemoteException(AccessControlException.class,
270          InvalidToken.class,
271          AuthenticationException.class,
272          AuthorizationException.class,
273          FileAlreadyExistsException.class,
274          FileNotFoundException.class,
275          ParentNotDirectoryException.class,
276          UnresolvedPathException.class,
277          SafeModeException.class,
278          DSQuotaExceededException.class,
279          NSQuotaExceededException.class);
280    }
281    return null;
282  }
283
284  /**
285   * Return a URL pointing to given path on the namenode.
286   *
287   * @param path to obtain the URL for
288   * @param query string to append to the path
289   * @return namenode URL referring to the given path
290   * @throws IOException on error constructing the URL
291   */
292  private URL getNamenodeURL(String path, String query) throws IOException {
293    final URL url = new URL("http", nnAddr.getHostName(),
294          nnAddr.getPort(), path + '?' + query);
295    if (LOG.isTraceEnabled()) {
296      LOG.trace("url=" + url);
297    }
298    return url;
299  }
300  
301  Param<?,?>[] getAuthParameters(final HttpOpParam.Op op) throws IOException {
302    List<Param<?,?>> authParams = Lists.newArrayList();    
303    // Skip adding delegation token for token operations because these
304    // operations require authentication.
305    Token<?> token = null;
306    if (UserGroupInformation.isSecurityEnabled() && !op.getRequireAuth()) {
307      token = getDelegationToken();
308    }
309    if (token != null) {
310      authParams.add(new DelegationParam(token.encodeToUrlString()));
311    } else {
312      UserGroupInformation userUgi = ugi;
313      UserGroupInformation realUgi = userUgi.getRealUser();
314      if (realUgi != null) { // proxy user
315        authParams.add(new DoAsParam(userUgi.getShortUserName()));
316        userUgi = realUgi;
317      }
318      authParams.add(new UserParam(userUgi.getShortUserName()));
319    }
320    return authParams.toArray(new Param<?,?>[0]);
321  }
322
323  URL toUrl(final HttpOpParam.Op op, final Path fspath,
324      final Param<?,?>... parameters) throws IOException {
325    //initialize URI path and query
326    final String path = PATH_PREFIX
327        + (fspath == null? "/": makeQualified(fspath).toUri().getRawPath());
328    final String query = op.toQueryString()
329        + Param.toSortedString("&", getAuthParameters(op))
330        + Param.toSortedString("&", parameters);
331    final URL url = getNamenodeURL(path, query);
332    if (LOG.isTraceEnabled()) {
333      LOG.trace("url=" + url);
334    }
335    return url;
336  }
337
338  private HttpURLConnection getHttpUrlConnection(final URL url,
339                                                 final boolean requireAuth)
340      throws IOException {
341    UserGroupInformation connectUgi = ugi.getRealUser();
342    if (connectUgi == null) {
343      connectUgi = ugi;
344    }
345    try {
346      return connectUgi.doAs(
347          new PrivilegedExceptionAction<HttpURLConnection>() {
348            @Override
349            public HttpURLConnection run() throws IOException {
350              return openHttpUrlConnection(url, requireAuth);
351            }
352          });
353    } catch (InterruptedException e) {
354      throw new IOException(e);
355    }
356  }
357
358  private HttpURLConnection openHttpUrlConnection(URL url, boolean requireAuth)
359      throws IOException {
360    final HttpURLConnection conn;
361    try {
362      if (requireAuth) {
363        LOG.debug("open AuthenticatedURL connection");
364        UserGroupInformation.getCurrentUser().checkTGTAndReloginFromKeytab();
365        final AuthenticatedURL.Token authToken = new AuthenticatedURL.Token();
366        conn = new AuthenticatedURL(AUTH).openConnection(url, authToken);
367      } else {
368        LOG.debug("open URL connection");
369        conn = (HttpURLConnection)url.openConnection();
370      }
371    } catch (AuthenticationException e) {
372      throw new IOException(e);
373    }
374    return conn;
375  }
376  
377  private HttpURLConnection httpConnect(final HttpOpParam.Op op, final Path fspath,
378      final Param<?,?>... parameters) throws IOException {
379    final URL url = toUrl(op, fspath, parameters);
380
381    //connect and get response
382    HttpURLConnection conn = getHttpUrlConnection(url, op.getRequireAuth());
383    try {
384      conn.setRequestMethod(op.getType().toString());
385      if (op.getDoOutput()) {
386        conn = twoStepWrite(conn, op);
387        conn.setRequestProperty("Content-Type", "application/octet-stream");
388      }
389      conn.setDoOutput(op.getDoOutput());
390      conn.connect();
391      return conn;
392    } catch (IOException e) {
393      conn.disconnect();
394      throw e;
395    }
396  }
397  
398  /**
399   * Two-step Create/Append:
400   * Step 1) Submit a Http request with neither auto-redirect nor data. 
401   * Step 2) Submit another Http request with the URL from the Location header with data.
402   * 
403   * The reason of having two-step create/append is for preventing clients to
404   * send out the data before the redirect. This issue is addressed by the
405   * "Expect: 100-continue" header in HTTP/1.1; see RFC 2616, Section 8.2.3.
406   * Unfortunately, there are software library bugs (e.g. Jetty 6 http server
407   * and Java 6 http client), which do not correctly implement "Expect:
408   * 100-continue". The two-step create/append is a temporary workaround for
409   * the software library bugs.
410   */
411  static HttpURLConnection twoStepWrite(HttpURLConnection conn,
412      final HttpOpParam.Op op) throws IOException {
413    //Step 1) Submit a Http request with neither auto-redirect nor data. 
414    conn.setInstanceFollowRedirects(false);
415    conn.setDoOutput(false);
416    conn.connect();
417    validateResponse(HttpOpParam.TemporaryRedirectOp.valueOf(op), conn);
418    final String redirect = conn.getHeaderField("Location");
419    conn.disconnect();
420
421    //Step 2) Submit another Http request with the URL from the Location header with data.
422    conn = (HttpURLConnection)new URL(redirect).openConnection();
423    conn.setRequestMethod(op.getType().toString());
424    conn.setChunkedStreamingMode(32 << 10); //32kB-chunk
425    return conn;
426  }
427
428  /**
429   * Run a http operation.
430   * Connect to the http server, validate response, and obtain the JSON output.
431   * 
432   * @param op http operation
433   * @param fspath file system path
434   * @param parameters parameters for the operation
435   * @return a JSON object, e.g. Object[], Map<?, ?>, etc.
436   * @throws IOException
437   */
438  private Map<?, ?> run(final HttpOpParam.Op op, final Path fspath,
439      final Param<?,?>... parameters) throws IOException {
440    final HttpURLConnection conn = httpConnect(op, fspath, parameters);
441    try {
442      final Map<?, ?> m = validateResponse(op, conn);
443      return m != null? m: jsonParse(conn, false);
444    } finally {
445      conn.disconnect();
446    }
447  }
448
449  private FsPermission applyUMask(FsPermission permission) {
450    if (permission == null) {
451      permission = FsPermission.getDefault();
452    }
453    return permission.applyUMask(FsPermission.getUMask(getConf()));
454  }
455
456  private HdfsFileStatus getHdfsFileStatus(Path f) throws IOException {
457    final HttpOpParam.Op op = GetOpParam.Op.GETFILESTATUS;
458    final Map<?, ?> json = run(op, f);
459    final HdfsFileStatus status = JsonUtil.toFileStatus(json, true);
460    if (status == null) {
461      throw new FileNotFoundException("File does not exist: " + f);
462    }
463    return status;
464  }
465
466  @Override
467  public FileStatus getFileStatus(Path f) throws IOException {
468    statistics.incrementReadOps(1);
469    return makeQualified(getHdfsFileStatus(f), f);
470  }
471
472  private FileStatus makeQualified(HdfsFileStatus f, Path parent) {
473    return new FileStatus(f.getLen(), f.isDir(), f.getReplication(),
474        f.getBlockSize(), f.getModificationTime(), f.getAccessTime(),
475        f.getPermission(), f.getOwner(), f.getGroup(),
476        f.isSymlink() ? new Path(f.getSymlink()) : null,
477        f.getFullPath(parent).makeQualified(getUri(), getWorkingDirectory()));
478  }
479
480  @Override
481  public boolean mkdirs(Path f, FsPermission permission) throws IOException {
482    statistics.incrementWriteOps(1);
483    final HttpOpParam.Op op = PutOpParam.Op.MKDIRS;
484    final Map<?, ?> json = run(op, f,
485        new PermissionParam(applyUMask(permission)));
486    return (Boolean)json.get("boolean");
487  }
488
489  /**
490   * Create a symlink pointing to the destination path.
491   * @see org.apache.hadoop.fs.Hdfs#createSymlink(Path, Path, boolean) 
492   */
493  public void createSymlink(Path destination, Path f, boolean createParent
494      ) throws IOException {
495    statistics.incrementWriteOps(1);
496    final HttpOpParam.Op op = PutOpParam.Op.CREATESYMLINK;
497    run(op, f, new DestinationParam(makeQualified(destination).toUri().getPath()),
498        new CreateParentParam(createParent));
499  }
500
501  @Override
502  public boolean rename(final Path src, final Path dst) throws IOException {
503    statistics.incrementWriteOps(1);
504    final HttpOpParam.Op op = PutOpParam.Op.RENAME;
505    final Map<?, ?> json = run(op, src,
506        new DestinationParam(makeQualified(dst).toUri().getPath()));
507    return (Boolean)json.get("boolean");
508  }
509
510  @SuppressWarnings("deprecation")
511  @Override
512  public void rename(final Path src, final Path dst,
513      final Options.Rename... options) throws IOException {
514    statistics.incrementWriteOps(1);
515    final HttpOpParam.Op op = PutOpParam.Op.RENAME;
516    run(op, src, new DestinationParam(makeQualified(dst).toUri().getPath()),
517        new RenameOptionSetParam(options));
518  }
519
520  @Override
521  public void setOwner(final Path p, final String owner, final String group
522      ) throws IOException {
523    if (owner == null && group == null) {
524      throw new IOException("owner == null && group == null");
525    }
526
527    statistics.incrementWriteOps(1);
528    final HttpOpParam.Op op = PutOpParam.Op.SETOWNER;
529    run(op, p, new OwnerParam(owner), new GroupParam(group));
530  }
531
532  @Override
533  public void setPermission(final Path p, final FsPermission permission
534      ) throws IOException {
535    statistics.incrementWriteOps(1);
536    final HttpOpParam.Op op = PutOpParam.Op.SETPERMISSION;
537    run(op, p, new PermissionParam(permission));
538  }
539
540  @Override
541  public boolean setReplication(final Path p, final short replication
542     ) throws IOException {
543    statistics.incrementWriteOps(1);
544    final HttpOpParam.Op op = PutOpParam.Op.SETREPLICATION;
545    final Map<?, ?> json = run(op, p, new ReplicationParam(replication));
546    return (Boolean)json.get("boolean");
547  }
548
549  @Override
550  public void setTimes(final Path p, final long mtime, final long atime
551      ) throws IOException {
552    statistics.incrementWriteOps(1);
553    final HttpOpParam.Op op = PutOpParam.Op.SETTIMES;
554    run(op, p, new ModificationTimeParam(mtime), new AccessTimeParam(atime));
555  }
556
557  @Override
558  public long getDefaultBlockSize() {
559    return getConf().getLongBytes(DFSConfigKeys.DFS_BLOCK_SIZE_KEY,
560        DFSConfigKeys.DFS_BLOCK_SIZE_DEFAULT);
561  }
562
563  @Override
564  public short getDefaultReplication() {
565    return (short)getConf().getInt(DFSConfigKeys.DFS_REPLICATION_KEY,
566        DFSConfigKeys.DFS_REPLICATION_DEFAULT);
567  }
568
569  FSDataOutputStream write(final HttpOpParam.Op op,
570      final HttpURLConnection conn, final int bufferSize) throws IOException {
571    return new FSDataOutputStream(new BufferedOutputStream(
572        conn.getOutputStream(), bufferSize), statistics) {
573      @Override
574      public void close() throws IOException {
575        try {
576          super.close();
577        } finally {
578          try {
579            validateResponse(op, conn);
580          } finally {
581            conn.disconnect();
582          }
583        }
584      }
585    };
586  }
587
588  @Override
589  public FSDataOutputStream create(final Path f, final FsPermission permission,
590      final boolean overwrite, final int bufferSize, final short replication,
591      final long blockSize, final Progressable progress) throws IOException {
592    statistics.incrementWriteOps(1);
593
594    final HttpOpParam.Op op = PutOpParam.Op.CREATE;
595    final HttpURLConnection conn = httpConnect(op, f, 
596        new PermissionParam(applyUMask(permission)),
597        new OverwriteParam(overwrite),
598        new BufferSizeParam(bufferSize),
599        new ReplicationParam(replication),
600        new BlockSizeParam(blockSize));
601    return write(op, conn, bufferSize);
602  }
603
604  @Override
605  public FSDataOutputStream append(final Path f, final int bufferSize,
606      final Progressable progress) throws IOException {
607    statistics.incrementWriteOps(1);
608
609    final HttpOpParam.Op op = PostOpParam.Op.APPEND;
610    final HttpURLConnection conn = httpConnect(op, f, 
611        new BufferSizeParam(bufferSize));
612    return write(op, conn, bufferSize);
613  }
614
615  @SuppressWarnings("deprecation")
616  @Override
617  public boolean delete(final Path f) throws IOException {
618    return delete(f, true);
619  }
620
621  @Override
622  public boolean delete(Path f, boolean recursive) throws IOException {
623    final HttpOpParam.Op op = DeleteOpParam.Op.DELETE;
624    final Map<?, ?> json = run(op, f, new RecursiveParam(recursive));
625    return (Boolean)json.get("boolean");
626  }
627
628  @Override
629  public FSDataInputStream open(final Path f, final int buffersize
630      ) throws IOException {
631    statistics.incrementReadOps(1);
632    final HttpOpParam.Op op = GetOpParam.Op.OPEN;
633    final URL url = toUrl(op, f, new BufferSizeParam(buffersize));
634    return new FSDataInputStream(new OffsetUrlInputStream(
635        new OffsetUrlOpener(url), new OffsetUrlOpener(null)));
636  }
637
638  class OffsetUrlOpener extends ByteRangeInputStream.URLOpener {
639    /** The url with offset parameter */
640    private URL offsetUrl;
641  
642    OffsetUrlOpener(final URL url) {
643      super(url);
644    }
645
646    /** Open connection with offset url. */
647    @Override
648    protected HttpURLConnection openConnection() throws IOException {
649      return getHttpUrlConnection(offsetUrl, false);
650    }
651
652    /** Setup offset url before open connection. */
653    @Override
654    protected HttpURLConnection openConnection(final long offset) throws IOException {
655      offsetUrl = offset == 0L? url: new URL(url + "&" + new OffsetParam(offset));
656      final HttpURLConnection conn = openConnection();
657      conn.setRequestMethod("GET");
658      return conn;
659    }  
660  }
661
662  private static final String OFFSET_PARAM_PREFIX = OffsetParam.NAME + "=";
663
664  /** Remove offset parameter, if there is any, from the url */
665  static URL removeOffsetParam(final URL url) throws MalformedURLException {
666    String query = url.getQuery();
667    if (query == null) {
668      return url;
669    }
670    final String lower = query.toLowerCase();
671    if (!lower.startsWith(OFFSET_PARAM_PREFIX)
672        && !lower.contains("&" + OFFSET_PARAM_PREFIX)) {
673      return url;
674    }
675
676    //rebuild query
677    StringBuilder b = null;
678    for(final StringTokenizer st = new StringTokenizer(query, "&");
679        st.hasMoreTokens();) {
680      final String token = st.nextToken();
681      if (!token.toLowerCase().startsWith(OFFSET_PARAM_PREFIX)) {
682        if (b == null) {
683          b = new StringBuilder("?").append(token);
684        } else {
685          b.append('&').append(token);
686        }
687      }
688    }
689    query = b == null? "": b.toString();
690
691    final String urlStr = url.toString();
692    return new URL(urlStr.substring(0, urlStr.indexOf('?')) + query);
693  }
694
695  static class OffsetUrlInputStream extends ByteRangeInputStream {
696    OffsetUrlInputStream(OffsetUrlOpener o, OffsetUrlOpener r) {
697      super(o, r);
698    }
699    
700    @Override
701    protected void checkResponseCode(final HttpURLConnection connection
702        ) throws IOException {
703      validateResponse(GetOpParam.Op.OPEN, connection);
704    }
705
706    /** Remove offset parameter before returning the resolved url. */
707    @Override
708    protected URL getResolvedUrl(final HttpURLConnection connection
709        ) throws MalformedURLException {
710      return removeOffsetParam(connection.getURL());
711    }
712  }
713
714  @Override
715  public FileStatus[] listStatus(final Path f) throws IOException {
716    statistics.incrementReadOps(1);
717
718    final HttpOpParam.Op op = GetOpParam.Op.LISTSTATUS;
719    final Map<?, ?> json  = run(op, f);
720    final Map<?, ?> rootmap = (Map<?, ?>)json.get(FileStatus.class.getSimpleName() + "es");
721    final Object[] array = (Object[])rootmap.get(FileStatus.class.getSimpleName());
722
723    //convert FileStatus
724    final FileStatus[] statuses = new FileStatus[array.length];
725    for(int i = 0; i < array.length; i++) {
726      final Map<?, ?> m = (Map<?, ?>)array[i];
727      statuses[i] = makeQualified(JsonUtil.toFileStatus(m, false), f);
728    }
729    return statuses;
730  }
731
732  @Override
733  public Token<DelegationTokenIdentifier> getDelegationToken(
734      final String renewer) throws IOException {
735    final HttpOpParam.Op op = GetOpParam.Op.GETDELEGATIONTOKEN;
736    final Map<?, ?> m = run(op, null, new RenewerParam(renewer));
737    final Token<DelegationTokenIdentifier> token = JsonUtil.toDelegationToken(m); 
738    SecurityUtil.setTokenService(token, nnAddr);
739    return token;
740  }
741
742  @Override
743  public Token<?> getRenewToken() {
744    return delegationToken;
745  }
746
747  @Override
748  public <T extends TokenIdentifier> void setDelegationToken(
749      final Token<T> token) {
750    synchronized(this) {
751      delegationToken = token;
752    }
753  }
754
755  private synchronized long renewDelegationToken(final Token<?> token
756      ) throws IOException {
757    final HttpOpParam.Op op = PutOpParam.Op.RENEWDELEGATIONTOKEN;
758    TokenArgumentParam dtargParam = new TokenArgumentParam(
759        token.encodeToUrlString());
760    final Map<?, ?> m = run(op, null, dtargParam);
761    return (Long) m.get("long");
762  }
763
764  private synchronized void cancelDelegationToken(final Token<?> token
765      ) throws IOException {
766    final HttpOpParam.Op op = PutOpParam.Op.CANCELDELEGATIONTOKEN;
767    TokenArgumentParam dtargParam = new TokenArgumentParam(
768        token.encodeToUrlString());
769    run(op, null, dtargParam);
770  }
771  
772  @Override
773  public BlockLocation[] getFileBlockLocations(final FileStatus status,
774      final long offset, final long length) throws IOException {
775    if (status == null) {
776      return null;
777    }
778    return getFileBlockLocations(status.getPath(), offset, length);
779  }
780
781  @Override
782  public BlockLocation[] getFileBlockLocations(final Path p, 
783      final long offset, final long length) throws IOException {
784    statistics.incrementReadOps(1);
785
786    final HttpOpParam.Op op = GetOpParam.Op.GET_BLOCK_LOCATIONS;
787    final Map<?, ?> m = run(op, p, new OffsetParam(offset),
788        new LengthParam(length));
789    return DFSUtil.locatedBlocks2Locations(JsonUtil.toLocatedBlocks(m));
790  }
791
792  @Override
793  public ContentSummary getContentSummary(final Path p) throws IOException {
794    statistics.incrementReadOps(1);
795
796    final HttpOpParam.Op op = GetOpParam.Op.GETCONTENTSUMMARY;
797    final Map<?, ?> m = run(op, p);
798    return JsonUtil.toContentSummary(m);
799  }
800
801  @Override
802  public MD5MD5CRC32FileChecksum getFileChecksum(final Path p
803      ) throws IOException {
804    statistics.incrementReadOps(1);
805  
806    final HttpOpParam.Op op = GetOpParam.Op.GETFILECHECKSUM;
807    final Map<?, ?> m = run(op, p);
808    return JsonUtil.toMD5MD5CRC32FileChecksum(m);
809  }
810
811  /** Delegation token renewer. */
812  public static class DtRenewer extends TokenRenewer {
813    @Override
814    public boolean handleKind(Text kind) {
815      return kind.equals(TOKEN_KIND);
816    }
817  
818    @Override
819    public boolean isManaged(Token<?> token) throws IOException {
820      return true;
821    }
822
823    private static WebHdfsFileSystem getWebHdfs(
824        final Token<?> token, final Configuration conf) throws IOException {
825      
826      final InetSocketAddress nnAddr = SecurityUtil.getTokenServiceAddr(token);
827      final URI uri = DFSUtil.createUri(WebHdfsFileSystem.SCHEME, nnAddr);
828      return (WebHdfsFileSystem)FileSystem.get(uri, conf);
829    }
830
831    @Override
832    public long renew(final Token<?> token, final Configuration conf
833        ) throws IOException, InterruptedException {
834      return getWebHdfs(token, conf).renewDelegationToken(token);
835    }
836  
837    @Override
838    public void cancel(final Token<?> token, final Configuration conf
839        ) throws IOException, InterruptedException {
840      getWebHdfs(token, conf).cancelDelegationToken(token);
841    }
842  }
843  
844  private static class WebHdfsDelegationTokenSelector
845  extends AbstractDelegationTokenSelector<DelegationTokenIdentifier> {
846    private static final DelegationTokenSelector hdfsTokenSelector =
847        new DelegationTokenSelector();
848    
849    public WebHdfsDelegationTokenSelector() {
850      super(TOKEN_KIND);
851    }
852    
853    Token<DelegationTokenIdentifier> selectToken(URI nnUri,
854        Collection<Token<?>> tokens, Configuration conf) {
855      Token<DelegationTokenIdentifier> token =
856          selectToken(SecurityUtil.buildTokenService(nnUri), tokens);
857      if (token == null) {
858        token = hdfsTokenSelector.selectToken(nnUri, tokens, conf); 
859      }
860      return token;
861    }
862  }
863}