summaryrefslogtreecommitdiffstats
path: root/jdisc_core/src/main/java/com/yahoo/jdisc/application/UriPattern.java
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /jdisc_core/src/main/java/com/yahoo/jdisc/application/UriPattern.java
Publish
Diffstat (limited to 'jdisc_core/src/main/java/com/yahoo/jdisc/application/UriPattern.java')
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/UriPattern.java217
1 files changed, 217 insertions, 0 deletions
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/UriPattern.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/UriPattern.java
new file mode 100644
index 00000000000..6f587057c77
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/UriPattern.java
@@ -0,0 +1,217 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import java.net.URI;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * <p>This class holds a regular expression designed so that it only matches certain {@link URI}s. The constructor of
+ * this class accepts a simplified pattern string, and turns that into something that can be used to quickly match
+ * against URIs. This class also implements {@link Comparable} in such a way that stricter patterns order before looser
+ * patterns.</p>
+ *
+ * <p>Here are some examples of ordering:</p>
+ * <ul>
+ * <li><code>http://host/path</code> evaluated before <code>*://host/path</code></li>
+ * <li><code>http://host/path</code> evaluated before <code>http://&#42;/path</code></li>
+ * <li><code>http://a.host/path</code> evaluated before <code>http://*.host/path</code></li>
+ * <li><code>http://*.host/path</code> evaluated before <code>http://host/path</code></li>
+ * <li><code>http://host.a/path</code> evaluated before <code>http://host.&#42;/path</code></li>
+ * <li><code>http://host.&#42;/path</code> evaluated before <code>http://host/path</code></li>
+ * <li><code>http://host:80/path</code> evaluated before <code>http://host:&#42;/path</code></li>
+ * <li><code>http://host/path</code> evaluated before <code>http://host/*</code></li>
+ * <li><code>http://host/path/*</code> evaluated before <code>http://host/path</code></li>
+ * </ul>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class UriPattern implements Comparable<UriPattern> {
+
+ public static final int DEFAULT_PRIORITY = 0;
+ private static final Pattern PATTERN = Pattern.compile("([^:]+)://([^:/]+)(:((\\*)|([0-9]+)))?/(.*)",
+ Pattern.UNICODE_CASE | Pattern.CANON_EQ);
+ private final String pattern;
+ private final GlobPattern scheme;
+ private final GlobPattern host;
+ private final int port;
+ private final GlobPattern path;
+ private final int priority;
+
+ /**
+ * <p>Creates a new instance of this class that represents the given pattern string, with a priority of <tt>0</tt>.
+ * The input string must be on the form <code>&lt;scheme&gt;://&lt;host&gt;[:&lt;port&gt;]&lt;path&gt;</code>, where
+ * '*' can be used as a wildcard character at any position.</p>
+ *
+ * @param uri The pattern to parse.
+ * @throws IllegalArgumentException If the pattern could not be parsed.
+ */
+ public UriPattern(String uri) {
+ this(uri, DEFAULT_PRIORITY);
+ }
+
+ /**
+ * <p>Creates a new instance of this class that represents the given pattern string, with the given priority. The
+ * input string must be on the form <code>&lt;scheme&gt;://&lt;host&gt;[:&lt;port&gt;]&lt;path&gt;</code>, where
+ * '*' can be used as a wildcard character at any position.</p>
+ *
+ * @param uri The pattern to parse.
+ * @param priority The priority of this pattern.
+ * @throws IllegalArgumentException If the pattern could not be parsed.
+ */
+ public UriPattern(String uri, int priority) {
+ Matcher matcher = PATTERN.matcher(uri);
+ if (!matcher.find()) {
+ throw new IllegalArgumentException(uri);
+ }
+ scheme = GlobPattern.compile(resolvePatternComponent(matcher.group(1)));
+ host = GlobPattern.compile(resolvePatternComponent(matcher.group(2)));
+ port = resolvePortPattern(matcher.group(4));
+ path = GlobPattern.compile(resolvePatternComponent(matcher.group(7)));
+ pattern = scheme + "://" + host + ":" + (port > 0 ? port : "*") + "/" + path;
+ this.priority = priority;
+ }
+
+ /**
+ * <p>Attempts to match the given {@link URI} to this pattern. Note that only the scheme, host, port, and path
+ * components of the URI are used. Any query or fragment part is simply ignored.</p>
+ *
+ * @param uri The URI to match.
+ * @return A {@link Match} object describing the match found, or null if not found.
+ */
+ public Match match(URI uri) {
+ // Performance optimization: Match path first since scheme and host are often the same in a given binding repository.
+ String uriPath = resolveUriComponent(uri.getPath());
+ GlobPattern.Match pathMatch = path.match(uriPath, uriPath.startsWith("/") ? 1 : 0);
+ if (pathMatch == null) {
+ return null;
+ }
+ if (port > 0 && port != uri.getPort()) {
+ return null;
+ }
+ // Match scheme before host because it has a higher chance of differing (e.g. http versus https)
+ GlobPattern.Match schemeMatch = scheme.match(resolveUriComponent(uri.getScheme()));
+ if (schemeMatch == null) {
+ return null;
+ }
+ GlobPattern.Match hostMatch = host.match(resolveUriComponent(uri.getHost()));
+ if (hostMatch == null) {
+ return null;
+ }
+ return new Match(schemeMatch, hostMatch, port > 0 ? 0 : uri.getPort(), pathMatch);
+ }
+
+ @Override
+ public int hashCode() {
+ return pattern.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof UriPattern && pattern.equals(((UriPattern)obj).pattern);
+ }
+
+ @Override
+ public String toString() {
+ return pattern;
+ }
+
+ @Override
+ public int compareTo(UriPattern rhs) {
+ int cmp;
+ cmp = rhs.priority - priority;
+ if (cmp != 0) {
+ return cmp;
+ }
+ cmp = scheme.compareTo(rhs.scheme);
+ if (cmp != 0) {
+ return cmp;
+ }
+ cmp = host.compareTo(rhs.host);
+ if (cmp != 0) {
+ return cmp;
+ }
+ cmp = path.compareTo(rhs.path);
+ if (cmp != 0) {
+ return cmp;
+ }
+ cmp = rhs.port - port;
+ if (cmp != 0) {
+ return cmp;
+ }
+ return 0;
+ }
+
+ private static String resolveUriComponent(String str) {
+ return str != null ? str : "";
+ }
+
+ private static String resolvePatternComponent(String val) {
+ return val != null ? val : "*";
+ }
+
+ private static int resolvePortPattern(String str) {
+ if (str == null || str.equals("*")) {
+ return 0;
+ }
+ return Integer.parseInt(str);
+ }
+
+ /**
+ * <p>This class holds the result of a {@link UriPattern#match(URI)} operation. It contains methods to inspect the
+ * groups captured during matching, where a <em>group</em> is defined as a sequence of characters matches by a
+ * wildcard in the {@link UriPattern}.</p>
+ */
+ public static class Match {
+
+ private final GlobPattern.Match scheme;
+ private final GlobPattern.Match host;
+ private final int port;
+ private final GlobPattern.Match path;
+
+ private Match(GlobPattern.Match scheme, GlobPattern.Match host, int port, GlobPattern.Match path) {
+ this.scheme = scheme;
+ this.host = host;
+ this.port = port;
+ this.path = path;
+ }
+
+ /**
+ * <p>Returns the number of captured groups of this match. Any non-negative integer smaller than the value
+ * returned by this method is a valid group index for this match.</p>
+ *
+ * @return The number of captured groups.
+ */
+ public int groupCount() {
+ return scheme.groupCount() + host.groupCount() + (port > 0 ? 1 : 0) + path.groupCount();
+ }
+
+ /**
+ * <p>Returns the input subsequence captured by the given group by this match. Groups are indexed from left to
+ * right, starting at zero. Note that some groups may match an empty string, in which case this method returns
+ * the empty string. This method never returns null.</p>
+ *
+ * @param idx The index of the group to return.
+ * @return The (possibly empty) substring captured by the group during matching, never <tt>null</tt>.
+ * @throws IndexOutOfBoundsException If there is no group in the match with the given index.
+ */
+ public String group(int idx) {
+ int len = scheme.groupCount();
+ if (idx < len) {
+ return scheme.group(idx);
+ }
+ idx = idx - len;
+ len = host.groupCount();
+ if (idx < len) {
+ return host.group(idx);
+ }
+ idx = idx - len;
+ len = port > 0 ? 1 : 0;
+ if (idx < len) {
+ return String.valueOf(port);
+ }
+ idx = idx - len;
+ return path.group(idx);
+ }
+ }
+}