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

io.micronaut.http.uri.UriTemplateParser Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2017-2024 original authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://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 io.micronaut.http.uri;

import io.micronaut.core.annotation.Internal;
import io.micronaut.core.util.CollectionUtils;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * The URI template parser rfc6570.
 *
 * @author Denis Stepanov
 * @since 4.6.0
 */
@Internal
final class UriTemplateParser {

    private UriTemplateParser() {
    }

    /**
     * Parse the template according to the spec.
     *
     * @param template The template
     * @return The parts of the template
     */
    public static List parse(String template) {
        List parts = new ArrayList<>(10);
        int expressionStartIndex = -1;
        char[] input = template.toCharArray();
        StringBuilder literal = new StringBuilder();
        boolean isText = true;
        for (int i = 0; i < input.length; i++) {
            char c = input[i];
            if (c == '{') {
                isText = false;
                if (!literal.isEmpty()) {
                    parts.add(new Literal(literal.toString()));
                    literal.setLength(0);
                }
                expressionStartIndex = i;
            } else if (c == '}' && expressionStartIndex != -1) {
                Expression expression = parseExpression(input, expressionStartIndex + 1, i);
                parts.add(expression);
                isText = true;
            } else if (isText) {
                if (Character.isISOControl(c) || Character.isWhitespace(c) || isAllowedCharacter(c) || c == '%') {
                    literal.append(c);
                } else {
                    throw new IllegalStateException("Unexpected character '" + c + "' at position " + i + " in " + template);
                }
            }
        }
        if (!literal.isEmpty()) {
            parts.add(new Literal(literal.toString()));
            literal.setLength(0);
        }
        return parts;
    }

    /**
     * Concat two collections of parts. Properly merging `/`.
     *
     * @param parts1 The first collection
     * @param parts2 The second collection
     * @return the merged collection
     */
    public static List concat(List parts1, List parts2) {
        parts1 = new ArrayList<>(parts1);
        parts2 = new ArrayList<>(parts2);
        List queryParams = new ArrayList<>();
        List fragmentParams = new ArrayList<>();
        // Query params should be last
        removeQueryParams(parts1, queryParams);
        removeQueryParams(parts2, queryParams);
        // Fragment params should be before query params
        removeFragmentParams(parts1, fragmentParams);
        removeFragmentParams(parts2, fragmentParams);
        if (parts1.isEmpty()) {
            return CollectionUtils.concat(parts2, CollectionUtils.concat(fragmentParams, queryParams));
        }
        if (parts2.isEmpty()) {
            return CollectionUtils.concat(parts1, CollectionUtils.concat(fragmentParams, queryParams));
        }

        if (parts2.get(0) instanceof UriTemplateParser.Expression expression
            && expression.type() == UriTemplateParser.ExpressionType.NONE) {
            parts2.add(0, new UriTemplateParser.Literal("/"));
        }
        List concat = new ArrayList<>(parts1.size() + parts2.size());
        UriTemplateParser.Part parts1Last = parts1.get(parts1.size() - 1);
        UriTemplateParser.Part parts2First = parts2.get(0);
        concat.addAll(parts1.subList(0, parts1.size() - 1));
        if (parts1Last instanceof UriTemplateParser.Literal literalPart1) {
            String literal1 = literalPart1.text();
            boolean literal1EndsWithSlash = literal1.endsWith("/");
            if (literal1EndsWithSlash
                && parts2First instanceof UriTemplateParser.Expression expression
                && expression.type() == UriTemplateParser.ExpressionType.PATH_SEGMENT_EXPANSION) {
                // Strip / from the last part of parts1, because it's needed for the new part
                parts1Last = new UriTemplateParser.Literal(literal1.substring(0, literal1.length() - 1));
            }
            if (parts2First instanceof UriTemplateParser.Literal literalPart2) {
                String literal2 = literalPart2.text();
                boolean literal2StartsWithSlash = literal2.startsWith("/");
                if (literal1EndsWithSlash && literal2StartsWithSlash) {
                    literal1 = literal1.substring(0, literal1.length() - 1);
                } else if (!literal1EndsWithSlash && !literal2StartsWithSlash) {
                    literal1 += "/";
                } else if (literal2.equals("/") && parts2.size() == 1 && queryParams.isEmpty() && fragmentParams.isEmpty()) {
                    literal2 = "";
                }
                parts1Last = new UriTemplateParser.Literal(literal1.concat(literal2));
                parts2First = null;
            }
        }
        concat.add(parts1Last);
        if (parts2First != null) {
            concat.add(parts2First);
        }
        concat.addAll(parts2.subList(1, parts2.size()));
        concat.addAll(fragmentParams);
        concat.addAll(queryParams);
        return concat;
    }

    private static void removeQueryParams(List parts1, List params) {
        for (Iterator iterator = parts1.iterator(); iterator.hasNext(); ) {
            UriTemplateParser.Part part = iterator.next();
            if (part instanceof UriTemplateParser.Expression expression && expression.type().isQueryPart()) {
                params.add(expression);
                iterator.remove();
            }
        }
    }

    private static void removeFragmentParams(List parts1, List params) {
        for (Iterator iterator = parts1.iterator(); iterator.hasNext(); ) {
            UriTemplateParser.Part part = iterator.next();
            if (part instanceof UriTemplateParser.Expression expression && expression.type() == UriTemplateParser.ExpressionType.FRAGMENT_EXPANSION) {
                params.add(expression);
                iterator.remove();
            }
        }
    }

    private static boolean isAllowedCharacter(char c) {
        return switch (c) {
            case '<', '>', '\\', '^', '`', '{', '|', '}' -> false;
            default -> true;
        };
    }

    private static Expression parseExpression(char[] input, int fromIndex, int toIndex) {
        List variables = new ArrayList<>(2);
        ExpressionType expressionType = parseOperator(input[fromIndex]);
        int variableNameStartIndex = expressionType == ExpressionType.NONE ? fromIndex : fromIndex + 1;
        int variableNameEndIndex = -1;
        int variableModifierStartIndex = -1;
        int variableModifierEndIndex = -1;
        for (int i = variableNameStartIndex; i < toIndex; i++) {
            char c = input[i];
            if (c == ',') {
                if (variableModifierStartIndex != -1) {
                    variableModifierEndIndex = i;
                } else {
                    variableNameEndIndex = i;
                }
                variables.add(createVariable(
                    input,
                    variableNameStartIndex,
                    variableNameEndIndex,
                    variableModifierStartIndex,
                    variableModifierEndIndex
                ));
                variableNameStartIndex = i + 1;
                variableNameEndIndex = -1;
                variableModifierStartIndex = -1;
                variableModifierEndIndex = -1;
            } else if (c == ':') {
                variableNameEndIndex = i;
                variableModifierStartIndex = i + 1;
            } else {
                if (variableModifierStartIndex != -1) {
                    variableModifierEndIndex = i;
                } else {
                    variableNameEndIndex = i;
                }
            }
        }
        if (variableModifierStartIndex != -1) {
            variableModifierEndIndex = toIndex;
        } else {
            variableNameEndIndex = toIndex;
        }
        variables.add(createVariable(
            input,
            variableNameStartIndex,
            variableNameEndIndex,
            variableModifierStartIndex,
            variableModifierEndIndex
        ));
        return new Expression(expressionType, variables);
    }

    private static Variable createVariable(char[] input,
                                           int variableNameStartIndex,
                                           int variableNameEndIndex,
                                           int variableModifierStartIndex,
                                           int variableModifierEndIndex) {
        boolean explore = false;
        if (input[variableNameEndIndex - 1] == '*') {
            explore = true;
            variableNameEndIndex--;
        }
        String modifier = null;
        if (variableModifierStartIndex != -1) {
            modifier = new String(input, variableModifierStartIndex, variableModifierEndIndex - variableModifierStartIndex);
        }
        return new Variable(
            new String(input, variableNameStartIndex, variableNameEndIndex - variableNameStartIndex),
            modifier,
            explore
        );
    }

    private static ExpressionType parseOperator(char c) {
        return switch (c) {
            case '+' -> ExpressionType.RESERVED_EXPANSION;
            case '#' -> ExpressionType.FRAGMENT_EXPANSION;
            case '.' -> ExpressionType.LABEL_EXPANSION;
            case '/' -> ExpressionType.PATH_SEGMENT_EXPANSION;
            case ';' -> ExpressionType.PATH_STYLE_PARAMETER_EXPANSION;
            case '?' -> ExpressionType.FORM_STYLE_PARAMETER_EXPANSION;
            case '&' -> ExpressionType.FORM_STYLE_QUERY_CONTINUATION;
            default -> ExpressionType.NONE;
        };
    }

    /**
     * The variable.
     *
     * @param name     The name
     * @param modifier The modifier
     * @param explode  Is exploded
     */
    public record Variable(String name, String modifier, boolean explode) {
    }

    /**
     * The expression part.
     *
     * @param type      The type
     * @param variables The variables
     */
    public record Expression(ExpressionType type, List variables) implements Part {
        @Override
        public void visit(PartVisitor visitor) {
            visitor.visitExpression(type, variables);
        }
    }

    /**
     * The literal part.
     *
     * @param text The text
     */
    public record Literal(String text) implements Part {
        @Override
        public void visit(PartVisitor visitor) {
            visitor.visitLiteral(text);
        }
    }

    /**
     * The interface representing a template part.
     */
    public interface Part {

        /**
         * Visit parts using a visitor.
         *
         * @param visitor The visitor
         */
        void visit(PartVisitor visitor);

    }

    /**
     * The parts visitor.
     */
    public interface PartVisitor {

        /**
         * Visits a literal.
         *
         * @param literal The literal value
         */
        void visitLiteral(String literal);

        /**
         * Visits and expression.
         *
         * @param type      The type
         * @param variables The variables
         */
        void visitExpression(ExpressionType type, List variables);

    }

    /**
     * The expression type.
     */
    public enum ExpressionType {
        NONE('0', ',', false, true),
        RESERVED_EXPANSION('+', ',', false, false),
        FRAGMENT_EXPANSION('#', ',', false, false),
        LABEL_EXPANSION('.', '.', false, true),
        PATH_SEGMENT_EXPANSION('/', '/', false, true),
        PATH_STYLE_PARAMETER_EXPANSION(';', ';', true, true),
        FORM_STYLE_PARAMETER_EXPANSION('?', '&', true, true),
        FORM_STYLE_QUERY_CONTINUATION('&', '&', true, true);

        private final char separator;
        private final char operator;
        private final boolean isQueryPart;
        private final boolean encode;

        ExpressionType(char operator, char separator, boolean isQueryPart, boolean encode) {
            this.separator = separator;
            this.operator = operator;
            this.isQueryPart = isQueryPart;
            this.encode = encode;
        }

        /**
         * @return Teh query part
         */
        public boolean isQueryPart() {
            return isQueryPart;
        }

        /**
         * @return The operator
         */
        public char getOperator() {
            return operator;
        }

        /**
         * @return The separator
         */
        public char getSeparator() {
            return separator;
        }

        /**
         * @return Is encoded
         */
        public boolean isEncode() {
            return encode;
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy