/**
 * 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.cxf.jaxrs.impl;

import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.ws.rs.Path;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.PathSegment;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriBuilderException;

import org.apache.cxf.common.util.CollectionUtils;
import org.apache.cxf.common.util.PropertyUtils;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.jaxrs.model.URITemplate;
import org.apache.cxf.jaxrs.utils.HttpUtils;
import org.apache.cxf.jaxrs.utils.JAXRSUtils;

/**
 * Implements the {@link UriBuilder} abstract class. A few notes with respect to CXF specifics:
 * <ul>
 *   <li>it uses standard {@link URI} internally, so it does not conform to 
 *   RFC-3986 which {@link UriBuilder} mandates</li>
 *   <li>it encodes URI components if it encounters '%XX' pattern within name (if applicable) or value (those
 *   are treated as partially encoded)</li>
 *  </ul
 */
public class UriBuilderImpl extends UriBuilder implements Cloneable {
    private static final String EXPAND_QUERY_VALUE_AS_COLLECTION = "expand.query.value.as.collection";
    private static final String USE_ARRAY_SYNTAX_FOR_QUERY_VALUES = "use.array.syntax.for.query.values";

    private String scheme;
    private String userInfo;
    private int port = -1;
    private String host;
    private List<PathSegment> paths = new ArrayList<>();
    private boolean originalPathEmpty;
    private boolean leadingSlash;
    private String fragment;
    private String schemeSpecificPart;
    private MultivaluedMap<String, String> query = new MetadataMap<>();
    private MultivaluedMap<String, String> matrix = new MetadataMap<>();

    private Map<String, Object> resolvedTemplates;
    private Map<String, Object> resolvedTemplatesPathEnc;
    private Map<String, Object> resolvedEncodedTemplates;

    private boolean queryValueIsCollection;
    private boolean useArraySyntaxForQueryParams;

    /**
     * Creates builder with empty URI.
     */
    public UriBuilderImpl() {
    }

    /**
     * Creates builder with empty URI and properties
     */
    public UriBuilderImpl(Map<String, Object> properties) {
        queryValueIsCollection = PropertyUtils.isTrue(properties, EXPAND_QUERY_VALUE_AS_COLLECTION);
        useArraySyntaxForQueryParams = PropertyUtils.isTrue(properties, USE_ARRAY_SYNTAX_FOR_QUERY_VALUES);
    }

    /**
     * Creates builder initialized with given URI.
     *
     * @param uri initial value for builder
     * @throws IllegalArgumentException when uri is null
     */
    public UriBuilderImpl(URI uri) throws IllegalArgumentException {
        setUriParts(uri);
    }

    @Override
    public URI build(Object... values) throws IllegalArgumentException, UriBuilderException {
        return doBuild(false, true, values);
    }

    private static Map<String, Object> getResolvedTemplates(Map<String, Object> rtemplates) {
        return rtemplates == null
            ? Collections.<String, Object>emptyMap() : new LinkedHashMap<String, Object>(rtemplates);
    }

    private URI doBuild(boolean fromEncoded, boolean encodePathSlash, Object... values) {
        if (values == null) {
            throw new IllegalArgumentException("Template parameter values are set to null");
        }
        for (int i = 0; i < values.length; i++) {
            if (values[i] == null) {
                throw new IllegalArgumentException("Template parameter value at position " + i + " is set to null");
            }
        }

        UriParts parts = doBuildUriParts(fromEncoded, encodePathSlash, false, values);
        try {
            final URI uri = buildURI(fromEncoded, parts.path, parts.query, parts.fragment);
            
            if (!Rfc3986UriValidator.validate(uri)) {
                throw new UriBuilderException("[" + uri + "] is not a valid HTTP URL");
            }
            
            return uri;
        } catch (URISyntaxException ex) {
            throw new UriBuilderException("URI can not be built", ex);
        }
    }

    private UriParts doBuildUriParts(boolean fromEncoded, boolean encodePathSlash,
                                     boolean allowUnresolved, Object... values) {

        Map<String, Object> alreadyResolvedTs = getResolvedTemplates(resolvedTemplates);
        Map<String, Object> alreadyResolvedTsPathEnc = getResolvedTemplates(resolvedTemplatesPathEnc);
        Map<String, Object> alreadyResolvedEncTs = getResolvedTemplates(resolvedEncodedTemplates);
        final int resolvedTsSize = alreadyResolvedTs.size()
            + alreadyResolvedEncTs.size()
            + alreadyResolvedTsPathEnc.size();

        String thePath = buildPath();
        URITemplate pathTempl = URITemplate.createExactTemplate(thePath);
        thePath = substituteVarargs(pathTempl, alreadyResolvedTs, alreadyResolvedTsPathEnc,
                                    alreadyResolvedEncTs, values, 0, false, fromEncoded,
                                    allowUnresolved, encodePathSlash);
        int pathTemplateVarsSize = pathTempl.getVariables().size();

        String theQuery = buildQuery();
        int queryTemplateVarsSize = 0;
        if (theQuery != null) {
            URITemplate queryTempl = URITemplate.createExactTemplate(theQuery);
            queryTemplateVarsSize = queryTempl.getVariables().size();
            if (queryTemplateVarsSize > 0) {
                int lengthDiff = values.length + resolvedTsSize
                    - alreadyResolvedTs.size() - alreadyResolvedTsPathEnc.size() - alreadyResolvedEncTs.size()
                    - pathTemplateVarsSize;
                theQuery = substituteVarargs(queryTempl, alreadyResolvedTs, alreadyResolvedTsPathEnc,
                                             alreadyResolvedEncTs, values, values.length - lengthDiff,
                                             true, fromEncoded, allowUnresolved, false);
            }
        }

        String theFragment = fragment;
        if (theFragment != null) {
            URITemplate fragmentTempl = URITemplate.createExactTemplate(theFragment);
            if (fragmentTempl.getVariables().size() > 0) {
                int lengthDiff = values.length  + resolvedTsSize
                    - alreadyResolvedTs.size() - alreadyResolvedTsPathEnc.size() - alreadyResolvedEncTs.size()
                    - pathTemplateVarsSize - queryTemplateVarsSize;
                theFragment = substituteVarargs(fragmentTempl, alreadyResolvedTs, alreadyResolvedTsPathEnc,
                                                alreadyResolvedEncTs, values, values.length - lengthDiff,
                                                true, fromEncoded, allowUnresolved, false);
            }
        }

        return new UriParts(thePath, theQuery, theFragment);
    }

    private URI buildURI(boolean fromEncoded, String thePath, String theQuery, String theFragment)
        throws URISyntaxException {
        if (fromEncoded) {
            return buildURIFromEncoded(thePath, theQuery, theFragment);
        } else if (!isSchemeOpaque()) {
            if ((scheme != null || host != null || userInfo != null)
                && thePath.length() != 0 && !(thePath.startsWith("/") || thePath.startsWith(";"))) {
                thePath = "/" + thePath;
            }
            try {
                return buildURIFromEncoded(thePath, theQuery, theFragment);
            } catch (Exception ex) {
                // lets try the option below
            }
            URI uri = new URI(scheme, userInfo, host, port,
                           thePath, theQuery, theFragment);
            if (thePath.contains("%2F")) {
                // TODO: the bogus case of segments containing encoded '/'
                // Not sure if we have a cleaner solution though.
                String realPath = uri.getRawPath().replace("%252F", "%2F");
                uri = buildURIFromEncoded(realPath, uri.getRawQuery(), uri.getRawFragment());
            }
            return uri;
        } else {
            return new URI(scheme, schemeSpecificPart, theFragment);
        }
    }

    private URI buildURIFromEncoded(String thePath, String theQuery, String theFragment)
        throws URISyntaxException {
        return new URI(buildUriString(thePath, theQuery, theFragment));
    }

    private String buildUriString(String thePath, String theQuery, String theFragment) {
        StringBuilder b = new StringBuilder();
        if (scheme != null) {
            b.append(scheme).append(':');
        }
        if (!isSchemeOpaque()) {
            if (scheme != null) {
                b.append("//");
            }
            if (userInfo != null) {
                b.append(userInfo).append('@');
            }
            if (host != null) {
                b.append(host);
            }
            if (port != -1) {
                b.append(':').append(port);
            }
            if (thePath != null && thePath.length() > 0) {
                b.append(thePath.startsWith("/") || b.length() == 0 || originalPathEmpty
                    ? thePath : '/' + thePath);
            }
            if (theQuery != null && theQuery.length() != 0) {
                b.append('?').append(theQuery);
            }
        } else {
            b.append(schemeSpecificPart);
        }
        if (theFragment != null) {
            b.append('#').append(theFragment);
        }
        return b.toString();
    }

    private boolean isSchemeOpaque() {
        return schemeSpecificPart != null;
    }

    @Override
    public URI buildFromEncoded(Object... values) throws IllegalArgumentException, UriBuilderException {
        return doBuild(true, false, values);
    }

    @Override
    public URI buildFromMap(Map<String, ?> map) throws IllegalArgumentException,
        UriBuilderException {
        return doBuildFromMap(map, false, true);
    }

    private URI doBuildFromMap(Map<String, ? extends Object> map, boolean fromEncoded,
                               boolean encodePathSlash)
        throws IllegalArgumentException, UriBuilderException {
        try {
            Map<String, Object> alreadyResolvedTs = getResolvedTemplates(resolvedTemplates);
            Map<String, Object> alreadyResolvedTsPathEnc = getResolvedTemplates(resolvedTemplatesPathEnc);
            Map<String, Object> alreadyResolvedEncTs = getResolvedTemplates(resolvedEncodedTemplates);

            String thePath = buildPath();
            thePath = substituteMapped(thePath, map, alreadyResolvedTs, alreadyResolvedTsPathEnc,
                                       alreadyResolvedEncTs, false, fromEncoded, encodePathSlash);

            String theQuery = buildQuery();
            if (theQuery != null) {
                theQuery = substituteMapped(theQuery, map, alreadyResolvedTs, alreadyResolvedTsPathEnc,
                                            alreadyResolvedEncTs, true, fromEncoded, false);
            }

            String theFragment = fragment == null
                ? null : substituteMapped(fragment, map, alreadyResolvedTs, alreadyResolvedTsPathEnc,
                                          alreadyResolvedEncTs, true, fromEncoded, encodePathSlash);

            return buildURI(fromEncoded, thePath, theQuery, theFragment);
        } catch (URISyntaxException ex) {
            throw new UriBuilderException("URI can not be built", ex);
        }
    }
    //CHECKSTYLE:OFF
    @SuppressWarnings("PMD.ExcessiveParameterList")
    private String substituteVarargs(URITemplate templ,
                                     Map<String, Object> alreadyResolvedTs,
                                     Map<String, Object> alreadyResolvedTsPathEnc,
                                     Map<String, Object> alreadyResolvedTsEnc,
                                     Object[] values,
                                     int ind,
                                     boolean isQuery,
                                     boolean fromEncoded,
                                     boolean allowUnresolved,
                                     boolean encodePathSlash) {

   //CHECKSTYLE:ON
        Map<String, String> varValueMap = new HashMap<>();

        // vars in set are properly ordered due to linking in hash set
        Set<String> uniqueVars = new LinkedHashSet<>(templ.getVariables());
        if (!allowUnresolved && values.length + alreadyResolvedTs.size() + alreadyResolvedTsEnc.size()
            + alreadyResolvedTsPathEnc.size() < uniqueVars.size()) {
            throw new IllegalArgumentException("Unresolved variables; only " + values.length
                                               + " value(s) given for " + uniqueVars.size()
                                               + " unique variable(s)");
        }
        int idx = ind;
        Set<String> pathEncodeVars = alreadyResolvedTsPathEnc.isEmpty() && !encodePathSlash
            ? Collections.<String>emptySet() : new HashSet<>();
        for (String var : uniqueVars) {

            boolean resolvedPathVarHasToBeEncoded = alreadyResolvedTsPathEnc.containsKey(var);
            boolean varValueHasToBeEncoded = resolvedPathVarHasToBeEncoded || alreadyResolvedTs.containsKey(var);

            Map<String, Object> resolved = !varValueHasToBeEncoded ? alreadyResolvedTsEnc
                : resolvedPathVarHasToBeEncoded ? alreadyResolvedTsPathEnc : alreadyResolvedTs;
            Object oval = resolved.isEmpty() ? null : resolved.remove(var);
            boolean valueFromEncodedMap = false;
            if (oval == null) {
                if (allowUnresolved) {
                    continue;
                }
                oval = values[idx++];
            } else {
                valueFromEncodedMap = resolved == alreadyResolvedTsEnc;
            }

            if (oval == null) {
                throw new IllegalArgumentException("No object for " + var);
            }
            String value = oval.toString();
            if (fromEncoded || valueFromEncodedMap) {
                value = HttpUtils.encodePartiallyEncoded(value, isQuery);
            } else {
                value = isQuery ? HttpUtils.queryEncode(value) : HttpUtils.pathEncode(value);
            }

            varValueMap.put(var, value);

            if (!isQuery && (resolvedPathVarHasToBeEncoded
                || encodePathSlash && !varValueHasToBeEncoded)) {
                pathEncodeVars.add(var);
            }

        }
        return templ.substitute(varValueMap, pathEncodeVars, allowUnresolved);
    }

    //CHECKSTYLE:OFF
    private String substituteMapped(String path,
                                    Map<String, ? extends Object> varValueMap,
                                    Map<String, Object> alreadyResolvedTs,
                                    Map<String, Object> alreadyResolvedTsPathEnc,
                                    Map<String, Object> alreadyResolvedTsEnc,
                                    boolean isQuery,
                                    boolean fromEncoded,
                                    boolean encodePathSlash) {
    //CHECKSTYLE:ON
        URITemplate templ = URITemplate.createExactTemplate(path);

        Set<String> uniqueVars = new HashSet<>(templ.getVariables());
        if (varValueMap.size() + alreadyResolvedTs.size() + alreadyResolvedTsEnc.size()
            + alreadyResolvedTsPathEnc.size() < uniqueVars.size()) {
            throw new IllegalArgumentException("Unresolved variables; only " + varValueMap.size()
                                               + " value(s) given for " + uniqueVars.size()
                                               + " unique variable(s)");
        }

        Set<String> pathEncodeVars = alreadyResolvedTsPathEnc.isEmpty() && !encodePathSlash
            ? Collections.<String>emptySet() : new HashSet<>();

        Map<String, Object> theMap = new LinkedHashMap<>();
        for (String var : uniqueVars) {
            boolean isPathEncVar = !isQuery && alreadyResolvedTsPathEnc.containsKey(var);

            boolean isVarEncoded = !(isPathEncVar || alreadyResolvedTs.containsKey(var));
            Map<String, Object> resolved = isVarEncoded ? alreadyResolvedTsEnc
                : isPathEncVar ? alreadyResolvedTsPathEnc : alreadyResolvedTs;
            Object oval = resolved.isEmpty() ? null : resolved.remove(var);
            if (oval == null) {
                oval = varValueMap.get(var);
            }
            if (oval == null) {
                throw new IllegalArgumentException("No object for " + var);
            }
            if (fromEncoded) {
                oval = HttpUtils.encodePartiallyEncoded(oval.toString(), isQuery);
            } else {
                oval = isQuery ? HttpUtils.queryEncode(oval.toString()) : HttpUtils.pathEncode(oval.toString());
            }
            theMap.put(var, oval);
            if (!isQuery && (isPathEncVar || encodePathSlash)) {
                pathEncodeVars.add(var);
            }
        }
        return templ.substitute(theMap, pathEncodeVars, false);
    }

    @Override
    public URI buildFromEncodedMap(Map<String, ?> map) throws IllegalArgumentException,
        UriBuilderException {

        Map<String, String> decodedMap = new HashMap<>(map.size());
        for (Map.Entry<String, ? extends Object> entry : map.entrySet()) {
            if (entry.getValue() == null) {
                throw new IllegalArgumentException("Value is null");
            }
            String theValue = entry.getValue().toString();
            if (theValue.contains("/")) {
                // protecting '/' from being encoded here assumes that a given value may constitute multiple
                // path segments - very questionable especially given that queries and fragments may also
                // contain template vars - technically this can be covered by checking where a given template
                // var is coming from and act accordingly. Confusing nonetheless.
                StringBuilder buf = new StringBuilder();
                String[] values = theValue.split("/");
                for (int i = 0; i < values.length; i++) {
                    buf.append(HttpUtils.encodePartiallyEncoded(values[i], false));
                    if (i + 1 < values.length) {
                        buf.append('/');
                    }
                }
                decodedMap.put(entry.getKey(), buf.toString());
            } else {
                decodedMap.put(entry.getKey(), HttpUtils.encodePartiallyEncoded(theValue, false));
            }

        }
        return doBuildFromMap(decodedMap, true, false);
    }

    // CHECKSTYLE:OFF
    @Override
    public UriBuilder clone() { //NOPMD
        UriBuilderImpl builder = new UriBuilderImpl();
        builder.scheme = scheme;
        builder.userInfo = userInfo;
        builder.port = port;
        builder.host = host;
        builder.paths = new ArrayList<>(paths);
        builder.fragment = fragment;
        builder.query = new MetadataMap<>(query);
        builder.matrix = new MetadataMap<>(matrix);
        builder.schemeSpecificPart = schemeSpecificPart;
        builder.leadingSlash = leadingSlash;
        builder.originalPathEmpty = originalPathEmpty;
        builder.queryValueIsCollection = queryValueIsCollection;
        builder.resolvedEncodedTemplates =
            resolvedEncodedTemplates == null ? null : new HashMap<String, Object>(resolvedEncodedTemplates);
        builder.resolvedTemplates =
            resolvedTemplates == null ? null : new HashMap<String, Object>(resolvedTemplates);
        builder.resolvedTemplatesPathEnc =
            resolvedTemplatesPathEnc == null ? null : new HashMap<String, Object>(resolvedTemplatesPathEnc);
        builder.useArraySyntaxForQueryParams = useArraySyntaxForQueryParams;
        return builder;
    }
    // CHECKSTYLE:ON

    @Override
    public UriBuilder fragment(String theFragment) throws IllegalArgumentException {
        this.fragment = theFragment;
        return this;
    }

    @Override
    public UriBuilder host(String theHost) throws IllegalArgumentException {
        if ("".equals(theHost)) {
            throw new IllegalArgumentException("Host cannot be empty");
        }
        this.host = theHost;
        return this;
    }

    @Override
    public UriBuilder path(@SuppressWarnings("rawtypes") Class resource) throws IllegalArgumentException {
        if (resource == null) {
            throw new IllegalArgumentException("resource is null");
        }
        Class<?> cls = resource;
        Path ann = cls.getAnnotation(Path.class);
        if (ann == null) {
            throw new IllegalArgumentException("Class '" + resource.getCanonicalName()
                                               + "' is not annotated with Path");
        }
        // path(String) decomposes multi-segment path when necessary
        return path(ann.value());
    }

