/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.wicket.request;

import java.io.Serializable;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;

import org.apache.wicket.util.encoding.UrlDecoder;
import org.apache.wicket.util.encoding.UrlEncoder;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.lang.Generics;
import org.apache.wicket.util.lang.Objects;
import org.apache.wicket.util.string.StringValue;
import org.apache.wicket.util.string.Strings;

/**
 * Represents the URL to an external resource or internal resource/component.
 * <p>
 * A url could be:
 * <ul>
 *     <li>full - consists of an optional protocol/scheme, a host name, an optional port,
 * optional segments and and optional query parameters.</li>
 *      <li>non-full:
 *      <ul>
 *          <li>absolute - a url relative to the host name. Such url may escape from the application by using
 *          different context path and/or different filter path. For example: <code>/foo/bar</code></li>
 *          <li>relative - a url relative to the current base url. The base url is the url of the currently rendered page.
 *          For example: <code>foo/bar</code>, <code>../foo/bar</code></li>
 *      </ul>
 * </ul>
 *
 * </p>
 *
 * Example URLs:
 * 
 * <ul>
 *     <li>http://hostname:1234/foo/bar?a=b#baz - protocol: http, host: hostname, port: 1234, segments: [&quot;foo&quot;,&quot;bar&quot;], fragment: baz </li>
 *     <li>http://hostname:1234/foo/bar?a=b - protocol: http, host: hostname, port: 1234, segments: [&quot;foo&quot;,&quot;bar&quot;] </li>
 *     <li>//hostname:1234/foo/bar?a=b - protocol: null, host: hostname, port: 1234, segments: [&quot;foo&quot;,&quot;bar&quot;] </li>
 *     <li>foo/bar/baz?a=1&amp;b=5    - segments: [&quot;foo&quot;,&quot;bar&quot;,&quot;baz&quot;], query parameters: [&quot;a&quot;=&quot;1&quot;, &quot;b&quot;=&quot;5&quot;]</li>
 *     <li>foo/bar//baz?=4&amp;6      - segments: [&quot;foo&quot;, &quot;bar&quot;, &quot;&quot;, &quot;baz&quot;], query parameters: [&quot;&quot;=&quot;4&quot;, &quot;6&quot;=&quot;&quot;]</li>
 *     <li>/foo/bar/              - segments: [&quot;&quot;, &quot;foo&quot;, &quot;bar&quot;, &quot;&quot;]</li>
 *     <li>foo/bar//              - segments: [&quot;foo&quot;, &quot;bar&quot;, &quot;&quot;, &quot;&quot;]</li>
 *     <li>?a=b                   - segments: [ ], query parameters: [&quot;a&quot;=&quot;b&quot;]</li>
 *     <li></li>
 * </ul>
 *
 * The Url class takes care of encoding and decoding of the segments and parameters.
 * 
 * @author Matej Knopp
 * @author Igor Vaynberg
 */
public class Url implements Serializable
{
	private static final long serialVersionUID = 1L;

	private final List<String> segments;

	private final List<QueryParameter> parameters;

	private String charsetName;
	private transient Charset _charset;

	private String protocol;
	private Integer port;
	private String host;
	private String fragment;

	/**
	 * Flags the URL as relative to the application context. It is a special type of URL, used
	 * internally to hold the client data: protocol + host + port + relative segments
	 * 
	 * Unlike absolute URLs, a context relative one has no context or filter mapping segments
	 */
	private boolean contextRelative;

	/**
	 * Modes with which urls can be stringized
	 * 
	 * @author igor
	 */
	public static enum StringMode {
		/** local urls are rendered without the host name */
		LOCAL,
		/**
		 * full urls are written with hostname. if the hostname is not set or one of segments is
		 * {@literal ..} an {@link IllegalStateException} is thrown.
		 */
		FULL;
	}

	/**
	 * Construct.
	 */
	public Url()
	{
		segments = Generics.newArrayList();
		parameters = Generics.newArrayList();
	}

	/**
	 * Construct.
	 * 
	 * @param charset
	 */
	public Url(final Charset charset)
	{
		this();
		setCharset(charset);
	}


	/**
	 * copy constructor
	 * 
	 * @param url
	 *            url being copied
	 */
	public Url(final Url url)
	{
		Args.notNull(url, "url");

		protocol = url.protocol;
		host = url.host;
		port = url.port;
		segments = new ArrayList<>(url.segments);
		parameters = new ArrayList<>(url.parameters);
		charsetName = url.charsetName;
		_charset = url._charset;
	}

	/**
	 * Construct.
	 * 
	 * @param segments
	 * @param parameters
	 */
	public Url(final List<String> segments, final List<QueryParameter> parameters)
	{
		this(segments, parameters, null);
	}

	/**
	 * Construct.
	 * 
	 * @param segments
	 * @param charset
	 */
	public Url(final List<String> segments, final Charset charset)
	{
		this(segments, Collections.<QueryParameter> emptyList(), charset);
	}

	/**
	 * Construct.
	 * 
	 * @param segments
	 * @param parameters
	 * @param charset
	 */
	public Url(final List<String> segments, final List<QueryParameter> parameters,
		final Charset charset)
	{
		Args.notNull(segments, "segments");
		Args.notNull(parameters, "parameters");

		this.segments = new ArrayList<>(segments);
		this.parameters = new ArrayList<>(parameters);
		setCharset(charset);
	}

	/**
	 * Parses the given URL string.
	 * 
	 * @param url
	 *            absolute or relative url with query string
	 * @return Url object
	 */
	public static Url parse(final CharSequence url)
	{
		return parse(url, null);
	}

	/**
	 * Parses the given URL string.
	 * 
	 * @param _url
	 *            absolute or relative url with query string
	 * @param charset
	 * @return Url object
	 */
	public static Url parse(CharSequence _url, Charset charset)
	{
		return parse(_url, charset, true);
	}

	/**
	 * Parses the given URL string.
	 *
	 * @param _url
	 *            absolute or relative url with query string
	 * @param charset
	 * @param isFullHint
	 *            a hint whether to try to parse the protocol, host and port part of the url
	 * @return Url object
	 */
	public static Url parse(CharSequence _url, Charset charset, boolean isFullHint)
	{
		Args.notNull(_url, "_url");

		final Url result = new Url(charset);

		// the url object resolved the charset, use that
		charset = result.getCharset();

		String url = _url.toString();
		// extract query string part
		final String queryString;
		final String absoluteUrl;

		final int fragmentAt = url.indexOf('#');

		// matches url fragment, but doesn't match optional path parameter (e.g. .../#{optional}/...)
		if (fragmentAt > -1 && url.length() > fragmentAt + 1 && url.charAt(fragmentAt + 1) != '{')
		{
			result.fragment = url.substring(fragmentAt + 1);
			url = url.substring(0, fragmentAt);
		}

		final int queryAt = url.indexOf('?');

		if (queryAt == -1)
		{
			queryString = "";
			absoluteUrl = url;
		}
		else
		{
			absoluteUrl = url.substring(0, queryAt);
			queryString = url.substring(queryAt + 1);
		}

		// get absolute / relative part of url
		String relativeUrl;

		final int idxOfFirstSlash = absoluteUrl.indexOf('/');
		final int protocolAt = absoluteUrl.indexOf("://");

		// full urls start either with a "scheme://" or with "//"
		boolean protocolLess = absoluteUrl.startsWith("//");
		final boolean isFull = (protocolAt > 1 && (protocolAt < idxOfFirstSlash)) || protocolLess;

		if (isFull && isFullHint)
		{
			if (protocolLess == false)
			{
				result.protocol = absoluteUrl.substring(0, protocolAt).toLowerCase(Locale.US);
			}

			final String afterProto = absoluteUrl.substring(protocolAt + 3);
			final String hostAndPort;

			int relativeAt = afterProto.indexOf('/');
			if (relativeAt == -1)
			{
				relativeAt = afterProto.indexOf(';');
			}
			if (relativeAt == -1)
			{
				relativeUrl = "";
				hostAndPort = afterProto;
			}
			else
			{
				relativeUrl = afterProto.substring(relativeAt);
				hostAndPort = afterProto.substring(0, relativeAt);
			}

			final int credentialsAt = hostAndPort.lastIndexOf('@') + 1;
			//square brackets are used for ip6 URLs
			final int closeSqrBracketAt = hostAndPort.lastIndexOf(']') + 1;
			final int portAt = hostAndPort.substring(credentialsAt)
										  .substring(closeSqrBracketAt)
									      .lastIndexOf(':');

			if (portAt == -1)
			{
				result.host = hostAndPort;
				result.port = getDefaultPortForProtocol(result.protocol);
			}
			else
			{
				final int portOffset = portAt + credentialsAt + closeSqrBracketAt;
				
				result.host = hostAndPort.substring(0, portOffset);
				result.port = Integer.parseInt(hostAndPort.substring(portOffset + 1));
			}

			if (relativeAt < 0)
			{
				relativeUrl = "/";
			}
		}
		else
		{
			relativeUrl = absoluteUrl;
		}

		if (relativeUrl.length() > 0)
		{
			boolean removeLast = false;
			if (relativeUrl.endsWith("/"))
			{
				// we need to append something and remove it after splitting
				// because otherwise the
				// trailing slashes will be lost
				relativeUrl += "/x";
				removeLast = true;
			}

			String segmentArray[] = Strings.split(relativeUrl, '/');

			if (removeLast)
			{
				segmentArray[segmentArray.length - 1] = null;
			}

			for (String s : segmentArray)
			{
				if (s != null)
				{
					result.segments.add(decodeSegment(s, charset));
				}
			}
		}

		if (queryString.length() > 0)
		{
			String queryArray[] = Strings.split(queryString, '&');
			for (String s : queryArray)
			{
				if (Strings.isEmpty(s) == false)
				{
					result.parameters.add(parseQueryParameter(s, charset));
				}
			}
		}

		return result;
	}

