/**
   * This format supports Shared Key authentication for the 2009-09-19 version of the Blob and Queue
   * services. Construct the CanonicalizedResource string in this format as follows:
   *
   * <p>1. Beginning with an empty string (""), append a forward slash (/), followed by the name of
   * the account that owns the resource being accessed.
   *
   * <p>2. Append the resource's encoded URI path, without any query parameters.
   *
   * <p>3. Retrieve all query parameters on the resource URI, including the comp parameter if it
   * exists.
   *
   * <p>4. Convert all parameter names to lowercase.
   *
   * <p>5. Sort the query parameters lexicographically by parameter name, in ascending order.
   *
   * <p>6. URL-decode each query parameter name and value.
   *
   * <p>7. Append each query parameter name and value to the string in the following format, making
   * sure to include the colon (:) between the name and the value:
   *
   * <p>parameter-name:parameter-value
   *
   * <p>8. If a query parameter has more than one value, sort all values lexicographically, then
   * include them in a comma-separated list:
   *
   * <p>parameter-name:parameter-value-1,parameter-value-2,parameter-value-n
   *
   * <p>9. Append a new line character (\n) after each name-value pair.
   */
  private String getCanonicalizedResource(ClientRequest cr) {
    // 1. Beginning with an empty string (""), append a forward slash (/), followed by the name of
    // the account that owns
    //    the resource being accessed.
    String result = "/" + this.accountName;

    // 2. Append the resource's encoded URI path, without any query parameters.
    result += cr.getURI().getPath();

    // 3. Retrieve all query parameters on the resource URI, including the comp parameter if it
    // exists.
    // 6. URL-decode each query parameter name and value.
    List<QueryParam> queryParams = SharedKeyUtils.getQueryParams(cr.getURI().getQuery());

    // 4. Convert all parameter names to lowercase.
    for (QueryParam param : queryParams) {
      param.setName(param.getName().toLowerCase(Locale.US));
    }

    // 5. Sort the query parameters lexicographically by parameter name, in ascending order.
    Collections.sort(queryParams);

    // 7. Append each query parameter name and value to the string
    // 8. If a query parameter has more than one value, sort all values lexicographically, then
    // include them in a comma-separated list
    for (int i = 0; i < queryParams.size(); i++) {
      QueryParam param = queryParams.get(i);

      List<String> values = param.getValues();
      // Collections.sort(values);

      // 9. Append a new line character (\n) after each name-value pair.
      result += "\n";
      result += param.getName();
      result += ":";
      for (int j = 0; j < values.size(); j++) {
        if (j > 0) {
          result += ",";
        }
        result += values.get(j);
      }
    }

    return result;
  }
 private Request buildRequest(ClientRequest req) throws IOException {
   final Request request =
       client.newRequest(req.getURI()).method(HttpMethod.fromString(req.getMethod()));
   if (req.getEntity() != null) {
     final RequestEntityWriter writer = getRequestEntityWriter(req);
     final PipedOutputStream output = new PipedOutputStream();
     final PipedInputStream input = new PipedInputStream(output);
     request.content(new InputStreamContentProvider(input));
     writer.writeRequestEntity(output);
   }
   return request;
 }
  public ClientResponse handle(ClientRequest clientRequest) {
    byte[] requestEntity = writeRequestEntity(clientRequest);

    InBoundHeaders rh = getInBoundHeaders(clientRequest.getMetadata());

    final ContainerRequest cRequest =
        new ContainerRequest(
            w,
            clientRequest.getMethod(),
            baseUri,
            clientRequest.getURI(),
            rh,
            new ByteArrayInputStream(requestEntity));

    // TODO this is a hack
    List<String> cookies = cRequest.getRequestHeaders().get("Cookie");
    if (cookies != null) {
      for (String cookie : cookies) {
        if (cookie != null) cRequest.getCookies().putAll(HttpHeaderReader.readCookies(cookie));
      }
    }

    final TstContainerResponseWriter writer = new TstContainerResponseWriter();
    final ContainerResponse cResponse = new ContainerResponse(w, cRequest, writer);

    try {
      w.handleRequest(cRequest, cResponse);
    } catch (IOException e) {
      throw new ContainerException(e);
    }

    byte[] responseEntity = writer.baos.toByteArray();
    ClientResponse clientResponse =
        new ClientResponse(
            cResponse.getStatus(),
            getInBoundHeaders(cResponse.getHttpHeaders()),
            new ByteArrayInputStream(responseEntity),
            getMessageBodyWorkers());

    clientResponse.getProperties().put("request.entity", requestEntity);
    clientResponse.getProperties().put("response.entity", responseEntity);
    return clientResponse;
  }
  private ClientResponse _invoke(final ClientRequest ro) throws IOException {
    final HttpURLConnection uc;

    if (this.httpURLConnectionFactory == null) {
      uc = (HttpURLConnection) ro.getURI().toURL().openConnection();
    } else {
      uc = this.httpURLConnectionFactory.getHttpURLConnection(ro.getURI().toURL());
    }

    Integer readTimeout = (Integer) ro.getProperties().get(ClientConfig.PROPERTY_READ_TIMEOUT);
    if (readTimeout != null) {
      uc.setReadTimeout(readTimeout);
    }

    Integer connectTimeout =
        (Integer) ro.getProperties().get(ClientConfig.PROPERTY_CONNECT_TIMEOUT);
    if (connectTimeout != null) {
      uc.setConnectTimeout(connectTimeout);
    }

    Boolean followRedirects =
        (Boolean) ro.getProperties().get(ClientConfig.PROPERTY_FOLLOW_REDIRECTS);
    if (followRedirects != null) {
      uc.setInstanceFollowRedirects(followRedirects);
    }

    if (uc instanceof HttpsURLConnection) {
      HTTPSProperties httpsProperties =
          (HTTPSProperties) ro.getProperties().get(HTTPSProperties.PROPERTY_HTTPS_PROPERTIES);
      if (httpsProperties != null) {
        httpsProperties.setConnection((HttpsURLConnection) uc);
      }
    }

    Boolean httpUrlConnectionSetMethodWorkaround =
        (Boolean) ro.getProperties().get(PROPERTY_HTTP_URL_CONNECTION_SET_METHOD_WORKAROUND);
    if (httpUrlConnectionSetMethodWorkaround != null
        && httpUrlConnectionSetMethodWorkaround == true) {
      setRequestMethodUsingWorkaroundForJREBug(uc, ro.getMethod());
    } else {
      uc.setRequestMethod(ro.getMethod());
    }

    // Write the request headers
    writeOutBoundHeaders(ro.getHeaders(), uc);

    // Write the entity (if any)
    Object entity = ro.getEntity();
    if (entity != null) {
      uc.setDoOutput(true);

      if (ro.getMethod().equalsIgnoreCase("GET")) {
        final Logger logger = Logger.getLogger(URLConnectionClientHandler.class.getName());
        if (logger.isLoggable(Level.INFO)) {
          logger.log(
              Level.INFO,
              "GET method with entity will be most likely replaced by POST, see http://java.net/jira/browse/JERSEY-1161");
        }
      }

      writeRequestEntity(
          ro,
          new RequestEntityWriterListener() {
            public void onRequestEntitySize(long size) {
              if (size != -1 && size < Integer.MAX_VALUE) {
                // HttpURLConnection uses the int type for content length
                uc.setFixedLengthStreamingMode((int) size);
              } else {
                // TODO it appears HttpURLConnection has some bugs in
                // chunked encoding
                // uc.setChunkedStreamingMode(0);
                Integer chunkedEncodingSize =
                    (Integer) ro.getProperties().get(ClientConfig.PROPERTY_CHUNKED_ENCODING_SIZE);
                if (chunkedEncodingSize != null) {
                  uc.setChunkedStreamingMode(chunkedEncodingSize);
                }
              }
            }

            public OutputStream onGetOutputStream() throws IOException {
              return new CommittingOutputStream() {
                @Override
                protected OutputStream getOutputStream() throws IOException {
                  return uc.getOutputStream();
                }

                @Override
                public void commit() throws IOException {
                  writeOutBoundHeaders(ro.getHeaders(), uc);
                }
              };
            }
          });
    } else {
      writeOutBoundHeaders(ro.getHeaders(), uc);
    }

    // Return the in-bound response
    return new URLConnectionResponse(
        uc.getResponseCode(), getInBoundHeaders(uc), getInputStream(uc), ro.getMethod(), uc);
  }
  @Override
  public ClientResponse handle(final ClientRequest request) throws ClientHandlerException {

    // Remember if we sent a request a with headers
    boolean reqHadAuthHeaders = false;

    // Have we already login ? : Then add authorization info to the headers
    if (state.get().nextNonce != null) {

      // Remember we sent headers
      reqHadAuthHeaders = true;

      // Alias to string representation of qop
      String qopStr = null;
      if (state.get().qop != null) qopStr = (state.get().qop == QOP.AUTH_INT) ? "auth-int" : "auth";

      // Init the value of the "authorized" header
      StringBuffer buff = new StringBuffer();

      // Authorization scheme
      buff.append("Digest ");

      // Key/val pairs
      addKeyVal(buff, "username", this.user);
      addKeyVal(buff, "realm", state.get().realm);
      addKeyVal(buff, "nonce", state.get().nextNonce);
      if (state.get().opaque != null) addKeyVal(buff, "opaque", state.get().opaque);
      if (state.get().algorithm != null) addKeyVal(buff, "algorithm", state.get().algorithm, false);
      if (state.get().qop != null) addKeyVal(buff, "qop", qopStr, false);
      // if (this.domain != null) addKeyVal(buff, "domain", this.domain);

      // -------------------------------------------------------
      // Compute the Digest Hash
      // -------------------------------------------------------

      // HA1
      String HA1 = concatMD5(this.user, state.get().realm, this.pass);

      // Get exact requested URI
      String uri = request.getURI().getPath();

      // Repeat URI in header
      addKeyVal(buff, "uri", uri);

      // HA2 : Switch on qop
      String HA2;
      if (state.get().qop == QOP.AUTH_INT && (request.getEntity() != null)) {
        HA2 = concatMD5(request.getMethod(), uri, request.getEntity().toString());
      } else {
        HA2 = concatMD5(request.getMethod(), uri);
      }

      // Compute response
      String response;
      if (state.get().qop == null) { // Simple response

        response = concatMD5(HA1, state.get().nextNonce, HA2);

      } else { // Quality of protection is set

        // Generate client nonce (UID)
        String cnonce = randHexBytes(CNONCE_NB_BYTES);

        // Counter in hexadecimal
        String nc = String.format("%08x", state.get().counter);
        state.get().counter += 1;

        // Add them to key/value pairs
        addKeyVal(buff, "cnonce", cnonce);
        addKeyVal(buff, "nc", nc, false);

        response = concatMD5(HA1, state.get().nextNonce, nc, cnonce, qopStr, HA2);
      }

      // Append the response
      addKeyVal(buff, "response", response);

      // Remove the last coma
      buff.deleteCharAt(buff.length() - 1);
      String authLine = buff.toString();

      // Add the whole Authorization line to the header
      request.getMetadata().add(HttpHeaders.AUTHORIZATION, authLine);
    } // End of "we already logged in ?"

    // Forward the request to the next filter and get the result back
    ClientResponse response = getNext().handle(request);

    // The server asked for authentication ? (status 401)
    if (response.getResponseStatus() == Status.UNAUTHORIZED) {

      // Parse the www-authentication headers
      HashMap<String, String> map =
          parseHeaders(response.getHeaders().get(HttpHeaders.WWW_AUTHENTICATE));

      // No digest authentication request found ? => We can do nothing more
      if (map == null) return response;

      // Get header values
      state.get().realm = map.get("realm");
      state.get().nextNonce = map.get("nonce");
      state.get().opaque = map.get("opaque");
      state.get().algorithm = map.get("algorithm");
      state.get().domain = map.get("domain");

      // Parse Qop
      String qop = map.get("qop");
      if (qop == null) {
        state.get().qop = null;
      } else {
        if (qop.contains("auth-int")) {
          state.get().qop = QOP.AUTH_INT;
        } else if (qop.contains("auth")) {
          state.get().qop = QOP.AUTH;
        } else {
          state.get().qop = null;
        }
      }

      // Parse "stale"
      String staleStr = map.get("stale");

      boolean stale = (staleStr != null) && staleStr.toLowerCase().equals("true");

      // Did we send the initial request without headers ?
      // Or the server asked to retry with new nonce ?
      if (stale || !reqHadAuthHeaders) {
        // Then try to resent same request with updated headers
        return this.handle(request);
      } else {
        // We already tried to log, but the authentication failed :
        // Just forward this response
        return response;
      }
    }

    // Not 401 status : no authentication issue
    return response;
  } // End of #handle()