UriMatchers.java

/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * 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 androidx.test.espresso.intent.matcher;

import static androidx.test.espresso.intent.Checks.checkArgument;
import static androidx.test.espresso.intent.Checks.checkNotNull;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;

import android.net.Uri;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;

/**
 * A collection of matchers for {@link Uri}s, which can match Uris on their properties (host, path,
 * ...).
 */
public final class UriMatchers {

  /** Container class for paramName and pramValues. */
  static class QueryParamEntry {

    String paramName;
    Iterable<String> paramVals;

    public QueryParamEntry(String paramName, Iterable<String> paramVals) {
      this.paramName = paramName;
      this.paramVals = paramVals;
    }
  }

  /** Container class for schemeName and schemeValues. */
  static class SchemeParamValue {

    String schemeName;
    String schemeVals;

    public SchemeParamValue(String schemeName, String schemeVals) {
      this.schemeName = schemeName;
      this.schemeVals = schemeVals;
    }
  }

  /*
   * Private constructor to make sure no one instantiate this class.
   */
  private UriMatchers() {}

  /**
   * Returns a set of the unique names of all query parameters. Iterating over the set will return
   * the names in order of their first occurrence.
   *
   * <p>This method was added to Uri class in android api 11. So, for working with API level less
   * then that we had to re-implement it.
   *
   * @throws UnsupportedOperationException if this isn't a hierarchical URI
   * @return a set of decoded names
   */
  // VisibleForTesting
  static Set<String> getQueryParameterNames(Uri uri) {
    checkArgument(!uri.isOpaque(), "This isn't a hierarchical URI.");
    String query = uri.getEncodedQuery();
    if (query == null) {
      return Collections.emptySet();
    }

    Set<String> names = new LinkedHashSet<String>();
    int start = 0;
    do {
      int next = query.indexOf('&', start);
      int end = (next == -1) ? query.length() : next;

      int separator = query.indexOf('=', start);
      if (separator > end || separator == -1) {
        separator = end;
      }

      String name = query.substring(start, separator);
      names.add(Uri.decode(name));

      // Move start to end of name.
      start = end + 1;
    } while (start < query.length());

    return Collections.unmodifiableSet(names);
  }

  public static Matcher<Uri> hasHost(String host) {
    return hasHost(is(host));
  }

  public static Matcher<Uri> hasHost(final Matcher<String> hostMatcher) {
    checkNotNull(hostMatcher);

    return new TypeSafeMatcher<Uri>() {

      @Override
      public boolean matchesSafely(Uri uri) {
        return hostMatcher.matches(uri.getHost());
      }

      @Override
      public void describeTo(Description description) {
        description.appendText("has host: ");
        description.appendDescriptionOf(hostMatcher);
      }
    };
  }

  public static Matcher<Uri> hasParamWithName(String paramName) {
    return hasParamWithName(is(paramName));
  }

  public static Matcher<Uri> hasParamWithName(final Matcher<String> paramName) {
    checkNotNull(paramName);

    return new TypeSafeMatcher<Uri>() {

      @Override
      public boolean matchesSafely(Uri uri) {
        return hasItem(paramName).matches(getQueryParameterNames(uri));
      }

      @Override
      public void describeTo(Description description) {
        description.appendText("has param with name: ");
        description.appendDescriptionOf(paramName);
      }
    };
  }

  public static Matcher<Uri> hasPath(String pathName) {
    return hasPath(is(pathName));
  }

  public static Matcher<Uri> hasPath(final Matcher<String> pathName) {
    checkNotNull(pathName);

    return new TypeSafeMatcher<Uri>() {

      @Override
      public boolean matchesSafely(Uri uri) {
        return pathName.matches(uri.getPath());
      }

      @Override
      public void describeTo(Description description) {
        description.appendText("has path: ");
        description.appendDescriptionOf(pathName);
      }
    };
  }

  public static Matcher<Uri> hasParamWithValue(String paramName, String paramVal) {
    return hasParamWithValue(is(paramName), is(paramVal));
  }

  public static Matcher<Uri> hasParamWithValue(
      final Matcher<String> paramName, final Matcher<String> paramVal) {
    checkNotNull(paramName);
    checkNotNull(paramVal);
    final Matcher<QueryParamEntry> qpe = queryParamEntry(paramName, paramVal);
    final Matcher<Iterable<? super QueryParamEntry>> matcherImpl = hasItem(qpe);

    return new TypeSafeMatcher<Uri>() {

      @Override
      public boolean matchesSafely(Uri uri) {
        List<QueryParamEntry> qpes = new ArrayList<>();
        for (String name : getQueryParameterNames(uri)) {
          qpes.add(new QueryParamEntry(name, uri.getQueryParameters(name)));
        }
        return matcherImpl.matches(qpes);
      }

      @Override
      public void describeTo(Description description) {
        description.appendText("has param with: name: ");
        description.appendDescriptionOf(paramName);
        description.appendText(" value: ");
        description.appendDescriptionOf(paramVal);
      }
    };
  }

  private static Matcher<QueryParamEntry> queryParamEntry(
      final Matcher<String> paramName, final Matcher<String> paramVal) {
    final Matcher<Iterable<? super String>> valMatcher = hasItem(paramVal);

    return new TypeSafeMatcher<QueryParamEntry>(QueryParamEntry.class) {

      @Override
      public boolean matchesSafely(QueryParamEntry qpe) {
        return paramName.matches(qpe.paramName) && valMatcher.matches(qpe.paramVals);
      }

      @Override
      public void describeTo(Description description) {
        description.appendDescriptionOf(paramName);
        description.appendDescriptionOf(paramVal);
      }
    };
  }

  public static Matcher<Uri> hasScheme(String scheme) {
    return hasScheme(is(scheme));
  }

  public static Matcher<Uri> hasScheme(final Matcher<String> schemeMatcher) {
    checkNotNull(schemeMatcher);

    return new TypeSafeMatcher<Uri>() {

      @Override
      public boolean matchesSafely(Uri uri) {
        return schemeMatcher.matches(uri.getScheme());
      }

      @Override
      public void describeTo(Description description) {
        description.appendText("has scheme: ");
        description.appendDescriptionOf(schemeMatcher);
      }
    };
  }

  public static Matcher<Uri> hasSchemeSpecificPart(String scheme, String schemeSpecificPart) {
    return hasSchemeSpecificPart(is(scheme), is(schemeSpecificPart));
  }

  public static Matcher<Uri> hasSchemeSpecificPart(
      final Matcher<String> schemeMatcher, final Matcher<String> schemeSpecificPartMatcher) {
    checkNotNull(schemeMatcher);
    checkNotNull(schemeSpecificPartMatcher);

    return new TypeSafeMatcher<Uri>() {

      @Override
      public boolean matchesSafely(Uri uri) {
        return schemeMatcher.matches(uri.getScheme())
            && schemeSpecificPartMatcher.matches(uri.getSchemeSpecificPart());
      }

      @Override
      public void describeTo(Description description) {
        description.appendText("has scheme specific part: scheme: ");
        description.appendDescriptionOf(schemeMatcher);
        description.appendText(" scheme specific part: ");
        description.appendDescriptionOf(schemeSpecificPartMatcher);
      }
    };
  }
}