From 694c7edb9ecfd6a29bb8d36cdee59ede0b29d40e Mon Sep 17 00:00:00 2001 From: "Stephen M. Coakley" Date: Thu, 9 May 2019 13:49:06 -0500 Subject: [PATCH] feature: Add ability to get existing query params (#9) - You can now get query params already set on the builder without converting to a query string using `getQueryParameters()`. This returns an ordered map contains a list of all values for each query parameter name. - Also add `toURI()` and `toURL()` convenience methods. This also fixes an issue in how query parameters are parsed, where "value-less" parameters are being discarded. (e.g. /foo/bar?valueless). --- build.gradle | 21 +- .../java/com/widen/urlbuilder/UrlBuilder.java | 330 ++++++++---------- .../widen/urlbuilder/UrlBuilderSpec.groovy | 44 +++ 3 files changed, 205 insertions(+), 190 deletions(-) create mode 100644 src/test/groovy/com/widen/urlbuilder/UrlBuilderSpec.groovy diff --git a/build.gradle b/build.gradle index c435773..9e78e06 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,5 @@ plugins { + id 'groovy' id 'idea' id 'java' id 'maven-publish' @@ -10,19 +11,27 @@ plugins { group = 'com.widen' ext.repo = 'https://github.com/Widen/' + project.name -sourceCompatibility = 1.6 -targetCompatibility = 1.6 +sourceCompatibility = 1.8 +targetCompatibility = 1.8 repositories { jcenter() } dependencies { - compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.54' - compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.54' + compileOnly 'org.projectlombok:lombok:1.18.2' + annotationProcessor 'org.projectlombok:lombok:1.18.2' - testCompile group: 'junit', name: 'junit', version: '4.8.2' - testCompile group: 'commons-io', name: 'commons-io', version: '2.4' + compile 'org.bouncycastle:bcprov-jdk15on:1.54' + compile 'org.bouncycastle:bcpkix-jdk15on:1.54' + + testCompile 'cglib:cglib-nodep:3.2.5' + testCompile 'commons-io:commons-io:2.4' + testCompile 'junit:junit:4.8.2' + testCompile 'org.codehaus.groovy:groovy-all:2.4.9' + testCompile 'org.objenesis:objenesis:2.6' + testCompile 'org.slf4j:slf4j-simple:1.7.25' + testCompile 'org.spockframework:spock-core:1.1-groovy-2.4' } bintray { diff --git a/src/main/java/com/widen/urlbuilder/UrlBuilder.java b/src/main/java/com/widen/urlbuilder/UrlBuilder.java index e60a23c..9a4cc8c 100644 --- a/src/main/java/com/widen/urlbuilder/UrlBuilder.java +++ b/src/main/java/com/widen/urlbuilder/UrlBuilder.java @@ -1,23 +1,27 @@ /* - * Copyright 2010 Widen Enterprises, Inc. + * Copyright 2019 Widen Enterprises, Inc. * Madison, Wisconsin USA -- www.widen.com - * + * * 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 com.widen.urlbuilder; +import lombok.SneakyThrows; + import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -35,8 +39,7 @@ * * @version 0.9.3 */ -public class UrlBuilder -{ +public class UrlBuilder { private boolean ssl = false; private String hostname; @@ -61,8 +64,7 @@ public class UrlBuilder * @see #withHostname(String) * @see #withPath(String) */ - public UrlBuilder() - { + public UrlBuilder() { } /** @@ -73,26 +75,25 @@ public UrlBuilder() * * @throws NonParsableUrl if input is not parsable into a java.net.URL object */ - public UrlBuilder(String spec) - { + public UrlBuilder(String spec) { URL url = parseUrlInput(spec); - usingSsl(url.getProtocol().equals("https") ? true : false); + usingSsl(url.getProtocol().equals("https")); withHostname(url.getHost()); setPort(url.getPort()); withPath(url.getPath()); String params = url.getQuery(); - if (params != null) - { + if (params != null) { String[] pairs = params.split("&"); - for (String pair : pairs) - { + for (String pair : pairs) { String[] keyValue = pair.split("="); - if (keyValue.length == 2) - { + if (keyValue.length == 2) { addParameter(decodeValue(keyValue[0]), decodeValue(keyValue[1])); } + else if (keyValue.length == 1) { + addParameter(decodeValue(keyValue[0]), ""); + } } } @@ -101,22 +102,17 @@ public UrlBuilder(String spec) mode = GenerationMode.FULLY_QUALIFIED; } - private URL parseUrlInput(String spec) - { - try - { + private URL parseUrlInput(String spec) { + try { return new URL(spec); } - catch (MalformedURLException e) - { + catch (MalformedURLException e) { throw new NonParsableUrl(e); } } - public class NonParsableUrl extends RuntimeException - { - public NonParsableUrl(MalformedURLException e) - { + public class NonParsableUrl extends RuntimeException { + public NonParsableUrl(MalformedURLException e) { super(e); } } @@ -124,16 +120,14 @@ public NonParsableUrl(MalformedURLException e) /** * Construct a UrlBuilder with a hostname and an initial path. */ - public UrlBuilder(String hostname, String path) - { + public UrlBuilder(String hostname, String path) { this(hostname, 80, path); } /** * Construct a UrlBuilder with a hostname, port, and an initial path. */ - public UrlBuilder(String hostname, int port, String path) - { + public UrlBuilder(String hostname, int port, String path) { withHostname(hostname); setPort(port); withPath(path); @@ -141,42 +135,61 @@ public UrlBuilder(String hostname, int port, String path) } - public boolean isSslEnabled() - { + public boolean isSslEnabled() { return ssl; } - public int getPort() - { + public int getPort() { return port; } - public String getHostname() - { + public String getHostname() { return hostname; } - public String getPath() - { + public String getPath() { return "/" + StringUtilsInternal.join(path, "/"); } - public String getFragment() - { + public String getFragment() { return fragment; } - public GenerationMode getMode() - { + public GenerationMode getMode() { return mode; } + /** + * Get the query parameters as a map of multiple values. + * + * @return A map of parameter values. + */ + public Map> getQueryParameters() { + Map> map = new LinkedHashMap<>(); + + for (QueryParam queryParam : queryParams) { + map.computeIfAbsent(queryParam.key, k -> new ArrayList<>()) + .add(queryParam.value); + } + + return map; + } + + /** + * Text of query parameters as they would be append to the generated URL + *
    + *
  • Keys and values will be URL encoded + *
  • Key value pairs will be separated by an ampersand (&) + *
+ */ + public String getQueryParameterString() { + return buildParams(); + } /** * @param hostname FQDN to be used when generating fully qualified URLs */ - public UrlBuilder withHostname(String hostname) - { + public UrlBuilder withHostname(String hostname) { this.hostname = StringUtilsInternal.trimToEmpty(hostname); return this; } @@ -184,8 +197,7 @@ public UrlBuilder withHostname(String hostname) /** * @param encoder alternative URL encoder */ - public UrlBuilder usingEncoder(Encoder encoder) - { + public UrlBuilder usingEncoder(Encoder encoder) { this.encoder = encoder; return this; } @@ -193,12 +205,10 @@ public UrlBuilder usingEncoder(Encoder encoder) /** * @param port port to be appended after the hostname on fully qualified URLs */ - public void setPort(int port) - { + public void setPort(int port) { this.port = port; - if (port == 443) - { + if (port == 443) { usingSsl(); } } @@ -208,41 +218,33 @@ public void setPort(int port) *

* Use {@link #addPathSegment(String)} to append onto the path */ - public UrlBuilder withPath(String newPath) - { + public UrlBuilder withPath(String newPath) { path = makePathSegments(newPath, true); return this; } - public UrlBuilder withPathEncoded(String newPath) - { + public UrlBuilder withPathEncoded(String newPath) { path = makePathSegments(newPath, false); return this; } - List makePathSegments(String in, boolean encodeSegments) - { + List makePathSegments(String in, boolean encodeSegments) { ArrayList list = new ArrayList(); - if (in == null) - { + if (in == null) { return list; } String[] split = in.split("/"); - for (String s : split) - { - if (StringUtilsInternal.isNotBlank(s)) - { - if (encodeSegments) - { + for (String s : split) { + if (StringUtilsInternal.isNotBlank(s)) { + if (encodeSegments) { list.add(encodeValue(s)); } - else - { + else { list.add(s); } } @@ -254,8 +256,7 @@ List makePathSegments(String in, boolean encodeSegments) /** * URL protocol will be "https" */ - public UrlBuilder usingSsl() - { + public UrlBuilder usingSsl() { ssl = true; return this; } @@ -263,8 +264,7 @@ public UrlBuilder usingSsl() /** * URL protocol will be "https" when useSsl = true */ - public UrlBuilder usingSsl(boolean useSsl) - { + public UrlBuilder usingSsl(boolean useSsl) { ssl = useSsl; return this; } @@ -277,10 +277,8 @@ public UrlBuilder usingSsl(boolean useSsl) * * @param value Text to append to the path segment of the URL */ - public UrlBuilder addPathSegment(String value) - { - if (StringUtilsInternal.isNotBlank(value)) - { + public UrlBuilder addPathSegment(String value) { + if (StringUtilsInternal.isNotBlank(value)) { path.addAll(makePathSegments(value, true)); } @@ -293,10 +291,8 @@ public UrlBuilder addPathSegment(String value) * @param value * @return */ - public UrlBuilder addPrefixedPathSegment(String value) - { - if (StringUtilsInternal.isNotBlank(value)) - { + public UrlBuilder addPrefixedPathSegment(String value) { + if (StringUtilsInternal.isNotBlank(value)) { path.addAll(0, makePathSegments(value, true)); } @@ -306,8 +302,7 @@ public UrlBuilder addPrefixedPathSegment(String value) /** * By default, the path will not end with a trailing slash. */ - public UrlBuilder includeTrailingSlash() - { + public UrlBuilder includeTrailingSlash() { trailingPathSlash = true; return this; } @@ -315,13 +310,11 @@ public UrlBuilder includeTrailingSlash() /** * Append parameter to the query string. * - * @param key text for the query parameter key + * @param key text for the query parameter key * @param value toString() result will be added as the value */ - public UrlBuilder addParameter(String key, Object value) - { - if (StringUtilsInternal.isNotBlank(key)) - { + public UrlBuilder addParameter(String key, Object value) { + if (StringUtilsInternal.isNotBlank(key)) { queryParams.add(new QueryParam(key, value != null ? value.toString() : null, encoder)); } @@ -331,14 +324,12 @@ public UrlBuilder addParameter(String key, Object value) /** * Append parameter to the query string. * - * @param key text for the query parameter key - * @param value toString() result will be added as the value + * @param key text for the query parameter key + * @param value toString() result will be added as the value * @param encoder encoder to use for this value */ - public UrlBuilder addParameter(String key, Object value, Encoder encoder) - { - if (StringUtilsInternal.isNotBlank(key)) - { + public UrlBuilder addParameter(String key, Object value, Encoder encoder) { + if (StringUtilsInternal.isNotBlank(key)) { queryParams.add(new QueryParam(key, value != null ? value.toString() : null, encoder)); } @@ -350,12 +341,10 @@ public UrlBuilder addParameter(String key, Object value, Encoder encoder) * will be escaped when added. * * @param params String key = text for the query parameter key
- * Object value = toString() result at the time of will be added as the value + * Object value = toString() result at the time of will be added as the value */ - public UrlBuilder addParameters(Map params) - { - for (Entry e : params.entrySet()) - { + public UrlBuilder addParameters(Map params) { + for (Entry e : params.entrySet()) { addParameter(e.getKey(), e.getValue().toString()); } return this; @@ -364,8 +353,7 @@ public UrlBuilder addParameters(Map params) /** * Clear any previously added parameters. */ - public UrlBuilder clearParameters() - { + public UrlBuilder clearParameters() { queryParams.clear(); return this; @@ -374,18 +362,14 @@ public UrlBuilder clearParameters() /** * Remove previously added query parameters */ - public UrlBuilder clearParameter(String... params) - { - if (params != null) - { + public UrlBuilder clearParameter(String... params) { + if (params != null) { List remove = Arrays.asList(params); - for (Iterator iter = queryParams.iterator(); iter.hasNext(); ) - { + for (Iterator iter = queryParams.iterator(); iter.hasNext(); ) { QueryParam next = iter.next(); - if (remove.contains(next.key)) - { + if (remove.contains(next.key)) { iter.remove(); } } @@ -394,25 +378,11 @@ public UrlBuilder clearParameter(String... params) return this; } - /** - * Text of query parameters as they would be append to the generated URL - *

    - *
  • Keys and values will be URL encoded - *
  • Key value pairs will be separated by an ampersand (&) - *
- */ - public String getQueryParameterString() - { - return buildParams(); - } - /** * @param fragment text to appear after the '#' in the generated URL. Will not be URLEncoded. */ - public UrlBuilder withFragment(String fragment) - { - if (StringUtilsInternal.isNotBlank(fragment)) - { + public UrlBuilder withFragment(String fragment) { + if (StringUtilsInternal.isNotBlank(fragment)) { this.fragment = fragment; } return this; @@ -421,8 +391,7 @@ public UrlBuilder withFragment(String fragment) /** * Set generation mode to Protocol Relative; e.g. "//my.host.com/foo/bar.html" */ - public UrlBuilder modeProtocolRelative() - { + public UrlBuilder modeProtocolRelative() { mode = GenerationMode.PROTOCOL_RELATIVE; return this; } @@ -430,21 +399,39 @@ public UrlBuilder modeProtocolRelative() /** * Set generation mode to Hostname Relative; e.g. "/foo/bar.html" */ - public UrlBuilder modeHostnameRelative() - { + public UrlBuilder modeHostnameRelative() { mode = GenerationMode.HOSTNAME_RELATIVE; return this; } /** - * Set generation mode to Fully Qualified. This is the default mode; e.g. "http://my.host.com/foo/bar.html" + * Set generation mode to Fully Qualified. This is the default mode; e.g. "http://my.host.com/foo/bar + * .html" */ - public UrlBuilder modeFullyQualified() - { + public UrlBuilder modeFullyQualified() { mode = GenerationMode.FULLY_QUALIFIED; return this; } + /** + * Construct a {@link URI} for the current configuration. + * + * @see #toString() + */ + public URI toURI() { + return URI.create(toString()); + } + + /** + * Construct a {@link URL} for the current configuration. + * + * @see #toString() + */ + @SneakyThrows + public URL toURL() { + return toURI().toURL(); + } + /** * Construct URL for the current configuration. *

@@ -455,75 +442,60 @@ public UrlBuilder modeFullyQualified() * @see #modeProtocolRelative() */ @Override - public String toString() - { + public String toString() { StringBuilder url = new StringBuilder(); - if (GenerationMode.FULLY_QUALIFIED.equals(mode) && StringUtilsInternal.isBlank(hostname)) - { + if (GenerationMode.FULLY_QUALIFIED.equals(mode) && StringUtilsInternal.isBlank(hostname)) { throw new IllegalArgumentException("Hostname cannot be blank when generation mode is FULLY_QUALIFIED."); } - if (GenerationMode.FULLY_QUALIFIED.equals(mode)) - { - if (ssl) - { - url.append("https://" + hostname); + if (GenerationMode.FULLY_QUALIFIED.equals(mode)) { + if (ssl) { + url.append("https://").append(hostname); } - else - { - url.append("http://" + hostname); + else { + url.append("http://").append(hostname); } } - else if (GenerationMode.PROTOCOL_RELATIVE.equals(mode)) - { - url.append("//" + hostname); + else if (GenerationMode.PROTOCOL_RELATIVE.equals(mode)) { + url.append("//").append(hostname); } - if (!GenerationMode.HOSTNAME_RELATIVE.equals(mode)) - { - if (port != 80 && port != 443 && port > 0) - { - url.append(":" + port); + if (!GenerationMode.HOSTNAME_RELATIVE.equals(mode)) { + if (port != 80 && port != 443 && port > 0) { + url.append(":").append(port); } } url.append("/"); - if (!path.isEmpty()) - { + if (!path.isEmpty()) { url.append(StringUtilsInternal.join(path, "/")); - if (trailingPathSlash) - { + if (trailingPathSlash) { url.append("/"); } } - if (!queryParams.isEmpty()) - { + if (!queryParams.isEmpty()) { url.append("?"); url.append(buildParams()); } - if (StringUtilsInternal.isNotBlank(fragment)) - { - url.append("#" + fragment); + if (StringUtilsInternal.isNotBlank(fragment)) { + url.append("#").append(fragment); } return url.toString(); } - private String buildParams() - { + private String buildParams() { StringBuilder params = new StringBuilder(); boolean first = true; - for (QueryParam qp : queryParams) - { - if (!first) - { + for (QueryParam qp : queryParams) { + if (!first) { params.append("&"); } @@ -535,60 +507,50 @@ private String buildParams() return params.toString(); } - private String encodeValue(String value) - { - if (value == null) - { + private String encodeValue(String value) { + if (value == null) { return ""; } return encoder.encode(value); } - private String decodeValue(String value) - { - if (value == null) - { + private String decodeValue(String value) { + if (value == null) { return ""; } return encoder.decode(value); } - public enum GenerationMode - { + public enum GenerationMode { FULLY_QUALIFIED, PROTOCOL_RELATIVE, HOSTNAME_RELATIVE } - class QueryParam - { + class QueryParam { String key; String value; Encoder encoder; - QueryParam(String key, String value, Encoder encoder) - { + QueryParam(String key, String value, Encoder encoder) { this.key = key; this.value = value; this.encoder = encoder; } @Override - public String toString() - { + public String toString() { StringBuilder sb = new StringBuilder(); sb.append(encoder.encode(key)); - if (StringUtilsInternal.isNotBlank(value)) - { + if (StringUtilsInternal.isNotBlank(value)) { sb.append("=").append(encoder.encode(value)); } return sb.toString(); } } - } diff --git a/src/test/groovy/com/widen/urlbuilder/UrlBuilderSpec.groovy b/src/test/groovy/com/widen/urlbuilder/UrlBuilderSpec.groovy new file mode 100644 index 0000000..ce30db6 --- /dev/null +++ b/src/test/groovy/com/widen/urlbuilder/UrlBuilderSpec.groovy @@ -0,0 +1,44 @@ +package com.widen.urlbuilder + + +import spock.lang.Specification +import spock.lang.Unroll + +class UrlBuilderSpec extends Specification { + def "Happy path"() { + when: + def builder = new UrlBuilder("my.host.com", "foo").addPathSegment("bar").addParameter("a", "b") + + then: + builder.toString() == "http://my.host.com/foo/bar?a=b" + } + + @Unroll + def "Round-trip parse and to string"() { + when: + def builder = new UrlBuilder(url) + + then: + builder.toString() == url + + where: + url << [ + "http://my.host.com:8080/bar?a=b#foo", + "http://my.host.com/bar?a=b#foo", + "https://my.host.com/bar?a=b#foo", + "https://my.host.com:8080/bar?a=b&c=d" + ] + } + + def "Query parameters as map"() { + when: + def builder = new UrlBuilder('https://my.host.com/bar?a=x&b=2&c=3&c=4&a&d#foo') + + then: + builder.queryParameters.size() == 4 + builder.queryParameters.a == ['x', ''] + builder.queryParameters.b == ['2'] + builder.queryParameters.c == ['3', '4'] + builder.queryParameters.d == [''] + } +}