Java tutorial
/* * The MIT License (MIT) * * Copyright (c) 2015 Georg Koester, jURI Authors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * */ package com.pidoco.juri; import com.google.common.base.Splitter; import com.google.common.base.Supplier; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import com.google.common.net.InetAddresses; import com.google.common.net.UrlEscapers; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.URLDecoder; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Helps especially if you have to deal with changing (or even only getting) query parameters. * * <p> * The class doesn't verify the URI until {@link #getCurrentUri()} or any of the methods calling it like * {@link #toString()} are called. E.g. * <pre> * cut = JURI.parse("http://[::1.1.1.1]") * assertTrue(cut.setPath("dsfd/").isPathRelative()); * cut.getCurrentUri(); // fails here * </pre> * </p> * * <p>This is a mutable class - so it doesn't provide an equals or hashCode implementation. Use URI from * {@link #getCurrentUri()} or String from {@link #toString()} if you need to compare or store in a map. * It doesn't provide an equals or hashCode implementation so as to fail early when it is used in this anti-pattern. * </p> * * Why not {@link java.net.URL} as the underlying class? Its equals calls DNS and compares IP addresses.... */ @ParametersAreNonnullByDefault public class JURI implements Cloneable { private static final Logger LOG = LoggerFactory.getLogger(JURI.class); public static final URI EMPTY_URI; static { URI protoEmptyUri = null; try { protoEmptyUri = new URI(""); } catch (URISyntaxException e) { e.printStackTrace(); // shouldn't happen } EMPTY_URI = protoEmptyUri; } private URI prototype; private URI currentUri; private boolean changeUnderway = false; private Multimap<String, String> currentQueryParameters; private String scheme; private boolean removeAuthorityAndScheme = false; private String rawUserInfo; private String host; private Integer port; private String rawPath; private String fragment; /** * Creates a URI with empty content "". */ private JURI() { prototype = EMPTY_URI; } /** * Init to a URI of '' (empty string). */ public static JURI createEmpty() { return new JURI(); } /** * @throws IllegalArgumentException if the given URI cannot be parsed. */ public static JURI parse(String rawURI) { JURI result = new JURI(); try { result.prototype = result.currentUri = new URI(rawURI); } catch (URISyntaxException e) { throw new IllegalArgumentException( String.format("Cannot parse as URI: '%s'. Reason: %s", rawURI, e.getMessage()), e); } return result; } public static JURI create(URI uri) { JURI result = new JURI(); result.prototype = result.currentUri = uri; return result; } /** * Recreates the URI if changed, should not be used while changing it. */ public URI getCurrentUri() { if (currentUri == null) { try { buildAndReset(); } catch (URISyntaxException use) { throw new IllegalStateException(use); } } return currentUri; } public void buildAndReset() throws URISyntaxException { currentUri = new URI(buildNoSideEffects().toString()); reset(); } protected CharSequence buildNoSideEffects() throws URISyntaxException { String scheme = this.scheme == null ? prototype.getScheme() : this.scheme; String rawUserInfo = this.rawUserInfo == null ? prototype.getRawUserInfo() : this.rawUserInfo; CharSequence rawHost = this.host == null ? prototype.getHost() : this.buildHostString(); int port = this.port == null ? this.prototype.getPort() : this.port; CharSequence rawPath = this.rawPath == null ? this.prototype.getRawPath() : this.rawPath; CharSequence rawQuery = this.buildQueryParametersString(); String rawFragment = this.fragment == null ? this.prototype.getRawFragment() : UrlEscapers.urlFragmentEscaper().escape(this.fragment); StringBuilder builder = new StringBuilder(32); if (!removeAuthorityAndScheme) { if (StringUtils.isNotBlank(scheme)) { builder.append(scheme); builder.append(':'); } boolean hasAuthority = StringUtils.isNotBlank(rawHost) || port > -1 || StringUtils.isNotBlank(rawUserInfo); if (hasAuthority) { builder.append("//"); } if (StringUtils.isNotBlank(rawUserInfo)) { builder.append(rawUserInfo).append('@'); } if (StringUtils.isNotBlank(rawHost)) { builder.append(rawHost); } if (port > 0) { builder.append(':').append(port); } } if (StringUtils.isNotBlank(rawPath)) { builder.append(rawPath); } if (StringUtils.isNotBlank(rawQuery)) { builder.append('?').append(rawQuery); } if (StringUtils.isNotBlank(rawFragment)) { builder.append('#').append(rawFragment); } return builder; } @Nullable private CharSequence buildHostString() { String decodedHost = this.host; if (decodedHost == null) { decodedHost = prototype.getHost(); } if (StringUtils.startsWith(decodedHost, "[") && StringUtils.endsWith(decodedHost, "]")) { return decodedHost; // ipv6 and other address literal that is not encoded (at least currently) } return urlEncode(decodedHost); } private void reset() { if (currentUri != null) { prototype = currentUri; } currentQueryParameters = null; scheme = null; removeAuthorityAndScheme = false; rawUserInfo = null; host = null; port = null; rawPath = null; fragment = null; } /** * @param scheme null not supported */ public JURI setScheme(String scheme) { startChange(); this.removeAuthorityAndScheme = false; this.scheme = scheme; changed(); return this; } @Nullable public String getScheme() { if (removeAuthorityAndScheme) { return null; } if (scheme == null) { scheme = prototype.getScheme(); } return scheme; } /** * Use {@link #urlEncode(String)} if you want to encode the scheme specific part, or the http-specific manipulation * methods provided by this class. * * <p>If no scheme is currently set the scheme will become 'unspecified'.</p> * * @param rawSchemeSpecificPart null or "" not allowed. */ public JURI setRawSchemeSpecificPart(String rawSchemeSpecificPart) { rawSchemeSpecificPart = StringUtils.defaultString(rawSchemeSpecificPart); String newUri; if (StringUtils.isBlank(getScheme())) { newUri = "unspecified:" + rawSchemeSpecificPart; } else { newUri = getScheme() + ":" + rawSchemeSpecificPart; } URI newUriObj; try { newUriObj = new URI(newUri); } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } currentUri = newUriObj; reset(); return this; } /** * @return maybe "" */ public String getRawSchemeSpecificPart() { return getCurrentUri().getRawSchemeSpecificPart(); } /** * BEWARE, this cannot be used to escape many http scheme specific parts, as possibly other scheme's specific parts. * Problem is the different escaping of some characters depending on the (semantic) location. * * <p>If no scheme is currently set the scheme will become 'unspecified'.</p> * * @param schemeSpecificPart null or "" not allowed. */ public JURI setSchemeSpecificPart(String schemeSpecificPart) { schemeSpecificPart = StringUtils.defaultString(schemeSpecificPart); setRawSchemeSpecificPart(UrlEscapers.urlFragmentEscaper().escape(schemeSpecificPart)); return this; } /** * Return e.g. the decoded mail address for URI <code>mailto:Pel@domain.org</code>. * @return maybe "" */ public String getSchemeSpecificPart() { return getCurrentUri().getSchemeSpecificPart(); } public JURI removeAuthorityAndScheme() { startChange(); removeAuthorityAndScheme = true; changed(); return this; } public JURI setUserInfo(String user, @Nullable String pw) { startChange(); if (StringUtils.isBlank(user)) { this.rawUserInfo = ""; } else { this.removeAuthorityAndScheme = false; this.rawUserInfo = urlEncode(user); if (pw != null) { this.rawUserInfo = this.rawUserInfo + ":" + urlEncode(pw); } } changed(); return this; } @Nullable public String getRawUserInfo() { if (removeAuthorityAndScheme) { return null; } if (rawUserInfo == null) { rawUserInfo = prototype.getRawUserInfo(); } return rawUserInfo; } @Nullable public String getUser() { String raw = getRawUserInfo(); if (StringUtils.isBlank(raw)) { return null; } String[] split = StringUtils.splitPreserveAllTokens(raw, ':'); if (split.length > 0) { return urlDecode(split[0]); } return null; } @Nullable public String getPassword() { String raw = getRawUserInfo(); if (StringUtils.isBlank(raw)) { return null; } String[] split = StringUtils.splitPreserveAllTokens(raw, ':'); if (split.length > 1) { return urlDecode(split[1]); } return null; } public JURI removeUserInfo() { setUserInfo("", ""); return this; } /** * @param host null or "" remove host. */ public JURI setHost(@Nullable String host) { startChange(); this.host = host; this.removeAuthorityAndScheme = false; changed(); return this; } /** * Sets the host to the given numeric ip address. * */ public JURI setHost(@Nullable InetAddress address) { return setHost(address, false); } /** * Does not attempt name resolution (and therefore does not block). * * @param address to set as hostname. Works with both ipv4 and ipv6 addresses. ipv4 addresses in ipv6 form are * set as ipv6 address. A given ipv4 address is not converted into an ipv6 address. * @param useHostIfAvailable if true, checks if the address has a known hostname * and uses that name instead of the address. Consider using {@link #setHost(String)}} * directly instead. * No name resolution is performed at the penalty of additional string concatenation * when using {@link InetAddress#toString()}. */ public JURI setHost(@Nullable InetAddress address, boolean useHostIfAvailable) { if (address == null) { return setHost(""); } if (useHostIfAvailable && checkIfAddressHasHostnameWithoutNameLookup(address)) { this.setHost(address.getHostName()); } else { this.setHost(InetAddresses.toUriString(address)); } return this; } /** * Relies on implementation details of the InetAddress class, hopefully not changing. There is a test, of course. */ protected static boolean checkIfAddressHasHostnameWithoutNameLookup(InetAddress address) { // Cannot check with getHostName or getCanonicalName because they can incur a name lookup and we want to avoid // blocking. // Remark: InetSocketAddress's unresolved and holder functionality not better suited. return !address.toString().startsWith("/"); } @Nullable public String getHost() { if (removeAuthorityAndScheme) { return null; } if (host == null) { host = prototype.getHost(); } return host; } /** * * @param port null or -1 remove */ public JURI setPort(@Nullable Integer port) { startChange(); if (port == null || port <= 0) { this.port = Integer.valueOf(-1); } else { this.port = port; } changed(); return this; } public int getPort() { if (removeAuthorityAndScheme) { return -1; } if (port == null) { return prototype.getPort(); } return port; } public boolean isHavingPort() { return getPort() > 0; } /** * Replace current path with the concatenation of the given segments. * * @param absolute (not relative) if true will set the path with '/' at * beginning. Also if no segments are given. * @param slashAtEnd if true and if there is at least one segment, will add a terminating '/' to path. * @param segments here even slash ('/') characters can be specified without escaping them - they will be * escaped to %2F by the method. */ public JURI setPathSegments(boolean absolute, boolean slashAtEnd, String... segments) { StringBuilder result = buildRawPathString(absolute, slashAtEnd, segments); setRawPath(result.toString()); return this; } public StringBuilder buildRawPathString(boolean absolute, boolean slashAtEnd, String[] segments) { StringBuilder result = new StringBuilder(); if (absolute) { result.append('/'); } boolean addedOne = false; for (String s : segments) { result.append(UrlEscapers.urlPathSegmentEscaper().escape(s)); result.append('/'); addedOne = true; } if (addedOne && !slashAtEnd) { result.deleteCharAt(result.length() - 1); } return result; } public JURI addPathSegments(boolean slashAtEnd, String... segments) { if (segments.length < 1) { return this; } StringBuilder toAdd = buildRawPathString(false, slashAtEnd, segments); addRawPath(toAdd); return this; } /** * <pre> * "".addRawPath("") -> "" * "/".addRawPath("") -> "/" * "".addRawPath("/") -> "/" * "a".addRawPath("") -> "a/" * "a".addRawPath("b") -> "a/b" * "/".addRawPath("/") -> "/" * </pre> */ public JURI addRawPath(CharSequence toAdd) { String currentRawPath = StringUtils.defaultString(getRawPath()); setRawPath(concatRawPaths(currentRawPath, toAdd)); return this; } /** * <pre> * "".addRawPath("") -> "" * "/".addRawPath("") -> "/" * "".addRawPath("/") -> "/" * "a".addRawPath("") -> "a/" * "a".addRawPath("b") -> "a/b" * "/".addRawPath("/") -> "/" * </pre> */ public static String concatRawPaths(CharSequence left, CharSequence right) { boolean needsSeparator = false; boolean rightStartsWithSlash = StringUtils.startsWith(right, "/"); int rightStart = 0; if (left.length() > 0) { if (StringUtils.endsWith(left, "/")) { if (rightStartsWithSlash) { rightStart = 1; } } else { if (!rightStartsWithSlash) { needsSeparator = true; } } } return left + (needsSeparator ? "/" : "") + right.subSequence(rightStart, right.length()); } /** * @param unescapedPath null or "" clear the path. Path may contain characters that need escaping like umlauts etc. * Segments are separated by '/'. With this method '/' cannot be escaped, use one of the * {@link #setPathSegments(boolean, boolean, String...)} if your path segments may contain * '/' characters, but * beware that the {@link URI} class does not support that. Path is not * canonicalized (//, ../ resolved etc) until URI is created with e.g. {@link #getCurrentUri()} or * {@link #toString()}, which uses {@link #getCurrentUri()}. */ public JURI setPath(@Nullable String unescapedPath) { startChange(); if (unescapedPath == null) { unescapedPath = ""; } setRawPath(escapeMultiSegmentPath(unescapedPath).toString()); changed(); return this; } /** * Escapes non-path characters. Slash ('/') characters may be included in segments if escaped by backslash. * @param completeUnescapedPath e.g. //b\/f//kf/ -> //b%XXf//kf/ * @return the used string builder. */ public static StringBuilder escapeMultiSegmentPath(String completeUnescapedPath) { StringBuilder pathBuilder = new StringBuilder(); StringBuilder temp = new StringBuilder(); for (int i = 0; i < completeUnescapedPath.length(); i++) { char current = completeUnescapedPath.charAt(i); if (current == '\\') { if (isSlashAtPos(completeUnescapedPath, i + 1)) { temp.append('/'); i++; continue; } } if (current == '/') { pathBuilder.append(UrlEscapers.urlPathSegmentEscaper().escape(temp.toString())); pathBuilder.append('/'); temp.setLength(0); continue; } temp.append(current); } if (temp.length() > 0) { pathBuilder.append(UrlEscapers.urlPathSegmentEscaper().escape(temp.toString())); } return pathBuilder; } private static boolean isSlashAtPos(@Nullable String in, int i) { in = StringUtils.defaultString(in); return in.length() > i && in.charAt(i) == '/'; } /** * @param rawPath Use an empty string to remove the path. */ public JURI setRawPath(@Nullable String rawPath) { startChange(); this.rawPath = StringUtils.defaultString(rawPath); changed(); return this; } @Nullable public String getRawPath() { if (rawPath == null) { rawPath = prototype.getRawPath(); } return rawPath; } @Nullable public String getPath() { String raw = getRawPath(); if (StringUtils.isBlank(raw)) { return null; } return urlDecode(raw); } /** * @return true if a path is set: http://a.com/ : true, http://a.com : false. */ public boolean isHavingPath() { String rawPath = StringUtils.defaultIfBlank(getRawPath(), ""); return rawPath.length() > 0; } /** * @return true if a path is set and the path doesn't begin with a '/'. */ public boolean isPathRelative() { String rawPath = StringUtils.defaultString(getRawPath()); return rawPath.length() > 0 && !rawPath.startsWith("/"); } /** * @return true if a path is set and the path begins with a '/'. */ public boolean isPathAbsolute() { return isHavingPath() && !isPathRelative(); } /** * Decodes the path segments on request. * @return a new list that the caller may manipulate. Empty string if no path or the root path is set. Returns * empty path segments (//as//df// has three empty * segments). */ public List<String> getPathSegments() { ArrayList<String> result = getRawPathSegments(); for (int i = 0; i < result.size(); i++) { String segment = result.get(i); result.set(i, urlDecode(segment)); } return result; } /** * Doesn't decode the path segments. * @return a new list that the caller may manipulate. Empty string if no path or the root path is set. Returns * empty path segments (//as//df// has three empty * segments). */ public ArrayList<String> getRawPathSegments() { return splitRawPath(StringUtils.defaultString(getRawPath())); } public static ArrayList<String> splitRawPath(String rawPath) { ArrayList<String> result = new ArrayList<>(); if (rawPath.length() == 0) { return result; } boolean dropFirstSegment = false; boolean dropLastSegment = false; if (rawPath.startsWith("/")) { dropFirstSegment = true; } if (rawPath.endsWith("/")) { dropLastSegment = true; } boolean first = true; Iterator<String> splitIter = Splitter.on('/').split(rawPath).iterator(); while (splitIter.hasNext()) { String current = splitIter.next(); if (first) { first = false; if (dropFirstSegment) { continue; } } if (!splitIter.hasNext() && dropLastSegment) { continue; } result.add(current); } return result; } public String getFragment() { if (fragment == null) { fragment = prototype.getFragment(); } return fragment; } /** * * @param fragment null or "" clears the fragment part */ public JURI setFragment(@Nullable String fragment) { startChange(); this.fragment = StringUtils.defaultString(fragment); changed(); return this; } public CharSequence buildQueryParametersString() { if (currentQueryParameters == null) { return prototype.getRawQuery(); } return buildQueryParametersString(currentQueryParameters); } public static CharSequence buildQueryParametersString(Multimap<String, String> currentQueryParameters) { StringBuilder paramsString = new StringBuilder(); boolean first = true; for (Map.Entry<String, String> entry : currentQueryParameters.entries()) { if (!first) { paramsString.append('&'); } String keyEnc = UrlEscapers.urlFormParameterEscaper().escape(entry.getKey()); if (entry.getValue() != null) { String valueEnc = UrlEscapers.urlFormParameterEscaper().escape(entry.getValue()); paramsString.append(keyEnc).append('=').append(valueEnc); } else { paramsString.append(keyEnc); } first = false; } return paramsString; } public Map<String, Collection<String>> getQueryParameters() { return getQueryParametersMultimap().asMap(); } public String getQueryParameterFirstValue(String name) { Collection<String> vals = getQueryParametersMultimap().get(name); if (vals.isEmpty()) { return null; } return vals.iterator().next(); } public boolean isHavingQueryParams() { if (currentQueryParameters != null && !currentQueryParameters.isEmpty()) { return true; } // to be sure must parse (for cases like ?& ): return !getQueryParametersMultimap().isEmpty(); } protected Multimap<String, String> getQueryParametersMultimap() { if (currentQueryParameters != null) { return currentQueryParameters; } currentQueryParameters = parseQueryParameters(prototype); return currentQueryParameters; } public static Multimap<String, String> parseQueryParameters(URI uri) { return parseQueryParameters(StringUtils.defaultString(uri.getRawQuery())); } public static Multimap<String, String> parseQueryParameters(String rawQuery) { Multimap<String, String> result = createParamsMultimap(); for (String singleParam : StringUtils.split(rawQuery, '&')) { if (StringUtils.isBlank(singleParam)) { continue; } String[] split = StringUtils.split(singleParam, '='); String key = urlDecode(split[0]); String value = ""; if (split.length > 1) { value = urlDecode(split[1]); } result.put(key, value); } return result; } public JURI addQueryParameter(String name, String unencodedValue) { startChange(); Multimap<String, String> params = getQueryParametersMultimap(); params.put(name, unencodedValue); changed(); return this; } public JURI addQueryParameters(String name, String... unencodedValues) { return addQueryParameters(name, Arrays.asList(unencodedValues)); } public JURI addQueryParameters(String name, Collection<String> unencodedValues) { startChange(); Multimap<String, String> params = getQueryParametersMultimap(); params.putAll(name, unencodedValues); changed(); return this; } public JURI removeQueryParameter(String name) { startChange(); Multimap<String, String> params = getQueryParametersMultimap(); params.removeAll(name); changed(); return this; } public JURI replaceQueryParameter(String name, String unencodedValue) { startChange(); Multimap<String, String> params = getQueryParametersMultimap(); params.removeAll(name); params.put(name, unencodedValue); changed(); return this; } public JURI replaceQueryParameters(String name, String... unencodedValues) { return replaceQueryParameters(name, Arrays.asList(unencodedValues)); } public JURI replaceQueryParameters(String name, Collection<String> unencodedValues) { startChange(); Multimap<String, String> params = getQueryParametersMultimap(); params.removeAll(name); params.putAll(name, unencodedValues); changed(); return this; } public JURI clearQueryParameters() { startChange(); currentQueryParameters = createParamsMultimap(); changed(); return this; } protected static Multimap<String, String> createParamsMultimap() { return Multimaps.<String, String>newListMultimap(new LinkedHashMap<String, Collection<String>>(), new Supplier<ArrayList<String>>() { @Override public ArrayList<String> get() { return new ArrayList<String>(); } }); } public JURI addQueryParameters(Map<String, String> params) { startChange(); Multimap<String, String> queryParameters = getQueryParametersMultimap(); for (Map.Entry<String, String> entry : params.entrySet()) { queryParameters.put(entry.getKey(), entry.getValue()); } changed(); return this; } public JURI addQueryParametersMulti(Map<String, Collection<String>> params) { startChange(); Multimap<String, String> queryParameters = getQueryParametersMultimap(); for (Map.Entry<String, Collection<String>> entry : params.entrySet()) { queryParameters.putAll(entry.getKey(), entry.getValue()); } changed(); return this; } public JURI replaceQueryParameters(Map<String, String> params) { startChange(); Multimap<String, String> queryParameters = getQueryParametersMultimap(); for (Map.Entry<String, String> entry : params.entrySet()) { queryParameters.removeAll(entry.getKey()); queryParameters.put(entry.getKey(), entry.getValue()); } changed(); return this; } public JURI replaceQueryParametersMulti(Map<String, Collection<String>> params) { startChange(); Multimap<String, String> queryParameters = getQueryParametersMultimap(); for (Map.Entry<String, Collection<String>> entry : params.entrySet()) { queryParameters.removeAll(entry.getKey()); queryParameters.putAll(entry.getKey(), entry.getValue()); } changed(); return this; } public boolean isNeedingCurrentUriConstruction() { return currentUri == null; } private void startChange() { this.changeUnderway = true; this.currentUri = null; } /** * Splitting into startChange and changed isn't strictly necessary - but it is when people debug the calling * methods. */ private void changed() { this.changeUnderway = false; this.currentUri = null; } public static String urlDecode(String s) { try { return URLDecoder.decode(s, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } public static String urlEncode(String s) { try { return URLEncoder.encode(s, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } /** * Provides encoded URI - recreates the URI, should not be used if more changes will be applied to the wrapper. */ @Override public String toString() { if (changeUnderway) { CharSequence detail; try { detail = buildNoSideEffects(); } catch (URISyntaxException e) { detail = e.getMessage(); } LOG.warn("Called toString while change is underway - this must only happen during debugging!"); return detail.toString(); } return getCurrentUri().toASCIIString(); } /** * @throws IllegalStateException wrapping URISyntaxException if the internal state cannot result in a working URI. */ @Override public JURI clone() throws IllegalStateException { JURI clone = null; try { clone = (JURI) super.clone(); } catch (CloneNotSupportedException e) { throw new IllegalStateException(e); } if (clone.currentUri == null) { // goal here is to avoid having stateful objects in the clone and the original. try { clone.buildAndReset(); } catch (URISyntaxException e) { throw new IllegalStateException(e); } } return clone; } }