diff --git a/pom.xml b/pom.xml index 0363a53..dbd5b09 100644 --- a/pom.xml +++ b/pom.xml @@ -53,6 +53,7 @@ utils lang-de + web slf4j test @@ -115,6 +116,12 @@ 4.12 test + + org.hamcrest + hamcrest-core + 1.3 + test + org.hamcrest hamcrest-library diff --git a/test/matchers/pom.xml b/test/matchers/pom.xml new file mode 100644 index 0000000..ee61810 --- /dev/null +++ b/test/matchers/pom.xml @@ -0,0 +1,41 @@ + + + + + 4.0.0 + + + io.redlink.utils + redlink-utils + 2.1.0-SNAPSHOT + ../../ + + + hamcrest-matchers + Redlink Hamcrest Matchers + + + + + org.hamcrest + hamcrest-core + compile + + + diff --git a/test/matchers/src/main/java/io/redlink/utils/hamcrest/UriMatchers.java b/test/matchers/src/main/java/io/redlink/utils/hamcrest/UriMatchers.java new file mode 100644 index 0000000..f65daa7 --- /dev/null +++ b/test/matchers/src/main/java/io/redlink/utils/hamcrest/UriMatchers.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2019 Redlink GmbH. + */ +package io.redlink.utils.hamcrest; + +import java.net.URI; +import java.util.function.Function; +import org.hamcrest.CoreMatchers; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeDiagnosingMatcher; + +public final class UriMatchers { + + private UriMatchers() {} + + public static TypeSafeDiagnosingMatcher isURI(String uri) { + return isURI(URI.create(uri)); + } + + public static TypeSafeDiagnosingMatcher isURI(URI uri) { + return new TypeSafeDiagnosingMatcher() { + @Override + protected boolean matchesSafely(URI item, Description mismatchDescription) { + mismatchDescription.appendText("URI ").appendValue(item); + return uri == null ? item == null : uri.toASCIIString().equals(item.toASCIIString()); + } + + @Override + public void describeTo(Description description) { + description.appendText("URI ").appendValue(uri); + } + }; + } + + public static TypeSafeDiagnosingMatcher hasScheme(String scheme) { + return hasScheme(CoreMatchers.is(scheme)); + } + + public static TypeSafeDiagnosingMatcher hasScheme(Matcher schemeMatcher) { + return create("scheme", schemeMatcher, URI::getScheme); + } + + public static TypeSafeDiagnosingMatcher hasHost(String host) { + return hasHost(CoreMatchers.is(host)); + } + + public static TypeSafeDiagnosingMatcher hasHost(Matcher hostMatcher) { + return create("host", hostMatcher, URI::getHost); + } + + public static TypeSafeDiagnosingMatcher hasPort(int port) { + return hasPort(CoreMatchers.is(port)); + } + + public static TypeSafeDiagnosingMatcher hasPort(Matcher portMatcher) { + return create("port", portMatcher, URI::getPort); + } + + public static TypeSafeDiagnosingMatcher hasPath(String path) { + return hasPath(CoreMatchers.is(path)); + } + + public static TypeSafeDiagnosingMatcher hasPath(Matcher pathMatcher) { + return create("path", pathMatcher, URI::getPath); + } + + public static TypeSafeDiagnosingMatcher hasFragment(String fragment) { + return hasFragment(CoreMatchers.is(fragment)); + } + + public static TypeSafeDiagnosingMatcher hasFragment(Matcher fragmentMatcher) { + return create("fragment", fragmentMatcher, URI::getFragment); + } + + private static TypeSafeDiagnosingMatcher create(String uriPart, Matcher partMatcher, Function fkt) { + return new TypeSafeDiagnosingMatcher() { + @Override + protected boolean matchesSafely(URI item, Description mismatchDescription) { + if (item == null) { + mismatchDescription.appendText("URI ").appendValue(null); + return false; + } + + mismatchDescription.appendText("URI with ").appendText(uriPart).appendText(" "); + partMatcher.describeMismatch(fkt.apply(item), mismatchDescription); + + return partMatcher.matches(fkt.apply(item)); + } + + @Override + public void describeTo(Description description) { + description.appendText("URI with ").appendText(uriPart).appendText(" ") + .appendDescriptionOf(partMatcher); + } + }; + } + +} diff --git a/test/pom.xml b/test/pom.xml index ebbf5e1..ac3b46c 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -24,29 +24,14 @@ 2.1.0-SNAPSHOT - test + io.redlink.utils.test + test-utils pom Redlink Test Utilities testcontainers + matchers - - - - maven-install-plugin - - true - - - - maven-deploy-plugin - - true - - - - - \ No newline at end of file diff --git a/test/testcontainers/pom.xml b/test/testcontainers/pom.xml index fe66f08..649ac06 100644 --- a/test/testcontainers/pom.xml +++ b/test/testcontainers/pom.xml @@ -70,4 +70,4 @@ - \ No newline at end of file + diff --git a/web/pom.xml b/web/pom.xml new file mode 100644 index 0000000..4400e4a --- /dev/null +++ b/web/pom.xml @@ -0,0 +1,55 @@ + + + + + 4.0.0 + + + io.redlink.utils + redlink-utils + 2.1.0-SNAPSHOT + + + web + pom + Redlink Web Utilities + + + uri-builder + + + + + + maven-install-plugin + + true + + + + maven-deploy-plugin + + true + + + + + + + diff --git a/web/uri-builder/pom.xml b/web/uri-builder/pom.xml new file mode 100644 index 0000000..0288aad --- /dev/null +++ b/web/uri-builder/pom.xml @@ -0,0 +1,51 @@ + + + + + 4.0.0 + + + io.redlink.utils + redlink-utils + 2.1.0-SNAPSHOT + ../../ + + + uri-builder + URI Builder + + + + junit + junit + test + + + org.hamcrest + hamcrest-library + test + + + io.redlink.utils + hamcrest-matchers + ${project.version} + test + + + diff --git a/web/uri-builder/src/main/java/io/redlink/utils/web/uribuilder/UriBuilder.java b/web/uri-builder/src/main/java/io/redlink/utils/web/uribuilder/UriBuilder.java new file mode 100644 index 0000000..92f2aad --- /dev/null +++ b/web/uri-builder/src/main/java/io/redlink/utils/web/uribuilder/UriBuilder.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2019 Redlink GmbH. + */ +package io.redlink.utils.web.uribuilder; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Simple, light-weight UriBuilder. + */ +public class UriBuilder { + + private String scheme; + + private String userInfo; + + private String host; + + private int port = -1; + + private List pathSegments = new LinkedList<>(); + + private Map> queryParams = new LinkedHashMap<>(); + + private String fragment; + + public UriBuilder scheme(String scheme) { + this.scheme = scheme; + return this; + } + + public UriBuilder userInfo(String user, String password) { + return userInfo(user + ":" + password); + } + + public UriBuilder userInfo(String userInfo) { + this.userInfo = userInfo; + return this; + } + + public UriBuilder host(String host) { + this.host = host; + return this; + } + + public UriBuilder port(int port) { + this.port = port; + return this; + } + + public UriBuilder defaultPort() { + return port(-1); + } + + public UriBuilder removeFragment() { + return fragment(null); + } + + public UriBuilder fragment(String fragment) { + this.fragment = fragment; + return this; + } + + public UriBuilder path() { + return path(null); + } + + public UriBuilder path(String path) { + this.pathSegments = splitPath(path); + return this; + } + + public UriBuilder pathSegment(String pathSegment) { + this.pathSegments.addAll(splitPath(pathSegment)); + return this; + } + + public UriBuilder query(String name, String value) { + return query(name, Collections.singleton(value)); + } + + public UriBuilder query(String name, Collection values) { + this.queryParams.put(name, new LinkedList<>(values)); + return this; + } + + public UriBuilder query(String name, String... values) { + return query(name, Arrays.asList(values)); + } + + public UriBuilder removeQuery() { + this.queryParams.clear(); + return this; + } + + public UriBuilder removeQuery(String name) { + this.queryParams.remove(name); + return this; + } + + public UriBuilder addQuery(String name, String value) { + return addQuery(name, Collections.singleton(value)); + } + + public UriBuilder addQuery(String name, String... values) { + return addQuery(name, Arrays.asList(values)); + } + + public UriBuilder addQuery(String name, Collection values) { + this.queryParams.computeIfAbsent(name, n -> new LinkedList<>()).addAll(values); + return this; + } + + + + public URI build() throws URISyntaxException { + return new URI(scheme, userInfo, host, port, joinPath(pathSegments), toQueryString(queryParams), fragment); + } + + + + public static UriBuilder copy(UriBuilder builder) { + final UriBuilder copy = new UriBuilder(); + + copy.scheme = builder.scheme; + copy.userInfo = builder.userInfo; + copy.host = builder.host; + copy.port = builder.port; + + copy.pathSegments.addAll(builder.pathSegments); + builder.queryParams.forEach((k, v) -> copy.queryParams.put(k, new LinkedList<>(v))); + + return copy; + } + + public static UriBuilder create(String scheme, String host) { + return new UriBuilder() + .scheme(scheme) + .host(host); + } + + public static UriBuilder fromString(String uri) { + return fromUri(URI.create(uri)); + } + + public static UriBuilder fromUri(URI uri) { + final UriBuilder uriBuilder = new UriBuilder(); + + uriBuilder.scheme = uri.getScheme(); + uriBuilder.userInfo = uri.getUserInfo(); + uriBuilder.host = uri.getHost(); + uriBuilder.port = uri.getPort(); + + uriBuilder.pathSegments = splitPath(uri.getPath()); + uriBuilder.queryParams = fromQueryString(uri.getQuery()); + + return uriBuilder; + } + + private static String joinPath(List elements) { + return String.join("/", elements); + } + + private static List splitPath(String path) { + List segments = new LinkedList<>(); + if (path != null) { + segments.addAll(Arrays.asList(path.split("/"))); + } + return segments; + } + + private static String toQueryString(Map> elements) { + return elements.entrySet().stream() + .flatMap(e -> e.getValue().stream().map(v -> encode(e.getKey()) + "=" + encode(v))) + .collect(Collectors.joining("&")); + + } + + private static Map> fromQueryString(String queryString) { + final HashMap> query = new HashMap<>(); + if (queryString != null) { + Arrays.stream(queryString.split("&")) + .map(e -> e.split("=", 2)) + .map(Arrays::asList) + .map(v -> v.stream().map(UriBuilder::decode).collect(Collectors.toList())) + .forEach(v -> query.computeIfAbsent(v.get(0), k -> new LinkedList<>()).add(v.get(1))); + + + } + return query; + } + + private static String decode(String encoded) { + try { + return URLDecoder.decode(encoded, "utf-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } + + private static String encode(String raw) { + try { + return URLEncoder.encode(raw, "utf-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } +} + diff --git a/web/uri-builder/src/test/java/io/redlink/utils/web/uribuilder/UriBuilderTest.java b/web/uri-builder/src/test/java/io/redlink/utils/web/uribuilder/UriBuilderTest.java new file mode 100644 index 0000000..e333c9b --- /dev/null +++ b/web/uri-builder/src/test/java/io/redlink/utils/web/uribuilder/UriBuilderTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2019 redlink GmbH + * + * 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 io.redlink.utils.web.uribuilder; + +import io.redlink.utils.hamcrest.UriMatchers; +import io.redlink.utils.web.uribuilder.UriBuilder; +import java.net.URI; +import java.net.URISyntaxException; +import org.hamcrest.CoreMatchers; +import org.hamcrest.Matchers; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class UriBuilderTest { + + @Test + public void testFromString() throws URISyntaxException { + final UriBuilder uriBuilder = UriBuilder.fromString("http://www.example.com/foo/bar.xml?foo=bar#h1"); + + assertNotNull(uriBuilder); + + final URI uri = uriBuilder.scheme("https") + .port(123) + .query("bar", "foo1", "foo2") + .fragment("h2") + .build(); + + assertThat(uri.toString(), Matchers.is("https://www.example.com:123/foo/bar.xml?bar=foo1&bar=foo2&foo=bar#h2")); + + } + + @Test + public void testFromURI() { + assertNotNull(UriBuilder.fromUri(URI.create("http://www.example.com/foo/bar.xml?foo=bar#h1"))); + } + + @Test + public void testBuild() throws URISyntaxException { + final UriBuilder builder = UriBuilder.create("http", "localhost"); + + assertThat(builder.build(), UriMatchers.hasScheme("http")); + assertThat(builder.build(), UriMatchers.hasHost("localhost")); + + } + + @Test + public void testSpecialChars() throws URISyntaxException { + final UriBuilder builder = UriBuilder.create("https", "example.com"); + + builder.pathSegment("/some/path/with space"); + builder.pathSegment("folder"); + builder.query("another space", "key and value"); + builder.query("reserved", "foo&bar"); + builder.query("encoded", "100%25"); + builder.query("non-ascii", "ยข"); + + builder.query("fragment", "#tag"); + + assertThat(builder.build().toASCIIString(), Matchers.allOf( + Matchers.containsString("/some/path/with%20space/folder"), + Matchers.anyOf( + Matchers.containsString("?another%20space=key%20and%20value&"), + Matchers.containsString("?another+space=key+and+value&") + ), + Matchers.containsString("&reserved=foo%26bar&"), + Matchers.containsString("&encoded=100%2525&"), + Matchers.containsString("&non-ascii=%C2%A2&"), + Matchers.containsString("&fragment=%23tag&") + )); + + } +}