All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.muserver.rest.MuUriBuilder Maven / Gradle / Ivy

There is a newer version: 2.0.3
Show newest version
package io.muserver.rest;

import io.muserver.Mutils;
import io.netty.handler.codec.http.QueryStringDecoder;

import javax.ws.rs.Path;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriBuilderException;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.Collections.emptyMap;
import static java.util.stream.Collectors.toList;

class MuUriBuilder extends UriBuilder {
    static {
        MuRuntimeDelegate.ensureSet();
    }

    private String scheme;
    private String userInfo;
    private String host;
    private int port = -1;
    private List pathSegments = new ArrayList<>();
    private MultivaluedMap query = new MultivaluedHashMap<>();
    private String fragment;
    private boolean hasPrecedingSlash = false;
    private boolean hasTrailingSlash = false;

    MuUriBuilder() {
    }

    private MuUriBuilder(String scheme, String userInfo, String host, int port, List pathSegments,
                         boolean hasPrecedingSlash, boolean hasTrailingSlash, MultivaluedMap query, String fragment) {
        this.scheme = scheme;
        this.userInfo = userInfo;
        this.host = host;
        this.port = port;
        this.pathSegments = new ArrayList<>(pathSegments);
        this.hasTrailingSlash = hasTrailingSlash;
        this.hasPrecedingSlash = hasPrecedingSlash;
        MultivaluedMap copy = new MultivaluedHashMap<>();
        copy.putAll(query);
        this.query = copy;
        this.fragment = fragment;
    }

    @Override
    public UriBuilder clone() {
        return new MuUriBuilder(scheme, userInfo, host, port, pathSegments, hasPrecedingSlash, hasTrailingSlash, query, fragment);
    }

    @Override
    public UriBuilder uri(URI uri) {
        scheme(uri.getScheme());
        userInfo(uri.getUserInfo());
        host(uri.getHost());
        port(uri.getPort());
        replacePath(uri.getPath());
        replaceQuery(uri.getQuery());
        fragment(uri.getFragment());
        return this;
    }

    @Override
    public UriBuilder uri(String uriTemplate) {
        if (uriTemplate == null) {
            throw new IllegalArgumentException("uriTemplate is null");
        }
        Pattern parts = Pattern.compile("(?[^/]*//[^/]*)?(?[^?#]*)(?.*)");
        Matcher matcher = parts.matcher(uriTemplate);
        if (!matcher.matches()) {
            throw new IllegalArgumentException(uriTemplate + " is not a valid URI");
        }
        String path = matcher.group("path");
        UriBuilder builder = uri(URI.create(matcher.group("firstBit") + matcher.group("end")));
        builder.replacePath(path);
        return builder;
    }

    @Override
    public UriBuilder schemeSpecificPart(String ssp) {
        MuUriBuilder builder = (MuUriBuilder) fromUri(URI.create(scheme + "://" + ssp));
        this.scheme = builder.scheme;
        this.userInfo = builder.userInfo;
        this.host = builder.host;
        this.port = builder.port;
        this.pathSegments = builder.pathSegments;
        setSlashes(ssp);
        return this;
    }

    @Override
    public UriBuilder scheme(String scheme) {
        this.scheme = decode(scheme);
        return this;
    }

    @Override
    public UriBuilder userInfo(String userInfo) {
        this.userInfo = decode(userInfo);
        return this;
    }

    @Override
    public UriBuilder host(String host) {
        this.host = decode(host);
        return this;
    }

    @Override
    public UriBuilder port(int port) {
        this.port = port;
        return this;
    }

    @Override
    public UriBuilder path(String path) {
        Mutils.notNull("path", path);
        setSlashes(path);
        this.pathSegments.addAll(MuUriInfo.pathStringToSegments(decode(path), false).collect(toList()));
        return this;
    }

    @Override
    public UriBuilder replacePath(String path) {
        this.pathSegments.clear();
        return path(path);
    }

