
com.fizzed.crux.uri.MutableUri Maven / Gradle / Ivy
/*
* Copyright 2015 Fizzed, Inc.
*
* 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.fizzed.crux.uri;
import java.net.URI;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
/**
* Helps to build a URI. Why another one? Unlike Java's URI this one allows
* modification after it's been created. This one has a simple fluent style
* to help build uris.
*/
public class MutableUri extends Uri {
public MutableUri() {
// empty
}
public MutableUri(String uri) {
this(URI.create(uri));
}
public MutableUri(URI uri) {
this.apply(uri);
}
@SuppressWarnings("OverridableMethodCallInConstructor")
public MutableUri(Uri uri) {
this.scheme = uri.scheme;
this.userInfo = uri.userInfo;
this.host = uri.host;
this.port = uri.port;
this.rels = copy(uri.rels);
this.query = copy(uri.query);
this.fragment = uri.fragment;
}
@Deprecated
public Uri toImmutable() {
return this.immutable();
}
public Uri immutable() {
return new Uri(this.scheme, this.userInfo, this.host, this.port, this.rels, this.query, this.fragment);
}
public Uri toUri() {
return this.immutable();
}
public MutableUri scheme(String scheme) {
this.scheme = scheme;
return this;
}
public MutableUri userInfo(String userInfo) {
this.userInfo = userInfo;
return this;
}
public MutableUri host(String host) {
this.host = host;
return this;
}
public MutableUri port(Integer port) {
this.port = port;
return this;
}
/**
* Either sets an absolute or relative path. The path MUST be url-encoded. If
* the path starts with "/" then it will set the entire path, otherwise it will be
* considered relative to the current path. If you want
* to safely build a path and have this library do the url-encoding for you
* then take a look at the rel() method as an alternative.
*
* @param path The path to set such as "/a/b/c" if absolute or if the current
* path is "/a/b" and you call this method with "c" then the final
* path would be "/a/b/c".
* @return This instance
* @see #relPath(java.lang.String)
* @see #rel(java.lang.String[])
*/
public MutableUri path(String path) {
// clear it?
if (path == null) {
this.rels = null;
return this;
}
List newRels = splitPath(path, true);
// if absolute then normalize it (chop off leading empty rel)
if (path.length() > 0 && path.charAt(0) == '/') {
newRels = normalizeRootPath(newRels);
this.rels = null;
}
// create array if its missing
if (this.rels == null) {
this.rels = new ArrayList<>();
}
// append everything
this.rels.addAll(newRels);
return this;
}
/**
* Only adds the relative path if the value is present in the optional.
* @param rels One or more optionals.
* @return
*/
public MutableUri relIfPresent(Optional>... rels) {
if (rels == null || rels.length == 0) {
return this;
}
for (Optional> rel : rels) {
// optionals should always be non-null
Objects.requireNonNull(rel, "rel was null");
if (rel.isPresent()) {
this.rel(rel.get());
}
}
return this;
}
/**
* Adds one or more relative path components as-is. If the existing path
* is "/a/b" and you supply "c@/d" to this method then the underlying
* path would be "/a/b/c%40%2fd". This is the recommended method to safely
* build a path in a url.
* @param rels One or more relative path components to add
* @return This instance
*/
public MutableUri rel(Object... rels) {
if (rels == null || rels.length == 0) {
return this;
}
String[] strings = new String[rels.length];
for (int i = 0; i < rels.length; i++) {
Object rel = rels[i];
Objects.requireNonNull(rel, "rel was null");
strings[i] = Objects.toString(rel);
}
return rel(strings);
}
/**
* Adds one or more relative path components as-is. If the existing path
* is "/a/b" and you supply "c@/d" to this method then the underlying
* path would be "/a/b/c%40%2fd". This is the recommended method to safely
* build a path in a url.
* @param rels One or more relative path components to add
* @return This instance
*/
public MutableUri rel(String... rels) {
if (rels == null || rels.length == 0) {
return this; // nothing to do
}
if (this.rels == null) {
this.rels = new ArrayList<>();
}
for (String rel : rels) {
Objects.requireNonNull(rel, "rel was null");
this.rels.add(rel);
}
return this;
}
public MutableUri queryIfPresent(String name, Optional> value) {
Objects.requireNonNull(name, "name was null");
Objects.requireNonNull(name, "value was null");
if (value.isPresent()) {
this.query(name, value.get());
}
return this;
}
public MutableUri query(String name, Object value) {
Objects.requireNonNull(name, "name was null");
return this.query(name, Objects.toString(value, null));
}
public MutableUri query(String name, String value) {
Objects.requireNonNull(name, "name was null");
List values = getQueryValues(name);
values.add(value);
return this;
}
/**
* Adds the entire map as query values.
* @param queryMap The map to add
* @return
*/
public MutableUri query(Map queryMap) {
queryMap.forEach((name, value) -> this.query(name, value));
return this;
}
public MutableUri setQueryIfPresent(String name, Optional> value) {
Objects.requireNonNull(name, "name was null");
Objects.requireNonNull(name, "value was null");
if (value.isPresent()) {
this.setQuery(name, value.get());
}
return this;
}
public MutableUri setQuery(String name, Object value) {
Objects.requireNonNull(name, "name was null");
return this.setQuery(name, Objects.toString(value, null));
}
public MutableUri setQuery(String name, String value) {
Objects.requireNonNull(name, "name was null");
List values = getQueryValues(name);
values.clear();
values.add(value);
return this;
}
/**
* Adds the entire map as query values.
* @param queryMap The map to add
* @return
*/
public MutableUri setQuery(Map queryMap) {
queryMap.forEach((name, value) -> this.setQuery(name, value));
return this;
}
private List getQueryValues(String name) {
Objects.requireNonNull(name, "name cannot be null");
if (this.query == null) {
this.query = new LinkedHashMap<>(); // order of insertion important
}
return this.query.computeIfAbsent(name, (key) -> new ArrayList<>());
}
/**
* Sets the non url-encoded fragment.
* @param fragment Non url-encoded fragment.
* @return
*/
public MutableUri fragment(String fragment) {
this.fragment = fragment;
return this;
}
private MutableUri apply(URI uri) {
if (uri.getScheme() != null) {
this.scheme = uri.getScheme();
}
if (uri.getRawUserInfo() != null) {
this.userInfo = urlDecode(uri.getRawUserInfo());
}
if (uri.getHost() != null) {
this.host = uri.getHost();
}
if (uri.getPort() >= 0) {
this.port = uri.getPort();
}
// if the uri contains reserved, punctuation, and other chars in the
// host section of the uri, then those are actually set as the authority
// we're going to make a design decision to try and parse it as the host
// and potentially the port
if (uri.getHost() == null && uri.getUserInfo() == null && uri.getAuthority() != null) {
String authority = uri.getAuthority();
int portIndex = authority.indexOf(':');
if (portIndex >= 0) {
this.host = authority.substring(0, portIndex);
this.port = Integer.valueOf(authority.substring(portIndex+1));
} else {
this.host = authority;
}
}
String rawPath = uri.getRawPath();
if (rawPath != null && rawPath.length() > 0) {
this.path(uri.getRawPath());
}
if (uri.getRawQuery() != null) {
// get rid of map to rebuild
this.query = null;
// split on ampersand...
String[] pairs = uri.getRawQuery().split("&");
for (String pair : pairs) {
String[] nv = pair.split("=");
switch (nv.length) {
case 1:
this.query(urlDecode(nv[0]), null);
break;
case 2:
this.query(urlDecode(nv[0]), urlDecode(nv[1]));
break;
default:
throw new IllegalArgumentException("Name value pair [" + pair + "] in query [" + uri.getRawQuery() + "] missing = char");
}
}
}
if (uri.getRawFragment() != null) {
this.fragment = urlDecode(uri.getRawFragment());
}
return this;
}
static private List normalizeRootPath(List rels) {
if (rels.size() < 2) {
// no path at all
return null;
} else {
// drop off the empty root
return rels.subList(1, rels.size());
}
}
// not sure we want this public (so package-level for now)
static List splitPath(String path, boolean decode) {
List paths = new ArrayList<>();
splitPath(path, paths, decode);
return paths;
}
// not sure we want this public (so package-level for now)
static void splitPath(String path, List paths, boolean decode) {
int pos = 0;
for (int i = 0; i < path.length(); i++) {
// found slash or on last char?
if (path.charAt(i) == '/') {
String p = path.substring(pos, i);
paths.add((decode ? urlDecode(p) : p));
pos = i+1;
}
}
// add last token?
if (pos < path.length()) {
String p = path.substring(pos);
paths.add((decode ? urlDecode(p) : p));
} else if (pos == path.length()) {
// add an empty string at end
paths.add("");
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy