![JAR search and dependency download from the Maven repository](/logo.png)
io.inversion.Url Maven / Gradle / Ivy
/*
* Copyright (c) 2015-2019 Rocket Partners, LLC
* https://github.com/inversion-api
*
* 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.inversion;
import io.inversion.json.JSMap;
import io.inversion.json.JSParser;
import io.inversion.rql.Rql;
import io.inversion.rql.Term;
import io.inversion.utils.Path;
import io.inversion.utils.Utils;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;
/**
* Utility class for parsing and working with HTTP(S) URLs.
*
* Not for use with non HTTP(S) urls.
*
* A number of different utility methods are provided to make it simple to find or remove different query string keys.
*/
public final class Url {
/**
* The url string as supplied to the constructor, with 'http://localhost/' prepended if the constructor url arg did not contain a host
*/
private String original;
/**
* The url protocol, either http or https
*/
private String protocol = "http";
/**
* The url host.
*/
private String hostAsString = null;
private Path hostAsPath = null;
/**
* The url port number if a custom port was provided
*/
private int port = 0;
/**
* The part of the url after the host/port before the query string.
*/
private Path path = null;
/**
* A case insensitive map of query string name/value pairs that preserves iteration order
*
* Implementation Node: params
is not actually JSON, JSMap is used as the map
* implementation here simply because it is an affective case insensitive map that preserves
* the original key case key iteration order.
*/
private JSMap params = new JSMap();
public Url copy(){
Url url = new Url();
url.original = original;
url.protocol = protocol;
url.hostAsString = hostAsString;
url.hostAsPath = hostAsPath == null ? null : new Path(hostAsPath);
url.port = port;
url.path = path == null ? null : new Path(path);
url.params = params.size() == 0 ? url.params : JSParser.asJSMap(params.toString());
return url;
}
private Url(){
}
/**
* Parses url
into its protocol, host, port, path and query string param parts.
*
* If url
does not start with "http://" or "https://" then "http://127.0.0.1" will be prepended.
*
* @param url url string
*/
public Url(String url) {
String origionalUrl = url;
try {
//-- chopping out querystring is easy so start there
int queryIndex = url.indexOf('?');
if (queryIndex >= 0) {
String query = url.substring(queryIndex + 1);
url = url.substring(0, queryIndex);
withQueryString(query);
}
if(url.startsWith("//") || url.startsWith("http:/") || url.startsWith("https:/")){
if(url.startsWith("//")) {
withProtocol("//");
}
else if(url.startsWith("http://")){
withProtocol("http");
url = url.substring(5);
}
else if(url.startsWith("https://")){
withProtocol("https");
url = url.substring(6);
}
else{
throw Utils.ex("Your requested URL '{}' is malformed.", origionalUrl);
}
//-- trim off any type extra "/" characters
while(url.startsWith("/"))
url = url.substring(1);
int slash = url.indexOf("/");
int colon = url.indexOf(":");
if(colon > 0 && (slash < 0 || colon < slash)){
withHost(url.substring(0, colon));
url = url.substring(colon);
}
else if(slash > 0){
withHost(url.substring(0, slash));
url = url.substring(slash);
}
else{
withHost(url);
url = "";
}
if(url.startsWith(":")){
slash = url.indexOf("/");
if(slash > 0){
withPort(Integer.parseInt(url.substring(1, slash)));
url = url.substring(slash);
}
else{
withPort(Integer.parseInt(url.substring(1)));
url = "";
}
}
}
else{
protocol = "//";
}
while(url.startsWith("/"))
url = url.substring(1);
if(url.length() > 0){
Path path = new Path(url);
for(int i=0; i params) {
StringBuilder query = new StringBuilder();
List qs = new ArrayList();
for (String key : params.keySet()) {
if (key.indexOf("(") > 0)
qs.add(key);
}
if (qs.size() > 0) {
params = new LinkedHashMap<>(params);
for (String q : qs) {
params.remove(q);
}
params.put("q", Utils.implode(",", qs));
}
for (String key : params.keySet()) {
try {
if (params.get(key) != null) {
query.append(URLEncoder.encode(key, "UTF-8")).append("=").append(URLEncoder.encode(params.get(key).toString(), "UTF-8")).append("&");
} else {
query.append(URLEncoder.encode(key, "UTF-8")).append("&");
}
} catch (UnsupportedEncodingException e) {
Utils.rethrow(e);
}
}
if (query.length() > 0)
query = new StringBuilder(query.substring(0, query.length() - 1));
String str = query.toString();
str = str.replace("%28", "(");
str = str.replace("%29", ")");
str = str.replace("%2C", ",");
return str;
}
public static String encode(String str) {
try {
str = URLEncoder.encode(str, "UTF-8");
str = str.replace("%2C", ",");
str = str.replace("%7E", "~");
return str;
} catch (Exception ex) {
throw Utils.ex(ex);
}
}
/**
* Generates a string string representation of this url with any query string parameters URL encoded and port number included only if it differs from the standard protocol port.
*
* @return the string representation of this url
*/
public String toString() {
String url = protocol;
if(!protocol.equals("//"))
url += "://";
url += hostAsString != null ? hostAsString : "127.0.0.1";
if (port > 0) {
if (!((port == 80 && "http".equalsIgnoreCase(protocol)) || (port == 443 && "https".equalsIgnoreCase(protocol))))
url += ":" + port;
}
else if(hostAsString == null){
url += ":8080";
}
if (path != null && path.size() > 0) {
url += "/" + path;
}
if (params.size() > 0) {
while (url.endsWith("/"))
url = url.substring(0, url.length() - 1);
url += "?";
url += toQueryString((Map) params);
}
return url;
}
/**
* Checks url equality based on type and toString equality
*
* @return true if url
is a Url with a matching toString
*/
public boolean equals(Object url) {
if (url instanceof Url) {
return toString().equals(url.toString());
}
return false;
}
public String getDomain() {
String domain = hostAsString;
if (domain.lastIndexOf('.') > domain.indexOf('.')) {
domain = domain.substring(domain.indexOf('.') + 1);
}
return domain;
}
/**
* Generates a URL encode query string for params
*
* @return a URL encoded query string if params.size() is @gt; 0
else empty string
* @see #toQueryString(Map)
*/
public String getQueryString() {
if (params.size() == 0)
return "";
return toQueryString((Map) params);
}
public String getHost() {
return hostAsString;
}
public Url withHost(String host) {
this.hostAsString = host;
this.hostAsPath = new Path(hostAsString.replace('.', '/'));
return this;
}
public Path getHostAsPath(){
return hostAsPath;
}
public int getPort() {
return port;
}
public Url withPort(int port) {
this.port = port;
return this;
}
public String getProtocol() {
return protocol;
}
public Url withProtocol(String protocol) {
this.protocol = protocol;
return this;
}
public Path getPath() {
if(path == null)
return new Path();
return new Path(path);
}
public Url withPath(Path path) {
this.path = path;
return this;
}
/**
* Gets the last url path part if it exists
*
* @return path.last() if it exists otherwise null
*/
public String getFile() {
if (path != null)
return path.last();
return null;
}
/**
* Parses queryString
and replace params
*
* @param queryString query prams to add
* @return this
*/
public Url withQueryString(String queryString) {
withParams(Utils.parseQueryString(queryString));
return this;
}
/**
* Adds all key/value pairs from newParams
to params
* overwriting any exiting keys on a case insensitive basis
*
* @param params name/value pairs to add to the query string params map
* @return this
*/
public Url withParams(Map params) {
if (params != null) {
for (String key : params.keySet()) {
withParam(key, params.get(key));
}
}
return this;
}
public Url withParam(Term term){
params.put(term.toString(), null);
return this;
}
/**
* Adds name/value to params
overwriting any preexisting key/value pair
* on a key case insensitive basis.
*
* @param name the key to add or overwrite, may not be null
* @param value the value, may be null
* @return this
*/
public Url withParam(String name, String value) {
int paren = name.indexOf("(");
if (paren > 0) {
String func = fixLegacyParamName(name.substring(0, paren));
if (Utils.in(func, "page", "size", "sort", "include", "exclude", "expand", "collapse")) {
value = name.substring(paren + 1, name.lastIndexOf(")"));
name = func;
}
} else {
name = fixLegacyParamName(name);
}
if (!Utils.empty(name)) {
if ("q".equalsIgnoreCase(name)) {
value = "and(" + value + ")";
Term term = Rql.parse(value);
for (Term child : term.getTerms())
withParam(child.toString(), null);
} else {
params.put(name, value);
}
}
return this;
}
String fixLegacyParamName(String name) {
switch (name.toLowerCase()) {
case "pagenum":
return "page";
case "limit":
return "size";
case "order":
return "sort";
case "includes":
return "include";
case "excludes":
return "exclude";
case "expands":
return "expand";
case "collapses":
return "collapse";
}
return name;
}
public Url withParams(String... nvpairs) {
if (nvpairs != null) {
for (int i = 0; i < nvpairs.length - 1; i = i + 2)
withParam(nvpairs[i], nvpairs[i + 1]);
if (nvpairs.length % 2 == 1)
withParam(nvpairs[nvpairs.length - 1], null);
}
return this;
}
// /**
// * Replaces any existing param that has key
as a whole word case insensitive substring in its key.
// *
// * If the key/value pair "eq(dog,Fido)" = null
is in the map replaceParam("DOG", "Fifi")
// * would cause it to be removed and the pair "DOG" / "Fifi" would be added.
// *
// * @param key the key to add overwrite, may not be null, also used a a regex whole world case insensitive token to search for other keys to remove.
// * @param value the value, may be null
// * @see Utils#containsToken
// */
// public void replaceParam(String key, String value) {
// for (String existing : new ArrayList<>(params.keySet())) {
// if (Utils.containsToken(key, existing)) {
// params.remove(existing);
// }
// }
//
// withParam(key, value);
// }
/**
* Removes any param that has one of tokens
as a whole word case insensitive substring in the key.
*
* @param tokens string tokens when found in a param key will cause them to be removed
* @return the first value found that contained any one of tokens
* @see Utils#containsToken
*/
public String clearParams(String... tokens) {
String oldValue = null;
for (String token : tokens) {
for (String key : new LinkedHashSet<>(params.keySet())) {
String value = (String) params.get(key);
if (Utils.containsToken(token, key)) {
String removed = (String) params.remove(key);
if (oldValue == null)
oldValue = removed;
}
}
}
return oldValue;
}
public void clearParams() {
params.clear();
}
/**
* Finds a key that has any one of tokens
as a whole word case insensitive substring
*
* @param tokens substrings to search params.keySet() for
* @return the first param key that has anyone of tokens
as a whole word case insensitive substring
*/
public String findKey(String... tokens) {
for (String token : tokens) {
for (String key : params.keySet()) {
if (Utils.containsToken(token, key)) {
return key;
}
}
}
return null;
}
/**
* Finds the value associated with findKey(tokens)
*
* @param tokens substrings to search params.keySet() for
* @return the value for the first param key that has anyone of tokens
as a whole word case insensitive substring
* @see #findKey(String...)
*/
public String findKeyValue(String... tokens) {
String key = findKey(tokens);
if (key != null)
return (String)params.get(key);
return null;
}
/**
* Gets the param value with key
based on a case insensitive match.
*
* @param key the key to get
* @return the param value for key
based on a case insensitive match.
*/
public String getParam(String key) {
return (String) params.get(key);
}
/**
* @return a new case insensitive order preserving map copy of params
*/
public Map getParams() {
return (Map) params;
}
/**
* @return the url string used in constructing this Url.
*/
public String getOriginal() {
return original;
}
}