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 */
018package org.apache.hadoop.fs.http.client;
019
020import java.util.ArrayList;
021import java.util.EnumSet;
022import java.util.List;
023
024import com.google.common.base.Charsets;
025import org.apache.hadoop.classification.InterfaceAudience;
026import org.apache.hadoop.conf.Configuration;
027import org.apache.hadoop.fs.ContentSummary;
028import org.apache.hadoop.fs.DelegationTokenRenewer;
029import org.apache.hadoop.fs.FSDataInputStream;
030import org.apache.hadoop.fs.FSDataOutputStream;
031import org.apache.hadoop.fs.FileChecksum;
032import org.apache.hadoop.fs.FileStatus;
033import org.apache.hadoop.fs.FileSystem;
034import org.apache.hadoop.fs.Path;
035import org.apache.hadoop.fs.PositionedReadable;
036import org.apache.hadoop.fs.Seekable;
037import org.apache.hadoop.fs.XAttrCodec;
038import org.apache.hadoop.fs.XAttrSetFlag;
039import org.apache.hadoop.fs.permission.AclEntry;
040import org.apache.hadoop.fs.permission.AclStatus;
041import org.apache.hadoop.fs.permission.FsPermission;
042import org.apache.hadoop.hdfs.DFSConfigKeys;
043import org.apache.hadoop.hdfs.protocol.FsPermissionExtension;
044import org.apache.hadoop.lib.wsrs.EnumSetParam;
045import org.apache.hadoop.security.UserGroupInformation;
046import org.apache.hadoop.security.token.Token;
047import org.apache.hadoop.security.token.TokenIdentifier;
048import org.apache.hadoop.security.token.delegation.web.DelegationTokenAuthenticatedURL;
049import org.apache.hadoop.security.token.delegation.web.DelegationTokenAuthenticator;
050import org.apache.hadoop.security.token.delegation.web.KerberosDelegationTokenAuthenticator;
051import org.apache.hadoop.util.HttpExceptionUtils;
052import org.apache.hadoop.util.Progressable;
053import org.apache.hadoop.util.ReflectionUtils;
054import org.apache.hadoop.util.StringUtils;
055import org.json.simple.JSONArray;
056import org.json.simple.JSONObject;
057import org.json.simple.parser.JSONParser;
058import org.json.simple.parser.ParseException;
059
060import com.google.common.base.Preconditions;
061import com.google.common.collect.Lists;
062import com.google.common.collect.Maps;
063
064import java.io.BufferedInputStream;
065import java.io.BufferedOutputStream;
066import java.io.DataInput;
067import java.io.DataOutput;
068import java.io.FileNotFoundException;
069import java.io.FilterInputStream;
070import java.io.IOException;
071import java.io.InputStream;
072import java.io.OutputStream;
073import java.net.HttpURLConnection;
074import java.net.URI;
075import java.net.URISyntaxException;
076import java.net.URL;
077import java.security.PrivilegedExceptionAction;
078import java.text.MessageFormat;
079import java.util.HashMap;
080import java.util.Map;
081
082/**
083 * HttpFSServer implementation of the FileSystemAccess FileSystem.
084 * <p/>
085 * This implementation allows a user to access HDFS over HTTP via a HttpFSServer server.
086 */
087@InterfaceAudience.Private
088public class HttpFSFileSystem extends FileSystem
089  implements DelegationTokenRenewer.Renewable {
090
091  public static final String SERVICE_NAME = HttpFSUtils.SERVICE_NAME;
092
093  public static final String SERVICE_VERSION = HttpFSUtils.SERVICE_VERSION;
094
095  public static final String SCHEME = "webhdfs";
096
097  public static final String OP_PARAM = "op";
098  public static final String DO_AS_PARAM = "doas";
099  public static final String OVERWRITE_PARAM = "overwrite";
100  public static final String REPLICATION_PARAM = "replication";
101  public static final String BLOCKSIZE_PARAM = "blocksize";
102  public static final String PERMISSION_PARAM = "permission";
103  public static final String ACLSPEC_PARAM = "aclspec";
104  public static final String DESTINATION_PARAM = "destination";
105  public static final String RECURSIVE_PARAM = "recursive";
106  public static final String SOURCES_PARAM = "sources";
107  public static final String OWNER_PARAM = "owner";
108  public static final String GROUP_PARAM = "group";
109  public static final String MODIFICATION_TIME_PARAM = "modificationtime";
110  public static final String ACCESS_TIME_PARAM = "accesstime";
111  public static final String XATTR_NAME_PARAM = "xattr.name";
112  public static final String XATTR_VALUE_PARAM = "xattr.value";
113  public static final String XATTR_SET_FLAG_PARAM = "flag";
114  public static final String XATTR_ENCODING_PARAM = "encoding";
115  public static final String START_AFTER_PARAM = "startAfter";
116
117  public static final Short DEFAULT_PERMISSION = 0755;
118  public static final String ACLSPEC_DEFAULT = "";
119
120  public static final String RENAME_JSON = "boolean";
121
122  public static final String DELETE_JSON = "boolean";
123
124  public static final String MKDIRS_JSON = "boolean";
125
126  public static final String HOME_DIR_JSON = "Path";
127
128  public static final String TRASH_DIR_JSON = "Path";
129
130  public static final String SET_REPLICATION_JSON = "boolean";
131
132  public static final String UPLOAD_CONTENT_TYPE= "application/octet-stream";
133
134  public static enum FILE_TYPE {
135    FILE, DIRECTORY, SYMLINK;
136
137    public static FILE_TYPE getType(FileStatus fileStatus) {
138      if (fileStatus.isFile()) {
139        return FILE;
140      }
141      if (fileStatus.isDirectory()) {
142        return DIRECTORY;
143      }
144      if (fileStatus.isSymlink()) {
145        return SYMLINK;
146      }
147      throw new IllegalArgumentException("Could not determine filetype for: " +
148                                         fileStatus.getPath());
149    }
150  }
151
152  public static final String FILE_STATUSES_JSON = "FileStatuses";
153  public static final String FILE_STATUS_JSON = "FileStatus";
154  public static final String PATH_SUFFIX_JSON = "pathSuffix";
155  public static final String TYPE_JSON = "type";
156  public static final String LENGTH_JSON = "length";
157  public static final String OWNER_JSON = "owner";
158  public static final String GROUP_JSON = "group";
159  public static final String PERMISSION_JSON = "permission";
160  public static final String ACCESS_TIME_JSON = "accessTime";
161  public static final String MODIFICATION_TIME_JSON = "modificationTime";
162  public static final String BLOCK_SIZE_JSON = "blockSize";
163  public static final String REPLICATION_JSON = "replication";
164  public static final String XATTRS_JSON = "XAttrs";
165  public static final String XATTR_NAME_JSON = "name";
166  public static final String XATTR_VALUE_JSON = "value";
167  public static final String XATTRNAMES_JSON = "XAttrNames";
168
169  public static final String FILE_CHECKSUM_JSON = "FileChecksum";
170  public static final String CHECKSUM_ALGORITHM_JSON = "algorithm";
171  public static final String CHECKSUM_BYTES_JSON = "bytes";
172  public static final String CHECKSUM_LENGTH_JSON = "length";
173
174  public static final String CONTENT_SUMMARY_JSON = "ContentSummary";
175  public static final String CONTENT_SUMMARY_DIRECTORY_COUNT_JSON = "directoryCount";
176  public static final String CONTENT_SUMMARY_FILE_COUNT_JSON = "fileCount";
177  public static final String CONTENT_SUMMARY_LENGTH_JSON = "length";
178  public static final String CONTENT_SUMMARY_QUOTA_JSON = "quota";
179  public static final String CONTENT_SUMMARY_SPACE_CONSUMED_JSON = "spaceConsumed";
180  public static final String CONTENT_SUMMARY_SPACE_QUOTA_JSON = "spaceQuota";
181
182  public static final String ACL_STATUS_JSON = "AclStatus";
183  public static final String ACL_STICKY_BIT_JSON = "stickyBit";
184  public static final String ACL_ENTRIES_JSON = "entries";
185  public static final String ACL_BIT_JSON = "aclBit";
186
187  public static final String ENC_BIT_JSON = "encBit";
188
189  public static final String DIRECTORY_LISTING_JSON = "DirectoryListing";
190  public static final String PARTIAL_LISTING_JSON = "partialListing";
191  public static final String REMAINING_ENTRIES_JSON = "remainingEntries";
192
193  public static final int HTTP_TEMPORARY_REDIRECT = 307;
194
195  private static final String HTTP_GET = "GET";
196  private static final String HTTP_PUT = "PUT";
197  private static final String HTTP_POST = "POST";
198  private static final String HTTP_DELETE = "DELETE";
199
200  @InterfaceAudience.Private
201  public static enum Operation {
202    OPEN(HTTP_GET), GETFILESTATUS(HTTP_GET), LISTSTATUS(HTTP_GET),
203    GETHOMEDIRECTORY(HTTP_GET), GETCONTENTSUMMARY(HTTP_GET),
204    GETFILECHECKSUM(HTTP_GET),  GETFILEBLOCKLOCATIONS(HTTP_GET),
205    INSTRUMENTATION(HTTP_GET), GETACLSTATUS(HTTP_GET), GETTRASHROOT(HTTP_GET),
206    APPEND(HTTP_POST), CONCAT(HTTP_POST),
207    CREATE(HTTP_PUT), MKDIRS(HTTP_PUT), RENAME(HTTP_PUT), SETOWNER(HTTP_PUT),
208    SETPERMISSION(HTTP_PUT), SETREPLICATION(HTTP_PUT), SETTIMES(HTTP_PUT),
209    MODIFYACLENTRIES(HTTP_PUT), REMOVEACLENTRIES(HTTP_PUT),
210    REMOVEDEFAULTACL(HTTP_PUT), REMOVEACL(HTTP_PUT), SETACL(HTTP_PUT),
211    DELETE(HTTP_DELETE), SETXATTR(HTTP_PUT), GETXATTRS(HTTP_GET),
212    REMOVEXATTR(HTTP_PUT), LISTXATTRS(HTTP_GET), LISTSTATUS_BATCH(HTTP_GET);
213
214    private String httpMethod;
215
216    Operation(String httpMethod) {
217      this.httpMethod = httpMethod;
218    }
219
220    public String getMethod() {
221      return httpMethod;
222    }
223
224  }
225
226  private DelegationTokenAuthenticatedURL authURL;
227  private DelegationTokenAuthenticatedURL.Token authToken =
228      new DelegationTokenAuthenticatedURL.Token();
229  private URI uri;
230  private Path workingDir;
231  private UserGroupInformation realUser;
232
233
234
235  /**
236   * Convenience method that creates a <code>HttpURLConnection</code> for the
237   * HttpFSServer file system operations.
238   * <p/>
239   * This methods performs and injects any needed authentication credentials
240   * via the {@link #getConnection(URL, String)} method
241   *
242   * @param method the HTTP method.
243   * @param params the query string parameters.
244   * @param path the file path
245   * @param makeQualified if the path should be 'makeQualified'
246   *
247   * @return a <code>HttpURLConnection</code> for the HttpFSServer server,
248   *         authenticated and ready to use for the specified path and file system operation.
249   *
250   * @throws IOException thrown if an IO error occurrs.
251   */
252  private HttpURLConnection getConnection(final String method,
253      Map<String, String> params, Path path, boolean makeQualified)
254      throws IOException {
255    return getConnection(method, params, null, path, makeQualified);
256  }
257
258  /**
259   * Convenience method that creates a <code>HttpURLConnection</code> for the
260   * HttpFSServer file system operations.
261   * <p/>
262   * This methods performs and injects any needed authentication credentials
263   * via the {@link #getConnection(URL, String)} method
264   *
265   * @param method the HTTP method.
266   * @param params the query string parameters.
267   * @param multiValuedParams multi valued parameters of the query string
268   * @param path the file path
269   * @param makeQualified if the path should be 'makeQualified'
270   *
271   * @return HttpURLConnection a <code>HttpURLConnection</code> for the
272   *         HttpFSServer server, authenticated and ready to use for the
273   *         specified path and file system operation.
274   *
275   * @throws IOException thrown if an IO error occurrs.
276   */
277  private HttpURLConnection getConnection(final String method,
278      Map<String, String> params, Map<String, List<String>> multiValuedParams,
279      Path path, boolean makeQualified) throws IOException {
280    if (makeQualified) {
281      path = makeQualified(path);
282    }
283    final URL url = HttpFSUtils.createURL(path, params, multiValuedParams);
284    try {
285      return UserGroupInformation.getCurrentUser().doAs(
286          new PrivilegedExceptionAction<HttpURLConnection>() {
287            @Override
288            public HttpURLConnection run() throws Exception {
289              return getConnection(url, method);
290            }
291          }
292      );
293    } catch (Exception ex) {
294      if (ex instanceof IOException) {
295        throw (IOException) ex;
296      } else {
297        throw new IOException(ex);
298      }
299    }
300  }
301
302  /**
303   * Convenience method that creates a <code>HttpURLConnection</code> for the specified URL.
304   * <p/>
305   * This methods performs and injects any needed authentication credentials.
306   *
307   * @param url url to connect to.
308   * @param method the HTTP method.
309   *
310   * @return a <code>HttpURLConnection</code> for the HttpFSServer server, authenticated and ready to use for
311   *         the specified path and file system operation.
312   *
313   * @throws IOException thrown if an IO error occurrs.
314   */
315  private HttpURLConnection getConnection(URL url, String method) throws IOException {
316    try {
317      HttpURLConnection conn = authURL.openConnection(url, authToken);
318      conn.setRequestMethod(method);
319      if (method.equals(HTTP_POST) || method.equals(HTTP_PUT)) {
320        conn.setDoOutput(true);
321      }
322      return conn;
323    } catch (Exception ex) {
324      throw new IOException(ex);
325    }
326  }
327
328  /**
329   * Called after a new FileSystem instance is constructed.
330   *
331   * @param name a uri whose authority section names the host, port, etc. for this FileSystem
332   * @param conf the configuration
333   */
334  @Override
335  public void initialize(URI name, Configuration conf) throws IOException {
336    UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
337
338    //the real use is the one that has the Kerberos credentials needed for
339    //SPNEGO to work
340    realUser = ugi.getRealUser();
341    if (realUser == null) {
342      realUser = UserGroupInformation.getLoginUser();
343    }
344    super.initialize(name, conf);
345    try {
346      uri = new URI(name.getScheme() + "://" + name.getAuthority());
347    } catch (URISyntaxException ex) {
348      throw new IOException(ex);
349    }
350
351    Class<? extends DelegationTokenAuthenticator> klass =
352        getConf().getClass("httpfs.authenticator.class",
353            KerberosDelegationTokenAuthenticator.class,
354            DelegationTokenAuthenticator.class);
355    DelegationTokenAuthenticator authenticator =
356        ReflectionUtils.newInstance(klass, getConf());
357    authURL = new DelegationTokenAuthenticatedURL(authenticator);
358  }
359
360  @Override
361  public String getScheme() {
362    return SCHEME;
363  }
364
365  /**
366   * Returns a URI whose scheme and authority identify this FileSystem.
367   *
368   * @return the URI whose scheme and authority identify this FileSystem.
369   */
370  @Override
371  public URI getUri() {
372    return uri;
373  }
374
375  /**
376   * Get the default port for this file system.
377   * @return the default port or 0 if there isn't one
378   */
379  @Override
380  protected int getDefaultPort() {
381    return getConf().getInt(DFSConfigKeys.DFS_NAMENODE_HTTP_PORT_KEY,
382        DFSConfigKeys.DFS_NAMENODE_HTTP_PORT_DEFAULT);
383  }
384
385  /**
386   * HttpFSServer subclass of the <code>FSDataInputStream</code>.
387   * <p/>
388   * This implementation does not support the
389   * <code>PositionReadable</code> and <code>Seekable</code> methods.
390   */
391  private static class HttpFSDataInputStream extends FilterInputStream implements Seekable, PositionedReadable {
392
393    protected HttpFSDataInputStream(InputStream in, int bufferSize) {
394      super(new BufferedInputStream(in, bufferSize));
395    }
396
397    @Override
398    public int read(long position, byte[] buffer, int offset, int length) throws IOException {
399      throw new UnsupportedOperationException();
400    }
401
402    @Override
403    public void readFully(long position, byte[] buffer, int offset, int length) throws IOException {
404      throw new UnsupportedOperationException();
405    }
406
407    @Override
408    public void readFully(long position, byte[] buffer) throws IOException {
409      throw new UnsupportedOperationException();
410    }
411
412    @Override
413    public void seek(long pos) throws IOException {
414      throw new UnsupportedOperationException();
415    }
416
417    @Override
418    public long getPos() throws IOException {
419      throw new UnsupportedOperationException();
420    }
421
422    @Override
423    public boolean seekToNewSource(long targetPos) throws IOException {
424      throw new UnsupportedOperationException();
425    }
426  }
427
428  /**
429   * Opens an FSDataInputStream at the indicated Path.
430   * </p>
431   * IMPORTANT: the returned <code><FSDataInputStream/code> does not support the
432   * <code>PositionReadable</code> and <code>Seekable</code> methods.
433   *
434   * @param f the file name to open
435   * @param bufferSize the size of the buffer to be used.
436   */
437  @Override
438  public FSDataInputStream open(Path f, int bufferSize) throws IOException {
439    Map<String, String> params = new HashMap<String, String>();
440    params.put(OP_PARAM, Operation.OPEN.toString());
441    HttpURLConnection conn = getConnection(Operation.OPEN.getMethod(), params,
442                                           f, true);
443    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
444    return new FSDataInputStream(
445      new HttpFSDataInputStream(conn.getInputStream(), bufferSize));
446  }
447
448  /**
449   * HttpFSServer subclass of the <code>FSDataOutputStream</code>.
450   * <p/>
451   * This implementation closes the underlying HTTP connection validating the Http connection status
452   * at closing time.
453   */
454  private static class HttpFSDataOutputStream extends FSDataOutputStream {
455    private HttpURLConnection conn;
456    private int closeStatus;
457
458    public HttpFSDataOutputStream(HttpURLConnection conn, OutputStream out, int closeStatus, Statistics stats)
459      throws IOException {
460      super(out, stats);
461      this.conn = conn;
462      this.closeStatus = closeStatus;
463    }
464
465    @Override
466    public void close() throws IOException {
467      try {
468        super.close();
469      } finally {
470        HttpExceptionUtils.validateResponse(conn, closeStatus);
471      }
472    }
473
474  }
475
476  /**
477   * Converts a <code>FsPermission</code> to a Unix octal representation.
478   *
479   * @param p the permission.
480   *
481   * @return the Unix string symbolic reprentation.
482   */
483  public static String permissionToString(FsPermission p) {
484    return  Integer.toString((p == null) ? DEFAULT_PERMISSION : p.toShort(), 8);
485  }
486
487  /*
488   * Common handling for uploading data for create and append operations.
489   */
490  private FSDataOutputStream uploadData(String method, Path f, Map<String, String> params,
491                                        int bufferSize, int expectedStatus) throws IOException {
492    HttpURLConnection conn = getConnection(method, params, f, true);
493    conn.setInstanceFollowRedirects(false);
494    boolean exceptionAlreadyHandled = false;
495    try {
496      if (conn.getResponseCode() == HTTP_TEMPORARY_REDIRECT) {
497        exceptionAlreadyHandled = true;
498        String location = conn.getHeaderField("Location");
499        if (location != null) {
500          conn = getConnection(new URL(location), method);
501          conn.setRequestProperty("Content-Type", UPLOAD_CONTENT_TYPE);
502          try {
503            OutputStream os = new BufferedOutputStream(conn.getOutputStream(), bufferSize);
504            return new HttpFSDataOutputStream(conn, os, expectedStatus, statistics);
505          } catch (IOException ex) {
506            HttpExceptionUtils.validateResponse(conn, expectedStatus);
507            throw ex;
508          }
509        } else {
510          HttpExceptionUtils.validateResponse(conn, HTTP_TEMPORARY_REDIRECT);
511          throw new IOException("Missing HTTP 'Location' header for [" + conn.getURL() + "]");
512        }
513      } else {
514        throw new IOException(
515          MessageFormat.format("Expected HTTP status was [307], received [{0}]",
516                               conn.getResponseCode()));
517      }
518    } catch (IOException ex) {
519      if (exceptionAlreadyHandled) {
520        throw ex;
521      } else {
522        HttpExceptionUtils.validateResponse(conn, HTTP_TEMPORARY_REDIRECT);
523        throw ex;
524      }
525    }
526  }
527
528
529  /**
530   * Opens an FSDataOutputStream at the indicated Path with write-progress
531   * reporting.
532   * <p/>
533   * IMPORTANT: The <code>Progressable</code> parameter is not used.
534   *
535   * @param f the file name to open.
536   * @param permission file permission.
537   * @param overwrite if a file with this name already exists, then if true,
538   * the file will be overwritten, and if false an error will be thrown.
539   * @param bufferSize the size of the buffer to be used.
540   * @param replication required block replication for the file.
541   * @param blockSize block size.
542   * @param progress progressable.
543   *
544   * @throws IOException
545   * @see #setPermission(Path, FsPermission)
546   */
547  @Override
548  public FSDataOutputStream create(Path f, FsPermission permission,
549                                   boolean overwrite, int bufferSize,
550                                   short replication, long blockSize,
551                                   Progressable progress) throws IOException {
552    Map<String, String> params = new HashMap<String, String>();
553    params.put(OP_PARAM, Operation.CREATE.toString());
554    params.put(OVERWRITE_PARAM, Boolean.toString(overwrite));
555    params.put(REPLICATION_PARAM, Short.toString(replication));
556    params.put(BLOCKSIZE_PARAM, Long.toString(blockSize));
557    params.put(PERMISSION_PARAM, permissionToString(permission));
558    return uploadData(Operation.CREATE.getMethod(), f, params, bufferSize,
559                      HttpURLConnection.HTTP_CREATED);
560  }
561
562
563  /**
564   * Append to an existing file (optional operation).
565   * <p/>
566   * IMPORTANT: The <code>Progressable</code> parameter is not used.
567   *
568   * @param f the existing file to be appended.
569   * @param bufferSize the size of the buffer to be used.
570   * @param progress for reporting progress if it is not null.
571   *
572   * @throws IOException
573   */
574  @Override
575  public FSDataOutputStream append(Path f, int bufferSize,
576                                   Progressable progress) throws IOException {
577    Map<String, String> params = new HashMap<String, String>();
578    params.put(OP_PARAM, Operation.APPEND.toString());
579    return uploadData(Operation.APPEND.getMethod(), f, params, bufferSize,
580                      HttpURLConnection.HTTP_OK);
581  }
582
583  /**
584   * Concat existing files together.
585   * @param f the path to the target destination.
586   * @param psrcs the paths to the sources to use for the concatenation.
587   *
588   * @throws IOException
589   */
590  @Override
591  public void concat(Path f, Path[] psrcs) throws IOException {
592    List<String> strPaths = new ArrayList<String>(psrcs.length);
593    for(Path psrc : psrcs) {
594      strPaths.add(psrc.toUri().getPath());
595    }
596    String srcs = StringUtils.join(",", strPaths);
597
598    Map<String, String> params = new HashMap<String, String>();
599    params.put(OP_PARAM, Operation.CONCAT.toString());
600    params.put(SOURCES_PARAM, srcs);
601    HttpURLConnection conn = getConnection(Operation.CONCAT.getMethod(),
602        params, f, true);
603    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
604  }
605
606  /**
607   * Renames Path src to Path dst.  Can take place on local fs
608   * or remote DFS.
609   */
610  @Override
611  public boolean rename(Path src, Path dst) throws IOException {
612    Map<String, String> params = new HashMap<String, String>();
613    params.put(OP_PARAM, Operation.RENAME.toString());
614    params.put(DESTINATION_PARAM, dst.toString());
615    HttpURLConnection conn = getConnection(Operation.RENAME.getMethod(),
616                                           params, src, true);
617    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
618    JSONObject json = (JSONObject) HttpFSUtils.jsonParse(conn);
619    return (Boolean) json.get(RENAME_JSON);
620  }
621
622  /**
623   * Delete a file.
624   *
625   * @deprecated Use delete(Path, boolean) instead
626   */
627  @Deprecated
628  @Override
629  public boolean delete(Path f) throws IOException {
630    return delete(f, false);
631  }
632
633  /**
634   * Delete a file.
635   *
636   * @param f the path to delete.
637   * @param recursive if path is a directory and set to
638   * true, the directory is deleted else throws an exception. In
639   * case of a file the recursive can be set to either true or false.
640   *
641   * @return true if delete is successful else false.
642   *
643   * @throws IOException
644   */
645  @Override
646  public boolean delete(Path f, boolean recursive) throws IOException {
647    Map<String, String> params = new HashMap<String, String>();
648    params.put(OP_PARAM, Operation.DELETE.toString());
649    params.put(RECURSIVE_PARAM, Boolean.toString(recursive));
650    HttpURLConnection conn = getConnection(Operation.DELETE.getMethod(),
651                                           params, f, true);
652    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
653    JSONObject json = (JSONObject) HttpFSUtils.jsonParse(conn);
654    return (Boolean) json.get(DELETE_JSON);
655  }
656
657  private FileStatus[] toFileStatuses(JSONObject json, Path f) {
658    json = (JSONObject) json.get(FILE_STATUSES_JSON);
659    JSONArray jsonArray = (JSONArray) json.get(FILE_STATUS_JSON);
660    FileStatus[] array = new FileStatus[jsonArray.size()];
661    f = makeQualified(f);
662    for (int i = 0; i < jsonArray.size(); i++) {
663      array[i] = createFileStatus(f, (JSONObject) jsonArray.get(i));
664    }
665    return array;
666  }
667
668  /**
669   * List the statuses of the files/directories in the given path if the path is
670   * a directory.
671   *
672   * @param f given path
673   *
674   * @return the statuses of the files/directories in the given patch
675   *
676   * @throws IOException
677   */
678  @Override
679  public FileStatus[] listStatus(Path f) throws IOException {
680    Map<String, String> params = new HashMap<String, String>();
681    params.put(OP_PARAM, Operation.LISTSTATUS.toString());
682    HttpURLConnection conn = getConnection(Operation.LISTSTATUS.getMethod(),
683                                           params, f, true);
684    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
685    JSONObject json = (JSONObject) HttpFSUtils.jsonParse(conn);
686    return toFileStatuses(json, f);
687  }
688
689  @Override
690  public DirectoryEntries listStatusBatch(Path f, byte[] token) throws
691      FileNotFoundException, IOException {
692    Map<String, String> params = new HashMap<String, String>();
693    params.put(OP_PARAM, Operation.LISTSTATUS_BATCH.toString());
694    if (token != null) {
695      params.put(START_AFTER_PARAM, new String(token, Charsets.UTF_8));
696    }
697    HttpURLConnection conn = getConnection(
698        Operation.LISTSTATUS_BATCH.getMethod(),
699        params, f, true);
700    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
701    // Parse the FileStatus array
702    JSONObject json = (JSONObject) HttpFSUtils.jsonParse(conn);
703    JSONObject listing = (JSONObject) json.get(DIRECTORY_LISTING_JSON);
704    FileStatus[] statuses = toFileStatuses(
705        (JSONObject) listing.get(PARTIAL_LISTING_JSON), f);
706    // New token is the last FileStatus entry
707    byte[] newToken = null;
708    if (statuses.length > 0) {
709      newToken = statuses[statuses.length - 1].getPath().getName().toString()
710          .getBytes(Charsets.UTF_8);
711    }
712    // Parse the remainingEntries boolean into hasMore
713    final long remainingEntries = (Long) listing.get(REMAINING_ENTRIES_JSON);
714    final boolean hasMore = remainingEntries > 0 ? true : false;
715    return new DirectoryEntries(statuses, newToken, hasMore);
716  }
717
718  /**
719   * Set the current working directory for the given file system. All relative
720   * paths will be resolved relative to it.
721   *
722   * @param newDir new directory.
723   */
724  @Override
725  public void setWorkingDirectory(Path newDir) {
726    workingDir = newDir;
727  }
728
729  /**
730   * Get the current working directory for the given file system
731   *
732   * @return the directory pathname
733   */
734  @Override
735  public Path getWorkingDirectory() {
736    if (workingDir == null) {
737      workingDir = getHomeDirectory();
738    }
739    return workingDir;
740  }
741
742  /**
743   * Make the given file and all non-existent parents into
744   * directories. Has the semantics of Unix 'mkdir -p'.
745   * Existence of the directory hierarchy is not an error.
746   */
747  @Override
748  public boolean mkdirs(Path f, FsPermission permission) throws IOException {
749    Map<String, String> params = new HashMap<String, String>();
750    params.put(OP_PARAM, Operation.MKDIRS.toString());
751    params.put(PERMISSION_PARAM, permissionToString(permission));
752    HttpURLConnection conn = getConnection(Operation.MKDIRS.getMethod(),
753                                           params, f, true);
754    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
755    JSONObject json = (JSONObject) HttpFSUtils.jsonParse(conn);
756    return (Boolean) json.get(MKDIRS_JSON);
757  }
758
759  /**
760   * Return a file status object that represents the path.
761   *
762   * @param f The path we want information from
763   *
764   * @return a FileStatus object
765   *
766   * @throws FileNotFoundException when the path does not exist;
767   * IOException see specific implementation
768   */
769  @Override
770  public FileStatus getFileStatus(Path f) throws IOException {
771    Map<String, String> params = new HashMap<String, String>();
772    params.put(OP_PARAM, Operation.GETFILESTATUS.toString());
773    HttpURLConnection conn = getConnection(Operation.GETFILESTATUS.getMethod(),
774                                           params, f, true);
775    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
776    JSONObject json = (JSONObject) HttpFSUtils.jsonParse(conn);
777    json = (JSONObject) json.get(FILE_STATUS_JSON);
778    f = makeQualified(f);
779    return createFileStatus(f, json);
780  }
781
782  /**
783   * Return the current user's home directory in this filesystem.
784   * The default implementation returns "/user/$USER/".
785   */
786  @Override
787  public Path getHomeDirectory() {
788    Map<String, String> params = new HashMap<String, String>();
789    params.put(OP_PARAM, Operation.GETHOMEDIRECTORY.toString());
790    try {
791      HttpURLConnection conn =
792        getConnection(Operation.GETHOMEDIRECTORY.getMethod(), params,
793                      new Path(getUri().toString(), "/"), false);
794      HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
795      JSONObject json = (JSONObject) HttpFSUtils.jsonParse(conn);
796      return new Path((String) json.get(HOME_DIR_JSON));
797    } catch (IOException ex) {
798      throw new RuntimeException(ex);
799    }
800  }
801
802  /**
803   * Get the root directory of Trash for a path in HDFS.
804   * 1. File in encryption zone returns /ez1/.Trash/username.
805   * 2. File not in encryption zone, or encountered exception when checking
806   *    the encryption zone of the path, returns /users/username/.Trash.
807   * Caller appends either Current or checkpoint timestamp
808   * for trash destination.
809   * The default implementation returns "/user/username/.Trash".
810   * @param fullPath the trash root of the path to be determined.
811   * @return trash root
812   */
813  @Override
814  public Path getTrashRoot(Path fullPath) {
815    Map<String, String> params = new HashMap<>();
816    params.put(OP_PARAM, Operation.GETTRASHROOT.toString());
817    try {
818      HttpURLConnection conn = getConnection(
819              Operation.GETTRASHROOT.getMethod(), params, fullPath, true);
820      HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
821      JSONObject json = (JSONObject) HttpFSUtils.jsonParse(conn);
822      return new Path((String) json.get(TRASH_DIR_JSON));
823    } catch (IOException ex) {
824      LOG.warn("Cannot find trash root of " + fullPath, ex);
825      return super.getTrashRoot(fullPath);
826    }
827  }
828
829  /**
830   * Set owner of a path (i.e. a file or a directory).
831   * The parameters username and groupname cannot both be null.
832   *
833   * @param p The path
834   * @param username If it is null, the original username remains unchanged.
835   * @param groupname If it is null, the original groupname remains unchanged.
836   */
837  @Override
838  public void setOwner(Path p, String username, String groupname)
839    throws IOException {
840    Map<String, String> params = new HashMap<String, String>();
841    params.put(OP_PARAM, Operation.SETOWNER.toString());
842    params.put(OWNER_PARAM, username);
843    params.put(GROUP_PARAM, groupname);
844    HttpURLConnection conn = getConnection(Operation.SETOWNER.getMethod(),
845                                           params, p, true);
846    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
847  }
848
849  /**
850   * Set permission of a path.
851   *
852   * @param p path.
853   * @param permission permission.
854   */
855  @Override
856  public void setPermission(Path p, FsPermission permission) throws IOException {
857    Map<String, String> params = new HashMap<String, String>();
858    params.put(OP_PARAM, Operation.SETPERMISSION.toString());
859    params.put(PERMISSION_PARAM, permissionToString(permission));
860    HttpURLConnection conn = getConnection(Operation.SETPERMISSION.getMethod(), params, p, true);
861    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
862  }
863
864  /**
865   * Set access time of a file
866   *
867   * @param p The path
868   * @param mtime Set the modification time of this file.
869   * The number of milliseconds since Jan 1, 1970.
870   * A value of -1 means that this call should not set modification time.
871   * @param atime Set the access time of this file.
872   * The number of milliseconds since Jan 1, 1970.
873   * A value of -1 means that this call should not set access time.
874   */
875  @Override
876  public void setTimes(Path p, long mtime, long atime) throws IOException {
877    Map<String, String> params = new HashMap<String, String>();
878    params.put(OP_PARAM, Operation.SETTIMES.toString());
879    params.put(MODIFICATION_TIME_PARAM, Long.toString(mtime));
880    params.put(ACCESS_TIME_PARAM, Long.toString(atime));
881    HttpURLConnection conn = getConnection(Operation.SETTIMES.getMethod(),
882                                           params, p, true);
883    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
884  }
885
886  /**
887   * Set replication for an existing file.
888   *
889   * @param src file name
890   * @param replication new replication
891   *
892   * @return true if successful;
893   *         false if file does not exist or is a directory
894   *
895   * @throws IOException
896   */
897  @Override
898  public boolean setReplication(Path src, short replication)
899    throws IOException {
900    Map<String, String> params = new HashMap<String, String>();
901    params.put(OP_PARAM, Operation.SETREPLICATION.toString());
902    params.put(REPLICATION_PARAM, Short.toString(replication));
903    HttpURLConnection conn =
904      getConnection(Operation.SETREPLICATION.getMethod(), params, src, true);
905    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
906    JSONObject json = (JSONObject) HttpFSUtils.jsonParse(conn);
907    return (Boolean) json.get(SET_REPLICATION_JSON);
908  }
909
910  /**
911   * Modify the ACL entries for a file.
912   *
913   * @param path Path to modify
914   * @param aclSpec List<AclEntry> describing modifications
915   * @throws IOException
916   */
917  @Override
918  public void modifyAclEntries(Path path, List<AclEntry> aclSpec)
919          throws IOException {
920    Map<String, String> params = new HashMap<String, String>();
921    params.put(OP_PARAM, Operation.MODIFYACLENTRIES.toString());
922    params.put(ACLSPEC_PARAM, AclEntry.aclSpecToString(aclSpec));
923    HttpURLConnection conn = getConnection(
924            Operation.MODIFYACLENTRIES.getMethod(), params, path, true);
925    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
926  }
927
928  /**
929   * Remove the specified ACL entries from a file
930   * @param path Path to modify
931   * @param aclSpec List<AclEntry> describing entries to remove
932   * @throws IOException
933   */
934  @Override
935  public void removeAclEntries(Path path, List<AclEntry> aclSpec)
936          throws IOException {
937    Map<String, String> params = new HashMap<String, String>();
938    params.put(OP_PARAM, Operation.REMOVEACLENTRIES.toString());
939    params.put(ACLSPEC_PARAM, AclEntry.aclSpecToString(aclSpec));
940    HttpURLConnection conn = getConnection(
941            Operation.REMOVEACLENTRIES.getMethod(), params, path, true);
942    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
943  }
944
945  /**
946   * Removes the default ACL for the given file
947   * @param path Path from which to remove the default ACL.
948   * @throws IOException
949   */
950  @Override
951  public void removeDefaultAcl(Path path) throws IOException {
952    Map<String, String> params = new HashMap<String, String>();
953    params.put(OP_PARAM, Operation.REMOVEDEFAULTACL.toString());
954    HttpURLConnection conn = getConnection(
955            Operation.REMOVEDEFAULTACL.getMethod(), params, path, true);
956    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
957  }
958
959  /**
960   * Remove all ACLs from a file
961   * @param path Path from which to remove all ACLs
962   * @throws IOException
963   */
964  @Override
965  public void removeAcl(Path path) throws IOException {
966    Map<String, String> params = new HashMap<String, String>();
967    params.put(OP_PARAM, Operation.REMOVEACL.toString());
968    HttpURLConnection conn = getConnection(Operation.REMOVEACL.getMethod(),
969            params, path, true);
970    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
971  }
972
973  /**
974   * Set the ACLs for the given file
975   * @param path Path to modify
976   * @param aclSpec List<AclEntry> describing modifications, must include
977   *                entries for user, group, and others for compatibility
978   *                with permission bits.
979   * @throws IOException
980   */
981  @Override
982  public void setAcl(Path path, List<AclEntry> aclSpec) throws IOException {
983    Map<String, String> params = new HashMap<String, String>();
984    params.put(OP_PARAM, Operation.SETACL.toString());
985    params.put(ACLSPEC_PARAM, AclEntry.aclSpecToString(aclSpec));
986    HttpURLConnection conn = getConnection(Operation.SETACL.getMethod(),
987                                           params, path, true);
988    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
989  }
990
991  /**
992   * Get the ACL information for a given file
993   * @param path Path to acquire ACL info for
994   * @return the ACL information in JSON format
995   * @throws IOException
996   */
997  @Override
998  public AclStatus getAclStatus(Path path) throws IOException {
999    Map<String, String> params = new HashMap<String, String>();
1000    params.put(OP_PARAM, Operation.GETACLSTATUS.toString());
1001    HttpURLConnection conn = getConnection(Operation.GETACLSTATUS.getMethod(),
1002            params, path, true);
1003    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
1004    JSONObject json = (JSONObject) HttpFSUtils.jsonParse(conn);
1005    json = (JSONObject) json.get(ACL_STATUS_JSON);
1006    return createAclStatus(json);
1007  }
1008
1009  /** Convert a string to a FsPermission object. */
1010  static FsPermission toFsPermission(JSONObject json) {
1011    final String s = (String) json.get(PERMISSION_JSON);
1012    final Boolean aclBit = (Boolean) json.get(ACL_BIT_JSON);
1013    final Boolean encBit = (Boolean) json.get(ENC_BIT_JSON);
1014    FsPermission perm = new FsPermission(Short.parseShort(s, 8));
1015    final boolean aBit = (aclBit != null) ? aclBit : false;
1016    final boolean eBit = (encBit != null) ? encBit : false;
1017    if (aBit || eBit) {
1018      return new FsPermissionExtension(perm, aBit, eBit);
1019    } else {
1020      return perm;
1021    }
1022  }
1023
1024  private FileStatus createFileStatus(Path parent, JSONObject json) {
1025    String pathSuffix = (String) json.get(PATH_SUFFIX_JSON);
1026    Path path = (pathSuffix.equals("")) ? parent : new Path(parent, pathSuffix);
1027    FILE_TYPE type = FILE_TYPE.valueOf((String) json.get(TYPE_JSON));
1028    long len = (Long) json.get(LENGTH_JSON);
1029    String owner = (String) json.get(OWNER_JSON);
1030    String group = (String) json.get(GROUP_JSON);
1031    final FsPermission permission = toFsPermission(json);
1032    long aTime = (Long) json.get(ACCESS_TIME_JSON);
1033    long mTime = (Long) json.get(MODIFICATION_TIME_JSON);
1034    long blockSize = (Long) json.get(BLOCK_SIZE_JSON);
1035    short replication = ((Long) json.get(REPLICATION_JSON)).shortValue();
1036    FileStatus fileStatus = null;
1037
1038    switch (type) {
1039      case FILE:
1040      case DIRECTORY:
1041        fileStatus = new FileStatus(len, (type == FILE_TYPE.DIRECTORY),
1042                                    replication, blockSize, mTime, aTime,
1043                                    permission, owner, group, path);
1044        break;
1045      case SYMLINK:
1046        Path symLink = null;
1047        fileStatus = new FileStatus(len, false,
1048                                    replication, blockSize, mTime, aTime,
1049                                    permission, owner, group, symLink,
1050                                    path);
1051    }
1052    return fileStatus;
1053  }
1054
1055  /**
1056   * Convert the given JSON object into an AclStatus
1057   * @param json Input JSON representing the ACLs
1058   * @return Resulting AclStatus
1059   */
1060  private AclStatus createAclStatus(JSONObject json) {
1061    AclStatus.Builder aclStatusBuilder = new AclStatus.Builder()
1062            .owner((String) json.get(OWNER_JSON))
1063            .group((String) json.get(GROUP_JSON))
1064            .stickyBit((Boolean) json.get(ACL_STICKY_BIT_JSON));
1065    JSONArray entries = (JSONArray) json.get(ACL_ENTRIES_JSON);
1066    for ( Object e : entries ) {
1067      aclStatusBuilder.addEntry(AclEntry.parseAclEntry(e.toString(), true));
1068    }
1069    return aclStatusBuilder.build();
1070  }
1071
1072  @Override
1073  public ContentSummary getContentSummary(Path f) throws IOException {
1074    Map<String, String> params = new HashMap<String, String>();
1075    params.put(OP_PARAM, Operation.GETCONTENTSUMMARY.toString());
1076    HttpURLConnection conn =
1077      getConnection(Operation.GETCONTENTSUMMARY.getMethod(), params, f, true);
1078    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
1079    JSONObject json = (JSONObject) ((JSONObject)
1080      HttpFSUtils.jsonParse(conn)).get(CONTENT_SUMMARY_JSON);
1081    return new ContentSummary((Long) json.get(CONTENT_SUMMARY_LENGTH_JSON),
1082                              (Long) json.get(CONTENT_SUMMARY_FILE_COUNT_JSON),
1083                              (Long) json.get(CONTENT_SUMMARY_DIRECTORY_COUNT_JSON),
1084                              (Long) json.get(CONTENT_SUMMARY_QUOTA_JSON),
1085                              (Long) json.get(CONTENT_SUMMARY_SPACE_CONSUMED_JSON),
1086                              (Long) json.get(CONTENT_SUMMARY_SPACE_QUOTA_JSON)
1087    );
1088  }
1089
1090  @Override
1091  public FileChecksum getFileChecksum(Path f) throws IOException {
1092    Map<String, String> params = new HashMap<String, String>();
1093    params.put(OP_PARAM, Operation.GETFILECHECKSUM.toString());
1094    HttpURLConnection conn =
1095      getConnection(Operation.GETFILECHECKSUM.getMethod(), params, f, true);
1096    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
1097    final JSONObject json = (JSONObject) ((JSONObject)
1098      HttpFSUtils.jsonParse(conn)).get(FILE_CHECKSUM_JSON);
1099    return new FileChecksum() {
1100      @Override
1101      public String getAlgorithmName() {
1102        return (String) json.get(CHECKSUM_ALGORITHM_JSON);
1103      }
1104
1105      @Override
1106      public int getLength() {
1107        return ((Long) json.get(CHECKSUM_LENGTH_JSON)).intValue();
1108      }
1109
1110      @Override
1111      public byte[] getBytes() {
1112        return StringUtils.hexStringToByte((String) json.get(CHECKSUM_BYTES_JSON));
1113      }
1114
1115      @Override
1116      public void write(DataOutput out) throws IOException {
1117        throw new UnsupportedOperationException();
1118      }
1119
1120      @Override
1121      public void readFields(DataInput in) throws IOException {
1122        throw new UnsupportedOperationException();
1123      }
1124    };
1125  }
1126
1127
1128  @Override
1129  public Token<?> getDelegationToken(final String renewer)
1130    throws IOException {
1131    try {
1132      return UserGroupInformation.getCurrentUser().doAs(
1133          new PrivilegedExceptionAction<Token<?>>() {
1134            @Override
1135            public Token<?> run() throws Exception {
1136              return authURL.getDelegationToken(uri.toURL(), authToken,
1137                  renewer);
1138            }
1139          }
1140      );
1141    } catch (Exception ex) {
1142      if (ex instanceof IOException) {
1143        throw (IOException) ex;
1144      } else {
1145        throw new IOException(ex);
1146      }
1147    }
1148  }
1149
1150  public long renewDelegationToken(final Token<?> token) throws IOException {
1151    try {
1152      return UserGroupInformation.getCurrentUser().doAs(
1153          new PrivilegedExceptionAction<Long>() {
1154            @Override
1155            public Long run() throws Exception {
1156              return authURL.renewDelegationToken(uri.toURL(), authToken);
1157            }
1158          }
1159      );
1160    } catch (Exception ex) {
1161      if (ex instanceof IOException) {
1162        throw (IOException) ex;
1163      } else {
1164        throw new IOException(ex);
1165      }
1166    }
1167  }
1168
1169  public void cancelDelegationToken(final Token<?> token) throws IOException {
1170    authURL.cancelDelegationToken(uri.toURL(), authToken);
1171  }
1172
1173  @Override
1174  public Token<?> getRenewToken() {
1175    return null; //TODO : for renewer
1176  }
1177
1178  @Override
1179  @SuppressWarnings("unchecked")
1180  public <T extends TokenIdentifier> void setDelegationToken(Token<T> token) {
1181    //TODO : for renewer
1182  }
1183
1184  @Override
1185  public void setXAttr(Path f, String name, byte[] value,
1186      EnumSet<XAttrSetFlag> flag) throws IOException {
1187    Map<String, String> params = new HashMap<String, String>();
1188    params.put(OP_PARAM, Operation.SETXATTR.toString());
1189    params.put(XATTR_NAME_PARAM, name);
1190    if (value != null) {
1191      params.put(XATTR_VALUE_PARAM, 
1192          XAttrCodec.encodeValue(value, XAttrCodec.HEX));
1193    }
1194    params.put(XATTR_SET_FLAG_PARAM, EnumSetParam.toString(flag));
1195    HttpURLConnection conn = getConnection(Operation.SETXATTR.getMethod(),
1196        params, f, true);
1197    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
1198  }
1199
1200  @Override
1201  public byte[] getXAttr(Path f, String name) throws IOException {
1202    Map<String, String> params = new HashMap<String, String>();
1203    params.put(OP_PARAM, Operation.GETXATTRS.toString());
1204    params.put(XATTR_NAME_PARAM, name);
1205    HttpURLConnection conn = getConnection(Operation.GETXATTRS.getMethod(),
1206        params, f, true);
1207    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
1208    JSONObject json = (JSONObject) HttpFSUtils.jsonParse(conn);
1209    Map<String, byte[]> xAttrs = createXAttrMap(
1210        (JSONArray) json.get(XATTRS_JSON));
1211    return xAttrs != null ? xAttrs.get(name) : null;
1212  }
1213
1214  /** Convert xAttrs json to xAttrs map */
1215  private Map<String, byte[]> createXAttrMap(JSONArray jsonArray) 
1216      throws IOException {
1217    Map<String, byte[]> xAttrs = Maps.newHashMap();
1218    for (Object obj : jsonArray) {
1219      JSONObject jsonObj = (JSONObject) obj;
1220      final String name = (String)jsonObj.get(XATTR_NAME_JSON);
1221      final byte[] value = XAttrCodec.decodeValue(
1222          (String)jsonObj.get(XATTR_VALUE_JSON));
1223      xAttrs.put(name, value);
1224    }
1225
1226    return xAttrs;
1227  }
1228
1229  /** Convert xAttr names json to names list */
1230  private List<String> createXAttrNames(String xattrNamesStr) throws IOException {
1231    JSONParser parser = new JSONParser();
1232    JSONArray jsonArray;
1233    try {
1234      jsonArray = (JSONArray)parser.parse(xattrNamesStr);
1235      List<String> names = Lists.newArrayListWithCapacity(jsonArray.size());
1236      for (Object name : jsonArray) {
1237        names.add((String) name);
1238      }
1239      return names;
1240    } catch (ParseException e) {
1241      throw new IOException("JSON parser error, " + e.getMessage(), e);
1242    }
1243  }
1244
1245  @Override
1246  public Map<String, byte[]> getXAttrs(Path f) throws IOException {
1247    Map<String, String> params = new HashMap<String, String>();
1248    params.put(OP_PARAM, Operation.GETXATTRS.toString());
1249    HttpURLConnection conn = getConnection(Operation.GETXATTRS.getMethod(),
1250        params, f, true);
1251    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
1252    JSONObject json = (JSONObject) HttpFSUtils.jsonParse(conn);
1253    return createXAttrMap((JSONArray) json.get(XATTRS_JSON));
1254  }
1255
1256  @Override
1257  public Map<String, byte[]> getXAttrs(Path f, List<String> names)
1258      throws IOException {
1259    Preconditions.checkArgument(names != null && !names.isEmpty(), 
1260        "XAttr names cannot be null or empty.");
1261    Map<String, String> params = new HashMap<String, String>();
1262    params.put(OP_PARAM, Operation.GETXATTRS.toString());
1263    Map<String, List<String>> multiValuedParams = Maps.newHashMap();
1264    multiValuedParams.put(XATTR_NAME_PARAM, names);
1265    HttpURLConnection conn = getConnection(Operation.GETXATTRS.getMethod(),
1266        params, multiValuedParams, f, true);
1267    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
1268    JSONObject json = (JSONObject) HttpFSUtils.jsonParse(conn);
1269    return createXAttrMap((JSONArray) json.get(XATTRS_JSON));
1270  }
1271
1272  @Override
1273  public List<String> listXAttrs(Path f) throws IOException {
1274    Map<String, String> params = new HashMap<String, String>();
1275    params.put(OP_PARAM, Operation.LISTXATTRS.toString());
1276    HttpURLConnection conn = getConnection(Operation.LISTXATTRS.getMethod(),
1277        params, f, true);
1278    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
1279    JSONObject json = (JSONObject) HttpFSUtils.jsonParse(conn);
1280    return createXAttrNames((String) json.get(XATTRNAMES_JSON));
1281  }
1282
1283  @Override
1284  public void removeXAttr(Path f, String name) throws IOException {
1285    Map<String, String> params = new HashMap<String, String>();
1286    params.put(OP_PARAM, Operation.REMOVEXATTR.toString());
1287    params.put(XATTR_NAME_PARAM, name);
1288    HttpURLConnection conn = getConnection(Operation.REMOVEXATTR.getMethod(),
1289        params, f, true);
1290    HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK);
1291  }
1292}