/*
* Copyright 2013 <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
*
* 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 org.ocpsoft.urlbuilder;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.ocpsoft.urlbuilder.util.Decoder;
import org.ocpsoft.urlbuilder.util.Encoder;
/**
* Representation of a uniform resource locator, or web address. Internal state is stored as it is originally provided,
* and must be encoded or decoded as necessary.
*
* @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
*/
public class AddressBuilder
{
private volatile Address address;
protected volatile CharSequence scheme;
protected volatile CharSequence schemeSpecificPart;
protected volatile CharSequence domain;
protected volatile Integer port;
protected volatile CharSequence path;
protected Map<CharSequence, Parameter> parameters = new LinkedHashMap<CharSequence, Parameter>();
protected Map<CharSequence, Parameter> queries = new LinkedHashMap<CharSequence, Parameter>();
protected CharSequence anchor;
protected AddressBuilder()
{}
/**
* Create a new {@link AddressBuilder} instance.
*/
public static AddressBuilderBase begin()
{
return new AddressBuilderBase(new AddressBuilder());
}
/**
* Generate an {@link Address} representing the current state of this {@link AddressBuilder}.
*/
protected Address build()
{
if (address == null)
{
address = new ParameterizedAddressResult(this);
}
return address;
}
/**
* Generate an {@link Address} representing the current literal state of this {@link AddressBuilder}.
* <p>
* (Does not apply parameterization. E.g. The URL `/{foo}` will be treated as literal text, as opposed to calling
* {@link #build()}, which would result in `foo` being treated as a parameterized expression)
*/
protected Address buildLiteral()
{
if (address == null)
{
address = new AddressResult(this);
}
return address;
}
/**
* Create a new {@link Address} from the given URL. Improperly formatted or encoded URLs are not parse-able and will
* result in an exception. No builder parameterization is possible using this method.
*
* @see http://en.wikipedia.org/wiki/URI_scheme
* @throws IllegalArgumentException when the input URL or URL fragment is not valid.
*/
public static Address create(String url) throws IllegalArgumentException
{
try {
URI u = new URI(url);
String scheme = u.getScheme();
String host = u.getHost();
if (scheme != null && host == null)
return AddressBuilder.begin().scheme(u.getScheme()).schemeSpecificPart(u.getRawSchemeSpecificPart())
.buildLiteral();
else
return AddressBuilder.begin().scheme(scheme).domain(host).port(u.getPort())
.path(u.getRawPath()).queryLiteral(u.getRawQuery()).anchor(u.getRawFragment()).buildLiteral();
}
catch (URISyntaxException e) {
throw new IllegalArgumentException(
"[" + url + "] is not a valid URL fragment. Consider encoding relevant portions of the URL with ["
+ Encoder.class
+ "], or use the provided builder pattern via this class to specify part encoding.", e);
}
}
/**
* Set the scheme section of this {@link Address}.
*/
AddressBuilderScheme scheme(CharSequence scheme)
{
this.scheme = scheme;
return new AddressBuilderScheme(this);
}
/**
* Set the scheme section of this {@link Address}.
*/
AddressBuilderSchemeSpecificPart schemeSpecificPart(CharSequence schemeSpecificPart)
{
this.schemeSpecificPart = schemeSpecificPart;
return new AddressBuilderSchemeSpecificPart(this);
}
/**
* Set the domain section of this {@link Address}.
*/
AddressBuilderDomain domain(CharSequence domain)
{
this.domain = domain;
return new AddressBuilderDomain(this);
}
/**
* Set the port section of this {@link Address}.
*/
AddressBuilderPort port(int port)
{
if (port >= 0)
this.port = port;
return new AddressBuilderPort(this);
}
/**
* Set the path section of this {@link Address}. The given value will be stored without additional encoding or
* decoding.
*/
AddressBuilderPath path(CharSequence path)
{
this.path = path;
return new AddressBuilderPath(this);
}
/**
* Set the path section of this {@link Address}. The given value will be decoded before it is stored.
*/
AddressBuilderPath pathDecoded(CharSequence path)
{
this.path = Decoder.path(path);
return new AddressBuilderPath(this);
}
/**
* Set the path section of this {@link Address}. The given value will be encoded before it is stored.
*/
AddressBuilderPath pathEncoded(CharSequence path)
{
this.path = Encoder.path(path);
return new AddressBuilderPath(this);
}
/**
* Set a query-parameter to a value or multiple values. The given name and values will be stored without additional
* encoding or decoding.
*/
AddressBuilderQuery query(CharSequence name, Object... values)
{
if (name != null && values != null)
{
this.queries.put(name.toString(), Parameter.create(name.toString(), values));
}
return new AddressBuilderQuery(this);
}
/**
* Set a query-parameter value or multiple values. The given name and values be decoded before they are stored.
*/
AddressBuilderQuery queryDecoded(CharSequence name, Object... values)
{
if (name != null && values != null)
{
List<Object> encodedValues = new ArrayList<Object>(values.length);
for (Object value : values)
{
if (value == null)
encodedValues.add(value);
else
encodedValues.add(Decoder.query(value.toString()));
}
this.queries.put(Decoder.query(name.toString()), Parameter.create(name.toString(), encodedValues));
}
return new AddressBuilderQuery(this);
}
/**
* Set a query-parameter to a value or multiple values. The given name and values be encoded before they are stored.
*/
AddressBuilderQuery queryEncoded(CharSequence name, Object... values)
{
if (name != null && values != null)
{
List<Object> encodedValues = new ArrayList<Object>(values.length);
for (Object value : values)
{
if (value == null)
encodedValues.add(value);
else
encodedValues.add(Encoder.query(value.toString()));
}
this.queries.put(Encoder.query(name.toString()), Parameter.create(name.toString(), encodedValues));
}
return new AddressBuilderQuery(this);
}
/**
* Set a literal query string without additional encoding or decoding. A leading '?' character is optional; the
* builder will add one if necessary.
*/
AddressBuilderQuery queryLiteral(String query)
{
if (query != null)
{
if (query.startsWith("?"))
query = query.substring(1);
/*
* Linked hash map is important here in order to retain the order of query parameters.
*/
Map<CharSequence, List<CharSequence>> params = new LinkedHashMap<CharSequence, List<CharSequence>>();
query = decodeHTMLAmpersands(query);
int index = 0;
while ((index = query.indexOf('&')) >= 0 || !query.isEmpty())
{
String pair = query;
if (index >= 0)
{
pair = query.substring(0, index);
query = query.substring(index);
if (!query.isEmpty())
query = query.substring(1);
}
else
query = "";
String name;
String value;
int pos = pair.indexOf('=');
// for "n=", the value is "", for "n", the value is null
if (pos == -1)
{
name = pair;
value = null;
}
else
{
name = pair.substring(0, pos);
value = pair.substring(pos + 1, pair.length());
}
List<CharSequence> list = params.get(name);
if (list == null)
{
list = new ArrayList<CharSequence>();
params.put(name, list);
}
list.add(value);
}
for (Entry<CharSequence, List<CharSequence>> entry : params.entrySet()) {
query(entry.getKey(), entry.getValue().toArray());
}
}
return new AddressBuilderQuery(this);
}
private String decodeHTMLAmpersands(String url)
{
if (url != null)
{
int index = 0;
while ((index = url.indexOf("&")) >= 0)
{
url = url.substring(0, index + 1) + url.substring(index + 5);
}
}
return url;
}
/**
* Set the anchor section of this {@link Address}.
*/
AddressBuilderAnchor anchor(CharSequence anchor)
{
this.anchor = anchor;
return new AddressBuilderAnchor(this);
}
/**
* Set a parameter name and value or values. The supplied values will be stored without additional encoding.
*/
void set(CharSequence name, Object... values)
{
this.parameters.put(name.toString(), Parameter.create(name.toString(), values));
}
/**
* Set a parameter name and value or values. The values will be decoded before they are stored.
*/
void setDecoded(CharSequence name, Object... values)
{
if (name != null && values != null)
{
List<Object> encodedValues = new ArrayList<Object>(values.length);
for (Object value : values)
{
if (value == null)
encodedValues.add(value);
else
encodedValues.add(Decoder.path(value.toString()));
}
this.parameters.put(name.toString(), Parameter.create(name.toString(), encodedValues));
}
}
/**
* Set a parameter name and value or values. The values will be encoded before they are stored.
*/
void setEncoded(CharSequence name, Object... values)
{
if (name != null && values != null)
{
List<Object> encodedValues = new ArrayList<Object>(values.length);
for (Object value : values)
{
if (value == null)
encodedValues.add(value);
else
encodedValues.add(Encoder.path(value.toString()));
}
this.parameters.put(name.toString(), Parameter.create(name.toString(), encodedValues));
}
}
@Override
public String toString()
{
return buildLiteral().toString();
}
/**
* Package private method for {@link Address} implementations to use for rendering.
*/
static StringBuilder toString(Address address)
{
StringBuilder result = new StringBuilder();
if (address.isSchemeSet())
result.append(address.getScheme()).append(":");
if (address.isSchemeSpecificPartSet())
{
result.append(address.getSchemeSpecificPart());
}
else
{
if (address.isDomainSet())
result.append("//").append(address.getDomain());
if (address.isPortSet())
result.append(":").append(address.getPort());
if (address.isPathSet())
result.append(address.getPath());
if (address.isQuerySet())
{
if (address.isDomainSet() && !address.isPathSet())
result.append("/");
result.append('?').append(address.getQuery());
}
if (address.isAnchorSet())
result.append('#').append(address.getAnchor());
}
return result;
}
Map<String, List<Object>> getQueries()
{
Map<String, List<Object>> result = new LinkedHashMap<String, List<Object>>();
for (Entry<CharSequence, Parameter> entry : this.queries.entrySet()) {
CharSequence key = entry.getKey();
result.put(key == null ? null : key.toString(), entry.getValue().getValues());
}
return Collections.unmodifiableMap(result);
}
}