docs: public static <T> String makeS3CanonicalString(String method,
              String resource, SignableRequest<T> request, String expires,
              Collection<String> additionalQueryParamsToSign) {
  
          StringBuilder buf = new StringBuilder();
          buf.append(method + "\n");
  
          // Add all interesting headers to a list, then sort them.  "Interesting"
          // is defined as Content-MD5, Content-Type, Date, and x-amz-
          Map<String, String> headersMap = request.getHeaders();
          SortedMap<String, String> interestingHeaders = new TreeMap<String, String>();
          if (headersMap != null && headersMap.size() > 0) {
              Iterator<Map.Entry<String, String>> headerIter = headersMap.entrySet().iterator();
              while (headerIter.hasNext()) {
                  Map.Entry<String, String> entry = (Map.Entry<String, String>) headerIter.next();
                  String key = entry.getKey();
                  String value = entry.getValue();
  
                  if (key == null) continue;
                  String lk = StringUtils.lowerCase(key);
  
                  // Ignore any headers that are not particularly interesting.
                  if (lk.equals("content-type") || lk.equals("content-md5") || lk.equals("date") ||
                      lk.startsWith(Headers.AMAZON_PREFIX))
                  {
                      interestingHeaders.put(lk, value);
                  }
              }
          }
  
          // Remove default date timestamp if "x-amz-date" is set.
          if (interestingHeaders.containsKey(Headers.S3_ALTERNATE_DATE)) {
              interestingHeaders.put("date", "");
          }
  
          // Use the expires value as the timestamp if it is available. This trumps both the default
          // "date" timestamp, and the "x-amz-date" header.
          if (expires != null) {
              interestingHeaders.put("date", expires);
          }
  
          // These headers require that we still put a new line in after them,
          // even if they don't exist.
          if (! interestingHeaders.containsKey("content-type")) {
              interestingHeaders.put("content-type", "");
          }
          if (! interestingHeaders.containsKey("content-md5")) {
              interestingHeaders.put("content-md5", "");
          }
  
          // Any parameters that are prefixed with "x-amz-" need to be included
          // in the headers section of the canonical string to sign
          final Map<String, List<String>> requestParameters = request
                  .getParameters();
          for (Map.Entry<String, List<String>> parameter : requestParameters
                  .entrySet()) {
              if (parameter.getKey().startsWith("x-amz-")) {
                  StringBuilder parameterValueBuilder = new StringBuilder();
                  /**
                   *
                   * We don't need to url encode here. If a parameter has multiple
                   * values, then those values needs to be combined to a comma
                   * separated list and assigned to the header.
                   *
                   * Reference : http://docs.aws.amazon.com/AmazonS3/latest/dev/
                   * RESTAuthentication
                   * .html#RESTAuthenticationRequestCanonicalization
                   */
                  for (String value : parameter.getValue()) {
                      if (parameterValueBuilder.length() > 0) {
                          parameterValueBuilder.append(",");
                      }
                      parameterValueBuilder.append(value);
                  }
                  interestingHeaders.put(parameter.getKey(),
                          parameterValueBuilder.toString());
              }
          }
  
          // Add all the interesting headers (i.e.: all that startwith x-amz- ;-))
          for (Iterator<Map.Entry<String, String>> i = interestingHeaders.entrySet().iterator(); i.hasNext(); ) {
              Map.Entry<String, String> entry = (Map.Entry<String, String>) i.next();
              String key = (String) entry.getKey();
              String value = entry.getValue();
  
              if (key.startsWith(Headers.AMAZON_PREFIX)) {
                  buf.append(key).append(':');
                  if (value != null) {
                      buf.append(value);
                  }
              } else if (value != null) {
                  buf.append(value);
              }
              buf.append("\n");
          }
  
          // Add all the interesting parameters
          buf.append(resource);
          String[] parameterNames = requestParameters.keySet().toArray(
                                  new String[request.getParameters().size()]);
          Arrays.sort(parameterNames);
  
          StringBuilder queryParams = new StringBuilder();
          for (String parameterName : parameterNames) {
              if ( !SIGNED_PARAMETERS.contains(parameterName)
                   &&
                   (additionalQueryParamsToSign == null ||
                   !additionalQueryParamsToSign.contains(parameterName))
                 ) {
                  continue;
              }
  
              /**
               * As per the spec given in the below URL, it is not clear as to
               * whether we need to sort the parameter values when forming the
               * string to sign. This is something that needs to be watched if we
               * receive signing problems.
               *
               * Reference : http://docs.aws.amazon.com/AmazonS3/latest/dev/
               * RESTAuthentication
               * .html#RESTAuthenticationRequestCanonicalization
               */
              List<String> values = requestParameters.get(parameterName);
              for (String value : values) {
                  queryParams = queryParams.length() > 0 ? queryParams
                          .append("&") : queryParams.append("?");
  
                  queryParams.append(parameterName);
                  if (value != null) {
                      queryParams.append("=").append(value);
                  }
              }
          }
          buf.append(queryParams.toString());
  
          return buf.toString();
      }