    @Override
    public UriBuilder path(Class resource) {
        Mutils.notNull("resource", resource);
        return this.path(findResourcePath(resource));
    }

    private String findResourcePath(Class resource) {
        Path pathAn = (Path)resource.getDeclaredAnnotation(Path.class);
        if (pathAn == null) {
            throw new IllegalArgumentException(resource + " is not a JAX-RS class");
        }
        return pathAn.value();
    }

    @Override
    public UriBuilder path(Class resource, String method) {
        Method selected = null;
        for (Method m : resource.getDeclaredMethods()) {
            if (m.getName().equals(method)) {
                if (selected == null) {
                    selected = m;
                } else {
                    throw new IllegalArgumentException("There is more than one method named " + method + " in " + resource);
                }
            }
        }
        if (selected != null) {
            return path(selected);
        }
        throw new IllegalArgumentException("Could not find " + resource + "#" + method);
    }

    @Override
    public UriBuilder path(Method method) {
        path(method.getDeclaringClass());
        Path methodPath = method.getDeclaredAnnotation(Path.class);
        if (methodPath != null) {
            path(methodPath.value());
        }
        return this;
    }

    @Override
    public UriBuilder segment(String... segments) {
        Mutils.notNull("segments", segments);
        for (String segment : segments) {
            Mutils.notNull("segment", segment);
            this.pathSegments.addAll(MuUriInfo.pathStringToSegments(decode(segment), true).collect(toList()));
        }
        this.hasTrailingSlash = false;
        return this;
    }

    @Override
    public UriBuilder replaceMatrix(String matrix) {
        MultivaluedMap params = getOrCreateCurrentSegment().getMatrixParameters();
        params.clear();
        if (matrix != null) {
            String[] parts = matrix.split(";");
            for (String part : parts) {
                String[] nameValue = part.split("=");
                if (nameValue.length != 2) {
                    throw new IllegalArgumentException("Not a valid matrix string: " + matrix);
                }
                matrixParam(nameValue[0], nameValue[1]);
            }
        }
        return this;
    }

    @Override
    public UriBuilder matrixParam(String name, Object... values) {
        Mutils.notNull("name", name);
        Mutils.notNull("values", values);

        MultivaluedMap params = getOrCreateCurrentSegment().getMatrixParameters();
        for (Object value : values) {
            params.add(name, decode(value));
        }

        return this;
    }

    @Override
    public UriBuilder replaceMatrixParam(String name, Object... values) {
        Mutils.notNull("name", name);
        Mutils.notNull("values", values);
        MultivaluedMap params = getOrCreateCurrentSegment().getMatrixParameters();
        params.replace(name, Stream.of(values).map(Object::toString).collect(toList()));
        return this;
    }

    private MuPathSegment getOrCreateCurrentSegment() {
        MuPathSegment ps;
        if (pathSegments.isEmpty()) {
            ps = new MuPathSegment("", new MultivaluedHashMap<>());
            pathSegments.add(ps);
        } else {
            ps = pathSegments.get(pathSegments.size() - 1);
        }
        return ps;
    }

    @Override
    public UriBuilder replaceQuery(String qs) {
        if (qs == null) {
            this.query.clear();
        } else {
            query = new MultivaluedHashMap<>();
            QueryStringDecoder decoder = new QueryStringDecoder(qs, false);
            for (Map.Entry> entry : decoder.parameters().entrySet()) {
                List values = entry.getValue();
                queryParam(entry.getKey(), values.toArray(new Object[values.size()]));
            }
        }
        return this;
    }

    @Override
    public UriBuilder queryParam(String name, Object... values) {
        Mutils.notNull("name", name);
        Mutils.notNull("values", values);
        name = decode(name);
        for (Object value : values) {
            query.add(name, decode(value.toString()));
        }
        return this;
    }

    @Override
    public UriBuilder replaceQueryParam(String name, Object... values) {
        Mutils.notNull("name", name);
        query.remove(name);
        return values == null ? this : queryParam(name, values);
    }

    @Override
    public UriBuilder fragment(String fragment) {
        this.fragment = decode(fragment);
        return this;
    }


    @Override
    public UriBuilder resolveTemplate(String name, Object value) {
        Mutils.notNull("name", name);
        Mutils.notNull("value", value);
        return resolveTemplate(name, value, true);
    }

    @Override
    public UriBuilder resolveTemplateFromEncoded(String name, Object value) {
        Mutils.notNull("name", name);
        Mutils.notNull("value", value);
        String decoded = Jaxutils.leniantUrlDecode(value.toString());
        return resolveTemplate(name, decoded, true);
    }

    @Override
    public UriBuilder resolveTemplates(Map templateValues) {
        Mutils.notNull("templateValues", templateValues);
        return resolveTemplates(templateValues, true);
    }

    @Override
    public UriBuilder resolveTemplates(Map templateValues, boolean encodeSlashInPath) throws IllegalArgumentException {
        Mutils.notNull("templateValues", templateValues);
        for (Map.Entry entry : templateValues.entrySet()) {
            resolveTemplate(entry.getKey(), entry.getValue(), encodeSlashInPath);
        }
        return this;
    }

    @Override
    public UriBuilder resolveTemplatesFromEncoded(Map templateValues) {
        Mutils.notNull("templateValues", templateValues);
        for (Map.Entry entry : templateValues.entrySet()) {
            resolveTemplateFromEncoded(entry.getKey(), entry.getValue());
        }
        return this;
    }

    @Override
    public UriBuilder resolveTemplate(String name, Object value, boolean encodeSlashInPath) {
        Mutils.notNull("name", name);
        Mutils.notNull("value", value);
        String val = value.toString();

        scheme = resolve(scheme, name, val);
        userInfo = resolve(userInfo, name, val);
        host = resolve(host, name, val);
        pathSegments = pathSegments.stream()
            .flatMap(ps -> ps.resolve(name, val, encodeSlashInPath).stream())
            .collect(toList());
        if (!query.isEmpty()) {
            MultivaluedMap nq = new MultivaluedHashMap<>();
            for (Map.Entry> entry : query.entrySet()) {
                String key = resolve(entry.getKey(), name, val);
                nq.addAll(key, entry.getValue().stream().map(s -> resolve(s, name, val)).collect(toList()));
            }
            query = nq;
        }
        fragment = resolve(fragment, name, val);

        return this;
    }

    @Override
    public URI buildFromMap(Map values) {
        return buildFromMap(values, true);
    }

    @Override
    public URI buildFromMap(Map values, boolean encodeSlashInPath) throws IllegalArgumentException, UriBuilderException {
        MuUriBuilder resolved = cloneAndResolve(values, encodeSlashInPath, false);
        return URI.create(resolved.buildIt(Mutils::urlEncode));
    }

    @Override
    public URI buildFromEncodedMap(Map values) throws IllegalArgumentException, UriBuilderException {
        MuUriBuilder resolved = cloneAndResolve(values, true, true);
        return URI.create(resolved.buildIt(Mutils::urlEncode));

    }

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

    @Override
    public URI build(Object[] values, boolean encodeSlashInPath) throws IllegalArgumentException, UriBuilderException {
        Map valueMap = valuesToMap(values);
        return buildFromMap(valueMap, encodeSlashInPath);
    }

    @Override
    public URI buildFromEncoded(Object... values) throws IllegalArgumentException, UriBuilderException {
        Map valueMap = valuesToMap(values);
        return buildFromEncodedMap(valueMap);
    }

    @Override
    public String toTemplate() {
        return buildIt(s -> s);
    }


    private MuUriBuilder cloneAndResolve(Map values, boolean encodeSlashInPath, boolean valuesAlreadyEncoded) {
        if (values.isEmpty()) {
            return this;
        }
        MuUriBuilder copy = (MuUriBuilder) clone();
        for (Map.Entry entry : values.entrySet()) {
            Object value = entry.getValue();
            if (valuesAlreadyEncoded) {
                value = Jaxutils.leniantUrlDecode(value.toString());
            }
            copy.resolveTemplate(entry.getKey(), value, encodeSlashInPath);
        }
        return copy;
    }

