/*
 * 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.
 */

#include "URI.h"

#include <decaf/lang/Integer.h>
#include <decaf/lang/Character.h>
#include <decaf/net/URL.h>
#include <decaf/internal/net/URIHelper.h>
#include <decaf/internal/util/StringUtils.h>
#include <decaf/internal/net/URIEncoderDecoder.h>

using namespace std;
using namespace decaf;
using namespace decaf::net;
using namespace decaf::internal;
using namespace decaf::internal::net;
using namespace decaf::internal::util;
using namespace decaf::lang;
using namespace decaf::lang::exceptions;

////////////////////////////////////////////////////////////////////////////////
const std::string URI::unreserved = "_-!.~\'()*";
const std::string URI::punct = ",;:$&+=";
const std::string URI::reserved = punct + "?/[]@";
const std::string URI::someLegal = unreserved + punct;
const std::string URI::allLegal = unreserved + reserved;

////////////////////////////////////////////////////////////////////////////////
URI::URI() : uri(), uriString() {
}

////////////////////////////////////////////////////////////////////////////////
URI::URI(const URI& uri) : uri(uri.uri), uriString(uri.uriString) {
}

////////////////////////////////////////////////////////////////////////////////
URI::URI(const std::string& uri) : uri(), uriString(uri) {
    this->parseURI(uri, false);
}

////////////////////////////////////////////////////////////////////////////////
URI::URI(const std::string& scheme, const std::string& ssp, const std::string& fragment) : uri(), uriString() {

    std::string uri = "";

    if (scheme != "") {
        uri.append(scheme);
        uri.append(":");
    }

    if (ssp != "") {
        // QUOTE ILLEGAL CHARACTERS
        uri.append(quoteComponent(ssp, allLegal));
    }

    if (fragment != "") {
        uri.append("#");
        // QUOTE ILLEGAL CHARACTERS
        uri.append(quoteComponent(fragment, allLegal));
    }

    // Now hand of to the main parse function.
    this->parseURI(uri, false);
}

////////////////////////////////////////////////////////////////////////////////
URI::URI(const std::string& scheme, const std::string& userInfo,
         const std::string& host, int port,
         const std::string& path, const std::string& query,
         const std::string& fragment) : uri(), uriString() {

    if (scheme == "" && userInfo == "" && host == "" && path == "" && query == "" && fragment == "") {
        return;
    }

    if (scheme != "" && !path.empty() && path.at(0) != '/') {
        throw URISyntaxException(
            __FILE__, __LINE__, path,
            "URI::URI - Path string: %s starts with invalid char '/'" );
    }

    std::string uri = "";
    if (scheme != "") {
        uri.append(scheme);
        uri.append(":");
    }

    uri.append("//");

    if (userInfo != "") {
        // QUOTE ILLEGAL CHARACTERS in userinfo
        uri.append(quoteComponent(userInfo, someLegal));
        uri.append("@");
    }

    if (host != "") {
        std::string newHost = host;

        // check for ipv6 addresses that hasn't been enclosed
        // in square brackets
        if (host.find(":") != std::string::npos &&
            host.find("]") == std::string::npos &&
            host.find("[") == std::string::npos) {

            newHost = std::string("[") + host + "]";
        }

        uri.append(newHost);
    }

    if (port != -1) {
        uri.append(":");
        uri.append(Integer::toString(port));
    }

    if (path != "") {
        // QUOTE ILLEGAL CHARS
        uri.append(quoteComponent(path, "/@" + someLegal));
    }

    if (query != "") {
        uri.append("?");
        // QUOTE ILLEGAL CHARS
        uri.append(quoteComponent(query, allLegal));
    }

    if (fragment != "") {
        // QUOTE ILLEGAL CHARS
        uri.append("#");
        uri.append(quoteComponent(fragment, allLegal));
    }

    this->parseURI(uri, true);
}

