docs: static String createAuthorizationSignature(
              HttpServletRequest request, String uri, String credential,
              boolean queryAuth, boolean bothDateHeader) {
          // sort Amazon headers
          SortedSetMultimap<String, String> canonicalizedHeaders =
                  TreeMultimap.create();
          for (String headerName : Collections.list(request.getHeaderNames())) {
              Collection<String> headerValues = Collections.list(
                      request.getHeaders(headerName));
              headerName = headerName.toLowerCase();
              if (!headerName.startsWith("x-amz-") || (bothDateHeader &&
                    headerName.equalsIgnoreCase(AwsHttpHeaders.DATE))) {
                  continue;
              }
              if (headerValues.isEmpty()) {
                  canonicalizedHeaders.put(headerName, "");
              }
              for (String headerValue : headerValues) {
                  canonicalizedHeaders.put(headerName,
                          Strings.nullToEmpty(headerValue));
              }
          }
  
          // Build string to sign
          StringBuilder builder = new StringBuilder()
                  .append(request.getMethod())
                  .append('\n')
                  .append(Strings.nullToEmpty(request.getHeader(
                          HttpHeaders.CONTENT_MD5)))
                  .append('\n')
                  .append(Strings.nullToEmpty(request.getHeader(
                          HttpHeaders.CONTENT_TYPE)))
                  .append('\n');
          String expires = request.getParameter("Expires");
          if (queryAuth) {
              // If expires is not nil, then it is query string sign
              // If expires is nil, maybe also query string sign
              // So should check other accessid param, presign to judge.
              // not the expires
              builder.append(Strings.nullToEmpty(expires));
          }  else {
              if (!bothDateHeader) {
                  if (canonicalizedHeaders.containsKey(AwsHttpHeaders.DATE)) {
                      builder.append("");
                  } else {
                      builder.append(request.getHeader(HttpHeaders.DATE));
                  }
              }  else {
                  if (!canonicalizedHeaders.containsKey(AwsHttpHeaders.DATE)) {
                      builder.append(request.getHeader(AwsHttpHeaders.DATE));
                  }  else {
                      // panic
                  }
              }
          }
  
          builder.append('\n');
          for (Map.Entry<String, String> entry : canonicalizedHeaders.entries()) {
              builder.append(entry.getKey()).append(':')
                      .append(entry.getValue()).append('\n');
          }
          builder.append(uri);
  
          char separator = '?';
          List<String> subresources = Collections.list(
                  request.getParameterNames());
          Collections.sort(subresources);
          for (String subresource : subresources) {
              if (SIGNED_SUBRESOURCES.contains(subresource)) {
                  builder.append(separator).append(subresource);
  
                  String value = request.getParameter(subresource);
                  if (!"".equals(value)) {
                      builder.append('=').append(value);
                  }
                  separator = '&';
              }
          }
  
          String stringToSign = builder.toString();
          logger.trace("stringToSign: {}", stringToSign);
  
          // Sign string
          Mac mac;
          try {
              mac = Mac.getInstance("HmacSHA1");
              mac.init(new SecretKeySpec(credential.getBytes(
                      StandardCharsets.UTF_8), "HmacSHA1"));
          } catch (InvalidKeyException | NoSuchAlgorithmException e) {
              throw new RuntimeException(e);
          }
          return BaseEncoding.base64().encode(mac.doFinal(
                  stringToSign.getBytes(StandardCharsets.UTF_8)));
      }