From 05b8e06255bd3516fb586c7d6d345e318340c792 Mon Sep 17 00:00:00 2001 From: Nathan Fischer Date: Sun, 14 Aug 2016 19:02:49 -0700 Subject: [PATCH] Added support for reverse variable matching Bumped Java version to 8 --- .travis.yml | 3 +- README.md | 11 ++ notes.md | 24 ---- pom.xml | 4 +- .../com/damnhandy/uri/template/Either.java | 48 +++++++ .../damnhandy/uri/template/Expression.java | 135 ++++++++++++------ .../damnhandy/uri/template/UriTemplate.java | 29 ++-- .../damnhandy/uri/template/TestMatching.java | 120 +++++++++++++++- 8 files changed, 293 insertions(+), 81 deletions(-) delete mode 100644 notes.md create mode 100644 src/main/java/com/damnhandy/uri/template/Either.java diff --git a/.travis.yml b/.travis.yml index a8cc93cf..bab32b93 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: java jdk: - - openjdk7 - - oraclejdk7 + - openjdk8 - oraclejdk8 sudo: false install: ./mvnw clean diff --git a/README.md b/README.md index a7114797..706c491a 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,17 @@ This will yield the following URL template string: This API is still a work in progress an feedback is appreciated. +## Reverse Variable Matching + +A `UriTemplate` can also be used to extract variables from a URI like this: + +```java +UriTemplate template = UriTemplate.fromTemplate("https://example.com/collection{/id}{?orderBy}"); +template.setFrom("https://example.com/collection/9?orderBy=age"); +System.out.println(template.get("id")); // 9 +System.out.println(template.get("orderBy")); // age +``` + ## Using with HTTP Clients The API can be used with existing HTTP frameworks like the most excellent [Async Http Client](https://github.com/sonatype/async-http-client). Using the [GitHub API](http://developer.github.com/v3/repos/commits/), we can use the a `UriTemplate` to create a URI to look at this repository: diff --git a/notes.md b/notes.md deleted file mode 100644 index b59ea287..00000000 --- a/notes.md +++ /dev/null @@ -1,24 +0,0 @@ -challenges - -unreserved character encoding lives at the expression level, but we need that -info to build regex at varspec level - -matching of fragment and unreserved characters in list is seemingly impossible -since `,` is allowed in fragments, but is also the separator token. The RFC seems -to note this so most likely the solution is to document that we don't support -values with reserved delimiters - -does order need to be considered for key-value paired expressions like ; ? & ifso -named capture groups cannot be used here -idea: -expressions surface two methods, `getMatchPattern()` and `Map getMatches(Matcher)` -`getMatchPattern` returns a regex to match that expression, but rather than using -named capture groups which can be used at the top level, the expression uses -getMatches to return a map from variable names to values (which can internally use -ncg, or not). - -ex: `{/var,x}/here{?x,y,empty}` might generate pattern like -`(?(?\/[unreserved]*)(?\/[unreserved]*))\/here(?\?((x|y|empty)=([unreserved]*)&?){0,3})` -the expression `{/var,x}` would grab the group `TWFuIGlzI` and use the known group -names with ncg to get `var` and `x`, while `{?x,y,empty}` would look at each key value group tuple - diff --git a/pom.xml b/pom.xml index 89056cc8..a27bdce9 100755 --- a/pom.xml +++ b/pom.xml @@ -249,8 +249,8 @@ maven-compiler-plugin 3.5.1 - 1.6 - 1.6 + 1.8 + 1.8 diff --git a/src/main/java/com/damnhandy/uri/template/Either.java b/src/main/java/com/damnhandy/uri/template/Either.java new file mode 100644 index 00000000..7dd44bf1 --- /dev/null +++ b/src/main/java/com/damnhandy/uri/template/Either.java @@ -0,0 +1,48 @@ +package com.damnhandy.uri.template; + +import static java.util.Objects.requireNonNull; + +/** + * Created by nfischer on 8/14/2016. + */ +public class Either { + public final Left left; + public final Right right; + + private Either(Left left, Right right) { + this.left = left; + this.right = right; + } + + public static Either left(Left left){ + requireNonNull(left); + return new Either<>(left, null); + } + + public static Either right(Right right){ + requireNonNull(right); + return new Either<>(null, right); + } + + public Object get(){ + if(left == null) + return right; + else return left; + } + + public boolean isLeft(){ + return left != null; + } + + public boolean isRight(){ + return !isLeft(); + } + + @Override + public String toString() { + return "Either{" + + "left=" + left + + ", right=" + right + + '}'; + } +} diff --git a/src/main/java/com/damnhandy/uri/template/Expression.java b/src/main/java/com/damnhandy/uri/template/Expression.java index 8758b5d2..3b10a70f 100644 --- a/src/main/java/com/damnhandy/uri/template/Expression.java +++ b/src/main/java/com/damnhandy/uri/template/Expression.java @@ -20,11 +20,14 @@ import com.damnhandy.uri.template.impl.Operator; import com.damnhandy.uri.template.impl.VarSpec; -import java.util.ArrayList; -import java.util.List; +import java.security.SecureRandom; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static com.damnhandy.uri.template.Either.*; +import static java.util.stream.Collectors.toList; + /** *