////////////////////////////////////////////////////////////////////////////////
URI::URI(const std::string& scheme, const std::string& host,
         const std::string& path, const std::string& fragment) : uri(), uriString() {

    if (scheme == "" && host == "" && path == "" && fragment == "") {
        return;
    }

    if (scheme != "" && !path.empty() && path.at(0) != '/') {
        throw URISyntaxException(
            __FILE__, __LINE__, path,
            "URI::URI - Path string: %s starts with invalid char '/'" );
    }

    std::string uri = "";
    if (scheme != "") {
        uri.append(scheme);
        uri.append(":");
    }

    if (host != "") {
        uri.append("//");
    }

    if (host != "") {
        std::string newHost = host;

        // check for ipv6 addresses that hasn't been enclosed
        // in square brackets
        if (host.find(":") != std::string::npos &&
            host.find("]") == std::string::npos &&
            host.find("[") == std::string::npos) {

            newHost = std::string("[") + host + "]";
        }

        uri.append(newHost);
    }

    if (path != "") {
        // QUOTE ILLEGAL CHARS
        uri.append(quoteComponent(path, "/@" + someLegal));
    }

    if (fragment != "") {
        // QUOTE ILLEGAL CHARS
        uri.append("#");
        uri.append(quoteComponent(fragment, allLegal));
    }

    this->parseURI(uri, true);
}

////////////////////////////////////////////////////////////////////////////////
URI::URI(const std::string& scheme, const std::string& authority,
         const std::string& path, const std::string& query,
         const std::string& fragment) : uri(), uriString() {

    if (scheme != "" && !path.empty() && path.at(0) != '/') {
         throw URISyntaxException(
            __FILE__, __LINE__, path,
            "URI::URI - Path String %s must start with a '/'" );
     }

    std::string uri = "";

    if (scheme != "") {
        uri.append(scheme);
        uri.append(":");
    }

    uri.append("//");

    if (authority != "") {
        // QUOTE ILLEGAL CHARS
        uri.append(quoteComponent(authority, "@[]" + someLegal));
    }

    if (path != "") {
        // QUOTE ILLEGAL CHARS
        uri.append(quoteComponent(path, "/@" + someLegal));
    }

    if (query != "") {
        // QUOTE ILLEGAL CHARS
        uri.append("?");
        uri.append(quoteComponent(query, allLegal));
    }

    if (fragment != "") {
        // QUOTE ILLEGAL CHARS
        uri.append("#");
        uri.append(quoteComponent(fragment, allLegal));
    }

    this->parseURI(uri, false);
}

////////////////////////////////////////////////////////////////////////////////
void URI::parseURI(const std::string& uri, bool forceServer) {
    try {
        this->uri = URIHelper().parseURI(uri, forceServer);
    }
    DECAF_CATCH_RETHROW(URISyntaxException)
    DECAF_CATCHALL_THROW(URISyntaxException)
}

////////////////////////////////////////////////////////////////////////////////
int URI::compareTo( const URI& uri ) const {

    int ret = 0;

    // compare schemes
    if (this->uri.getScheme() == "" && uri.getScheme() != "") {
        return -1;
    } else if (this->uri.getScheme() != "" && uri.getScheme() == "") {
        return 1;
    } else if (this->uri.getScheme() != "" && uri.getScheme() != "") {
        ret = StringUtils::compareIgnoreCase(this->uri.getScheme().c_str(), uri.getScheme().c_str());
        if (ret != 0) {
            return ret > 0 ? 1 : -1;
        }
    }

    // compare opacities
    if (!this->uri.isOpaque() && uri.isOpaque()) {
        return -1;
    } else if (this->uri.isOpaque() && !uri.isOpaque()) {
        return 1;
    } else if (this->uri.isOpaque() && uri.isOpaque()) {
        ret = StringUtils::compare(this->getSchemeSpecificPart().c_str(),
                                   uri.getSchemeSpecificPart().c_str());
        if (ret != 0) {
            return ret > 0 ? 1 : -1;
        }
    } else {

        // otherwise both must be hierarchical

        // compare authorities
        if (this->uri.getAuthority() != "" && uri.getAuthority() == "") {
            return 1;
        } else if (this->uri.getAuthority() == "" && uri.getAuthority() != "") {
            return -1;
        } else if (this->uri.getAuthority() != "" && uri.getAuthority() != "") {

            if (this->uri.getHost() != "" && uri.getHost() != "") {

                // both are server based, so compare userinfo, host, port
                if (this->getUserInfo() != "" && uri.getUserInfo() == "") {
                    return 1;
                } else if (this->getUserInfo() == "" && uri.getUserInfo() != "") {
                    return -1;
                } else if (this->getUserInfo() != "" && uri.getUserInfo() != "") {
                    ret = StringUtils::compare(this->getUserInfo().c_str(),
                                               uri.getUserInfo().c_str());
                    if (ret != 0) {
                        return ret > 0 ? 1 : -1;
                    }
                }

                // userinfo's are the same, compare hostname
                ret = StringUtils::compareIgnoreCase(this->uri.getHost().c_str(), uri.getHost().c_str());
                if (ret != 0) {
                    return ret > 0 ? 1 : -1;
                }

                // compare port
                if (this->getPort() != uri.getPort()) {
                    return (getPort() - uri.getPort()) > 0 ? 1 : -1;
                }
            } else {
                // one or both are registry based, compare the whole authority
                ret = StringUtils::compare(this->uri.getAuthority().c_str(), uri.getAuthority().c_str());
                if (ret != 0) {
                    return ret > 0 ? 1 : -1;
                }
            }
        }

        // authorities are the same, compare paths
        ret = StringUtils::compare(this->getPath().c_str(), uri.getPath().c_str());
        if (ret != 0) {
            return ret > 0 ? 1 : -1;
        }

        // compare queries
        if (this->getQuery() != "" && uri.getQuery() == "") {
            return 1;
        } else if (this->getQuery() == "" && uri.getQuery() != "") {
            return -1;
        } else if (this->getQuery() != "" && uri.getQuery() != "") {
            ret = StringUtils::compare(this->getQuery().c_str(), uri.getQuery().c_str());
            if (ret != 0) {
                return ret > 0 ? 1 : -1;
            }
        }
    }

    // everything else is identical, so compare fragments
    if (this->getFragment() != "" && uri.getFragment() == "") {
        return 1;
    } else if (this->getFragment() == "" && uri.getFragment() != "") {
        return -1;
    } else if (this->getFragment() != "" && uri.getFragment() != "") {
        ret = StringUtils::compare(this->getFragment().c_str(), uri.getFragment().c_str());
        if (ret != 0) {
            return ret > 0 ? 1 : -1;
        }
    }

    // identical
    return 0;
}

////////////////////////////////////////////////////////////////////////////////
bool URI::equals( const URI& uri ) const {

    if ((uri.uri.getFragment() == "" && this->uri.getFragment() != "") ||
        (uri.uri.getFragment() != "" && this->uri.getFragment() == "")) {

        return false;

    } else if (uri.uri.getFragment() != "" && this->uri.getFragment() != "") {

        if (!equalsHexCaseInsensitive(uri.uri.getFragment(), this->uri.getFragment())) {
            return false;
        }
    }

    if ((uri.uri.getScheme() == "" && this->uri.getScheme() != "") ||
        (uri.uri.getScheme() != "" && this->uri.getScheme() == "")) {

        return false;

    } else if (uri.uri.getScheme() != "" && this->uri.getScheme() != "") {

        if (StringUtils::compareIgnoreCase(uri.uri.getScheme().c_str(), this->uri.getScheme().c_str()) != 0) {
            return false;
        }
    }

    if (uri.uri.isOpaque() && this->uri.isOpaque()) {

        return equalsHexCaseInsensitive(
            uri.uri.getSchemeSpecificPart(), this->uri.getSchemeSpecificPart());

    } else if (!uri.uri.isOpaque() && !this->uri.isOpaque()) {

        if (!equalsHexCaseInsensitive(this->uri.getPath(), uri.uri.getPath())) {
            return false;
        }

        if ((uri.uri.getQuery() != "" && this->uri.getQuery() == "") ||
            (uri.uri.getQuery() == "" && this->uri.getQuery() != "")) {

            return false;

        } else if(uri.uri.getQuery() != "" && this->uri.getQuery() != "") {
            if (!equalsHexCaseInsensitive(uri.uri.getQuery(), this->uri.getQuery())) {
                return false;
            }
        }

        if( ( uri.uri.getAuthority() != "" && this->uri.getAuthority() == "" ) ||
            ( uri.uri.getAuthority() == "" && this->uri.getAuthority() != "" ) ) {

            return false;

        } else if (uri.uri.getAuthority() != "" && this->uri.getAuthority() != "") {

            if ((uri.uri.getHost() != "" && this->uri.getHost() == "") ||
                (uri.uri.getHost() == "" && this->uri.getHost() != "")) {

                return false;

            } else if (uri.uri.getHost() == "" && this->uri.getHost() == "") {

                // both are registry based, so compare the whole authority
                return equalsHexCaseInsensitive(
                    uri.uri.getAuthority(), this->uri.getAuthority());

            } else { // uri.host != "" && host != "", so server-based

                if (StringUtils::compareIgnoreCase(uri.uri.getHost().c_str(), this->uri.getHost().c_str()) != 0) {
                    return false;
                }

                if (this->uri.getPort() != uri.uri.getPort()) {
                    return false;
                }

                if ((uri.uri.getUserInfo() != "" && this->uri.getUserInfo() == "") ||
                    (uri.uri.getUserInfo() == "" && this->uri.getUserInfo() != "")) {

                    return false;

                } else if (uri.uri.getUserInfo() != "" && this->uri.getUserInfo() != "") {
                    return equalsHexCaseInsensitive(this->uri.getUserInfo(), uri.uri.getUserInfo());
                } else {
                    return true;
                }
            }
        } else {
            // no authority
            return true;
        }

    } else {
        // one is opaque, the other hierarchical
        return false;
    }
}

////////////////////////////////////////////////////////////////////////////////
bool URI::operator==(const URI& value) const {
    return compareTo(value) == 0 ? true : false;
}

////////////////////////////////////////////////////////////////////////////////
bool URI::operator<(const URI& value) const {
    return compareTo(value) == -1 ? true : false;
}

////////////////////////////////////////////////////////////////////////////////
URI URI::create(const std::string uri) {

    try {
        return URI(uri);
    } catch (URISyntaxException& e) {
        throw IllegalArgumentException(e);
    }
}

////////////////////////////////////////////////////////////////////////////////
std::string URI::quoteComponent(const std::string& component, const std::string& legalset) {

    try {

        /*
         * Use a different encoder than URLEncoder since: 1. chars like "/",
         * "#", "@" etc needs to be preserved instead of being encoded, 2.
         * UTF-8 char set needs to be used for encoding instead of default
         * platform one
         */
        return URIEncoderDecoder::quoteIllegal(component, legalset);
    }
    DECAF_CATCH_RETHROW(decaf::lang::Exception)
    DECAF_CATCHALL_THROW(decaf::lang::Exception)
}

////////////////////////////////////////////////////////////////////////////////
std::string URI::encodeOthers(const std::string& src) const {

    try {
        /*
         * Use a different encoder than URLEncoder since: 1. chars like "/",
         * "#", "@" etc needs to be preserved instead of being encoded, 2.
         * UTF-8 char set needs to be used for encoding instead of default
         * platform one 3. Only other chars need to be converted
         */
        return URIEncoderDecoder::encodeOthers(src);
    }
    DECAF_CATCH_RETHROW(decaf::lang::Exception)
    DECAF_CATCHALL_THROW(decaf::lang::Exception)
}

////////////////////////////////////////////////////////////////////////////////
std::string URI::decode(const std::string& src) const {

    if (src == "") {
        return src;
    }

    try {
        return URIEncoderDecoder::decode(src);
    }
    DECAF_CATCH_RETHROW(decaf::lang::Exception)
    DECAF_CATCHALL_THROW(decaf::lang::Exception)
}

