public class OAuthCredential implements GalaxyFDSCredential {
  private static final String STORAGE_ACCESS_TOKEN = "storageAccessToken";
  private static final String APP_ID = "appId";
  private static final String OAUTH_APPID = "oauthAppId";
  private static final String OAUTH_ACCESS_TOKEN = "oauthAccessToken";
  private static final String OAUTH_PROVIDER = "oauthProvider";
  private static final String OAUTH_MAC_KEY = "oauthMacKey";
  private static final String OAUTH_MAC_ALGORITHM = "oauthMacAlgorithm";
  private final String HEADER_VALUE = "OAuth";

  private final String appId;
  private final StorageAccessToken storageAccessToken;

  public OAuthCredential(String fdsServiceBaseUri, String appId,
      String oauthAppId, String oauthAccessToken, String oauthProvider,
      String macKey, String macAlgorithm) throws GalaxyFDSClientException {
    this.appId = appId;
    this.storageAccessToken = getStorageAccessToken(fdsServiceBaseUri, appId,
        oauthAppId, oauthAccessToken, oauthProvider, macKey, macAlgorithm);
  }

  private StorageAccessToken getStorageAccessToken(String serviceBaseUri,
      String appId, String oauthAppId, String oauthAccessToken,
      String oauthProvider, String macKey, String macAlgorithm)
      throws GalaxyFDSClientException {
    try {
      HttpClient httpClient = new DefaultHttpClient();
      HttpGet get = new HttpGet(serviceBaseUri + "/?" + STORAGE_ACCESS_TOKEN
          + "&" + APP_ID + "=" + appId
          + "&" + OAUTH_APPID + "=" + oauthAppId
          + "&" + OAUTH_ACCESS_TOKEN + "=" + oauthAccessToken
          + "&" + OAUTH_PROVIDER + "=" + oauthProvider
          + "&" + OAUTH_MAC_ALGORITHM + "=" + macAlgorithm
          + "&" + OAUTH_MAC_KEY + "=" + macKey);
      get.setHeader(HttpHeaders.AUTHORIZATION, HEADER_VALUE);
      HttpResponse response = httpClient.execute(get);
      if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
        throw new GalaxyFDSClientException("Failed to get the storage access "
            + "token from FDS server. URI:" + get.getURI().toString()
            + ".Reason:" + response.getStatusLine().toString());
      }

      InputStream in = response.getEntity().getContent();
      Reader reader = new InputStreamReader(in);
      StorageAccessToken token = new Gson().fromJson(reader,
          StorageAccessToken.class);
      in.close();
      return token;
    } catch (IOException e) {
      throw new GalaxyFDSClientException("Failed to get the storage access "
          + "token", e);
    }
  }

  @Override
  public void addHeader(HttpRequestBase request) {
    request.setHeader(HttpHeaders.AUTHORIZATION, HEADER_VALUE);
  }

  @Override
  public String addParam(String uri) {
    StringBuilder builder = new StringBuilder(uri);
    if (uri.indexOf('?') == -1) {
      builder.append('?');
    } else {
      builder.append('&');
    }
    builder.append(APP_ID);
    builder.append('=');
    builder.append(this.appId);
    builder.append('&');
    builder.append(STORAGE_ACCESS_TOKEN);
    builder.append('=');
    builder.append(this.storageAccessToken.getToken());

    return builder.toString();
  }
} public class SSOCredential implements GalaxyFDSCredential {
  private final String HEADER_VALUE = "SSO";
  private final String SERVICE_TOKEN_PARAM = "serviceToken";
  private final String APP_ID = "appId";

  private final String serviceToken;
  private final String appId;

  @Deprecated
  public SSOCredential(String serviceToken) {
    Args.notNull(serviceToken, "Service token");
    Args.notEmpty(serviceToken, "Service token");
    this.serviceToken = serviceToken;
    this.appId = null;
  }

  public SSOCredential(String serviceToken, String appId) {
    Args.notNull(serviceToken, "Service token");
    Args.notEmpty(serviceToken, "Service token");
    Args.notNull(appId, "App id");
    Args.notEmpty(appId, "App id");
    this.serviceToken = serviceToken;
    this.appId = appId;
  }

  @Override
  public void addHeader(HttpRequestBase request) {
    request.addHeader(HttpHeaders.AUTHORIZATION, HEADER_VALUE);
  }

  @Override
  public String addParam(String uri) {
    StringBuilder builder = new StringBuilder(uri);
    if (uri.indexOf('?') == -1) {
      builder.append('?');
    } else {
      builder.append('&');
    }
    builder.append(SERVICE_TOKEN_PARAM);
    builder.append('=');
    builder.append(serviceToken);

    if (appId != null) {
      builder.append('&');
      builder.append(APP_ID);
      builder.append('=');
      builder.append(appId);
    }

    return builder.toString();
  }
} public class GalaxyFDSClientException extends Exception {

  public GalaxyFDSClientException() {}

  public GalaxyFDSClientException(String message) {
    super(message);
  }

  public GalaxyFDSClientException(Throwable cause) {
    super(cause);
  }

  public GalaxyFDSClientException(String message, Throwable cause) {
    super(message, cause);
  }
} public class ExpiresParam extends UserParam {
  private static final String EXPIRES = "expires";

  /**
   * @param expiresMs Expire timestamp in milliseconds
   */
  public ExpiresParam(long expiresMs) {
    params.put(EXPIRES, Long.toString(expiresMs));
  }
} public class FDSObject implements Closeable {

  /**
   * The name of the object
   */
  private final String objectName;

  /**
   * The name of the bucket in which this object is contained
   */
  private final String bucketName;

  /**
   * The metadata stored by Galaxy FDS for this object
   */
  private ObjectMetadata metadata;

  /**
   * The stream containing the contents of this object from FDS
   */
  private InputStream objectContent;

  public FDSObject(String bucketName, String objectName) {
    this.bucketName = bucketName;
    this.objectName = objectName;
  }

  /**
   * Gets the name of the object
   *
   * @return The name of the object
   */
  public String getObjectName() {
    return objectName;
  }

  /**
   * Gets the name of the bucket in which this object is contained.
   *
   * @return The name of the bucket in which this object is contained.
   */
  public String getBucketName() {
    return bucketName;
  }

  /**
   * Gets the metadata stored by Galaxy FDS for this object. The
   * {@link ObjectMetadata} object includes any custom user metadata supplied by
   * the caller when the object was uploaded, as well as HTTP metadata such as
   * content length and content type.
   *
   * @return The metadata stored by Galaxy FDS for this object.
   * @see FDSObject#getObjectContent()
   */
  public ObjectMetadata getObjectMetadata() {
    return metadata;
  }

  /**
   * Sets the object metadata for this object in memory.
   * <p/>
   * <b>NOTE:</b> This does not update the object metadata stored in Galaxy
   * FDS, but only updates this object in local memory.
   *
   * @param metadata The new metadata to set for this object in memory.
   */
  public void setObjectMetadata(ObjectMetadata metadata) {
    this.metadata = metadata;
  }

  /**
   * Gets an input stream containing the contents of this object. Callers should
   * close this input stream as soon as possible, because the object contents
   * aren't buffered in memory and stream directly from Galaxy FDS.
   *
   * @return An input stream containing the contents of this object.
   * @see FDSObject#setObjectContent(InputStream)
   */
  public InputStream getObjectContent() {
    return objectContent;
  }

  /**
   * Sets the input stream containing this object's contents.
   *
   * @param objectContent The input stream containing this object's contents.
   * @see FDSObject#getObjectContent()
   */
  public void setObjectContent(InputStream objectContent) {
    this.objectContent = objectContent;
  }

  /**
   * Releases any underlying system resources. If the resources are already
   * released then invoking this method has no effect.
   */
  @Override
  public void close() {
    if (objectContent != null) {
      try {
        objectContent.close();
      } catch (IOException e) {
        // ignored
      }
    }
  }
} public class InitMultipartUploadResult {
  private String bucketName;
  private String objectName;
  private String uploadId;

  public String getBucketName() {
    return bucketName;
  }

  public void setBucketName(String bucketName) {
    this.bucketName = bucketName;
  }

  public String getObjectName() {
    return objectName;
  }

  public void setObjectName(String objectName) {
    this.objectName = objectName;
  }

  public String getUploadId() {
    return uploadId;
  }

  public void setUploadId(String uploadId) {
    this.uploadId = uploadId;
  }
} public class ObjectMetadata {
  private static final Set<String> PREDEFINED_HEADERS = new HashSet<String>();
  static {
    PREDEFINED_HEADERS.add(HttpHeaders.LAST_MODIFIED);
    PREDEFINED_HEADERS.add(HttpHeaders.CONTENT_MD5);
    PREDEFINED_HEADERS.add(HttpHeaders.CONTENT_TYPE);
    PREDEFINED_HEADERS.add(HttpHeaders.CONTENT_LENGTH);
    PREDEFINED_HEADERS.add(HttpHeaders.CONTENT_ENCODING);
    PREDEFINED_HEADERS.add(HttpHeaders.CACHE_CONTROL);
  }

  public static ObjectMetadata parseObjectMetadata(Header[] headers) {
    ObjectMetadata metadata = new ObjectMetadata();

    for (Header header : headers) {
      String key = header.getName();
      String value = header.getValue();
      // If user defined metadata, keep it
      if (key.startsWith(Consts.XIAOMI_META_HEADER_PREFIX)) {
        metadata.addUserMetadata(key, value);
      } else if (PREDEFINED_HEADERS.contains(key)) {
        metadata.addPredefinedMetadata(key, value);
      }
    }
    return metadata;
  }

  /**
   * Custom user metadata, represented in responses with the x-xiaomi-meta-
   * header prefix
   */
  private final Map<String, String> userMetadata = new HashMap<String, String>();

  /**
   * Predefined metadata
   */
  private final Map<String, String> predefinedMetadata =
      new HashMap<String, String>();

  /**
   * <p>
   * Gets the custom user-metadata for the associated object.
   * </p>
   * <p>
   * Galaxy FDS can store additional metadata on objects by internally
   * representing it as HTTP headers prefixed with "x-xiaomi-meta-". Use
   * user-metadata to store arbitrary metadata alongside their data in Galaxy
   * FDS. When setting user metadata, callers <i>should not</i> include the
   * internal "x-xiaomi-meta-" prefix; this library will handle that for them.
   * Likewise, when callers retrieve custom user-metadata, they will not see
   * the "x-xiaomi-meta-" header prefix.
   * </p>
   * <p>
   * User-metadata keys are <b>case insensitive</b> and will be returned as
   * lowercase strings, even if they were originally specified with uppercase
   * strings.
   * </p>
   * <p>
   * Note that user-metadata for an object is limited by the HTTP request
   * header limit. All HTTP headers included in a request (including user
   * metadata headers and other standard HTTP headers) must be less than 8KB.
   * </p>
   *
   * @return The custom user metadata for the associated object.
   */
  public Map<String, String> getUserMetadata() {
    return userMetadata;
  }

  /**
   * <p>
   * Adds the key value pair of custom user-metadata for the associated
   * object. If the entry in the custom user-metadata map already contains the
   * specified key, it will be replaced with these new contents.
   * </p>
   * <p>
   * Galaxy FDS can store additional metadata on objects by internally
   * representing it as HTTP headers prefixed with "x-xiaomi-meta-".
   * Use user-metadata to store arbitrary metadata alongside their data in
   * Galaxy FDS. When setting user metadata, callers <i>should not</i> include
   * the internal "x-xiaomi-meta-" prefix; this library will handle that for
   * them. Likewise, when callers retrieve custom user-metadata, they will not
   * see the "x-xiaomi-meta-" header prefix.
   * </p>
   * <p>
   * Note that user-metadata for an object is limited by the HTTP request
   * header limit. All HTTP headers included in a request (including user
   * metadata headers and other standard HTTP headers) must be less than 8KB.
   * </p>
   *
   * @param key   The key for the custom user metadata entry. Note that the key
   *              should not include
   *              the internal FDS HTTP header prefix.
   * @param value The value for the custom user-metadata entry.
   */
  public void addUserMetadata(String key, String value) {
    this.checkMetadata(key);
    this.userMetadata.put(key, value);
  }

  /**
   * <p>
   * Gets the Content-Length HTTP header indicating the size of the
   * associated object in bytes.
   * </p>
   * <p>
   * This field is required when uploading objects to FDS, but the Galaxy FDS
   * Java client will automatically set it when working directly with files.
   * When uploading directly from a stream, set this field if possible.
   * Otherwise the client must buffer the entire stream in order to calculate
   * the content length before sending the data to Galaxy FDS.
   * </p>
   * <p>
   * For more information on the Content-Length HTTP header, see <a
   * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.13">
   * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.13</a>
   * </p>
   *
   * @return The Content-Length HTTP header indicating the size of the
   * associated object in bytes. Returns <code>-1</code> if it hasn't been set yet.
   */
  public long getContentLength() {
    String contentLength = predefinedMetadata.get(HttpHeaders.CONTENT_LENGTH);
    if (contentLength != null) {
      return Long.parseLong(contentLength);
    } else {
      return -1;
    }
  }

  /**
   * <p>
   * Sets the Content-Length HTTP header indicating the size of the
   * associated object in bytes.
   * </p>
   * <p>
   * This field is required when uploading objects to FDS, but the Galaxy FDS
   * Java client will automatically set it when working directly with files. When
   * uploading directly from a stream, set this field if
   * possible. Otherwise the client must buffer the entire stream in
   * order to calculate the content length before sending the data to
   * Galaxy FDS.
   * </p>
   * <p>
   * For more information on the Content-Length HTTP header, see <a
   * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.13">
   * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.13</a>
   * </p>
   *
   * @param contentLength The Content-Length HTTP header indicating the size of
   *                      the associated object in bytes.
   * @see ObjectMetadata#getContentLength()
   */
  public void setContentLength(long contentLength) {
    predefinedMetadata.put(HttpHeaders.CONTENT_LENGTH,
        Long.toString(contentLength));
  }

  /**
   * <p>
   * Gets the base64 encoded 128-bit MD5 digest of the associated object
   * (content - not including headers) according to RFC 1864. This data is
   * used as a message integrity check to verify that the data received by
   * Galaxy FDS is the same data that the caller sent.
   * </p>
   * <p>
   * This field represents the base64 encoded 128-bit MD5 digest digest of an
   * object's content as calculated on the caller's side.
   * FDS.
   * </p>
   * <p>
   * The Galaxy FDS Java client will attempt to calculate this field automatically
   * when uploading files to Galaxy FDS.
   * </p>
   *
   * @return The base64 encoded MD5 hash of the content for the associated
   * object.  Returns <code>null</code> if the MD5 hash of the content
   * hasn't been set.
   * @see ObjectMetadata#setContentMD5(String)
   */
  public String getContentMD5() {
    return predefinedMetadata.get(HttpHeaders.CONTENT_MD5);
  }

  /**
   * <p>
   * Sets the base64 encoded 128-bit MD5 digest of the associated object
   * (content - not including headers) according to RFC 1864. This data is used
   * as a message integrity check to verify that the data received by Galaxy FDS
   * is the same data that the caller sent.
   * </p>
   * <p>
   * The Galaxy FDS Java client will attempt to calculate this field
   * automatically when uploading files to Galaxy FDS.
   * </p>
   *
   * @param contentMD5 The base64 encoded MD5 hash of the content for the object
   *                   associated with this metadata.
   * @see ObjectMetadata#getContentMD5()
   */
  public void setContentMD5(String contentMD5) {
    predefinedMetadata.put(HttpHeaders.CONTENT_MD5, contentMD5);
  }

  /**
   * <p>
   * Gets the Content-Type HTTP header, which indicates the type of content
   * stored in the associated object. The value of this header is a standard
   * MIME type.
   * </p>
   * <p>
   * When uploading files, the Galaxy FDS Java client will attempt to determine
   * the correct content type if one hasn't been set yet. Users are
   * responsible for ensuring a suitable content type is set when uploading
   * streams. If no content type is provided and cannot be determined by
   * the filename, the default content type, "application/octet-stream", will
   * be used.
   * </p>
   * <p>
   * For more information on the Content-Type header, see <a
   * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17">
   * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17</a>
   * </p>
   *
   * @return The HTTP Content-Type header, indicating the type of content
   * stored in the associated FDS object. Returns <code>null</code>
   * if it hasn't been
   * set.
   * @see ObjectMetadata#setContentType(String)
   */
  public String getContentType() {
    return predefinedMetadata.get(HttpHeaders.CONTENT_TYPE);
  }

  /**
   * <p>
   * Sets the Content-Type HTTP header indicating the type of content stored in
   * the associated object. The value of this header is a standard MIME type.
   * </p>
   * <p>
   * When uploading files, the Galaxy FDS Java client will attempt to determine
   * the correct content type if one hasn't been set yet. Users are responsible
   * for ensuring a suitable content type is set when uploading streams. If no
   * content type is provided and cannot be determined by the filename, the
   * default content type "application/octet-stream" will be used.
   * </p>
   * <p>
   * For more information on the Content-Type header, see <a
   * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17">
   * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17</a>
   * </p>
   *
   * @param contentType The HTTP Content-Type header indicating the type of
   *                    content stored in the associated FDS object.
   * @see ObjectMetadata#getContentType()
   */
  public void setContentType(String contentType) {
    predefinedMetadata.put(HttpHeaders.CONTENT_TYPE, contentType);
  }

  /**
   * <p>
   * Gets the optional Content-Encoding HTTP header specifying what content
   * encodings have been applied to the object and what decoding mechanisms must
   * be applied in order to obtain the media-type referenced by the Content-Type
   * field.
   * </p>
   * <p>
   * For more information on how the Content-Encoding HTTP header works, see
   * <a
   * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11">
   * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11</a>
   * </p>
   *
   * @return The HTTP Content-Encoding header.
   * Returns <code>null</code> if it hasn't been set.
   * @see ObjectMetadata#setContentType(String)
   */
  public String getContentEncoding() {
    return predefinedMetadata.get(HttpHeaders.CONTENT_ENCODING);
  }

  /**
   * <p>
   * Sets the optional Content-Encoding HTTP header specifying what
   * content encodings have been applied to the object and what decoding
   * mechanisms must be applied in order to obtain the media-type referenced
   * by the Content-Type field.
   * </p>
   * <p>
   * For more information on how the Content-Encoding HTTP header works, see
   * <a
   * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11">
   * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11</a>
   * </p>
   *
   * @param contentEncoding The HTTP Content-Encoding header, as defined in RFC
   *                        2616.
   * @see <a
   * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11"
   * >http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11</a>
   * @see ObjectMetadata#getContentType()
   */
  public void setContentEncoding(String contentEncoding) {
    predefinedMetadata.put(HttpHeaders.CONTENT_ENCODING, contentEncoding);
  }

  /**
   * <p>
   * Gets the optional Cache-Control HTTP header which allows the user to
   * specify caching behavior along the HTTP request/reply chain.
   * </p>
   * <p>
   * For more information on how the Cache-Control HTTP header affects HTTP
   * requests and responses, see <a
   * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9">
   * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9</a>
   * </p>
   *
   * @return The HTTP Cache-Control header as defined in RFC 2616.
   * Returns <code>null</code>  if
   * it hasn't been set.
   * @see ObjectMetadata#setCacheControl(String)
   */
  public String getCacheControl() {
    return predefinedMetadata.get(HttpHeaders.CACHE_CONTROL);
  }

  /**
   * <p>
   * Sets the optional Cache-Control HTTP header which allows the user to
   * specify caching behavior along the HTTP request/reply chain.
   * </p>
   * <p>
   * For more information on how the Cache-Control HTTP header affects HTTP
   * requests and responses see <a
   * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9">
   * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9</a>
   * </p>
   *
   * @param cacheControl The HTTP Cache-Control header as defined in RFC 2616.
   * @see ObjectMetadata#getCacheControl()
   */
  public void setCacheControl(String cacheControl) {
    predefinedMetadata.put(HttpHeaders.CACHE_CONTROL, cacheControl);
  }

  /**
   * Gets the value of the Last-Modified header, indicating the date
   * and time at which Galaxy FDS last recorded a modification to the
   * associated object.
   *
   * @return The date and time at which Galaxy FDS last recorded a modification
   * to the associated object. Returns <code>null</code> if
   * the Last-Modified header hasn't been set.
   */
  public Date getLastModified() {
    String lastModified = predefinedMetadata.get(HttpHeaders.LAST_MODIFIED);
    if (lastModified != null) {
      try {
        return Util.parseDate(lastModified);
      } catch (ParseException e) {
        return null;
      }
    } else {
      return null;
    }
  }

  /**
   * For internal use only. Sets the Last-Modified header value
   * indicating the date and time at which Galaxy FDS last recorded a
   * modification to the associated object.
   *
   * @param lastModified The date and time at which Galaxy FDS last recorded a
   *                     modification to the associated object.
   */
  public void setLastModified(Date lastModified) {
    predefinedMetadata.put(HttpHeaders.LAST_MODIFIED,
        Util.formatDateString(lastModified));
  }

  /**
   * For internal use only. Add predefined metadata
   * @param key   The key for the predefined metadata
   * @param value The value for the predefined metadata entry.
   */
  public void addPredefinedMetadata(String key, String value) {
    this.checkMetadata(key);
    predefinedMetadata.put(key, value);
  }

  /**
   * Get all the metadata entries
   * @return All the metadata entries include predefined and user-defined metadata
   */
  public Map<String, String> getAllMetadata() {
    Map<String, String> copy = new HashMap<String, String>(predefinedMetadata);
    copy.putAll(userMetadata);
    return copy;
  }

  private void checkMetadata(String key) {
    boolean isValid = key.startsWith(Consts.XIAOMI_META_HEADER_PREFIX);

    if (!isValid) {
      for (String m : PREDEFINED_HEADERS) {
        if (key.equals(m)) {
          isValid = true;
          break;
        }
      }
    }

    if (!isValid) {
      throw new RuntimeException("Invalid metadata: " + key, null);
    }
  }
} public abstract class ProgressListener {

  /**
   * Called when some bytes have been transferred since the last time it was
   * called and the progress interval has passed
   *
   * @param transferred
   *            The number of bytes transferred.
   * @param total
   *            The size of the object in bytes
   *
   */
  public abstract void onProgress(long transferred, long total);

  /**
   * Should return how often transferred bytes should be reported to this
   * listener, in milliseconds. It is not guaranteed that updates will happen
   * at this exact interval, but that at least this amount of time will pass
   * between updates. The default implementation always returns 500 milliseconds.
   */
  public long progressInterval() {
    return 500;
  }
} public class PutObjectResult {
  /**
   * The bucket name where object resides
   */
  private String bucketName;

  /**
   * The unique object name generated by FDS service
   */
  private String objectName;

  /**
   * The access key of Xiaomi app
   */
  private String accessKeyId;

  /**
   * The signature for presigned URL
   */
  private String signature;

  /**
   * The base uri for FDS service
   */
  private String fdsServiceBaseUri;

  /**
   * The base uri for CDN service
   */
  private String cdnServiceBaseUri;

  /**
   * The expire timestamp for presigned URL
   */
  private long expires;

  public String getBucketName() {
    return bucketName;
  }

  public void setBucketName(String bucketName) {
    this.bucketName = bucketName;
  }

  public String getObjectName() {
    return objectName;
  }

  public void setObjectName(String objectName) {
    this.objectName = objectName;
  }

  public String getAccessKeyId() {
    return accessKeyId;
  }

  public void setAccessKeyId(String accessKeyId) {
    this.accessKeyId = accessKeyId;
  }

  public String getSignature() {
    return signature;
  }

  public void setSignature(String signature) {
    this.signature = signature;
  }

  public long getExpires() {
    return expires;
  }

  public void setExpires(long expires) {
    this.expires = expires;
  }

  public void setFdsServiceBaseUri(String fdsServiceBaseUri) {
    this.fdsServiceBaseUri = fdsServiceBaseUri;
  }

  public void setCdnServiceBaseUri(String cdnServiceBaseUri) {
    this.cdnServiceBaseUri = cdnServiceBaseUri;
  }

  public String getRelativePresignedUri() {
    return  "/" + bucketName + "/" + objectName + "?"
        + Consts.GALAXY_ACCESS_KEY_ID + "=" + accessKeyId + "&" + Consts.EXPIRES
        + "=" + expires + "&" + Consts.SIGNATURE + "=" + signature;
  }

  /**
   * Gets the presigned URL for HTTP GET
   * @return
   */
  public String getAbsolutePresignedUri() {
    return fdsServiceBaseUri + getRelativePresignedUri();
  }

  /**
   * Gets the CDN based presigned URL for HTTP GET
   * @return
   */
  public String getCdnPresignedUri() {
    return cdnServiceBaseUri + getRelativePresignedUri();
  }
} public class ResponseContentTypeParam extends UserParam {
  private static final String RESPONSE_CONTENT_TYPE = "response-content-type";

  public ResponseContentTypeParam() {
    params.put(RESPONSE_CONTENT_TYPE, null);
  }
} public class ResponseExpiresParam extends UserParam {
  private static final String RESPONSE_EXPIRES = "response-expires";

  public ResponseExpiresParam() {
    params.put(RESPONSE_EXPIRES, null);
  }
} public class StorageAccessToken {

  private String token;
  private long expireTime; // ms

  public StorageAccessToken() {}

  public StorageAccessToken(String token, long expireTime) {
    this.token = token;
    this.expireTime = expireTime;
  }

  public String getToken() {
    return token;
  }

  public void setToken(String token) {
    this.token = token;
  }

  public long getExpireTime() {
    return expireTime;
  }

  public void setExpireTime(long expireTime) {
    this.expireTime = expireTime;
  }
} public class ThumbParam extends UserParam {
  /**
   * @param width The width of thumbnail
   * @param height The height of thumbnail
   */
  public ThumbParam(int width, int height) {
    params.put("thumb", "1");
    params.put("w", Integer.toString(width));
    params.put("h", Integer.toString(height));
  }
} public class UploadPartResult {
  private int partNumber;
  private String etag;
  private long partSize;

  public UploadPartResult() { }

  public UploadPartResult(int partNumber, long partSize, String etag) {
    this.partNumber = partNumber;
    this.etag = etag;
    this.partSize = partSize;
  }

  public int getPartNumber() {
    return partNumber;
  }

  public void setPartNumber(int partNumber) {
    this.partNumber = partNumber;
  }

  public String getEtag() {
    return etag;
  }

  public void setEtag(String etag) {
    this.etag = etag;
  }

  public long getPartSize() {
    return partSize;
  }

  public void setPartSize(long partSize) {
    this.partSize = partSize;
  }
} public class UploadPartResultList {
  private List<UploadPartResult> uploadPartResultList;

  public List<UploadPartResult> getUploadPartResultList() {
    return uploadPartResultList;
  }

  public void setUploadPartResultList(List<UploadPartResult> uploadPartResultList) {
    this.uploadPartResultList = uploadPartResultList;
  }
} public class UserParam {
  protected final Map<String, String> params = new HashMap<String, String>();

  public Map<String, String> getParams() {
    return Collections.unmodifiableMap(params);
  }

  @Override
  public String toString() {
    StringBuilder stringBuilder = new StringBuilder();
    int index = 0;
    for (Map.Entry<String, String> entry : params.entrySet()) {
      if (index != 0) {
        stringBuilder.append('&');
      }
      stringBuilder.append(entry.getKey());
      String value = entry.getValue();
      if (value != null) {
        stringBuilder.append('=');
        stringBuilder.append(value);
      }
      index++;
    }
    return stringBuilder.toString();
  }
} public final class Consts {
  public static final String XIAOMI_HEADER_PREFIX = "x-xiaomi-";
  public static final String XIAOMI_META_HEADER_PREFIX = XIAOMI_HEADER_PREFIX
      + "meta-";
  public static final String ESTIMATED_OBJECT_SIZE = XIAOMI_HEADER_PREFIX
      + "estimated-object-size";
  public static final String GALAXY_ACCESS_KEY_ID = "GalaxyAccessKeyId";
  public static final String SIGNATURE = "Signature";
  public static final String EXPIRES = "Expires";
  public static String APPLICATION_OCTET_STREAM = "application/octet-stream";
} public class Util {
  private static final int BUFFER_SIZE = 4096;

  private static final ThreadLocal<SimpleDateFormat> DATE_FOPMAT =
      new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
          SimpleDateFormat format = new SimpleDateFormat(
              "EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
          format.setTimeZone(TimeZone.getTimeZone("GMT"));
          return format;
        }
      };

  /**
   * Download an object to the specified file
   *
   * @param object          The FDS object contains a reference to an
   *                        InputStream contains the object's data
   * @param destinationFile The file to store the object's data
   * @param isAppend          If append to the end of the file or overwrite it
   * @throws GalaxyFDSClientException
   */
  public static void downloadObjectToFile(FDSObject object,
      File destinationFile, boolean isAppend) throws GalaxyFDSClientException {
    // attempt to create the parent if it doesn't exist
    File parentDirectory = destinationFile.getParentFile();
    if (!isAppend && parentDirectory != null && !parentDirectory.exists() ) {
      parentDirectory.mkdirs();
    }

    int bytesRead = 0;
    byte[] buffer = new byte[BUFFER_SIZE];
    InputStream in = object.getObjectContent();
    OutputStream out = null;
    try {
      out = new BufferedOutputStream(new FileOutputStream(destinationFile,
          isAppend));
      while ((bytesRead = in.read(buffer, 0, BUFFER_SIZE)) != -1) {
        out.write(buffer, 0, bytesRead);
      }
    } catch (IOException e) {
      throw new GalaxyFDSClientException("Unable to store object["
          + object.getBucketName() + "/" + object.getObjectName() + "] content "
          + "to disk:" + e.getMessage(), e);
    } finally {
      try {
        in.close();
        if (out != null) {
          out.close();
        }
      } catch (IOException e) {
        // Ignore exceptions caused by close
      }
    }
  }

  public static String getStackTrace(Exception e) {
    StringWriter stringWriter = new StringWriter();
    PrintWriter printWriter = new PrintWriter(stringWriter);
    e.printStackTrace(printWriter);
    printWriter.flush();
    return stringWriter.toString();
  }

  /**
   * Determines the mimetype of a file by looking up the file's extension in an
   * internal listing to find the corresponding mime type. If the file has no
   * extension, or the extension is not available in the listing contained in
   * this class, the default mimetype <code>application/octet-stream</code>
   * is returned.
   * <p>
   * A file extension is one or more characters that occur after the last period
   * (.) in the file's name. If a file has no extension,
   * Guesses the mimetype of file data based on the file's extension.
   *
   * @param file * the file whose extension may match a known mimetype.
   *
   * @return the file's mimetype based on its extension, or a default value of
   * <code>application/octet-stream</code> if a mime type value cannot be found.
   */
  public static String getMimeType(File file) {
    String fileName = file.getName();
    int lastPeriodIndex = fileName.lastIndexOf(".");
    if (lastPeriodIndex > 0 && lastPeriodIndex + 1 < fileName.length()) {
      String ext = fileName.substring(lastPeriodIndex + 1);
      String mimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
      if (mimetype != null) {
        return mimetype;
      }
    }
    return Consts.APPLICATION_OCTET_STREAM;
  }

  /**
   * Parses date string to Date object
   * @throws ParseException
   */
  public static Date parseDate(String dateString) throws ParseException {
    return DATE_FOPMAT.get().parse(dateString);
  }

  /**
   * Formats the date object to string
   */
  public static String formatDateString(Date date) {
    return DATE_FOPMAT.get().format(date);
  }
}
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
import com.github.tomakehurst.wiremock.stubbing.Scenario;
import com.google.gson.Gson;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpStatus;
import org.apache.http.entity.ContentType;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import com.xiaomi.infra.galaxy.fds.android.auth.OAuthCredential;
import com.xiaomi.infra.galaxy.fds.android.auth.SSOCredential;
import com.xiaomi.infra.galaxy.fds.android.exception.GalaxyFDSClientException;
import com.xiaomi.infra.galaxy.fds.android.model.ExpiresParam;
import com.xiaomi.infra.galaxy.fds.android.model.FDSObject;
import com.xiaomi.infra.galaxy.fds.android.model.InitMultipartUploadResult;
import com.xiaomi.infra.galaxy.fds.android.model.ObjectMetadata;
import com.xiaomi.infra.galaxy.fds.android.model.ProgressListener;
import com.xiaomi.infra.galaxy.fds.android.model.PutObjectResult;
import com.xiaomi.infra.galaxy.fds.android.model.ResponseContentTypeParam;
import com.xiaomi.infra.galaxy.fds.android.model.ResponseExpiresParam;
import com.xiaomi.infra.galaxy.fds.android.model.StorageAccessToken;
import com.xiaomi.infra.galaxy.fds.android.model.ThumbParam;
import com.xiaomi.infra.galaxy.fds.android.model.UploadPartResult;
import com.xiaomi.infra.galaxy.fds.android.model.UploadPartResultList;
import com.xiaomi.infra.galaxy.fds.android.model.UserParam;
import com.xiaomi.infra.galaxy.fds.android.util.Consts;
import com.xiaomi.infra.galaxy.fds.android.util.Util;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.delete;
import static com.github.tomakehurst.wiremock.client.WireMock.deleteRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.head;
import static com.github.tomakehurst.wiremock.client.WireMock.headRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.put;
import static com.github.tomakehurst.wiremock.client.WireMock.putRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertFalse;
package com.xiaomi.infra.galaxy.fds.android;





public class TestGalaxyFDSClient {
  private static final int WIRE_MOCK_BASE_URI_PORT = 8964;
  private static final String SSO_SERVICE_TOKEN = "ossServiceToken";

  private static final String STORAGE_ACCESS_TOKEN = "storageAccessToken";
  private static final String APP_ID = "appId";
  private static final String OAUTH_APPID = "oauthAppId";
  private static final String OAUTH_ACCESS_TOKEN = "oauthAccessToken";
  private static final String OAUTH_PROVIDER = "oauthProvider";
  private static final String OAUTH_MAC_KEY = "oauthMacKey";
  private static final String OAUTH_MAC_ALGORITHM = "oauthMacAlgorithm";

  private final FDSClientConfiguration config;
  private final GalaxyFDSClient client;
  private final int partSize;

  @Rule
  public WireMockRule baseUriMockRule = new WireMockRule(WIRE_MOCK_BASE_URI_PORT);

  @Before
  public void setup() {
    WireMock.reset();
  }

  public TestGalaxyFDSClient() {
    config = new FDSClientConfiguration()
        .withCredential(new SSOCredential(SSO_SERVICE_TOKEN))
        .withUnitTestMode(true)
        .withBaseUriForUnitTest("http://localhost:" + WIRE_MOCK_BASE_URI_PORT + "/");
    client = new GalaxyFDSClientImpl(config);
    partSize = config.DEFAULT_UPLOAD_PART_SIZE;
  }

  @Test(timeout = 120*1000)
  public void testInvalidConfig() {
    try {
      new FDSClientConfiguration().withCredential(new SSOCredential(null));
      fail("null sso token. should fail");
    } catch (IllegalArgumentException e) {
      System.out.print(Util.getStackTrace(e));
    }

    try {
      new FDSClientConfiguration().withCredential(null);
      fail("null credential. should fail");
    } catch (IllegalArgumentException e) {
      System.out.print(Util.getStackTrace(e));
    }
  }

  @Test(timeout = 120*1000)
  public void testPutInvalidParam() {
    String bucketName = "testPutInvalidParam_bucket";
    String objectName = "testPutInvalidParam_object";
    try {
      client.putObject(bucketName, objectName, new File("/notexist/xxx"));
      fail("File not exist, should fail");
    } catch (GalaxyFDSClientException e) {