* An Expression represents the text between '{' and '}', including the enclosing @@ -82,6 +85,14 @@ public class Expression extends UriTemplateComponent */ private Pattern matchPattern; + private String groupName = uid(); + + private static Random RANDOM = new SecureRandom(); + private static String uid(){ + byte[] b = new byte[12]; + RANDOM.nextBytes(b); + return 'x'+Base64.getUrlEncoder().encodeToString(b).replace('-', '0').replace('_', '9'); + } /** * Creates a new {@link Builder} to create a simple expression according @@ -350,57 +361,101 @@ else if (varname.lastIndexOf(Modifier.EXPLODE.getValue()) > 0) this.varSpecs = varspecs; } + public Map>> variables(String uriPart){ + if(getOperator().isNamed()){ + return matchParameters(uriPart); + }else{ + return matchSegments(uriPart); + } + } + private Map>> matchParameters(String part){ + final String separator = getOperator().getSeparator(); + List varNames = getVarSpecs().stream() // todo use the varspecs to account for explosions and prefix mods + .map(VarSpec::getVariableName) + .collect(toList()); + StringBuilder regex; + Map> results = new HashMap<>(); - private Pattern buildMatchingPattern() - { - StringBuilder b = new StringBuilder(); + StringBuilder varNameRex = new StringBuilder("(?"); + for(String varName:varNames){ + varNameRex.append(Pattern.quote(varName)).append('|'); + } + varNameRex.deleteCharAt(varNameRex.length()-1); + varNameRex.append(')'); - if(getOperator() != Operator.RESERVED) //todo expression prefix vs expansion prefix - { - final String prefix = getOperator().getPrefix(); - if(prefix.length() > 0){ - b.append('\\').append(getOperator().getPrefix()); - } + regex = new StringBuilder() + .append(varNameRex) + .append('=') + .append("(?[^") + .append(separator) // todo replace this with character classes based on allowed encoding + .append("]*)"); + + Pattern pattern = Pattern.compile(regex.toString()); + Matcher matcher = pattern.matcher(part); + + while(matcher.find()){ + String key = matcher.group(1); + String value = matcher.group(2); + + List values = results.getOrDefault(key, new ArrayList<>()); + values.add(value); + + results.put(key, values); } - String unreserved = "[\\w-\\d.~]"; - String reserved = "[:\\/?#\\[\\]@!$&'()*+;=]"; //todo removing , for now - String encoded = "%[A-Fa-f\\d]{2}"; + Map>> ret = new HashMap<>(); + results.forEach((k,v) -> ret.put(k, v.size() == 1 ? left(v.get(0)) : right(v))); + return ret; + } - for(VarSpec v : getVarSpecs()){ + private Map>> matchSegments(String part){ + final String prefix = getOperator().getPrefix(); + final String separator = getOperator().getSeparator(); + List varNames = getVarSpecs().stream() + .map(VarSpec::getVariableName) + .collect(toList()); - if(getOperator() == Operator.MATRIX - || getOperator() == Operator.CONTINUATION - || getOperator() == Operator.QUERY) - b.append(v.getVariableName()) - .append('='); + StringBuilder regex = prefix == null || prefix.isEmpty() ? new StringBuilder() : new StringBuilder('\\').append(prefix); + Map>> results = new HashMap<>(); - b - .append("(?<") - .append(v.getVariableName()) - .append('>'); + for(String varName:varNames){ + regex.append("(?<").append(Pattern.quote(varName)).append(">[^").append(separator).append("]*)").append(separator); + } - if(getOperator().getEncoding() == UriTemplate.Encoding.U) - b.append("(?:" + unreserved + "|" + encoded + ')'); - else - b - .append("(?:") - .append(unreserved) - .append('|') - .append(reserved) - .append('|') - .append(encoded) - .append(')'); - b.append('*'); //todo this needs to look at the varspec to check for prefix modifier - b - .append(')') - .append(getOperator().getSeparator()) - .append('?'); + regex.deleteCharAt(regex.length() -1); + + Pattern pattern = Pattern.compile(regex.toString()); + Matcher matcher = pattern.matcher(part); + + if (!matcher.matches()) + throw new RuntimeException("doesn't match"); + + for(String varName:varNames){ + results.put(varName, left(matcher.group(varName))); } + return results; + } + + public String getGroupName(){ + return this.groupName; + } + + // todo improve this + private Pattern buildMatchingPattern() + { + String prefix = getOperator().getPrefix(); + prefix = prefix.equals("+") ? "" : prefix; + StringBuilder b = new StringBuilder("(?<") + .append(groupName) + .append('>') + .append('\\') + .append(prefix) + .append(".{1,9001}") + .append(')'); return Pattern.compile(b.toString()); } diff --git a/src/main/java/com/damnhandy/uri/template/UriTemplate.java b/src/main/java/com/damnhandy/uri/template/UriTemplate.java index baf3b950..7007d15d 100644 --- a/src/main/java/com/damnhandy/uri/template/UriTemplate.java +++ b/src/main/java/com/damnhandy/uri/template/UriTemplate.java @@ -27,6 +27,7 @@ import java.text.SimpleDateFormat; import java.util.*; import java.util.Map.Entry; +import java.util.regex.Matcher; import java.util.regex.Pattern; /** @@ -338,7 +339,7 @@ private void buildReverseMatchRegexFromComponents() StringBuilder b = new StringBuilder(); for (UriTemplateComponent c : components) { - b.append("(").append(c.getMatchPattern()).append(")"); + b.append('(').append(c.getMatchPattern()).append(')'); } this.reverseMatchPattern = Pattern.compile(b.toString()); } @@ -551,6 +552,18 @@ public UriTemplate set(String variableName, Date value) return this; } + public UriTemplate setFrom(String uri){ + Pattern pattern = getReverseMatchPattern(); + Matcher matcher = pattern.matcher(uri); + matcher.find(); + for(Expression expression :expressions){ + String part = matcher.group(expression.getGroupName()); + expression.variables(part).forEach( + (k, v) -> this.set(k, v.get())); + } + return this; + } + /** * Adds the name/value pairs in the supplied {@link Map} to the collection * of values within this URI template instance. @@ -1148,16 +1161,8 @@ private int[] getIndexForPartsWithNullsFirstIfQueryOrRegularSequnceIfNot(final E return index; } + public String toString(){ + return this.expand(); + } - - - /** - * - * - * @return - */ -// public String getRegexString() -// { -// return null; -// } } diff --git a/src/test/java/com/damnhandy/uri/template/TestMatching.java b/src/test/java/com/damnhandy/uri/template/TestMatching.java index 5a1bbe18..cbed5d7f 100644 --- a/src/test/java/com/damnhandy/uri/template/TestMatching.java +++ b/src/test/java/com/damnhandy/uri/template/TestMatching.java @@ -2,9 +2,14 @@ import org.junit.Test; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; /** @@ -12,7 +17,6 @@ */ public class TestMatching { - @Test public void test(){ Pattern temp1 = UriTemplate.buildFromTemplate("{hello}").build().getReverseMatchPattern(); String val1 = "Hello World!"; @@ -48,6 +52,7 @@ public void test(){ Pattern temp4 = UriTemplate.buildFromTemplate("{?x,y,empty}").build().getReverseMatchPattern(); String exp4 = "?x=1024&y=768&empty="; + System.out.println(temp4); Matcher m4 = temp4.matcher(exp4); m4.find(); @@ -57,5 +62,118 @@ public void test(){ assertEquals("1024", m4.group("x")); assertEquals("", m4.group("empty")); assertEquals("768", m4.group("y")); + + + System.out.println(matchParameters('?', '&', "?x=1024&y=768&empty=", asList("y", "x", "empty"))); + + print(matchSegments('#', ',', "#/foo/bar,1024", asList("path", "x"))); + print(matchSegments('\0', ',', "1024,Hello%20World%21,768", asList("x", "hello", "y"))); + + Map multi = matchParameters('?', '&', "?x=1024&y=768&y=othery&empty=", asList("y", "x", "empty")); + multi.entrySet().forEach(e -> { + System.out.print(e.getKey() + "="); + System.out.print(asList(e.getValue())); + System.out.print(' '); + }); + + + + + } + + @Test + public void test2(){ + UriTemplate template = UriTemplate.buildFromTemplate("mysite.com/some{/path}/stuff{?query}&parts=too{&extensions}").build(); + UriTemplate expanded = UriTemplate + .buildFromTemplate("mysite.com/some{/path}/stuff{?query}&parts=too{&extensions}") + .build() + .set("path", "pathVal") + .set("query", "queryVal") + .set("extensions", "extensionsVal"); + System.out.println(expanded.getValues()); + + template.setFrom("mysite.com/some/pathVal/stuff?query=queryVal&parts=too&extensions=extensionsVal"); + System.out.println(template.getValues()); + + template = UriTemplate.fromTemplate("https://example.com/collection{/id}{?orderBy}"); + template.setFrom("\"https://example.com/collection/9?orderBy=age"); + System.out.println(template.get("id")); // 9 + System.out.println(template.get("orderBy")); // age + System.out.println(template); + } + + void print(Map multi){ + System.out.print('{'); + multi.entrySet().forEach(e -> { + System.out.print(e.getKey() + "="); + System.out.print(asList(e.getValue())); + System.out.print(" "); + }); + System.out.println('}'); + } + + Map matchParameters(char prefix, char separator, String part, List varNames){ + String regex = "\\" + prefix; + Map> results = new HashMap<>(); + + String varNameRex = "(?"; + for(String varName:varNames){ + varNameRex += "\\Q" + varName + "\\E" + "|"; + } + varNameRex = varNameRex.substring(0, varNameRex.length() -1); + varNameRex += ")"; + + /* + for(int n = varNames.size(); n --> 0;){ + regex += varNameRex + '=' + "(?[^"+separator+"]*)" + separator; + } + */ + + regex = varNameRex + '=' + "(?[^"+separator+"]*)"; + + System.out.println(regex); + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(part); + + while(matcher.find()){ + String key = matcher.group(1); + String value = matcher.group(2); + + List values = results.getOrDefault(key, new ArrayList<>()); + values.add(value); + + results.put(key, values); + } + + Map ret = new HashMap<>(); + for(Map.Entry> entry:results.entrySet()) + ret.put(entry.getKey(), entry.getValue().toArray(new String[entry.getValue().size()])); + + return ret; + } + + Map matchSegments(char prefix, char separator, String part, List varNames){ + String regex = prefix == '\0' ? "" : "\\" + prefix; + Map results = new HashMap<>(); + + for(String varName:varNames){ + regex += "(?<"+varName+">[^"+separator+"]*)" + separator; + } + + regex = regex.substring(0, regex.length()-1); + + System.out.println(regex); + + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(part); + + if (!matcher.matches()) + throw new RuntimeException("doesn't match"); + + for(String varName:varNames){ + results.put(varName, new String[]{matcher.group(varName)}); + } + + return results; } }