content: /*
   * Copyright 2013 Agorava
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *   http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package org.agorava.rest;
  
  import org.agorava.api.exception.AgoravaException;
  import org.agorava.api.exception.ConnectionException;
  import org.agorava.api.rest.Request;
  import org.agorava.api.rest.RequestTuner;
  import org.agorava.api.rest.Verb;
  
  import java.io.IOException;
  import java.io.UnsupportedEncodingException;
  import java.net.HttpURLConnection;
  import java.net.MalformedURLException;
  import java.net.URL;
  import java.nio.charset.Charset;
  import java.util.HashMap;
  import java.util.Map;
  import java.util.concurrent.TimeUnit;
  
  /**
   * Represents an HTTP Request object
   *
   * @author Pablo Fernandez
   */
  public class RequestImpl implements Request {
      private static final String DEFAULT_CONTENT_TYPE = "application/x-www-form-urlencoded";
  
      private static final String CONTENT_LENGTH = "Content-Length";
  
      private static final String CONTENT_TYPE = "Content-Type";
  
      private static RequestTuner NOOP = new RequestTuner() {
          @Override
          public void tune(Request _) {
          }
      };
  
      private String url;
  
      private Verb verb;
  
      private HttpParametersImpl querystringParams;
  
      private HttpParametersImpl bodyParams;
  
      private Map<String, String> headers;
  
      private String payload = null;
  
      protected HttpURLConnection getConnection() {
          return connection;
      }
  
      private HttpURLConnection connection;
  
      private String charset;
  
      private byte[] bytePayload = null;
  
      private boolean connectionKeepAlive = false;
  
      private Long connectTimeout = null;
  
      private Long readTimeout = null;
  
      /**
       * Creates a new Http Request
       *
       * @param verb Http Verb (GET, POST, etc)
       * @param url  url with optional querystring parameters.
       */
      public RequestImpl(Verb verb, String url) {
          this.verb = verb;
          this.url = url;
          this.querystringParams = new HttpParametersImpl();
          this.bodyParams = new HttpParametersImpl();
          this.headers = new HashMap<String, String>();
      }
  
      @Override
      public ResponseImpl send(RequestTuner tuner) {
          try {
              createConnection();
              return doSend(tuner);
          } catch (Exception e) {
              throw new ConnectionException(e);
          }
      }
  
      @Override
      public ResponseImpl send() {
          return send(NOOP);
      }
  
      private void createConnection() throws IOException {
          String completeUrl = getCompleteUrl();
          if (connection == null) {
              System.setProperty("http.keepAlive", connectionKeepAlive ? "true" : "false");
              connection = (HttpURLConnection) new URL(completeUrl).openConnection();
          }
      }
  
      @Override
      public String getCompleteUrl() {
          return querystringParams.asUrl(url);
      }
  
      ResponseImpl doSend(RequestTuner tuner) throws IOException {
          connection.setRequestMethod(this.verb.name());
          if (connectTimeout != null) {
              connection.setConnectTimeout(connectTimeout.intValue());
          }
          if (readTimeout != null) {
              connection.setReadTimeout(readTimeout.intValue());
          }
          addHeaders(connection);
          if (verb.equals(Verb.PUT) || verb.equals(Verb.POST)) {
              addBody(connection, getByteBodyContents());
          }
          tuner.tune(this);
          return new ResponseImpl(this);
      }
  
      void addHeaders(HttpURLConnection conn) {
          for (String key : headers.keySet())
              conn.setRequestProperty(key, headers.get(key));
      }
  
      void addBody(HttpURLConnection conn, byte[] content) throws IOException {
          conn.setRequestProperty(CONTENT_LENGTH, String.valueOf(content.length));
  
          // Set default content type if none is set.
          if (conn.getRequestProperty(CONTENT_TYPE) == null) {
              conn.setRequestProperty(CONTENT_TYPE, DEFAULT_CONTENT_TYPE);
          }
          conn.setDoOutput(true);
          conn.getOutputStream().write(content);
      }
  
      @Override
      public void addHeader(String key, String value) {
          this.headers.put(key, value);
      }
  
      @Override
      public void addBodyParameter(String key, String value) {
          this.bodyParams.add(key, value);
      }
  
      @Override
      public void addQuerystringParameter(String key, String value) {
          this.querystringParams.add(key, value);
      }
  
      @Override
      public void addPayload(String payload) {
          this.payload = payload;
      }
  
      @Override
      public void addPayload(byte[] payload) {
          this.bytePayload = payload;
      }
  
      @Override
      public HttpParametersImpl getQueryStringParams() {
          try {
              HttpParametersImpl result = new HttpParametersImpl();
              String queryString = new URL(url).getQuery();
              result.addQuerystring(queryString);
              result.addAll(querystringParams);
              return result;
          } catch (MalformedURLException mue) {
              throw new AgoravaException("Malformed URL : " + url, mue);
          }
      }
  
      @Override
      public HttpParametersImpl getBodyParams() {
          return bodyParams;
      }
  
      @Override
      public String getUrl() {
          return url;
      }
  
      @Override
      public String getSanitizedUrl() {
          return url.replaceAll("\\?.*", "").replace("\\:\\d{4}", "");
      }
  
      @Override
      public String getBodyContents() {
          try {
              return new String(getByteBodyContents(), getCharset());
          } catch (UnsupportedEncodingException uee) {
              throw new AgoravaException("Unsupported Charset: " + charset, uee);
          }
      }
  
      byte[] getByteBodyContents() {
          if (bytePayload != null) return bytePayload;
          String body = (payload != null) ? payload : bodyParams.asFormUrlEncodedString();
          try {
              return body.getBytes(getCharset());
          } catch (UnsupportedEncodingException uee) {
              throw new AgoravaException("Unsupported Charset: " + getCharset(), uee);
          }
      }
  
      @Override
      public Verb getVerb() {
          return verb;
      }
  
      @Override
      public Map<String, String> getHeaders() {
          return headers;
      }
  
      @Override
      public String getCharset() {
          return charset == null ? Charset.defaultCharset().name() : charset;
      }
  
      @Override
      public void setConnectTimeout(int duration, TimeUnit unit) {
          this.connectTimeout = unit.toMillis(duration);
      }
  
      @Override
      public void setReadTimeout(int duration, TimeUnit unit) {
          this.readTimeout = unit.toMillis(duration);
      }
  
      @Override
      public void setCharset(String charsetName) {
          this.charset = charsetName;
      }
  
      @Override
      public void setConnectionKeepAlive(boolean connectionKeepAlive) {
          this.connectionKeepAlive = connectionKeepAlive;
      }
  
      /*
       * We need this in order to stub the connection object for test cases
       */
      void setConnection(HttpURLConnection connection) {
          this.connection = connection;
      }
  
      @Override
      public String toString() {
          return String.format("@Request(%s %s)", getVerb(), getUrl());
      }
  
      @Override
      public void addBodyParameters(Map<String, ?> toAdd) {
          for (String key : toAdd.keySet()) {
              addBodyParameter(key, toAdd.get(key).toString());
  
          }
      }
  }
  