Artifact Content
Not logged in

Artifact 4531bedf91ef79f9f1c609eb0f19a844dd2ab087:


package org.interledger.cryptoconditions;

import java.io.UnsupportedEncodingException;

import java.net.URI;
import java.net.URLEncoder;

import java.util.Arrays;
import java.util.Base64;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * Provides utility methods related to named information (ni://) URI's.
 */
public class NamedInformationUri {

  public static final String SCHEME = "ni";
  public static final String SCHEME_PREFIX = SCHEME + "://";
  public static final String REGEX_STRICT = "^" + SCHEME_PREFIX + "([A-Za-z0-9_-]?)/"
      + getHashFunctionRegexGroup() + ";([a-zA-Z0-9_-]{0,86})\\?(.+)$";

  // TODO Could implement a parse function but for now it's probably faster to just parse the
  // condition directly NamedInformationUri.getUri(HashFunction.SHA_256, null);and not wrap that
  // around an ni URI parser

  /**
   * Creates a URI for the hash function and values.
   *
   * @param hashFunction The hash function used.
   * @param hash The value of the hash.
   * @param queryStringParams Additional values to include in the query string portion of the URI
   * @return A URI containing the hash function, the hashed value and any additional query
   *     parameters.
   */
  public static URI getUri(HashFunction hashFunction, byte[] hash,
      Map<String, String> queryStringParams) {

    Objects.requireNonNull(hashFunction);
    Objects.requireNonNull(hash);
    Objects.requireNonNull(queryStringParams);

    StringBuilder sb = new StringBuilder();
    sb.append(SCHEME_PREFIX) // Scheme prefix
        // No authority portion
        .append("/") // Path prefix
        .append(hashFunction.getName()).append(";") // Hash function name
        .append(Base64.getUrlEncoder().withoutPadding().encodeToString(hash)); // Hash

    if (!queryStringParams.isEmpty()) {
      sb.append("?");

      queryStringParams.forEach((key, value) -> {
        if (value != null && !"".equals(value)) {
          // Some params might contain multiple values, separate by a comma.  If so, we need to
          // break them up (by comma), encode each value, and then re-combine each encoded value
          // separated by a comma.
          final String encodedValue = Optional.ofNullable(value)
              .filter(val -> val.contains(","))
              // If there's a comma, then split the string into components by that comma...
              .map(valueWithComma -> Arrays.asList(value.split(",")).stream())
              // Encode each portion, and re-assemble with commas intact.
              .map(stringStream -> stringStream
                  .filter(v -> v != "")
                  .map(NamedInformationUri::encode)
                  .distinct()
                  .collect(Collectors.joining(",")))
              // If there's no comma in the value, then just encode and return.
              .orElseGet(() -> encode(value));

          sb.append(key).append("=").append(encodedValue);
          sb.append("&");
        }
      });

      // Delete the trailing "&" or "?"
      sb.deleteCharAt(sb.length() - 1);
    }

    return URI.create(sb.toString());
  }

  private static String encode(final String value) {
    try {
      return URLEncoder.encode(value, "UTF-8");
    } catch (UnsupportedEncodingException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Returns a regex listing the names of the HashFunctions.
   */
  private static String getHashFunctionRegexGroup() {
    return "("
        + String.join("|",
        Arrays.stream(HashFunction.values()).map(s -> s.getName()).toArray(String[]::new))
        + ")";
  }

  /**
   * From https://www.iana.org/assignments/hash-function-text-names/hash-function-text-names.xhtml
   *
   * @author adrianhopebailie
   */
  public enum HashFunction {
    MD2("md2", "1.2.840.113549.2.2"),
    MD5("md5", "1.2.840.113549.2.5"),
    SHA_1("sha-1", "1.3.14.3.2.26"),
    SHA_224("sha-224", "2.16.840.1.101.3.4.2.4"),
    SHA_256("sha-256", "2.16.840.1.101.3.4.2.1"),
    SHA_384("sha-384", "2.16.840.1.101.3.4.2.2"),
    SHA_512("sha-512", "2.16.840.1.101.3.4.2.3");

    private String name;
    private String oid;

    HashFunction(String name, String oid) {
      this.name = name;
      this.oid = oid;
    }

    /**
     * Returns the name of the hash function.
     */
    public String getName() {
      return name;
    }

    /**
     * Returns the OID for the hash function.
     */
    public String getOid() {
      return oid;
    }

  }

}