////////////////////////////////////////////////////////////////////////////////
bool URI::equalsHexCaseInsensitive(const std::string& first, const std::string& second) const {

    if (first.find('%') != second.find('%')) {
        return StringUtils::compare(first.c_str(), second.c_str()) == 0;
    }

    std::size_t index = 0;
    std::size_t previndex = 0;

    while ((index = first.find('%', previndex)) != string::npos && second.find('%', previndex) == index) {

        bool match = first.substr(previndex, index - previndex) ==
                     second.substr(previndex, index - previndex);

        if (!match) {
            return false;
        }

        match = StringUtils::compareIgnoreCase(
            first.substr(index + 1, 3).c_str(), second.substr(index + 1, 3).c_str()) == 0;

        if (!match) {
            return false;
        }

        index += 3;
        previndex = index;
    }

    return first.substr(previndex) == second.substr(previndex);
}

////////////////////////////////////////////////////////////////////////////////
std::string URI::convertHexToLowerCase(const std::string& s) const {

    string result = "";

    if (s.find('%') == string::npos) {
        return s;
    }

    std::size_t index = 0;
    std::size_t previndex = 0;

    while ((index = s.find('%', previndex)) != string::npos) {
        result.append(s.substr(previndex, (index - previndex) + 1));

        string temp = s.substr(index + 1, 3);

        for (size_t i = 0; i < temp.length(); ++i) {
            result.append(1, Character::toLowerCase(temp.at(i)));
        }

        index += 3;
        previndex = index;
    }

    return result;
}

////////////////////////////////////////////////////////////////////////////////
std::string URI::normalize(const std::string& path) const {

    if (path == "") {
        return path;
    }

    // count the number of '/'s, to determine number of segments
    std::size_t index = -1;
    std::size_t pathlen = path.length();
    unsigned int size = 0;

    if (pathlen > 0 && path.at(0) != '/') {
        size++;
    }

    while ((index = path.find('/', index + 1)) != string::npos) {
        if (index + 1 < pathlen && path.at(index + 1) != '/') {
            size++;
        }
    }

    std::vector<string> seglist(size);
    std::vector<bool> include(size);

    // break the path into segments and store in the list
    std::size_t current = 0;
    std::size_t index2 = 0;

    index = (pathlen > 0 && path.at(0) == '/') ? 1 : 0;
    while ((index2 = path.find('/', index + 1)) != string::npos) {
        seglist[current++] = path.substr(index, index2 - index);
        index = index2 + 1;
    }

    // if current==size, then the last character was a slash
    // and there are no more segments
    if (current < size) {
        seglist[current] = path.substr(index);
    }

    // determine which segments get included in the normalized path
    for (unsigned int i = 0; i < size; i++) {

        include[i] = true;

        if (seglist[i] == "..") {

            int remove = i - 1;

            // search back to find a segment to remove, if possible
            while (remove > -1 && !include[remove]) {
                remove--;
            }

            // if we find a segment to remove, remove it and the ".."
            // segment
            if (remove > -1 && !(seglist[remove] == "..")) {
                include[remove] = false;
                include[i] = false;
            }

        } else if (seglist[i] == ".") {
            include[i] = false;
        }
    }

    // put the path back together
    string newpath;
    if (path.at(0) == '/') {
        newpath.append("/");
    }

    for (unsigned int i = 0; i < seglist.size(); i++) {
        if (include[i]) {
            newpath.append(seglist[i]);
            newpath.append("/");
        }
    }

    // if we used at least one segment and the path previously ended with
    // a slash and the last segment is still used, then delete the extra
    // trailing '/'
    if (path.at(path.length() - 1) != '/' && seglist.size() > 0 && include[seglist.size() - 1]) {

        newpath.erase(newpath.length() - 1, 1);
    }

    string result = newpath;

    // check for a ':' in the first segment if one exists,
    // prepend "./" to normalize
    index = result.find(':');
    index2 = result.find('/');

    if (index != string::npos && (index < index2 || index2 == string::npos)) {
        newpath.insert(0, "./");
        result = newpath;
    }

    return result;
}

////////////////////////////////////////////////////////////////////////////////
URI URI::normalize() const {

    if (isOpaque()) {
        return *this;
    }

    string normalizedPath = normalize(this->uri.getPath());

    // if the path is already normalized, return this
    if (this->uri.getPath() == normalizedPath) {
        return *this;
    }

    // get an exact copy of the URI re-calculate the scheme specific part
    // since the path of the normalized URI is different from this URI.
    URI result = *this;
    result.uri.setPath(normalizedPath);
    result.setSchemeSpecificPart();

    return result;
}

