/*
 * Copyright 2017-2020 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.beans.BeanMap;
import io.micronaut.core.reflect.ClassUtils;
import io.micronaut.core.util.StringUtils;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

A Fast Implementation of URI Template specification. See https://tools.ietf.org/html/rfc6570 and https://medialize.github.io/URI.js/uri-template.html.

Note: this class has a natural ordering that is inconsistent with equals.

Author:Graeme Rocher
Since:1.0
/** * <p>A Fast Implementation of URI Template specification. See https://tools.ietf.org/html/rfc6570 and * https://medialize.github.io/URI.js/uri-template.html.</p> * <p> * <p>Note: this class has a natural ordering that is inconsistent with equals.</p> * * @author Graeme Rocher * @since 1.0 */
public class UriTemplate implements Comparable<UriTemplate> { private static final String STRING_PATTERN_SCHEME = "([^:/?#]+):"; private static final String STRING_PATTERN_USER_INFO = "([^@\\[/?#]*)"; private static final String STRING_PATTERN_HOST_IPV4 = "[^\\[{/?#:]*"; private static final String STRING_PATTERN_HOST_IPV6 = "\\[[\\p{XDigit}\\:\\.]*[%\\p{Alnum}]*\\]"; private static final String STRING_PATTERN_HOST = "(" + STRING_PATTERN_HOST_IPV6 + "|" + STRING_PATTERN_HOST_IPV4 + ")"; private static final String STRING_PATTERN_PORT = "(\\d*(?:\\{[^/]+?\\})?)"; private static final String STRING_PATTERN_PATH = "([^#]*)"; private static final String STRING_PATTERN_QUERY = "([^#]*)"; private static final String STRING_PATTERN_REMAINING = "(.*)"; private static final char QUERY_OPERATOR = '?'; private static final char SLASH_OPERATOR = '/'; private static final char HASH_OPERATOR = '#'; private static final char EXPAND_MODIFIER = '*'; private static final char OPERATOR_NONE = '0'; private static final char VAR_START = '{'; private static final char VAR_END = '}'; private static final char AND_OPERATOR = '&'; private static final String SLASH_STRING = "/"; private static final char DOT_OPERATOR = '.'; // Regex patterns that matches URIs. See RFC 3986, appendix B static final Pattern PATTERN_SCHEME = Pattern.compile("^" + STRING_PATTERN_SCHEME + "//.*"); static final Pattern PATTERN_FULL_PATH = Pattern.compile("^([^#\\?]*)(\\?([^#]*))?(\\#(.*))?$"); static final Pattern PATTERN_FULL_URI = Pattern.compile( "^(" + STRING_PATTERN_SCHEME + ")?" + "(//(" + STRING_PATTERN_USER_INFO + "@)?" + STRING_PATTERN_HOST + "(:" + STRING_PATTERN_PORT + ")?" + ")?" + STRING_PATTERN_PATH + "(\\?" + STRING_PATTERN_QUERY + ")?" + "(#" + STRING_PATTERN_REMAINING + ")?"); private final String templateString; final List<PathSegment> segments = new ArrayList<>();
Construct a new URI template for the given template.
Params:
  • templateString – The template string
/** * Construct a new URI template for the given template. * * @param templateString The template string */
public UriTemplate(CharSequence templateString) { this(templateString, new Object[0]); }
Construct a new URI template for the given template.
Params:
  • templateString – The template string
  • parserArguments – The parsed arguments
/** * Construct a new URI template for the given template. * * @param templateString The template string * @param parserArguments The parsed arguments */
@SuppressWarnings("MagicNumber") protected UriTemplate(CharSequence templateString, Object... parserArguments) { if (templateString == null) { throw new IllegalArgumentException("Argument [templateString] should not be null"); } String templateAsString = templateString.toString(); if (templateAsString.endsWith(SLASH_STRING)) { int len = templateAsString.length(); if (len > 1) { templateAsString = templateAsString.substring(0, len - 1); } } if (PATTERN_SCHEME.matcher(templateAsString).matches()) { Matcher matcher = PATTERN_FULL_URI.matcher(templateAsString); if (matcher.find()) { this.templateString = templateAsString; String scheme = matcher.group(2); if (scheme != null) { segments.add(new UriTemplateParser.RawPathSegment(false, scheme + "://")); } String userInfo = matcher.group(5); String host = matcher.group(6); String port = matcher.group(8); String path = matcher.group(9); String query = matcher.group(11); String fragment = matcher.group(13); if (userInfo != null) { createParser(userInfo, parserArguments).parse(segments); } if (host != null) { createParser(host, parserArguments).parse(segments); } if (port != null) { createParser(':' + port, parserArguments).parse(segments); } if (path != null) { if (fragment != null) { createParser(path + HASH_OPERATOR + fragment).parse(segments); } else { createParser(path, parserArguments).parse(segments); } } if (query != null) { createParser(query, parserArguments).parse(segments); } } else { throw new IllegalArgumentException("Invalid URI template: " + templateString); } } else { this.templateString = templateAsString; createParser(this.templateString, parserArguments).parse(segments); } }
Params:
  • templateString – The template
  • segments – The list of segments
/** * @param templateString The template * @param segments The list of segments */
protected UriTemplate(String templateString, List<PathSegment> segments) { this.templateString = templateString; this.segments.addAll(segments); }
Returns:The number of segments that are variable
/** * @return The number of segments that are variable */
public long getVariableSegmentCount() { return segments.stream().filter(PathSegment::isVariable).count(); }
Returns:The number of path segments that are variable
/** * @return The number of path segments that are variable */
public long getPathVariableSegmentCount() { return segments.stream().filter(PathSegment::isVariable).filter(s -> !s.isQuerySegment()).count(); }
Returns:The number of segments that are raw
/** * @return The number of segments that are raw */
public long getRawSegmentCount() { return segments.stream().filter(segment -> !segment.isVariable()).count(); }
Returns:The number of segments that are raw
/** * @return The number of segments that are raw */
public int getRawSegmentLength() { return segments.stream() .filter(segment -> !segment.isVariable()) .map(CharSequence::length) .reduce(Integer::sum) .orElse(0); }
Nests another URI template with this template.
Params:
  • uriTemplate – The URI template. If it does not begin with forward slash it will automatically be appended with forward slash
Returns:The new URI template
/** * Nests another URI template with this template. * * @param uriTemplate The URI template. If it does not begin with forward slash it will automatically be appended with forward slash * @return The new URI template */
public UriTemplate nest(CharSequence uriTemplate) { return nest(uriTemplate, new Object[0]); }
Expand the string with the given parameters.
Params:
  • parameters – The parameters
Returns:The expanded URI
/** * Expand the string with the given parameters. * * @param parameters The parameters * @return The expanded URI */
public String expand(Map<String, Object> parameters) { StringBuilder builder = new StringBuilder(templateString.length()); boolean anyPreviousHasContent = false; boolean anyPreviousHasOperator = false; boolean queryParameter = false; for (PathSegment segment : segments) { String result = segment.expand(parameters, anyPreviousHasContent, anyPreviousHasOperator); if (result == null) { continue; } if (segment instanceof UriTemplateParser.VariablePathSegment) { UriTemplateParser.VariablePathSegment varPathSegment = (UriTemplateParser.VariablePathSegment) segment; if (varPathSegment.isQuerySegment && ! queryParameter) { // reset anyPrevious* when we reach query parameters queryParameter = true; anyPreviousHasContent = false; anyPreviousHasOperator = false; } final char operator = varPathSegment.getOperator(); if (operator != OPERATOR_NONE && result.contains(String.valueOf(operator))) { anyPreviousHasOperator = true; } anyPreviousHasContent = anyPreviousHasContent || result.length() > 0; } builder.append(result); } return builder.toString(); }
Expand the string with the given bean.
Params:
  • bean – The bean
Returns:The expanded URI
/** * Expand the string with the given bean. * * @param bean The bean * @return The expanded URI */
public String expand(Object bean) { return expand(BeanMap.of(bean)); } @Override public String toString() { return toString(pathSegment -> true); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } UriTemplate that = (UriTemplate) o; return templateString.equals(that.templateString); } @Override public int hashCode() { return templateString.hashCode(); } @Override public int compareTo(UriTemplate o) { if (this == o) { return 0; } Integer thisVariableCount = 0; Integer thatVariableCount = 0; Integer thisRawLength = 0; Integer thatRawLength = 0; for (PathSegment segment: this.segments) { if (segment.isVariable()) { if (!segment.isQuerySegment()) { thisVariableCount++; } } else { thisRawLength += segment.length(); } } for (PathSegment segment: o.segments) { if (segment.isVariable()) { if (!segment.isQuerySegment()) { thatVariableCount++; } } else { thatRawLength += segment.length(); } } //using that.compareTo because more raw length should have higher precedence int rawCompare = thatRawLength.compareTo(thisRawLength); if (rawCompare == 0) { return thisVariableCount.compareTo(thatVariableCount); } else { return rawCompare; } }
Create a new UriTemplate for the given URI.
Params:
  • uri – The URI
Returns:The template
/** * Create a new {@link UriTemplate} for the given URI. * * @param uri The URI * @return The template */
public static UriTemplate of(String uri) { return new UriTemplate(uri); }
Nests another URI template with this template.
Params:
  • uriTemplate – The URI template. If it does not begin with forward slash it will automatically be appended with forward slash
  • parserArguments – The parsed arguments
Returns:The new URI template
/** * Nests another URI template with this template. * * @param uriTemplate The URI template. If it does not begin with forward slash it will automatically be * appended with forward slash * @param parserArguments The parsed arguments * @return The new URI template */
protected UriTemplate nest(CharSequence uriTemplate, Object... parserArguments) { if (uriTemplate == null) { return this; } int len = uriTemplate.length(); if (len == 0) { return this; } List<PathSegment> newSegments = buildNestedSegments(uriTemplate, len, parserArguments); return newUriTemplate(uriTemplate, newSegments); }
Params:
  • uriTemplate – The URI template
  • newSegments – The new segments
Returns:The new UriTemplate
/** * @param uriTemplate The URI template * @param newSegments The new segments * @return The new {@link UriTemplate} */
protected UriTemplate newUriTemplate(CharSequence uriTemplate, List<PathSegment> newSegments) { return new UriTemplate(normalizeNested(this.templateString, uriTemplate), newSegments); }
Normalize a nested URI.
Params:
  • uri – The URI
  • nested – The nested URI
Returns:The new URI
/** * Normalize a nested URI. * @param uri The URI * @param nested The nested URI * @return The new URI */
protected String normalizeNested(String uri, CharSequence nested) { if (StringUtils.isEmpty(nested)) { return uri; } String nestedStr = nested.toString(); char firstNested = nestedStr.charAt(0); int len = nestedStr.length(); if (len == 1 && firstNested == SLASH_OPERATOR) { return uri; } switch (firstNested) { case VAR_START: if (len > 1) { switch (nested.charAt(1)) { case SLASH_OPERATOR: case HASH_OPERATOR: case QUERY_OPERATOR: case AND_OPERATOR: if (uri.endsWith(SLASH_STRING)) { return uri.substring(0, uri.length() - 1) + nestedStr; } else { return uri + nestedStr; } default: if (!uri.endsWith(SLASH_STRING)) { return uri + SLASH_STRING + nestedStr; } else { return uri + nestedStr; } } } else { return uri; } case SLASH_OPERATOR: if (uri.endsWith(SLASH_STRING)) { return uri + nestedStr.substring(1); } else { return uri + nestedStr; } default: if (uri.endsWith(SLASH_STRING)) { return uri + nestedStr; } else { return uri + SLASH_STRING + nestedStr; } } }
Params:
  • uriTemplate – The URI template
  • len – The lenght
  • parserArguments – The parsed arguments
Returns:A list of path segments
/** * @param uriTemplate The URI template * @param len The lenght * @param parserArguments The parsed arguments * @return A list of path segments */
protected List<PathSegment> buildNestedSegments(CharSequence uriTemplate, int len, Object... parserArguments) { List<PathSegment> newSegments = new ArrayList<>(); List<PathSegment> querySegments = new ArrayList<>(); for (PathSegment segment : segments) { if (!segment.isQuerySegment()) { newSegments.add(segment); } else { querySegments.add(segment); } } String templateString = uriTemplate.toString(); if (shouldPrependSlash(templateString, len)) { templateString = SLASH_OPERATOR + templateString; } else if (!segments.isEmpty() && templateString.startsWith(SLASH_STRING)) { if (len == 1 && uriTemplate.charAt(0) == SLASH_OPERATOR) { templateString = ""; } else { PathSegment last = segments.get(segments.size() - 1); if (last instanceof UriTemplateParser.RawPathSegment) { String v = ((UriTemplateParser.RawPathSegment) last).value; if (v.endsWith(SLASH_STRING)) { templateString = templateString.substring(1); } else { templateString = normalizeNested(SLASH_STRING, templateString.substring(1)); } } } } createParser(templateString, parserArguments).parse(newSegments); newSegments.addAll(querySegments); return newSegments; }
Creates a parser.
Params:
  • templateString – The template
  • parserArguments – The parsed arguments
Returns:The created parser
/** * Creates a parser. * * @param templateString The template * @param parserArguments The parsed arguments * @return The created parser */
protected UriTemplateParser createParser(String templateString, Object... parserArguments) { return new UriTemplateParser(templateString); }
Returns the template as a string filtering the segments with the provided filter.
Params:
  • filter – The filter to test segments
Returns:The template as a string
/** * Returns the template as a string filtering the segments * with the provided filter. * * @param filter The filter to test segments * @return The template as a string */
protected String toString(Predicate<PathSegment> filter) { StringBuilder builder = new StringBuilder(templateString.length()); UriTemplateParser.VariablePathSegment previousVariable = null; for (PathSegment segment : segments) { if (!filter.test(segment)) { continue; } boolean isVar = segment instanceof UriTemplateParser.VariablePathSegment; if (previousVariable != null && isVar) { UriTemplateParser.VariablePathSegment varSeg = (UriTemplateParser.VariablePathSegment) segment; if (varSeg.operator == previousVariable.operator && varSeg.modifierChar != EXPAND_MODIFIER) { builder.append(varSeg.delimiter); } else { builder.append(VAR_END); builder.append(VAR_START); char op = varSeg.operator; if (OPERATOR_NONE != op) { builder.append(op); } } builder.append(segment.toString()); previousVariable = varSeg; } else { if (isVar) { previousVariable = (UriTemplateParser.VariablePathSegment) segment; builder.append(VAR_START); char op = previousVariable.operator; if (OPERATOR_NONE != op) { builder.append(op); } builder.append(segment.toString()); } else { if (previousVariable != null) { builder.append(VAR_END); previousVariable = null; } builder.append(segment.toString()); } } } if (previousVariable != null) { builder.append(VAR_END); } return builder.toString(); } private boolean shouldPrependSlash(String templateString, int len) { String parentString = this.templateString; int parentLen = parentString.length(); return (parentLen > 0 && parentString.charAt(parentLen - 1) != SLASH_OPERATOR) && templateString.charAt(0) != SLASH_OPERATOR && isAdditionalPathVar(templateString, len); } private boolean isAdditionalPathVar(String templateString, int len) { if (len > 1) { boolean isVar = templateString.charAt(0) == VAR_START; if (isVar) { switch (templateString.charAt(1)) { case SLASH_OPERATOR: case QUERY_OPERATOR: case HASH_OPERATOR: return false; default: return true; } } } return templateString.charAt(0) != SLASH_OPERATOR; }
Represents an expandable path segment.
/** * Represents an expandable path segment. */
protected interface PathSegment extends CharSequence {
Returns:Whether this segment is part of the query string
/** * @return Whether this segment is part of the query string */
default boolean isQuerySegment() { return false; }
If this path segment represents a variable returns the underlying variable name.
Returns:The variable name if present
/** * If this path segment represents a variable returns the underlying variable name. * * @return The variable name if present */
default Optional<String> getVariable() { return Optional.empty(); }
Returns:True if this is a variable segment
/** * @return True if this is a variable segment */
default boolean isVariable() { return getVariable().isPresent(); }
Expands the query segment.
Params:
  • parameters – The parameters
  • previousHasContent – Whether there was previous content
  • anyPreviousHasOperator – Whether an operator is present
Returns:The expanded string
/** * Expands the query segment. * * @param parameters The parameters * @param previousHasContent Whether there was previous content * @param anyPreviousHasOperator Whether an operator is present * @return The expanded string */
String expand(Map<String, Object> parameters, boolean previousHasContent, boolean anyPreviousHasOperator); }
An URI template parser.
/** * An URI template parser. */
protected static class UriTemplateParser { private static final int STATE_TEXT = 0; // raw text private static final int STATE_VAR_START = 1; // the start of a URI variable ie. { private static final int STATE_VAR_CONTENT = 2; // within a URI variable. ie. {var} private static final int STATE_VAR_NEXT = 11; // within the next variable in a URI variable declaration ie. {var, var2} private static final int STATE_VAR_MODIFIER = 12; // within a variable modifier ie. {var:1} private static final int STATE_VAR_NEXT_MODIFIER = 13; // within a variable modifier of a next variable ie. {var, var2:1} String templateText; private int state = STATE_TEXT; private char operator = OPERATOR_NONE; // zero means no operator private char modifier = OPERATOR_NONE; // zero means no modifier private String varDelimiter; private boolean isQuerySegment = false;
Params:
  • templateText – The template
/** * @param templateText The template */
UriTemplateParser(String templateText) { this.templateText = templateText; }
Parse a list of segments.
Params:
  • segments – The list of segments
/** * Parse a list of segments. * * @param segments The list of segments */
protected void parse(List<PathSegment> segments) { char[] chars = templateText.toCharArray(); StringBuilder buff = new StringBuilder(); StringBuilder modBuff = new StringBuilder(); int varCount = 0; for (char c : chars) { switch (state) { case STATE_TEXT: if (c == VAR_START) { if (buff.length() > 0) { String val = buff.toString(); addRawContentSegment(segments, val, isQuerySegment); } buff.delete(0, buff.length()); state = STATE_VAR_START; continue; } else { if (c == QUERY_OPERATOR || c == HASH_OPERATOR) { isQuerySegment = true; } buff.append(c); continue; } case STATE_VAR_MODIFIER: case STATE_VAR_NEXT_MODIFIER: if (c == ' ') { continue; } case STATE_VAR_NEXT: case STATE_VAR_CONTENT: switch (c) { case ':': case EXPAND_MODIFIER: // arrived to expansion modifier if (state == STATE_VAR_MODIFIER || state == STATE_VAR_NEXT_MODIFIER) { modBuff.append(c); continue; } modifier = c; state = state == STATE_VAR_NEXT ? STATE_VAR_NEXT_MODIFIER : STATE_VAR_MODIFIER; continue; case ',': // arrived to new variable state = STATE_VAR_NEXT; case VAR_END: // arrived to variable end if (buff.length() > 0) { String val = buff.toString(); final String prefix; final String delimiter; final boolean encode; final boolean repeatPrefix; switch (operator) { case '+': encode = false; prefix = null; delimiter = ","; repeatPrefix = varCount < 1; break; case HASH_OPERATOR: encode = false; repeatPrefix = varCount < 1; prefix = String.valueOf(operator); delimiter = ","; break; case DOT_OPERATOR: case SLASH_OPERATOR: encode = true; repeatPrefix = varCount < 1; prefix = String.valueOf(operator); delimiter = modifier == EXPAND_MODIFIER ? prefix : ","; break; case ';': encode = true; repeatPrefix = true; prefix = operator + val + '='; delimiter = modifier == EXPAND_MODIFIER ? prefix : ","; break; case QUERY_OPERATOR: case AND_OPERATOR: encode = true; repeatPrefix = true; prefix = varCount < 1 ? operator + val + '=' : val + "="; delimiter = modifier == EXPAND_MODIFIER ? AND_OPERATOR + val + '=' : ","; break; default: repeatPrefix = varCount < 1; encode = true; prefix = null; delimiter = ","; } String modifierStr = modBuff.toString(); char modifierChar = modifier; String previous = state == STATE_VAR_NEXT || state == STATE_VAR_NEXT_MODIFIER ? this.varDelimiter : null; addVariableSegment(segments, val, prefix, delimiter, encode, repeatPrefix, modifierStr, modifierChar, operator, previous, isQuerySegment); } boolean hasAnotherVar = state == STATE_VAR_NEXT && c != VAR_END; if (hasAnotherVar) { String delimiter; switch (operator) { case ';': delimiter = null; break; case QUERY_OPERATOR: case AND_OPERATOR: delimiter = "&"; break; case DOT_OPERATOR: case SLASH_OPERATOR: delimiter = String.valueOf(operator); break; default: delimiter = ","; } varDelimiter = delimiter; varCount++; } else { varCount = 0; } state = hasAnotherVar ? STATE_VAR_NEXT : STATE_TEXT; modBuff.delete(0, modBuff.length()); buff.delete(0, buff.length()); modifier = OPERATOR_NONE; if (!hasAnotherVar) { operator = OPERATOR_NONE; } continue; default: switch (modifier) { case EXPAND_MODIFIER: throw new IllegalStateException("Expansion modifier * must be immediately followed by a closing brace '}'"); case ':': modBuff.append(c); continue; default: buff.append(c); continue; } } case STATE_VAR_START: switch (c) { case ' ': continue; case ';': case QUERY_OPERATOR: case AND_OPERATOR: case HASH_OPERATOR: isQuerySegment = true; case '+': case DOT_OPERATOR: case SLASH_OPERATOR: operator = c; state = STATE_VAR_CONTENT; continue; default: state = STATE_VAR_CONTENT; buff.append(c); } default: // no-op } } if (state == STATE_TEXT && buff.length() > 0) { String val = buff.toString(); addRawContentSegment(segments, val, isQuerySegment); } }
Adds a raw content segment.
Params:
  • segments – The segments
  • value – The value
  • isQuerySegment – Whether is a query segment
/** * Adds a raw content segment. * * @param segments The segments * @param value The value * @param isQuerySegment Whether is a query segment */
protected void addRawContentSegment(List<PathSegment> segments, String value, boolean isQuerySegment) { segments.add(new RawPathSegment(isQuerySegment, value)); }
Adds a new variable segment.
Params:
  • segments – The segments to augment
  • variable – The variable
  • prefix – The prefix to use when expanding the variable
  • delimiter – The delimiter to use when expanding the variable
  • encode – Whether to URL encode the variable
  • repeatPrefix – Whether to repeat the prefix for each expanded variable
  • modifierStr – The modifier string
  • modifierChar – The modifier as char
  • operator – The currently active operator
  • previousDelimiter – The delimiter to use if a variable appeared before this variable
  • isQuerySegment – Whether is a query segment
/** * Adds a new variable segment. * * @param segments The segments to augment * @param variable The variable * @param prefix The prefix to use when expanding the variable * @param delimiter The delimiter to use when expanding the variable * @param encode Whether to URL encode the variable * @param repeatPrefix Whether to repeat the prefix for each expanded variable * @param modifierStr The modifier string * @param modifierChar The modifier as char * @param operator The currently active operator * @param previousDelimiter The delimiter to use if a variable appeared before this variable * @param isQuerySegment Whether is a query segment */
protected void addVariableSegment(List<PathSegment> segments, String variable, String prefix, String delimiter, boolean encode, boolean repeatPrefix, String modifierStr, char modifierChar, char operator, String previousDelimiter, boolean isQuerySegment) { segments.add(new VariablePathSegment(isQuerySegment, variable, prefix, delimiter, encode, modifierChar, operator, modifierStr, previousDelimiter, repeatPrefix)); }
Raw path segment implementation.
/** * Raw path segment implementation. */
private static class RawPathSegment implements PathSegment { private final boolean isQuerySegment; private final String value; public RawPathSegment(boolean isQuerySegment, String value) { this.isQuerySegment = isQuerySegment; this.value = value; } @Override public boolean isQuerySegment() { return isQuerySegment; } @Override public String expand(Map<String, Object> parameters, boolean previousHasContent, boolean anyPreviousHasOperator) { return value; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } RawPathSegment that = (RawPathSegment) o; if (isQuerySegment != that.isQuerySegment) { return false; } return value != null ? value.equals(that.value) : that.value == null; } @Override public int hashCode() { int result = (isQuerySegment ? 1 : 0); result = 31 * result + (value != null ? value.hashCode() : 0); return result; } @Override public int length() { return value.length(); } @Override public char charAt(int index) { return value.charAt(index); } @Override public CharSequence subSequence(int start, int end) { return value.subSequence(start, end); } @Override public String toString() { return value; } }
Variable path segment implementation.
/** * Variable path segment implementation. */
private class VariablePathSegment implements PathSegment { private final boolean isQuerySegment; private final String variable; private final String prefix; private final String delimiter; private final boolean encode; private final char modifierChar; private final char operator; private final String modifierStr; private final String previousDelimiter; private final boolean repeatPrefix; public VariablePathSegment(boolean isQuerySegment, String variable, String prefix, String delimiter, boolean encode, char modifierChar, char operator, String modifierStr, String previousDelimiter, boolean repeatPrefix) { this.isQuerySegment = isQuerySegment; this.variable = variable; this.prefix = prefix; this.delimiter = delimiter; this.encode = encode; this.modifierChar = modifierChar; this.operator = operator; this.modifierStr = modifierStr; this.previousDelimiter = previousDelimiter; this.repeatPrefix = repeatPrefix; } @Override public Optional<String> getVariable() { return Optional.of(variable); } public char getOperator() { return this.operator; } @Override public boolean isQuerySegment() { return isQuerySegment; } @Override public int length() { return toString().length(); } @Override public char charAt(int index) { return toString().charAt(index); } @Override public CharSequence subSequence(int start, int end) { return toString().subSequence(start, end); } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(variable); if (modifierChar != OPERATOR_NONE) { builder.append(modifierChar); if (null != modifierStr) { builder.append(modifierStr); } } return builder.toString(); } private String escape(String v) { return v.replace("%", "%25").replaceAll("\\s", "%20"); } @Override public String expand(Map<String, Object> parameters, boolean previousHasContent, boolean anyPreviousHasOperator) { Object found = parameters.get(variable); boolean isOptional = found instanceof Optional; if (found != null && !(isOptional && !((Optional) found).isPresent())) { if (isOptional) { found = ((Optional) found).get(); } String prefixToUse = prefix; if (operator == QUERY_OPERATOR && !anyPreviousHasOperator && prefix != null && !prefix.startsWith(String.valueOf(operator))) { prefixToUse = operator + prefix; } String result; if (found.getClass().isArray()) { found = Arrays.asList((Object[]) found); } boolean isQuery = operator == QUERY_OPERATOR; if (modifierChar == EXPAND_MODIFIER) { found = expandPOJO(found); // Turn POJO into a Map } if (found instanceof Iterable) { Iterable iter = ((Iterable) found); if (iter instanceof Collection && ((Collection) iter).isEmpty()) { return ""; } StringJoiner joiner = new StringJoiner(delimiter); for (Object o : iter) { if (o != null) { String v = o.toString(); joiner.add(encode ? encode(v, isQuery) : escape(v)); } } result = joiner.toString(); } else if (found instanceof Map) { Map<Object, Object> map = (Map<Object, Object>) found; map.values().removeIf(Objects::isNull); if (map.isEmpty()) { return ""; } final StringJoiner joiner; if (modifierChar == EXPAND_MODIFIER) { switch (operator) { case AND_OPERATOR: case QUERY_OPERATOR: prefixToUse = String.valueOf(anyPreviousHasOperator ? AND_OPERATOR : operator); joiner = new StringJoiner(String.valueOf(AND_OPERATOR)); break; case ';': prefixToUse = String.valueOf(operator); joiner = new StringJoiner(String.valueOf(prefixToUse)); break; default: joiner = new StringJoiner(delimiter); } } else { joiner = new StringJoiner(delimiter); } map.forEach((key, some) -> { String ks = key.toString(); Iterable<?> values = (some instanceof Iterable) ? (Iterable) some : Collections.singletonList(some); for (Object value: values) { if (value == null) { continue; } String vs = value.toString(); String ek = encode ? encode(ks, isQuery) : escape(ks); String ev = encode ? encode(vs, isQuery) : escape(vs); if (modifierChar == EXPAND_MODIFIER) { String finalValue = ek + '=' + ev; joiner.add(finalValue); } else { joiner.add(ek); joiner.add(ev); } } }); result = joiner.toString(); } else { String str = found.toString(); str = applyModifier(modifierStr, modifierChar, str, str.length()); result = encode ? encode(str, isQuery) : escape(str); } int len = result.length(); StringBuilder finalResult = new StringBuilder(previousHasContent && previousDelimiter != null ? previousDelimiter : ""); if (len == 0) { switch (operator) { case SLASH_OPERATOR: break; case ';': if (prefixToUse != null && prefixToUse.endsWith("=")) { finalResult.append(prefixToUse.substring(0, prefixToUse.length() - 1)).append(result); break; } default: if (prefixToUse != null) { finalResult.append(prefixToUse).append(result); } else { finalResult.append(result); } } } else if (prefixToUse != null && repeatPrefix) { finalResult.append(prefixToUse).append(result); } else { finalResult.append(result); } return finalResult.toString(); } else { switch (operator) { case SLASH_OPERATOR: return null; default: return ""; } } } private String applyModifier(String modifierStr, char modifierChar, String result, int len) { if (modifierChar == ':' && modifierStr.length() > 0 && Character.isDigit(modifierStr.charAt(0))) { try { int subResult = Integer.parseInt(modifierStr.trim(), 10); if (subResult < len) { result = result.substring(0, subResult); } } catch (NumberFormatException e) { result = ":" + modifierStr; } } return result; } private String encode(String str, boolean query) { try { String encoded = URLEncoder.encode(str, "UTF-8"); if (query) { return encoded; } else { return encoded.replace("+", "%20"); } } catch (UnsupportedEncodingException e) { throw new IllegalStateException("No available encoding", e); } } private Object expandPOJO(Object found) { // Check for common expanded types, such as list or Map if (found instanceof Iterable || found instanceof Map) { return found; } // If a simple value, just use that if (found == null || ClassUtils.isJavaLangType(found.getClass())) { return found; } // Otherwise, expand the object into properties (after all, the user asked for an expanded parameter) return BeanMap.of(found); } } } }