All Downloads are FREE. Search and download functionalities are using the official Maven repository.

play.libs.ws.WSAsyncRequest Maven / Gradle / Ivy

There is a newer version: 2.6.2
Show newest version
package play.libs.ws;

import com.ning.http.client.AsyncCompletionHandler;
import com.ning.http.client.AsyncHttpClient;
import com.ning.http.client.Realm;
import com.ning.http.client.Response;
import com.ning.http.client.multipart.ByteArrayPart;
import com.ning.http.client.multipart.FilePart;
import com.ning.http.client.multipart.Part;
import java.io.InputStream;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import org.apache.commons.lang3.NotImplementedException;
import play.libs.MimeTypes;
import play.libs.Promise;

class WSAsyncRequest extends WSRequest {

  private final AsyncHttpClient httpClient;
  protected String type;
  private String generatedContentType;

  protected WSAsyncRequest(AsyncHttpClient httpClient, String url, Charset encoding) {
    super(url, encoding);
    this.httpClient = httpClient;
  }

  /**
   * Returns the URL but removed the queryString-part of it The QueryString-info is later added with addQueryString().
   *
   * @return The URL without the queryString-part
   */
  protected String getUrlWithoutQueryString() {
    int i = url.indexOf('?');
    if (i > 0) {
      return url.substring(0, i);
    } else {
      return url;
    }
  }

  /**
   * Adds the queryString-part of the url to the BoundRequestBuilder
   *
   * @param requestBuilder The request builder to add the queryString-part
   */
  protected void addQueryString(AsyncHttpClient.BoundRequestBuilder requestBuilder) {

    // AsyncHttpClient is by default encoding everything in UTF-8. So for us to be able to use a different encoding we
    // have configured AsyncHttpClient to use raw urls. When using raw urls, AsyncHttpClient does not encode url and
    // QueryParam with utf-8 - but there is another problem:
    // If we send raw (none-encoded) url (with queryString) to AsyncHttpClient, it does not url-encode it, but it
    // transforms all illegal chars to '?'. If we pre-encoded the url with QueryString before sending it to
    // AsyncHttpClient, it will decode it, and then later break it with '?'.

    // This method basically does the same as RequestBuilderBase.buildUrl() except from destroying the pre-encoding.

    int i = url.indexOf('?');
    // Only do this if the url contains one or more query parameters.
    if (i > 0) {

      // Extract query-parameters-part
      String queryPart = url.substring(i + 1);

      // Parse the query part and decode it... (it is going to be re-encoded later)
      for (String param : queryPart.split("&")) {

        i = param.indexOf('=');
        String name;
        String value = null;
        if (i <= 0) {
          // `name` is only a flag
          name = URLDecoder.decode(param, encoding);
        } else {
          name = URLDecoder.decode(param.substring(0, i), encoding);
          value = URLDecoder.decode(param.substring(i + 1), encoding);
        }

        if (value == null) {
          requestBuilder.addQueryParam(URLEncoder.encode(name, encoding), null);
        } else {
          requestBuilder.addQueryParam(URLEncoder.encode(name, encoding),
              URLEncoder.encode(value, encoding));
        }
      }
    }
  }

  private AsyncHttpClient.BoundRequestBuilder prepareAll(
      AsyncHttpClient.BoundRequestBuilder requestBuilder) {
    checkFileBody(requestBuilder);
    addQueryString(requestBuilder);
    addGeneratedContentType(requestBuilder);
    return requestBuilder;
  }

  public AsyncHttpClient.BoundRequestBuilder prepareGet() {
    return prepareAll(httpClient.prepareGet(getUrlWithoutQueryString()));
  }

  public AsyncHttpClient.BoundRequestBuilder prepareOptions() {
    return prepareAll(httpClient.prepareOptions(getUrlWithoutQueryString()));
  }

  public AsyncHttpClient.BoundRequestBuilder prepareHead() {
    return prepareAll(httpClient.prepareHead(getUrlWithoutQueryString()));
  }

  public AsyncHttpClient.BoundRequestBuilder preparePatch() {
    return prepareAll(httpClient.preparePatch(getUrlWithoutQueryString()));
  }

  public AsyncHttpClient.BoundRequestBuilder preparePost() {
    return prepareAll(httpClient.preparePost(getUrlWithoutQueryString()));
  }

  public AsyncHttpClient.BoundRequestBuilder preparePut() {
    return prepareAll(httpClient.preparePut(getUrlWithoutQueryString()));
  }

  public AsyncHttpClient.BoundRequestBuilder prepareDelete() {
    return prepareAll(httpClient.prepareDelete(getUrlWithoutQueryString()));
  }