////////////////////////////////////////////////////////////////////////////////
void URI::setSchemeSpecificPart() {

    // ssp = [//authority][path][?query]
    string ssp;

    if (this->uri.getAuthority() != "") {
        ssp.append(string("//") + this->uri.getAuthority());
    }

    if (this->uri.getPath() != "") {
        ssp.append(this->uri.getPath());
    }

    if (this->uri.getQuery() != "") {
        ssp.append("?" + this->uri.getQuery());
    }

    this->uri.setSchemeSpecificPart(ssp);

    // reset string, so that it can be re-calculated correctly when asked.
    this->uriString = "";
}

////////////////////////////////////////////////////////////////////////////////
URI URI::parseServerAuthority() const {

    URI newURI = *this;

    if (!newURI.uri.isServerAuthority()) {
        newURI.uri = URIHelper().parseAuthority(true, this->uri.getAuthority());
    }

    return newURI;
}

////////////////////////////////////////////////////////////////////////////////
URI URI::relativize( const URI& relative ) const {

    if (relative.isOpaque() || this->isOpaque()) {
        return relative;
    }

    if (this->uri.getScheme() == "" ? relative.uri.getScheme() != "" :
        this->uri.getScheme() != relative.uri.getScheme()) {

        return relative;
    }

    if (this->uri.getAuthority() == "" ? relative.uri.getAuthority() != "" :
        this->uri.getAuthority() != relative.uri.getAuthority()) {

        return relative;
    }

    // normalize both paths
    string thisPath = normalize( this->uri.getPath() );
    string relativePath = normalize( relative.uri.getPath() );

    /*
     * if the paths aren't equal, then we need to determine if this URI's
     * path is a parent path (begins with) the relative URI's path
     */
    if (thisPath != relativePath) {

        // if this URI's path doesn't end in a '/', add one
        if (thisPath.empty() || thisPath.at(thisPath.length() - 1) != '/') {
            thisPath = thisPath + '/';
        }

        /*
         * if the relative URI's path doesn't start with this URI's path,
         * then just return the relative URI; the URIs have nothing in
         * common
         */
        if (relativePath.find(thisPath) != 0) {
            return relative;
        }
    }

    URI result;
    result.uri.setFragment(relative.uri.getFragment());
    result.uri.setQuery(relative.uri.getQuery());
    // the result URI is the remainder of the relative URI's path
    result.uri.setPath(relativePath.substr(thisPath.length()));
    result.setSchemeSpecificPart();

    return result;
}

////////////////////////////////////////////////////////////////////////////////
URI URI::resolve(const URI& relative) const {

    if (relative.isAbsolute() || this->isOpaque()) {
        return relative;
    }

    URI result;
    if (relative.uri.getPath() == "" && relative.uri.getScheme() == "" && relative.uri.getAuthority() == ""
            && relative.uri.getQuery() == "" && relative.uri.getFragment() != "") {

        // if the relative URI only consists of fragment,
        // the resolved URI is very similar to this URI,
        // except that it has the fragment from the relative URI.
        result = *this;
        result.uri.setFragment(relative.uri.getFragment());
        result.uriString = "";

        // no need to re-calculate the scheme specific part,
        // since fragment is not part of scheme specific part.
        return result;
    }

    if (relative.uri.getAuthority() != "") {

        // if the relative URI has authority,
        // the resolved URI is almost the same as the relative URI,
        // except that it has the scheme of this URI.
        result = relative;
        result.uri.setScheme(this->uri.getScheme());
        result.uri.setAbsolute(this->uri.isAbsolute());
        result.uriString = "";

    } else {

        // since relative URI has no authority,
        // the resolved URI is very similar to this URI,
        // except that it has the query and fragment of the relative URI,
        // and the path is different.
        result = *this;
        result.uri.setFragment(relative.uri.getFragment());
        result.uri.setQuery(relative.uri.getQuery());
        result.uriString = "";

        if (relative.uri.getPath().at(0) == '/') {
            result.uri.setPath(relative.uri.getPath());
        } else {
            // resolve a relative reference
            std::size_t endindex = this->uri.getPath().find_last_of('/') + 1;
            result.uri.setPath(normalize(
                this->uri.getPath().substr(0, endindex) + relative.uri.getPath()));
        }

        // re-calculate the scheme specific part since
        // query and path of the resolved URI is different from this URI.
        result.setSchemeSpecificPart();
    }

    return result;
}