	/**
	 * 
	 * @param qp
	 * @param charset
	 * @return query parameters
	 */
	private static QueryParameter parseQueryParameter(final String qp, final Charset charset)
	{
		int idxOfEquals = qp.indexOf('=');
		if (idxOfEquals == -1)
		{
			// name => empty value
			return new QueryParameter(decodeParameter(qp, charset), "");
		}

		String parameterName = qp.substring(0, idxOfEquals);
		String parameterValue = qp.substring(idxOfEquals + 1);
		return new QueryParameter(decodeParameter(parameterName, charset), decodeParameter(parameterValue, charset));
	}

	/**
	 * get default port number for protocol
	 * 
	 * @param protocol
	 *            name of protocol
	 * @return default port for protocol or <code>null</code> if unknown
	 */
	private static Integer getDefaultPortForProtocol(String protocol)
	{
		if ("http".equals(protocol))
		{
			return 80;
		}
		else if ("https".equals(protocol))
		{
			return 443;
		}
		else if ("ftp".equals(protocol))
		{
			return 21;
		}
		else
		{
			return null;
		}
	}

	/**
	 * 
	 * @return charset
	 */
	public Charset getCharset()
	{
		if (_charset == null)
		{
			if (Strings.isEmpty(charsetName))
			{
				_charset = StandardCharsets.UTF_8;
			} else {
				_charset = Charset.forName(charsetName);
			}
		}
		return _charset;
	}

	/**
	 * 
	 * @param charset
	 */
	private void setCharset(final Charset charset)
	{
		if (charset == null)
		{
			charsetName = null;
			_charset = null;
		}
		else
		{
			charsetName = charset.name();
			_charset = charset;
		}
	}

	/**
	 * Returns segments of the URL. Segments form the part before query string.
	 * 
	 * @return mutable list of segments
	 */
	public List<String> getSegments()
	{
		return segments;
	}

	/**
	 * Returns query parameters of the URL.
	 * 
	 * @return mutable list of query parameters
	 */
	public List<QueryParameter> getQueryParameters()
	{
		return parameters;
	}

	/**
	 * 
	 * @return fragment
	 */
	public String getFragment()
	{
		return fragment;
	}

	/**
	 * 
	 * @param fragment
	 */
	public void setFragment(String fragment)
	{
		this.fragment = fragment;
	}

	/**
	 * Returns whether the Url is context absolute. Absolute Urls start with a '{@literal /}'.
	 *
	 * @return <code>true</code> if Url starts with the context path, <code>false</code> otherwise.
	 */
	public boolean isContextAbsolute()
	{
		return !contextRelative && !isFull() && !getSegments().isEmpty() && Strings.isEmpty(getSegments().get(0));
	}

	/**
	 * Returns whether the Url is a CSS data uri. Data uris start with '{@literal data:}'.
	 *
	 * @return <code>true</code> if Url starts with 'data:', <code>false</code> otherwise.
	 */
	public boolean isDataUrl()
	{
		return (getProtocol() != null && getProtocol().equals("data")) || (!getSegments().isEmpty() && getSegments()
				.get(0).startsWith("data"));
	}