  /**
   * Execute a GET request synchronously.
   */
  @Override
  public HttpResponse get() {
    this.type = "GET";
    try {
      return new HttpAsyncResponse(prepare(prepareGet()).execute().get());
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Execute a GET request asynchronously.
   */
  @Override
  public Promise getAsync() {
    this.type = "GET";
    return execute(prepareGet());
  }

  /**
   * Execute a PATCH request.
   */
  @Override
  public HttpResponse patch() {
    this.type = "PATCH";
    try {
      return new HttpAsyncResponse(prepare(preparePatch()).execute().get());
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Execute a PATCH request asynchronously.
   */
  @Override
  public Promise patchAsync() {
    this.type = "PATCH";
    return execute(preparePatch());
  }

  /**
   * Execute a POST request.
   */
  @Override
  public HttpResponse post() {
    this.type = "POST";
    try {
      return new HttpAsyncResponse(prepare(preparePost()).execute().get());
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Execute a POST request asynchronously.
   */
  @Override
  public Promise postAsync() {
    this.type = "POST";
    return execute(preparePost());
  }

  /**
   * Execute a PUT request.
   */
  @Override
  public HttpResponse put() {
    this.type = "PUT";
    try {
      return new HttpAsyncResponse(prepare(preparePut()).execute().get());
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Execute a PUT request asynchronously.
   */
  @Override
  public Promise putAsync() {
    this.type = "PUT";
    return execute(preparePut());
  }

  /**
   * Execute a DELETE request.
   */
  @Override
  public HttpResponse delete() {
    this.type = "DELETE";
    try {
      return new HttpAsyncResponse(prepare(prepareDelete()).execute().get());
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Execute a DELETE request asynchronously.
   */
  @Override
  public Promise deleteAsync() {
    this.type = "DELETE";
    return execute(prepareDelete());
  }

  /**
   * Execute a OPTIONS request.
   */
  @Override
  public HttpResponse options() {
    this.type = "OPTIONS";
    try {
      return new HttpAsyncResponse(prepare(prepareOptions()).execute().get());
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Execute a OPTIONS request asynchronously.
   */
  @Override
  public Promise optionsAsync() {
    this.type = "OPTIONS";
    return execute(prepareOptions());
  }

  /**
   * Execute a HEAD request.
   */
  @Override
  public HttpResponse head() {
    this.type = "HEAD";
    try {
      return new HttpAsyncResponse(prepare(prepareHead()).execute().get());
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Execute a HEAD request asynchronously.
   */
  @Override
  public Promise headAsync() {
    this.type = "HEAD";
    return execute(prepareHead());
  }

  /**
   * Execute a TRACE request.
   */
  @Override
  public HttpResponse trace() {
    this.type = "TRACE";
    throw new NotImplementedException();
  }

  /**
   * Execute a TRACE request asynchronously.
   */
  @Override
  public Promise traceAsync() {
    this.type = "TRACE";
    throw new NotImplementedException();
  }

  private AsyncHttpClient.BoundRequestBuilder prepare(AsyncHttpClient.BoundRequestBuilder builder) {
    if (this.username != null && this.password != null && this.scheme != null) {
      Realm.AuthScheme authScheme = switch (this.scheme) {
        case DIGEST -> Realm.AuthScheme.DIGEST;
        case NTLM -> Realm.AuthScheme.NTLM;
        case KERBEROS -> Realm.AuthScheme.KERBEROS;
        case SPNEGO -> Realm.AuthScheme.SPNEGO;
        case BASIC -> Realm.AuthScheme.BASIC;
      };
      builder.setRealm(
          (new Realm.RealmBuilder())
              .setScheme(authScheme)
              .setPrincipal(this.username)
              .setPassword(this.password)
              .setUsePreemptiveAuth(true)
              .build());
    }
    for (Map.Entry entry : this.headers.entrySet()) {
      builder.addHeader(entry.getKey(), entry.getValue());
    }
    builder.setFollowRedirects(this.followRedirects);
    builder.setRequestTimeout(this.timeout * 1000);
    if (this.virtualHost != null) {
      builder.setVirtualHost(this.virtualHost);
    }
    return builder;
  }

  private Promise execute(AsyncHttpClient.BoundRequestBuilder builder) {
    try {
      final Promise smartFuture = new Promise<>();
      prepare(builder)
          .execute(
              new AsyncCompletionHandler() {
                @Override
                public HttpResponse onCompleted(Response response) {
                  HttpResponse httpResponse = new HttpAsyncResponse(response);
                  smartFuture.accept(httpResponse);
                  return httpResponse;
                }

                @Override
                public void onThrowable(Throwable t) {
                  // An error happened, here the exception is being "forwarded" to the future that's awaiting the result
                  smartFuture.invokeWithException(t);
                }
              });

      return smartFuture;
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  private void checkFileBody(AsyncHttpClient.BoundRequestBuilder builder) {
    setResolvedContentType(null);
    if (this.fileParams != null) {
      // Could be optimized, we know the size of this array.
      for (FileParam fileParam : this.fileParams) {
        builder.addBodyPart(
            new FilePart(
                fileParam.paramName,
                fileParam.file,
                MimeTypes.getMimeType(fileParam.file.getName()),
                encoding));
      }
      // AsyncHttpClient only supports ASCII chars in keys in multipart
      for (Map.Entry entry : this.parameters.entrySet()) {
        String key = entry.getKey();
        Object value = entry.getValue();
        if (value instanceof Collection || value.getClass().isArray()) {
          Collection values = value.getClass().isArray() ? Arrays.asList((Object[]) value) : (Collection) value;
          for (Object v : values) {
            Part part = new ByteArrayPart(
                key,
                v.toString().getBytes(encoding),
                "text/plain",
                encoding,
                null);
            builder.addBodyPart(part);
          }
        } else {
          Part part = new ByteArrayPart(
              key,
              value.toString().getBytes(encoding),
              "text/plain",
              encoding,
              null);
          builder.addBodyPart(part);
        }
      }

      // Don't have to set content-type: AsyncHttpClient will automatically choose multipart

      return;
    }
    if (!this.parameters.isEmpty()) {
      boolean isPostPut = "POST".equals(this.type) || ("PUT".equals(this.type));

      if (isPostPut) {
        // Since AsyncHttpClient is hard-coded to encode to use UTF-8, we must build the content ourselves
        StringBuilder sb = new StringBuilder();

        for (Map.Entry entry : this.parameters.entrySet()) {
          String key = entry.getKey();
          Object value = entry.getValue();
          if (value == null) {
            continue;
          }

          if (value instanceof Collection || value.getClass().isArray()) {
            Collection values = value.getClass().isArray() ? Arrays.asList((Object[]) value) : (Collection) value;
            for (Object v : values) {
              if (!sb.isEmpty()) {
                sb.append('&');
              }
              sb.append(encode(key));
              sb.append('=');
              sb.append(encode(v.toString()));
            }
          } else {
            // Since AsyncHttpClient is hard-coded to encode using UTF-8, we must build the content ourselves.
            if (!sb.isEmpty()) {
              sb.append('&');
            }
            sb.append(encode(key));
            sb.append('=');
            sb.append(encode(value.toString()));
          }
        }
        byte[] bodyBytes = sb.toString().getBytes(this.encoding);
        builder.setBody(bodyBytes);

        setResolvedContentType("application/x-www-form-urlencoded; charset=" + encoding);

      } else {
        for (Map.Entry entry : this.parameters.entrySet()) {
          String key = entry.getKey();
          Object value = entry.getValue();
          if (value == null) {
            continue;
          }
          if (value instanceof Collection || value.getClass().isArray()) {
            Collection values = value.getClass().isArray()
                ? Arrays.asList((Object[]) value)
                : (Collection) value;
            for (Object v : values) {
              // Must encode it since AsyncHttpClient uses raw urls
              builder.addQueryParam(encode(key), encode(v.toString()));
            }
          } else {
            // Must encode it since AsyncHttpClient uses raw urls
            builder.addQueryParam(encode(key), encode(value.toString()));
          }
        }
        setResolvedContentType("text/html; charset=" + encoding);
      }
    }
    if (this.body != null) {
      if (!this.parameters.isEmpty()) {
        throw new RuntimeException("POST or PUT method with parameters AND body are not supported.");
      }
      if (this.body instanceof InputStream) {
        builder.setBody((InputStream) this.body);
      } else {
        byte[] bodyBytes = this.body.toString().getBytes(this.encoding);
        builder.setBody(bodyBytes);
      }
      setResolvedContentType("text/html; charset=" + encoding);
    }

    if (this.mimeType != null) {
      // The requester has specified mimeType
      this.headers.put("Content-Type", this.mimeType);
    }
  }

  /**
   * Sets the resolved Content-type. This is added as Content-type-header to AsyncHttpClient if the requester has not
   * specified Content-type or mimeType explicitly. Cannot add it directly to this.header since this causes problems
   * when Request-object is used multiple times, e.g.: first GET, then POST.
   */
  private void setResolvedContentType(String contentType) {
    generatedContentType = contentType;
  }

  /**
   * If generatedContentType is present AND if Content-type header is not already present, add generatedContentType as
   * Content-Type to headers in requestBuilder.
   */
  private void addGeneratedContentType(AsyncHttpClient.BoundRequestBuilder requestBuilder) {
    if (!headers.containsKey("Content-Type") && generatedContentType != null) {
      requestBuilder.addHeader("Content-Type", generatedContentType);
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy