/*
* The MIT License
*
* Copyright 2013 Tim Boudreau.
*
* 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.mastfrog.url;
import com.mastfrog.util.AbstractBuilder;
import com.mastfrog.util.Checks;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
/**
* Factory class for constructing URL objects w/ validation.
*
* @author Tim Boudreau
*/
public final class URLBuilder extends AbstractBuilder<PathElement, URL> {
static final char ANCHOR_DELIMITER = '#';
static final char PASSWORD_DELIMITER = ':';
static final String PATH_ELEMENT_DELIMITER = "/";
static final char PORT_DELIMITER = ':';
static final String PROTOCOL_DELIMITER = "://";
static final char LABEL_DELIMITER = '.';
static final char UNENCODABLE_CHARACTER = '?';
static final char URL_ESCAPE_PREFIX = '%';
static final char QUERY_PREFIX = '?';
private Protocol protocol;
private Host host;
private int port = -1;
private String userName;
private String password;
private Path path;
private Parameters query;
private Anchor anchor;
private List<ParametersElement> queryElements;
private List<Label> labels;
static final Charset ISO_LATIN = Charset.forName("ISO-8859-1");
private ParametersDelimiter delimiter;
static CharsetEncoder isoEncoder() {
return ISO_LATIN.newEncoder();
}
public URLBuilder(Protocol protocol) {
this.protocol = protocol;
}
public URLBuilder() {
this (Protocols.HTTP);
}
public URLBuilder (URL prototype) {
Checks.notNull("prototype", prototype);
protocol = prototype.getProtocol();
host = prototype.getHost();
if (prototype.getPort() == null) { //file protocol
port = 1;
} else {
port = prototype.getPort().intValue();
}
userName = prototype.getUserName() == null ? null :
prototype.getUserName().toString();
password = prototype.getPassword() == null ? null :
prototype.getPassword().toString();
if (prototype.getParameters() != null) {
Parameters p = prototype.getParameters();
if (p instanceof ParsedParameters) {
for (ParametersElement qe : ((ParsedParameters)p).getElements()) {
addQueryPair(qe);
}
} else {
this.query = p;
}
}
anchor = prototype.getAnchor();
Path origPath = prototype.getPath();
if (origPath != null) {
int sz = origPath.size();
for (int i=0; i < sz; i++) {
addPathElement(origPath.getElement(i));
}
}
}
public URLBuilder addPathElement (PathElement element) {
Checks.notNull("element", element);
if (this.path != null) {
throw new IllegalStateException ("Path explicitly set");
}
add (element);
return this;
}
public URLBuilder addPathElement (String element) {
Checks.notNull("element", element);
if (this.path != null) {
throw new IllegalStateException ("Path explicitly set");
}
add (element);
return this;
}
public URLBuilder addDomain(Label domain) {
Checks.notNull("domain", domain);
if (this.host != null) {
throw new IllegalStateException ("Host explicitly set");
}
if (this.labels == null) {
this.labels = new LinkedList<>();
}
labels.add (domain);
return this;
}
public URLBuilder addDomain (String domain) {
Checks.notNull("domain", domain);
return addDomain (new Label(domain));
}
public URLBuilder addQueryPair (String key, String value) {
Checks.notNull("value", value);
Checks.notNull("key", key);
if (query != null) {
throw new IllegalStateException ("Query explictly set");
}
if (queryElements == null) {
queryElements = new LinkedList<>();
}
queryElements.add (new ParametersElement(key, value));
return this;
}
public URLBuilder addQueryPair (ParametersElement element) {
Checks.notNull("element", element);
if (queryElements == null) {
queryElements = new LinkedList<>();
}
queryElements.add (element);
return this;
}
public URLBuilder setQueryDelimiter (ParametersDelimiter delimiter) {
Checks.notNull("delimiter", delimiter);
this.delimiter = delimiter;
return this;
}
public URLBuilder setProtocol (String protocol) {
Checks.notNull("protocol", protocol);
Protocol p = Protocols.forName(protocol);
return setProtocol (p);
}
public URLBuilder setProtocol (Protocol protocol) {
Checks.notNull("protocol", protocol);
this.protocol = protocol;
return this;
}
public URLBuilder setAnchor(Anchor anchor) {
Checks.notNull("anchor", anchor);
this.anchor = anchor;
return this;
}
public URLBuilder setAnchor(String anchor) {
Checks.notNull("anchor", anchor);
return setAnchor(new Anchor(anchor));
}
public URLBuilder setHost(Host host) {
Checks.notNull("host", host);
this.host = host;
return this;
}
public URLBuilder setPassword(String password) {
Checks.notNull("password", password);
this.password = password;
return this;
}
public URLBuilder setPath(Path path) {
Checks.notNull("path", path);
this.path = path;
return this;
}
public URLBuilder setPath(String path) {
Checks.notNull("path", path);
String[] els = path.split(URLBuilder.PATH_ELEMENT_DELIMITER);
List<PathElement> l = new LinkedList<>();
for (int i= 0; i < els.length; i++) {
String el = els[i];
if (i == els.length - 1 && path.trim().endsWith("/")) {
l.add(new PathElement(el, true));
} else {
l.add(new PathElement(el, false));
}
}
return setPath (new Path(l.toArray(new PathElement[l.size()])));
}
public URLBuilder setPort(int port) {
Checks.nonNegative("port", port);
this.port = port;
return this;
}
public URLBuilder setQuery(Parameters query) {
Checks.notNull("query", query);
if (queryElements != null && !queryElements.isEmpty()) {
throw new IllegalStateException ("Query elements set");
}
this.query = query;
return this;
}
public URLBuilder setUserName(String userName) {
Checks.notNull("userName", userName);
this.userName = userName;
return this;
}
public URLBuilder setHost (String host) {
Checks.notNull("host", host);
setHost(Host.parse(host));
return this;
}
@Override
public URLBuilder add(PathElement element) {
if (element.toString().equals("")) {
return this;
}
return (URLBuilder) super.add(element);
}
@Override
protected URLBuilder addElement(PathElement element) {
if (element.toString().equals("")) {
return this;
}
return (URLBuilder) super.addElement(element);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append (protocol);
sb.append (PROTOCOL_DELIMITER);
if (userName != null) {
append (sb, userName);
}
if (password != null) {
sb.append (PASSWORD_DELIMITER);
append (sb, password);
}
sb.append (host);
if (port != -1 && (protocol == null || port != protocol.getDefaultPort().intValue())) {
sb.append (PORT_DELIMITER);
sb.append (port);
}
sb.append (PATH_ELEMENT_DELIMITER);
if (path != null) {
sb.append (path);
}
if (queryElements != null && !queryElements.isEmpty()) {
ParsedParameters q = new ParsedParameters (queryElements.toArray(new ParametersElement[queryElements.size()]));
sb.append (delimiter == null ? q.toString(delimiter) : q);
}
if (anchor != null) {
sb.append (ANCHOR_DELIMITER);
sb.append (anchor);
}
return sb.toString();
}
static String escape (String toEscape, char... skip) {
Checks.notNull("toEscape", toEscape);
StringBuilder sb = new StringBuilder(toEscape.length() * 2);
append (sb, toEscape);
return sb.toString();
}
static void append (StringBuilder sb, String toEscape, char... skip) {
Checks.notNull("toEscape", toEscape);
Checks.notNull("sb", sb);
Arrays.sort(skip);
for (char c : toEscape.toCharArray()) {
if (skip.length > 0 && Arrays.binarySearch(skip, c) >= 0) {
sb.append (c);
continue;
}
if (!isoEncoder().canEncode(c)) {
sb.append (UNENCODABLE_CHARACTER);
continue;
}
if (isLetter(c) || isNumber(c) || c == '.') {
sb.append (c);
continue;
}
appendEscaped(c, sb);
}
}
static boolean isEncodableInLatin1(char c) {
return isoEncoder().canEncode(c);
}
static boolean isEncodableInLatin1(CharSequence seq) {
return isoEncoder().canEncode(seq);
}
static boolean isLetter(char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
}
static boolean isNumber(char c) {
return c >='0' && c <= '9';
}
static boolean isValidHostCharacter(char c) {
return isLetter(c) || isNumber(c);
}
static boolean isReserved(char c) {
switch (c) {
case '$':
case '&':
case '+':
case ',':
case '/':
case ':':
case ';':
case '=':
case '?':
case '@':
return true;
default :
return false;
}
}
static void appendEscaped (char c, StringBuilder to) {
Checks.notNull("to", to);
String hex = Integer.toHexString(c);
to.append (URL_ESCAPE_PREFIX);
if (hex.length() == 1) {
to.append ('0');
}
to.append (hex);
}
static String unescape (String seq) {
Checks.notNull("seq", seq);
if (seq.indexOf('%') < 0) {
return seq;
}
char[] chars = seq.toCharArray();
StringBuilder sb = new StringBuilder(seq.length());
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
if (c == URL_ESCAPE_PREFIX && i < chars.length - 2 && isHexCharacter(chars[i + 1]) && isHexCharacter(chars[i + 2])) {
String hex = new String(chars, i+1, 2);
int codePoint = Integer.valueOf(hex, 16);
c = (char) codePoint;
sb.append (c);
i+=2;
continue;
} else {
sb.append (c);
}
}
return sb.toString();
}
static boolean isHexCharacter(char c) {
switch (c) {
case 'a':
case 'b':
case 'c':
case 'd':
case 'e':
case 'f':
case 'A':
case 'B':
case 'C':
case 'D':
case 'E':
case 'F':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
return true;
default :
return false;
}
}
@Override
public URL create() {
Host h = host == null ? labels == null ? null : new Host(labels.toArray(new Label[labels.size()])) : host;
return new URL(userName, password, protocol, h, getPort(), getPath(), getQuery(), anchor);
}
Port getPort() {
if (port != -1) {
return new Port(port);
} else if (protocol != null) {
return protocol.getDefaultPort();
}
return null;
}
Path getPath() {
if (path != null) {
return path;
}
if (size() == 0) {
return null;
}
PathElement[] els = this.elements().toArray(new PathElement[size()]);
return new Path(els);
}
Parameters getQuery() {
if (query != null) {
return query;
}
if (queryElements != null && !queryElements.isEmpty()) {
return new ParsedParameters (queryElements.toArray(new ParametersElement[queryElements.size()]));
}
return null;
}
@Override
protected PathElement createElement(String string) {
Checks.notNull("string", string);
while (string.startsWith("/") && string.length() != 0) {
string = string.substring(1);
}
return new PathElement(string, false);
}
}