    @Override
    public UriBuilder path(@SuppressWarnings("rawtypes") Class resource, String method)
        throws IllegalArgumentException {
        if (resource == null) {
            throw new IllegalArgumentException("resource is null");
        }
        if (method == null) {
            throw new IllegalArgumentException("method is null");
        }
        Path foundAnn = null;
        for (Method meth : resource.getMethods()) {
            if (meth.getName().equals(method)) {
                Path ann = meth.getAnnotation(Path.class);
                if (foundAnn != null && ann != null) {
                    throw new IllegalArgumentException("Multiple Path annotations for '" + method
                        + "' overloaded method");
                }
                foundAnn = ann;
            }
        }
        if (foundAnn == null) {
            throw new IllegalArgumentException("No Path annotation for '" + method + "' method");
        }
        // path(String) decomposes multi-segment path when necessary
        return path(foundAnn.value());
    }


    @Override
    public UriBuilder path(Method method) throws IllegalArgumentException {
        if (method == null) {
            throw new IllegalArgumentException("method is null");
        }
        Path ann = method.getAnnotation(Path.class);
        if (ann == null) {
            throw new IllegalArgumentException("Method '" + method.getDeclaringClass().getCanonicalName() + "."
                                               + method.getName() + "' is not annotated with Path");
        }
        // path(String) decomposes multi-segment path when necessary
        return path(ann.value());
    }

    @Override
    public UriBuilder path(String path) throws IllegalArgumentException {
        return doPath(path, true);
    }

    private UriBuilder doPath(String path, boolean checkSegments) {
        if (path == null) {
            throw new IllegalArgumentException("path is null");
        }
        if (isAbsoluteUriPath(path)) {
            try {
                URI uri = URI.create(path);
                this.originalPathEmpty = StringUtils.isEmpty(uri.getPath());
                uri(uri);
            } catch (IllegalArgumentException ex) {
                if (!URITemplate.createExactTemplate(path).getVariables().isEmpty()) {
                    return uriAsTemplate(path);
                }
                String pathEncoded = HttpUtils.pathEncode(path);
                // Bad hack to bypass the TCK usage of bogus URI with empty paths containing matrix parameters,
                // which even URI class chokes upon; cheaper to do the following than try to challenge,
                // given that URI RFC mentions the possibility of empty paths, though no word on the possibility of
                // such empty paths having matrix parameters...
                int schemeIndex = pathEncoded.indexOf("//");
                if (schemeIndex != -1) {
                    int pathComponentStart = pathEncoded.indexOf('/', schemeIndex + 2);
                    if (pathComponentStart == -1) {
                        this.originalPathEmpty = true;
                        pathComponentStart = pathEncoded.indexOf(';');
                        if (pathComponentStart != -1) {
                            pathEncoded = pathEncoded.substring(0, pathComponentStart)
                                + "/" + pathEncoded.substring(pathComponentStart);
                        }
                    }
                }
                setUriParts(URI.create(pathEncoded));
            }
            return this;
        }

        if (paths.isEmpty()) {
            leadingSlash = path.startsWith("/");
        }

        List<PathSegment> segments;
        if (checkSegments) {
            segments = JAXRSUtils.getPathSegments(path, false, false);
        } else {
            segments = new ArrayList<>();
            path = path.replaceAll("/", "%2F");
            segments.add(new PathSegmentImpl(path, false));
        }
        if (!paths.isEmpty() && !matrix.isEmpty()) {
            PathSegment ps = paths.remove(paths.size() - 1);
            paths.add(replacePathSegment(ps));
        }
        paths.addAll(segments);
        matrix.clear();
        if (!paths.isEmpty()) {
            matrix = paths.get(paths.size() - 1).getMatrixParameters();
        }
        return this;
    }

    @Override
    public UriBuilder port(int thePort) throws IllegalArgumentException {
        if (thePort < 0 && thePort != -1) {
            throw new IllegalArgumentException("Port cannot be negative");
        }
        this.port = thePort;
        return this;
    }

    @Override
    public UriBuilder scheme(String s) throws IllegalArgumentException {
        scheme = s;
        return this;
    }

    @Override
    public UriBuilder schemeSpecificPart(String ssp) throws IllegalArgumentException {
        // scheme-specific part is whatever after ":" of URI
        // see: http://en.wikipedia.org/wiki/URI_scheme
        try {
            if (scheme == null) {
                scheme = "http";
            }
            URI uri = new URI(scheme, ssp, fragment);
            setUriParts(uri);
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException("Wrong syntax of scheme-specific part", e);
        }
        return this;
    }

    @Override
    public UriBuilder uri(URI uri) throws IllegalArgumentException {
        setUriParts(uri);
        return this;
    }

    @Override
    public UriBuilder userInfo(String ui) throws IllegalArgumentException {
        this.userInfo = ui;
        return this;
    }

    private void setUriParts(URI uri) {
        if (uri == null) {
            throw new IllegalArgumentException("uri is null");
        }
        String theScheme = uri.getScheme();
        if (theScheme != null) {
            scheme = theScheme;
        }
        String rawPath = uri.getRawPath();
        if (!uri.isOpaque() && schemeSpecificPart == null
                && (theScheme != null || rawPath != null)) {
            port = uri.getPort();
            host = uri.getHost();
            if (rawPath != null) {
                setPathAndMatrix(rawPath);
            }
            String rawQuery = uri.getRawQuery();
            if (rawQuery != null) {
                query = JAXRSUtils.getStructuredParams(rawQuery, "&", false, true);
            }
            userInfo = uri.getUserInfo();
            schemeSpecificPart = null;
        } else {
            schemeSpecificPart = uri.getSchemeSpecificPart();
        }
        if (scheme != null && host == null && port == -1 && userInfo == null
                && CollectionUtils.isEmpty(query)
                && uri.getSchemeSpecificPart() != null
                && !schemeSpecificPartMatchesUriPath(uri)) {
            schemeSpecificPart = uri.getSchemeSpecificPart();
        }
        String theFragment = uri.getFragment();
        if (theFragment != null) {
            fragment = theFragment;
        }
    }

    private boolean schemeSpecificPartMatchesUriPath(final URI uri) {
        return uri.getRawSchemeSpecificPart() != null
                && uri.getPath() != null
                && uri.getRawSchemeSpecificPart().equals("//" + uri.getPath());
    }

    private void setPathAndMatrix(String path) {
        leadingSlash = !originalPathEmpty && path.startsWith("/");
        paths = JAXRSUtils.getPathSegments(path, false, false);
        if (!paths.isEmpty()) {
            matrix = paths.get(paths.size() - 1).getMatrixParameters();
        } else {
            matrix.clear();
        }
    }

    private String buildPath() {
        StringBuilder sb = new StringBuilder();
        Iterator<PathSegment> iter = paths.iterator();
        while (iter.hasNext()) {
            PathSegment ps = iter.next();
            String p = ps.getPath();
            if (p.length() != 0 || !iter.hasNext()) {
                p = URITemplate.createExactTemplate(p).encodeLiteralCharacters(false);
                if (sb.length() == 0 && leadingSlash) {
                    sb.append('/');
                } else if (!p.startsWith("/") && sb.length() > 0) {
                    sb.append('/');
                }
                sb.append(p);
                if (iter.hasNext()) {
                    buildMatrix(sb, ps.getMatrixParameters());
                }
            }
        }
        buildMatrix(sb, matrix);
        return sb.toString();
    }

    private String buildQuery() {
        return buildParams(query, '&');
    }

    @Override
    public UriBuilder matrixParam(String name, Object... values) throws IllegalArgumentException {
        if (name == null || values == null) {
            throw new IllegalArgumentException("name or values is null");
        }
        List<String> list = matrix.get(name);
        if (list == null) {
            matrix.put(name, toStringList(true, values));
        } else {
            list.addAll(toStringList(true, values));
        }
        return this;
    }

    @Override
    public UriBuilder queryParam(String name, Object... values) throws IllegalArgumentException {
        if (name == null || values == null) {
            throw new IllegalArgumentException("name or values is null");
        }
        List<String> list = query.get(name);
        if (list == null) {
            query.put(name, toStringList(false, values));
        } else {
            list.addAll(toStringList(false, values));
        }
        return this;
    }

    @Override
    public UriBuilder replaceMatrix(String matrixValues) throws IllegalArgumentException {
        String encodedMatrixValues = matrixValues != null ? HttpUtils.pathEncode(matrixValues) : null;
        this.matrix = JAXRSUtils.getStructuredParams(encodedMatrixValues, ";", true, false);
        return this;
    }

    @Override
    public UriBuilder replaceMatrixParam(String name, Object... values) throws IllegalArgumentException {
        if (name == null) {
            throw new IllegalArgumentException("name is null");
        }
        if (values != null && values.length >= 1 && values[0] != null) {
            matrix.put(name, toStringList(true, values));
        } else {
            matrix.remove(name);
        }
        return this;
    }

    @Override
    public UriBuilder replacePath(String path) {
        if (path == null) {
            clearPathAndMatrix();
        } else if (isAbsoluteUriPath(path)) {
            clearPathAndMatrix();
            uri(URI.create(path));
        } else {
            setPathAndMatrix(path);
        }
        return this;
    }

    private void clearPathAndMatrix() {
        paths.clear();
        matrix.clear();
    }

    private boolean isAbsoluteUriPath(String path) {
        // This is the cheapest way to figure out if a given path is an absolute
        // URI with the http(s) scheme, more expensive way is to always convert
        // a path to URI and check if it starts from some scheme or not

        // Given that the list of schemes can be open-ended it is recommended that
        // UriBuilder.fromUri is called instead for schemes like 'file', 'jms', etc
        // be supported though the use of non-http schemes for *building* new URIs
        // is pretty limited in the context of working with JAX-RS services

        return path.startsWith("http:") || path.startsWith("https:");
    }

    @Override
    public UriBuilder replaceQuery(String queryValue) throws IllegalArgumentException {
        if (queryValue != null) {
            // workaround to do with a conflicting and confusing requirement where spaces
            // passed as part of replaceQuery are encoded as %20 while those passed as part
            // of quertyParam are encoded as '+'
            queryValue = queryValue.replace(" ", "%20");
        }
        query = JAXRSUtils.getStructuredParams(queryValue, "&", false, true);
        return this;
    }

    @Override
    public UriBuilder replaceQueryParam(String name, Object... values) throws IllegalArgumentException {
        if (name == null) {
            throw new IllegalArgumentException("name is null");
        }
        if (values != null && values.length >= 1 && values[0] != null) {
            query.put(name, toStringList(false, values));
        } else {
            query.remove(name);
        }
        return this;
    }

    @Override
    public UriBuilder segment(String... segments) throws IllegalArgumentException {
        if (segments == null) {
            throw new IllegalArgumentException("Segments should not be null");
        }
        for (String segment : segments) {
            doPath(segment, false);
        }
        return this;
    }

    /**
     * Query or matrix params convertion from object values vararg to list of strings. No encoding is
     * provided.
     *
     * @param values entry vararg values
     * @return list of strings
     * @throws IllegalArgumentException when one of values is null
     */
    private List<String> toStringList(boolean encodeSlash, Object... values) throws IllegalArgumentException {
        List<String> list = new ArrayList<>();
        if (values != null && values.length > 0) {
            for (int i = 0; i < values.length; i++) {
                Object value = values[i];
                if (value == null) {
                    throw new IllegalArgumentException("Null value on " + i + " position");
                }
                String strValue = value.toString();
                if (encodeSlash) {
                    strValue = strValue.replaceAll("/", "%2F");
                }
                list.add(strValue);
            }
        } else {
            list.add(null);
        }
        return list;
    }

    /**
     * Builds param string for query part or matrix part of URI.
     *
     * @param map query or matrix multivalued map
     * @param separator params separator, '&' for query ';' for matrix
     * @return stringified params.
     */
    private String buildParams(MultivaluedMap<String, String> map, char separator) {
        boolean isQuery = separator == '&';
        StringBuilder b = new StringBuilder();
        for (Iterator<Map.Entry<String, List<String>>> it = map.entrySet().iterator(); it.hasNext();) {
            Map.Entry<String, List<String>> entry = it.next();

            // Expand query parameter as "name=v1,v2,v3"
            if (isQuery && queryValueIsCollection) {
                b.append(entry.getKey());
                if (useArraySyntaxForQueryParams) {
                    b.append("[]");
                }
                b.append('=');

                for (Iterator<String> sit = entry.getValue().iterator(); sit.hasNext();) {
                    String val = sit.next();

                    if (val != null) {
                        boolean templateValue = val.startsWith("{") && val.endsWith("}");
                        if (!templateValue) {
                            val = HttpUtils.encodePartiallyEncoded(val, isQuery);
                            if (!isQuery) {
                                val = val.replaceAll("/", "%2F");
                            }
                        } else {
                            val = URITemplate.createExactTemplate(val).encodeLiteralCharacters(isQuery);
                        }

                        if (!val.isEmpty()) {
                            b.append(val);
                        }
                    }
                    if (sit.hasNext()) {
                        b.append(',');
                    }
                }

                if (it.hasNext()) {
                    b.append(separator);
                }
            } else {
                // Expand query parameter as "name=v1&name=v2&name=v3", or use dedicated
                // separator for matrix parameters
                for (Iterator<String> sit = entry.getValue().iterator(); sit.hasNext();) {
                    String val = sit.next();
                    b.append(entry.getKey());
                    if (useArraySyntaxForQueryParams) {
                        b.append("[]");
                    }
                    if (val != null) {
                        boolean templateValue = val.startsWith("{") && val.endsWith("}");
                        if (!templateValue) {
                            val = HttpUtils.encodePartiallyEncoded(val, isQuery);
                            if (!isQuery) {
                                val = val.replaceAll("/", "%2F");
                            }
                        } else {
                            val = URITemplate.createExactTemplate(val).encodeLiteralCharacters(isQuery);
                        }
                        b.append('=');
                        if (!val.isEmpty()) {
                            b.append(val);
                        }
                    }
                    if (sit.hasNext() || it.hasNext()) {
                        b.append(separator);
                    }
                }
            }
        }
        return b.length() > 0 ? b.toString() : null;
    }

    /**
     * Builds param string for matrix part of URI.
     *
     * @param sb buffer to add the matrix part to, will get ';' added if map is not empty
     * @param map matrix multivalued map
     */
    private void buildMatrix(StringBuilder sb, MultivaluedMap<String, String> map) {
        if (!map.isEmpty()) {
            sb.append(';');
            sb.append(buildParams(map, ';'));
        }
    }

    private PathSegment replacePathSegment(PathSegment ps) {
        StringBuilder sb = new StringBuilder();
        sb.append(ps.getPath());
        buildMatrix(sb, matrix);
        return new PathSegmentImpl(sb.toString());
    }


    public UriBuilder uri(String uriTemplate) throws IllegalArgumentException {
        if (StringUtils.isEmpty(uriTemplate)) {
            throw new IllegalArgumentException();
        }
        try {
            return uri(URI.create(uriTemplate));
        } catch (Exception ex) {
            if (URITemplate.createExactTemplate(uriTemplate).getVariables().isEmpty()) {
                throw new IllegalArgumentException(ex);
            }
            return uriAsTemplate(uriTemplate);

        }
    }

    public UriBuilder uriAsTemplate(String uri) {
        // This can be a start of replacing URI class Parser completely
        // but it can be too complicated, the following code is needed for now
        // to deal with URIs containing template variables.
        int index = uri.indexOf(':');
        if (index != -1) {
            this.scheme = uri.substring(0, index);
            uri = uri.substring(index + 1);
            if (uri.indexOf("//") == 0) {
                uri = uri.substring(2);
                index = uri.indexOf('/');
                if (index != -1) {
                    String[] schemePair = uri.substring(0, index).split(":");
                    this.host = schemePair[0];
                    this.port = schemePair.length == 2 ? Integer.parseInt(schemePair[1]) : -1;

                    uri = uri.substring(index);
                }
            }

        }
        String rawQuery = null;
        index = uri.indexOf('?');
        if (index != -1) {
            rawQuery = uri.substring(index + 1);
            uri = uri.substring(0, index);
        }
        setPathAndMatrix(uri);
        if (rawQuery != null) {
            query = JAXRSUtils.getStructuredParams(rawQuery, "&", false, true);
        }

        return this;
    }

    //the clarified rules for encoding values of uri templates are:
    //  - encode each value contextually based on the URI component containing the template
    //  - in path templates, by default, encode also slashes (i.e. treat all path templates as
    //    part of a single path segment, to be consistent with @Path annotation templates)
    //  - for special cases when the slash encoding in path templates is not desired,
    //    users may use the newly added build methods to override the default behavior

    @Override
    public URI build(Object[] vars, boolean encodePathSlash) throws IllegalArgumentException, UriBuilderException {
        return doBuild(false, encodePathSlash, vars);
    }

    @Override
    public URI buildFromMap(Map<String, ?> map, boolean encodePathSlash) throws IllegalArgumentException,
        UriBuilderException {
        return doBuildFromMap(map, false, encodePathSlash);
    }


    @Override
    public String toTemplate() {
        UriParts parts = doBuildUriParts(false, false, true);
        return buildUriString(parts.path, parts.query, parts.fragment);
    }

    @Override
    public UriBuilder resolveTemplate(String name, Object value) throws IllegalArgumentException {
        return resolveTemplate(name, value, true);
    }

    @Override
    public UriBuilder resolveTemplate(String name, Object value, boolean encodePathSlash)
        throws IllegalArgumentException {
        return resolveTemplates(Collections.singletonMap(name, value), encodePathSlash);
    }

    @Override
    public UriBuilder resolveTemplates(Map<String, Object> values) throws IllegalArgumentException {
        return resolveTemplates(values, true);
    }

    @Override
    public UriBuilder resolveTemplates(Map<String, Object> values, boolean encodePathSlash)
        throws IllegalArgumentException {
        if (encodePathSlash) {
            resolvedTemplatesPathEnc = fillInResolveTemplates(resolvedTemplatesPathEnc, values);
        } else {
            resolvedTemplates = fillInResolveTemplates(resolvedTemplates, values);
        }
        return this;
    }

    @Override
    public UriBuilder resolveTemplateFromEncoded(String name, Object value) throws IllegalArgumentException {
        return resolveTemplatesFromEncoded(Collections.singletonMap(name, value));
    }

    @Override
    public UriBuilder resolveTemplatesFromEncoded(Map<String, Object> values)
        throws IllegalArgumentException {
        resolvedEncodedTemplates = fillInResolveTemplates(resolvedEncodedTemplates, values);
        return this;
    }

    private static Map<String, Object> fillInResolveTemplates(Map<String, Object> map, Map<String, Object> values)
        throws IllegalArgumentException {
        if (values == null) {
            throw new IllegalArgumentException();
        }
        if (map == null) {
            map = new LinkedHashMap<>();
        }

        for (Map.Entry<String, Object> entry : values.entrySet()) {
            if (entry.getKey() == null || entry.getValue() == null) {
                throw new IllegalArgumentException();
            }
            map.put(entry.getKey(), entry.getValue());
        }
        return map;
    }

    private static class UriParts {
        String path;
        String query;
        String fragment;

        UriParts(String path, String query, String fragment) {
            this.path = path;
            this.query = query;
            this.fragment = fragment;
        }
    }
}
