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