/*
* The MIT License (MIT)
*
* Copyright (c) 2015 Lachlan Dowding
*
* 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 permafrost.tundra.net.uri;
import com.wm.app.b2b.server.ServiceException;
import com.wm.data.IData;
import com.wm.data.IDataCursor;
import com.wm.data.IDataFactory;
import com.wm.data.IDataUtil;
import permafrost.tundra.flow.variable.SubstitutionHelper;
import permafrost.tundra.lang.ArrayHelper;
import permafrost.tundra.lang.CharsetHelper;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
/**
* A collection of convenience methods for working with URIs.
*/
public final class URIHelper {
/**
* The default character set used for URI strings.
*/
public static final Charset DEFAULT_CHARSET = Charset.forName("US-ASCII");
/**
* The default character set name used for URI strings.
*/
public static final String DEFAULT_CHARSET_NAME = DEFAULT_CHARSET.name();
/**
* Disallow instantiation of this class.
*/
private URIHelper() {}
/**
* Parses a URI string into an IData representation.
*
* @param input The URI string to be parsed.
* @return An IData representation of the give URI.
* @throws URISyntaxException If the given string is an invalid URI.
*/
public static IData parse(String input) throws URISyntaxException {
if (input == null) return null;
// treat Windows UNC file URIs as server-based rather than path-based
if (input.toLowerCase().startsWith("file:////")) {
input = "file://" + input.substring(9, input.length());
}
IData output = IDataFactory.create();
IDataCursor cursor = output.getCursor();
try {
URI uri = new URI(input);
uri.normalize();
String scheme = uri.getScheme();
// schemes are case-insensitive, according to RFC 2396
if (scheme != null) scheme = scheme.toLowerCase();
if (scheme != null) IDataUtil.put(cursor, "scheme", scheme);
IData query = null;
String ssp = uri.getRawSchemeSpecificPart();
if (uri.isOpaque()) {
if (ssp.contains("?")) {
query = URIQueryHelper.parse(ssp.substring(ssp.indexOf("?") + 1, ssp.length()), true);
ssp = ssp.substring(0, ssp.indexOf("?"));
}
if (ssp != null) IDataUtil.put(cursor, "body", decode(ssp));
} else {
String authority = uri.getAuthority();
if (authority != null) {
IData server = IDataFactory.create();
IDataCursor sc = server.getCursor();
// parse user-info component according to the format user:[password]
String user = uri.getUserInfo();
String password = null;
String userInfoSeparator = ":";
if (user != null && user.contains(userInfoSeparator)) {
password = user.substring(user.indexOf(userInfoSeparator) + 1, user.length());
user = user.substring(0, user.indexOf(userInfoSeparator));
}
if (user != null) IDataUtil.put(sc, "user", user);
if (password != null) IDataUtil.put(sc, "password", password);
// hosts are case-insensitive, according to RFC 2396, but we will preserve the case to be safe
String host = uri.getHost();
if (host != null) IDataUtil.put(sc, "host", host);
// if port is -1, then it wasn't specified in the URI
int port = uri.getPort();
if (port >= 0) IDataUtil.put(sc, "port", "" + uri.getPort());
sc.destroy();
// if host is null, then this URI must be registry-based
IData authorityDocument = IDataFactory.create();
IDataCursor ac = authorityDocument.getCursor();
if (host == null) {
IDataUtil.put(ac, "registry", authority);
} else {
IDataUtil.put(ac, "server", server);
}
ac.destroy();
IDataUtil.put(cursor, "authority", authorityDocument);
}
String path = uri.getPath();
String[] paths = URIPathHelper.parse(path);
String file = null;
if (!path.endsWith("/")) {
file = ArrayHelper.get(paths, -1);
paths = ArrayHelper.drop(paths, -1);
}
if (paths != null && paths.length > 0) IDataUtil.put(cursor, "path", paths);
if (file != null && !file.equals("")) IDataUtil.put(cursor, "file", file);
query = URIQueryHelper.parse(uri.getRawQuery(), true);
}
if (query != null) IDataUtil.put(cursor, "query", query);
String fragment = uri.getFragment();
if (fragment != null) IDataUtil.put(cursor, "fragment", fragment);
IDataUtil.put(cursor, "absolute?", "" + uri.isAbsolute());
IDataUtil.put(cursor, "opaque?", "" + uri.isOpaque());
} finally {
cursor.destroy();
}
return output;
}
/**
* Emits a URI given as an IData document as a string.
*
* @param input An IData containing a specific URI structure to be serialized.
* @return The URI string representing the given IData.
* @throws URISyntaxException If the given string is an invalid URI.
*/
public static String emit(IData input) throws URISyntaxException {
if (input == null) return null;
String output = null;
IDataCursor cursor = input.getCursor();
try {
URI uri = null;
String type = IDataUtil.getString(cursor, "type");
String scheme = IDataUtil.getString(cursor, "scheme");
// schemes are case-insensitive, according to RFC 2396
if (scheme != null) scheme = scheme.toLowerCase();
String fragment = IDataUtil.getString(cursor, "fragment");
IData queryDocument = IDataUtil.getIData(cursor, "query");
String query = URIQueryHelper.emit(queryDocument, true);
String body = IDataUtil.getString(cursor, "body");
IData authority = IDataUtil.getIData(cursor, "authority");
String[] paths = IDataUtil.getStringArray(cursor, "path");
String file = IDataUtil.getString(cursor, "file");
if (body == null && !(authority == null && paths == null && file == null)) {
// create an hierarchical URI
String path = "";
if (paths != null) {
path = "/" + ArrayHelper.join(paths, "/");
}
path = path + "/";
if (file != null) {
path = path + file;
}
if (authority == null) {
uri = new URI(scheme, null, path, null, null);
} else {
IDataCursor ac = authority.getCursor();
String registry = IDataUtil.getString(ac, "registry");
IData server = IDataUtil.getIData(ac, "server");
ac.destroy();
if (registry == null) {
IDataCursor sc = server.getCursor();
// hosts are case-insensitive, according to RFC 2396, but we will preserve the case to be safe
String host = IDataUtil.getString(sc, "host");
String portString = IDataUtil.getString(sc, "port");
int port = -1;
if (portString != null) port = Integer.parseInt(portString);
String userinfo = IDataUtil.getString(sc, "user");
if (userinfo != null && !(userinfo.equals(""))) {
String password = IDataUtil.getString(sc, "password");
if (password != null && !(password.equals(""))) userinfo = userinfo + ":" + password;
} else {
userinfo = null; // ignore empty strings
}
sc.destroy();
uri = new URI(scheme, userinfo, host, port, path, null, null);
} else {
uri = new URI(scheme, registry, path, null, null);
}
}
} else {
uri = new URI(scheme, body, null);
}
output = uri.normalize().toASCIIString();
if (query != null) output = output + "?" + query;
if (fragment != null && !(fragment.equals(""))) output = output + "#" + encode(fragment);
// support Windows UNC file URIs, and work-around java bug with
// file URIs where scheme is followed by ':/' rather than '://'
if (output.startsWith("file:") && uri.getHost() == null) {
output = "file://" + output.substring(5, output.length());
}
} finally {
cursor.destroy();
}
return output;
}
/**
* Normalizes a URI string, refer: http://docs.oracle.com/javase/6/docs/api/java/net/URI.html#normalize().
*
* @param input The URI string to be normalized.
* @return The normalized URI string.
* @throws URISyntaxException If the given string is an invalid URI.
*/
public static String normalize(String input) throws URISyntaxException {
return emit(parse(input));
}
/**
* URI encodes a string, refer: http://docs.oracle.com/javase/6/docs/api/java/net/URLEncoder.html.
*
* @param input The string to be URI encoded.
* @return The string after being URI encoded.
*/
public static String encode(String input) {
return encode(input, DEFAULT_CHARSET);
}
/**
* URI encodes a string, refer: http://docs.oracle.com/javase/6/docs/api/java/net/URLEncoder.html.
*
* @param input The string to be URI encoded.
* @param charsetName The character set to use when URI encoding the string.
* @return The string after being URI encoded.
*/
public static String encode(String input, String charsetName) {
return encode(input, CharsetHelper.normalize(charsetName, DEFAULT_CHARSET));
}
/**
* URI encodes a string, refer: http://docs.oracle.com/javase/6/docs/api/java/net/URLEncoder.html.
*
* @param input The string to be URI encoded.
* @param charset The character set to use when URI encoding the string.
* @return The string after being URI encoded.
*/
public static String encode(String input, Charset charset) {
if (input == null) return null;
String output;
try {
output = URLEncoder.encode(input, CharsetHelper.normalize(charset, DEFAULT_CHARSET).name()).replace("+", "%20");
} catch (UnsupportedEncodingException ex) {
throw new IllegalArgumentException(ex); // this should never happen
}
return output;
}
/**
* URI encodes a string list, refer: http://docs.oracle.com/javase/6/docs/api/java/net/URLEncoder.html.
*
* @param input A list of strings to be URI encoded.
* @return The new copy of the list of strings after being URI encoded.
*/
public static String[] encode(String[] input) {
return encode(input, DEFAULT_CHARSET);
}
/**
* URI encodes a string list, refer: http://docs.oracle.com/javase/6/docs/api/java/net/URLEncoder.html.
*
* @param input A list of strings to be URI encoded.
* @param charsetName The character set to use when URI encoding the strings.
* @return The new copy of the list of strings after being URI encoded.
*/
public static String[] encode(String[] input, String charsetName) {
return encode(input, CharsetHelper.normalize(charsetName, DEFAULT_CHARSET));
}
/**
* URI encodes a string list, refer: http://docs.oracle.com/javase/6/docs/api/java/net/URLEncoder.html.
*
* @param input A list of strings to be URI encoded.
* @param charset The character set to use when URI encoding the strings.
* @return The new copy of the list of strings after being URI encoded.
*/
public static String[] encode(String[] input, Charset charset) {
if (input == null) return null;
String[] output = new String[input.length];
for (int i = 0; i < input.length; i++) {
output[i] = encode(input[i], charset);
}
return output;
}
/**
* URI decodes a string, refer: http://docs.oracle.com/javase/6/docs/api/java/net/URLDecoder.html.
*
* @param input The string to be URI decoded.
* @return The string after being URI decoded.
*/
public static String decode(String input) {
return decode(input, DEFAULT_CHARSET);
}
/**
* URI decodes a string, refer: http://docs.oracle.com/javase/6/docs/api/java/net/URLDecoder.html.
*
* @param input The string to be URI decoded.
* @param charsetName The character set to use when URI decoding the string.
* @return The string after it has been URI decoded.
*/
public static String decode(String input, String charsetName) {
return decode(input, CharsetHelper.normalize(charsetName, DEFAULT_CHARSET));
}
/**
* URI decodes a string, refer: http://docs.oracle.com/javase/6/docs/api/java/net/URLDecoder.html.
*
* @param input The string to be URI decoded.
* @param charset The character set to use when URI decoding the string.
* @return The string after it has been URI decoded.
*/
public static String decode(String input, Charset charset) {
if (input == null) return null;
String output;
try {
output = URLDecoder.decode(input, CharsetHelper.normalize(charset, DEFAULT_CHARSET).name());
} catch (UnsupportedEncodingException ex) {
throw new IllegalArgumentException(ex); // this should never happen
}
return output;
}
/**
* URI decodes a string list, refer: http://docs.oracle.com/javase/6/docs/api/java/net/URLDecoder.html.
*
* @param input The list of strings to be URI decoded.
* @return A new copy of the list of strings after being URI decoded.
*/
public static String[] decode(String[] input) {
return decode(input, DEFAULT_CHARSET);
}
/**
* URI decodes a string list, refer: http://docs.oracle.com/javase/6/docs/api/java/net/URLDecoder.html.
*
* @param input The list of strings to be URI decoded.
* @param charsetName The character set to use when URI decoding the strings.
* @return A new copy of the list of strings after being URI decoded.
*/
public static String[] decode(String[] input, String charsetName) {
return decode(input, CharsetHelper.normalize(charsetName, DEFAULT_CHARSET));
}
/**
* URI decodes a string list, refer: http://docs.oracle.com/javase/6/docs/api/java/net/URLDecoder.html.
*
* @param input The list of strings to be URI decoded.
* @param charset The character set to use when URI decoding the strings.
* @return A new copy of the list of strings after being URI decoded.
*/
public static String[] decode(String[] input, Charset charset) {
if (input == null) return null;
String[] output = new String[input.length];
for (int i = 0; i < input.length; i++) {
output[i] = decode(input[i], charset);
}
return output;
}
/**
* Performs variable substitution on the components of the given URI string.
*
* @param uri The URI string to perform variable substitution on.
* @param scope The scope variables are resolved against.
* @return The resulting URI string after variable substitution.
* @throws ServiceException If an error occurs during substitution.
* @throws URISyntaxException If the given string is not a valid URI.
*/
public static String substitute(String uri, IData scope) throws ServiceException, URISyntaxException {
return emit(SubstitutionHelper.substitute(parse(uri), scope, true));
}
}