////////////////////////////////////////////////////////////////////////////////
URI URI::resolve(const std::string& relative) const {
    return resolve(create(relative));
}

////////////////////////////////////////////////////////////////////////////////
string URI::toString() const {

    if (this->uriString == "") {

        string result = "";

        if (this->uri.getScheme() != "") {
            result.append(this->uri.getScheme());
            result.append(":");
        }

        if (this->isOpaque()) {
            result.append(this->uri.getSchemeSpecificPart());
        } else {

            if (this->uri.getAuthority() != "") {
                result.append("//");
                result.append(this->uri.getAuthority());
            }

            if (this->uri.getPath() != "") {
                result.append(this->uri.getPath());
            }

            if (this->uri.getQuery() != "") {
                result.append("?");
                result.append(this->uri.getQuery());
            }
        }

        if (this->uri.getFragment() != "") {
            result.append("#");
            result.append(this->uri.getFragment());
        }

        this->uriString = result;
    }

    return this->uriString;
}

////////////////////////////////////////////////////////////////////////////////
URL URI::toURL() const {

    if (!this->isAbsolute()) {
        throw IllegalArgumentException(
            __FILE__, __LINE__, "URI is not absolute, cannot convert to an URL.");
    }

    return URL(this->toString());
}

////////////////////////////////////////////////////////////////////////////////
std::string URI::getAuthority() const {
    return this->decode(this->uri.getAuthority());
}

////////////////////////////////////////////////////////////////////////////////
std::string URI::getFragment() const {
    return this->decode(this->uri.getFragment());
}

////////////////////////////////////////////////////////////////////////////////
std::string URI::getHost() const {
    return this->uri.getHost();
}

////////////////////////////////////////////////////////////////////////////////
std::string URI::getPath() const {
    return this->decode(this->uri.getPath());
}

////////////////////////////////////////////////////////////////////////////////
int URI::getPort() const {
    return this->uri.getPort();
}

////////////////////////////////////////////////////////////////////////////////
std::string URI::getQuery() const {
    return this->decode(this->uri.getQuery());
}

////////////////////////////////////////////////////////////////////////////////
std::string URI::getRawAuthority() const {
    return this->uri.getAuthority();
}

////////////////////////////////////////////////////////////////////////////////
std::string URI::getRawFragment() const {
    return this->uri.getFragment();
}

////////////////////////////////////////////////////////////////////////////////
std::string URI::getRawPath() const {
    return this->uri.getPath();
}

////////////////////////////////////////////////////////////////////////////////
std::string URI::getRawQuery() const {
    return this->uri.getQuery();
}

////////////////////////////////////////////////////////////////////////////////
std::string URI::getRawSchemeSpecificPart() const {
    return this->uri.getSchemeSpecificPart();
}

////////////////////////////////////////////////////////////////////////////////
std::string URI::getRawUserInfo() const {
    return this->uri.getUserInfo();
}

////////////////////////////////////////////////////////////////////////////////
std::string URI::getScheme() const {
    return this->uri.getScheme();
}

////////////////////////////////////////////////////////////////////////////////
std::string URI::getSchemeSpecificPart() const {
    return this->decode(this->uri.getSchemeSpecificPart());
}

////////////////////////////////////////////////////////////////////////////////
std::string URI::getUserInfo() const {
    return this->decode(this->uri.getUserInfo());
}

////////////////////////////////////////////////////////////////////////////////
bool URI::isAbsolute() const {
    return this->uri.isAbsolute();
}

////////////////////////////////////////////////////////////////////////////////
bool URI::isOpaque() const {
    return this->uri.isOpaque();
}
