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