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