    static String resolve(String template, String name, String value) {
        if (template == null) {
            return null;
        }
        return template.replaceAll("\\{\\s*" + Pattern.quote(name) + "\\s*(:[^}]*)?\\s*}", value);
    }


    private String buildIt(Function encodeFunction) {
        StringBuilder sb = new StringBuilder();
        if (scheme != null) {
            sb.append(encodeFunction.apply(scheme)).append("://");
        }
        if (userInfo != null) {
            String encodedUserInfo = encodeFunction.apply(userInfo);
            // If there is a colon in a user:pw pair, no way to know if it belongs to
            // the name or password, so just assume password.
            encodedUserInfo = encodedUserInfo.replaceFirst("%3A", ":");
            sb.append(encodedUserInfo).append('@');
        }
        if (host != null) {
            sb.append(encodeFunction.apply(host));
        }
        if (port != -1) {
            sb.append(':').append(port);
        }
        if (!pathSegments.isEmpty()) {
            if (hasPrecedingSlash || sb.length() > 0) {
                sb.append('/');
            }
            sb.append(pathSegments.stream()
                .map(muPathSegment -> muPathSegment.toString(encodeFunction))
                .collect(Collectors.joining("/")));
        }
        if (hasTrailingSlash) {
            sb.append('/');
        }
        if (!query.isEmpty()) {
            sb.append('?');
            boolean isFirst = true;
            for (Map.Entry> entry : query.entrySet()) {
                String key = encodeFunction.apply(entry.getKey());
                for (String val : entry.getValue()) {
                    if (!isFirst) {
                        sb.append('&');
                    }
                    sb.append(key).append('=').append(encodeFunction.apply(val));
                    isFirst = false;
                }
            }
        }
        if (fragment != null) {
            sb.append('#').append(encodeFunction.apply(fragment));
        }
        return sb.toString();
    }

    private Map valuesToMap(Object[] values) {
        if (values == null || values.length == 0) {
            return emptyMap();
        }
        List sorted = new ArrayList<>();
        addTemplateNames(sorted, scheme);
        addTemplateNames(sorted, userInfo);
        addTemplateNames(sorted, host);
        for (MuPathSegment pathSegment : pathSegments) {
            for (String pspp : pathSegment.pathParameters()) {
                if (!sorted.contains(pspp)) {
                    sorted.add(pspp);
                }
            }
        }
        for (Map.Entry> entry : query.entrySet()) {
            addTemplateNames(sorted, entry.getKey());
            for (String val : entry.getValue()) {
                addTemplateNames(sorted, val);
            }
        }
        addTemplateNames(sorted, fragment);

        if (sorted.size() != values.length) {
            throw new IllegalArgumentException("There are " + sorted.size() + " parameters (" + sorted + ") but " + values.length + " values " + Arrays.toString(values) + " were supplied.");
        }
        HashMap map = new HashMap<>();
        int index = 0;
        for (String name : sorted) {
            Object value = values[index];
            if (value == null) {
                throw new IllegalArgumentException("Value at index " + index + " was null");
            }
            map.put(name, value);
            index++;
        }
        return map;
    }

    private static void addTemplateNames(List sorted, String value) {
        if (value != null) {
            List toAdd = UriPattern.uriTemplateToRegex(value).namedGroups();
            for (String s : toAdd) {
                if (!sorted.contains(s)) {
                    sorted.add(s);
                }
            }
        }
    }

    private static String decode(Object value) {
        return value == null ? null : Jaxutils.leniantUrlDecode(value.toString());
    }

    private void setSlashes(String path) {
        if (this.pathSegments.isEmpty()) {
            this.hasPrecedingSlash = path.startsWith("/");
        }
        this.hasTrailingSlash = path.endsWith("/");
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy