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&")
+ ));
+
+ }
+}