	/**
	 * Returns whether the Url has a <em>host</em> attribute.
	 * The scheme is optional because the url may be <code>//host/path</code>.
	 * The port is also optional because there are defaults for the different protocols.
	 *
	 * @return <code>true</code> if Url has a <em>host</em> attribute, <code>false</code> otherwise.
	 */
	public boolean isFull()
	{
		return !contextRelative && getHost() != null;
	}

	/**
	 * Convenience method that removes all query parameters with given name.
	 * 
	 * @param name
	 *            query parameter name
	 */
	public void removeQueryParameters(final String name)
	{
		for (Iterator<QueryParameter> i = getQueryParameters().iterator(); i.hasNext();)
		{
			QueryParameter param = i.next();
			if (Objects.equal(name, param.getName()))
			{
				i.remove();
			}
		}
	}

	/**
	 * Convenience method that removes <code>count</code> leading segments
	 * 
	 * @param count
	 */
	public void removeLeadingSegments(final int count)
	{
		Args.withinRange(0, segments.size(), count, "count");
		for (int i = 0; i < count; i++)
		{
			segments.remove(0);
		}
	}

	/**
	 * Convenience method that prepends <code>segments</code> to the segments collection
	 * 
	 * @param newSegments
	 */
	public void prependLeadingSegments(final List<String> newSegments)
	{
		Args.notNull(newSegments, "segments");
		segments.addAll(0, newSegments);
	}

	/**
	 * Convenience method that removes all query parameters with given name and adds new query
	 * parameter with specified name and value
	 * 
	 * @param name
	 * @param value
	 */
	public void setQueryParameter(final String name, final Object value)
	{
		removeQueryParameters(name);
		addQueryParameter(name, value);
	}

	/**
	 * Convenience method that removes adds a query parameter with given name
	 * 
	 * @param name
	 * @param value
	 */
	public void addQueryParameter(final String name, final Object value)
	{
		if (value != null)
		{
			QueryParameter parameter = new QueryParameter(name, value.toString());
			getQueryParameters().add(parameter);
		}
	}

	/**
	 * Returns first query parameter with specified name or null if such query parameter doesn't
	 * exist.
	 * 
	 * @param name
	 * @return query parameter or <code>null</code>
	 */
	public QueryParameter getQueryParameter(final String name)
	{
		for (QueryParameter parameter : parameters)
		{
			if (Objects.equal(name, parameter.getName()))
			{
				return parameter;
			}
		}
		return null;
	}

	/**
	 * Returns the value of first query parameter with specified name. Note that this method never
	 * returns <code>null</code>. Not even if the parameter does not exist.
	 * 
	 * @see StringValue#isNull()
	 * 
	 * @param name
	 * @return {@link StringValue} instance wrapping the parameter value
	 */
	public StringValue getQueryParameterValue(final String name)
	{
		QueryParameter parameter = getQueryParameter(name);
		if (parameter == null)
		{
			return StringValue.valueOf((String)null);
		}
		else
		{
			return StringValue.valueOf(parameter.getValue());
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean equals(final Object obj)
	{
		if (this == obj)
		{
			return true;
		}
		if ((obj instanceof Url) == false)
		{
			return false;
		}
		Url rhs = (Url)obj;

		return getSegments().equals(rhs.getSegments()) &&
			getQueryParameters().equals(rhs.getQueryParameters()) &&
			Objects.isEqual(getFragment(), rhs.getFragment());
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public int hashCode()
	{
		return Objects.hashCode(getSegments(), getQueryParameters(), getFragment());
	}

	/**
	 * 
	 * @param string
	 * @param charset
	 * @return encoded segment
	 */
	private static String encodeSegment(final String string, final Charset charset)
	{
		return UrlEncoder.PATH_INSTANCE.encode(string, charset);
	}

	/**
	 * 
	 * @param string
	 * @param charset
	 * @return decoded segment
	 */
	private static String decodeSegment(final String string, final Charset charset)
	{
		return UrlDecoder.PATH_INSTANCE.decode(string, charset);
	}

	/**
	 * 
	 * @param string
	 * @param charset
	 * @return encoded parameter
	 */
	private static String encodeParameter(final String string, final Charset charset)
	{
		return UrlEncoder.QUERY_INSTANCE.encode(string, charset);
	}

	/**
	 * 
	 * @param string
	 * @param charset
	 * @return decoded parameter
	 */
	private static String decodeParameter(final String string, final Charset charset)
	{
		return UrlDecoder.QUERY_INSTANCE.decode(string, charset);
	}

	/**
	 * Renders a url with {@link StringMode#LOCAL} using the url's charset
	 */
	@Override
	public String toString()
	{
		return toString(getCharset());
	}

	/**
	 * Stringizes this url
	 * 
	 * @param mode
	 *            {@link StringMode} that determins how to stringize the url
	 * @param charset
	 *            charset
	 * @return sringized version of this url
	 * 
	 */
	public String toString(StringMode mode, Charset charset)
	{
		StringBuilder result = new StringBuilder();
		final String path = getPath(charset);

		if (StringMode.FULL == mode)
		{
			if (Strings.isEmpty(host))
			{
				throw new IllegalStateException("Cannot render this url in " +
					StringMode.FULL.name() + " mode because it does not have a host set.");
			}

			if (Strings.isEmpty(protocol) == false)
			{
				result.append(protocol);
				result.append("://");
			}
			else if (Strings.isEmpty(protocol) && Strings.isEmpty(host) == false)
			{
				result.append("//");
			}
			result.append(host);

			if (port != null && port.equals(getDefaultPortForProtocol(protocol)) == false)
			{
				result.append(':');
				result.append(port);
			}

			if (segments.contains(".."))
			{
				throw new IllegalStateException("Cannot render this url in " +
					StringMode.FULL.name() + " mode because it has a `..` segment: " + toString());
			}

			if (!path.startsWith("/"))
			{
				result.append('/');
			}

		}

		result.append(path);

		final String queryString = getQueryString(charset);
		if (queryString != null)
		{
			result.append('?').append(queryString);
		}

		String _fragment = getFragment();
		if (Strings.isEmpty(_fragment) == false)
		{
			result.append('#').append(_fragment);
		}

		return result.toString();
	}

	/**
	 * Stringizes this url using the specific {@link StringMode} and url's charset
	 * 
	 * @param mode
	 *            {@link StringMode} that determines how to stringize the url
	 * @return stringized url
	 */
	public String toString(StringMode mode)
	{
		return toString(mode, getCharset());
	}

	/**
	 * Stringizes this url using {@link StringMode#LOCAL} and the specified charset
	 * 
	 * @param charset
	 * @return stringized url
	 */
	public String toString(final Charset charset)
	{
		return toString(StringMode.LOCAL, charset);
	}

	/**
	 * 
	 * @return true if last segment contains a name and not something like "." or "..".
	 */
	private boolean isLastSegmentReal()
	{
		if (segments.isEmpty())
		{
			return false;
		}
		String last = segments.get(segments.size() - 1);
		return (last.length() > 0) && !".".equals(last) && !"..".equals(last);
	}

	/**
	 * @param segments
	 * @return true if last segment is empty
	 */
	private boolean isLastSegmentEmpty(final List<String> segments)
	{
		if (segments.isEmpty())
		{
			return false;
		}
		String last = segments.get(segments.size() - 1);
		return last.length() == 0;
	}

	/**
	 * 
	 * @return true, if last segement is empty
	 */
	private boolean isLastSegmentEmpty()
	{
		return isLastSegmentEmpty(segments);
	}

	/**
	 * 
	 * @param segments
	 * @return true if at least one segement is real
	 */
	private boolean isAtLeastOneSegmentReal(final List<String> segments)
	{
		for (String s : segments)
		{
			if ((s.length() > 0) && !".".equals(s) && !"..".equals(s))
			{
				return true;
			}
		}
		return false;
	}

	/**
	 * Concatenate the specified segments; The segments can be relative - begin with "." or "..".
	 * 
	 * @param segments
	 */
	public void concatSegments(List<String> segments)
	{
		boolean checkedLastSegment = false;

		if (!isAtLeastOneSegmentReal(segments) && !isLastSegmentEmpty(segments))
		{
			segments = new ArrayList<>(segments);
			segments.add("");
		}

		for (String s : segments)
		{
			if (".".equals(s))
			{
				continue;
			}
			else if ("..".equals(s) && !this.segments.isEmpty())
			{
				this.segments.remove(this.segments.size() - 1);
			}
			else
			{
				if (!checkedLastSegment)
				{
					if (isLastSegmentReal() || isLastSegmentEmpty())
					{
						this.segments.remove(this.segments.size() - 1);
					}
					checkedLastSegment = true;
				}
				this.segments.add(s);
			}
		}

		if ((this.segments.size() == 1) && (this.segments.get(0).length() == 0))
		{
			this.segments.clear();
		}
	}

	/**
	 * Represents a single query parameter
	 * 
	 * @author Matej Knopp
	 */
	public final static class QueryParameter implements Serializable
	{
		private static final long serialVersionUID = 1L;

		private final String name;
		private final String value;

		/**
		 * Creates new {@link QueryParameter} instance. The <code>name</code> and <code>value</code>
		 * parameters must not be <code>null</code>, though they can be empty strings.
		 * 
		 * @param name
		 *            parameter name
		 * @param value
		 *            parameter value
		 */
		public QueryParameter(final String name, final String value)
		{
			Args.notNull(name, "name");
			Args.notNull(value, "value");

			this.name = name;
			this.value = value;
		}

		/**
		 * Returns query parameter name.
		 * 
		 * @return query parameter name
		 */
		public String getName()
		{
			return name;
		}

		/**
		 * Returns query parameter value.
		 * 
		 * @return query parameter value
		 */
		public String getValue()
		{
			return value;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public boolean equals(final Object obj)
		{
			if (this == obj)
			{
				return true;
			}
			if ((obj instanceof QueryParameter) == false)
			{
				return false;
			}
			QueryParameter rhs = (QueryParameter)obj;
			return Objects.equal(getName(), rhs.getName()) &&
				Objects.equal(getValue(), rhs.getValue());
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public int hashCode()
		{
			return Objects.hashCode(getName(), getValue());
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public String toString()
		{
			return toString(StandardCharsets.UTF_8);
		}

		/**
		 * 
		 * @param charset
		 * @return see toString()
		 */
		public String toString(final Charset charset)
		{
			StringBuilder result = new StringBuilder();
			result.append(encodeParameter(getName(), charset));
			if (!Strings.isEmpty(getValue()))
			{
				result.append('=');
				result.append(encodeParameter(getValue(), charset));
			}
			return result.toString();
		}
	}

	/**
	 * Makes this url the result of resolving the {@code relative} url against this url.
	 * <p>
	 * Segments will be properly resolved, handling any {@code ..} references, while the query
	 * parameters will be completely replaced with {@code relative}'s query parameters.
	 * </p>
	 * <p>
	 * For example:
	 * 
	 * <pre>
	 * wicket/page/render?foo=bar
	 * </pre>
	 * 
	 * resolved with
	 * 
	 * <pre>
	 * ../component/render?a=b
	 * </pre>
	 * 
	 * will become
	 * 
	 * <pre>
	 * wicket/component/render?a=b
	 * </pre>
	 * 
	 * </p>
	 * 
	 * @param relative
	 *            relative url
	 */
	public void resolveRelative(final Url relative)
	{
		if (getSegments().size() > 0)
		{
			// strip the first non-folder segment (if it is not empty)
			getSegments().remove(getSegments().size() - 1);
		}

		// remove leading './' (current folder) and empty segments, process any ../ segments from
		// the relative url
		while (!relative.getSegments().isEmpty())
		{
			if (".".equals(relative.getSegments().get(0)))
			{
				relative.getSegments().remove(0);
			}
			else if ("".equals(relative.getSegments().get(0)))
			{
				relative.getSegments().remove(0);
			}
			else if ("..".equals(relative.getSegments().get(0)))
			{
				relative.getSegments().remove(0);
				if (getSegments().isEmpty() == false)
				{
					getSegments().remove(getSegments().size() - 1);
				}
			}
			else
			{
				break;
			}
		}

		if (!getSegments().isEmpty() && relative.getSegments().isEmpty())
		{
			getSegments().add("");
		}

		// append the remaining relative segments
		getSegments().addAll(relative.getSegments());

		// replace query params with the ones from relative
		parameters.clear();
		parameters.addAll(relative.getQueryParameters());
	}

	/**
	 * Gets the protocol of this url (http/https/etc)
	 * 
	 * @return protocol or {@code null} if none has been set
	 */
	public String getProtocol()
	{
		return protocol;
	}

	/**
	 * Sets the protocol of this url (http/https/etc)
	 * 
	 * @param protocol
	 */
	public void setProtocol(final String protocol)
	{
		this.protocol = protocol;
	}

	/**
	 * 
	 * Flags the URL as relative to the application context.
	 * 
	 * @param contextRelative
	 */
	public void setContextRelative(boolean contextRelative)
	{
		this.contextRelative = contextRelative;
	}

	/**
	 * Tests if the URL is relative to the application context. If so, it holds all the information
	 * an absolute URL would have, minus the context and filter mapping segments
	 * 
	 * @return contextRelative
	 */
	public boolean isContextRelative()
	{
		return contextRelative;
	}

	/**
	 * Gets the port of this url
	 * 
	 * @return port or {@code null} if none has been set
	 */
	public Integer getPort()
	{
		return port;
	}

	/**
	 * Sets the port of this url
	 * 
	 * @param port
	 */
	public void setPort(final Integer port)
	{
		this.port = port;
	}

	/**
	 * Gets the host name of this url
	 * 
	 * @return host name or {@code null} if none is seto
	 */
	public String getHost()
	{
		return host;
	}

	/**
	 * Sets the host name of this url
	 * 
	 * @param host
	 */
	public void setHost(final String host)
	{
		this.host = host;
	}

	/**
	 * return path for current url in given encoding
	 * 
	 * @param charset
	 *            character set for encoding
	 * 
	 * @return path string
	 */
	public String getPath(Charset charset)
	{
		Args.notNull(charset, "charset");

		StringBuilder path = new StringBuilder();
		boolean slash = false;

		for (String segment : getSegments())
		{
			if (slash)
			{
				path.append('/');
			}
			path.append(encodeSegment(segment, charset));
			slash = true;
		}
		return path.toString();
	}

	/**
	 * return path for current url in original encoding
	 * 
	 * @return path string
	 */
	public String getPath()
	{
		return getPath(getCharset());
	}

	/**
	 * return query string part of url in given encoding
	 * 
	 * @param charset
	 *            character set for encoding
	 * @since Wicket 7 
     *            the return value does not contain any "?" and could be null
	 * @return query string (null if empty)
	 */
	public String getQueryString(Charset charset)
	{
		Args.notNull(charset, "charset");

		String queryString = null;
		List<QueryParameter> queryParameters = getQueryParameters();

		if (queryParameters.size() != 0)
		{
			StringBuilder query = new StringBuilder();

			for (QueryParameter parameter : queryParameters)
			{
				if (query.length() != 0)
				{
					query.append('&');
				}
				query.append(parameter.toString(charset));
			}
			queryString = query.toString();
		}
		return queryString;
	}

	/**
	 * return query string part of url in original encoding
	 *
	 * @since Wicket 7
	 *              the return value does not contain any "?" and could be null
	 * @return query string (null if empty)
	 */
	public String getQueryString()
	{
		return getQueryString(getCharset());
	}

	/**
	 * Try to reduce url by eliminating '..' and '.' from the path where appropriate (this is
	 * somehow similar to {@link java.io.File#getCanonicalPath()}). Either by different / unexpected
	 * browser behavior or by malicious attacks it can happen that these kind of redundant urls are
	 * processed by wicket. These urls can cause some trouble when mapping the request.
	 * <p/>
	 * <strong>example:</strong>
	 * 
	 * the url
	 * 
	 * <pre>
	 * /example/..;jsessionid=234792?0
	 * </pre>
	 * 
	 * will not get normalized by the browser due to the ';jsessionid' string that gets appended by
	 * the servlet container. After wicket strips the jsessionid part the resulting internal url
	 * will be
	 * 
	 * <pre>
	 * /example/..
	 * </pre>
	 * 
	 * instead of
	 * 
	 * <pre>
	 * /
	 * </pre>
	 * 
	 * <p/>
	 * 
	 * This code correlates to <a
	 * href="https://issues.apache.org/jira/browse/WICKET-4303">WICKET-4303</a>
	 * 
	 * @return canonical url
	 */
	public Url canonical()
	{
		Url url = new Url(this);
		url.segments.clear();

		for (int i = 0; i < segments.size(); i++)
		{
			final String segment = segments.get(i);

			// drop '.' from path
			if (".".equals(segment))
			{
				// skip
			}
			else if ("..".equals(segment) && url.segments.isEmpty() == false)
			{
				url.segments.remove(url.segments.size() - 1);
			}
			// skip segment if following segment is a '..'
			else if ((i + 1) < segments.size() && "..".equals(segments.get(i + 1)))
			{
				i++;
			}
			else
			{
				url.segments.add(segment);
			}
		}
		return url;
	}
}
