From cb031c7cff8152c665db287e2d8e21181928149b Mon Sep 17 00:00:00 2001 From: jansupol Date: Wed, 26 Jul 2023 17:21:40 +0200 Subject: [PATCH] RFC 6570 implementation Reworked UriTemplateParser & UriTemplate to be better extendable and processed just once Signed-off-by: jansupol --- .../glassfish/jersey/uri/UriComponent.java | 4 +- .../org/glassfish/jersey/uri/UriTemplate.java | 194 ++---- .../jersey/uri/internal/TemplateVariable.java | 400 +++++++++++++ .../jersey/uri/internal/UriPart.java | 97 +++ .../uri/internal/UriTemplateParser.java | 562 ++++++++++++++---- .../glassfish/jersey/uri/UriTemplateTest.java | 306 +++++++++- .../jersey/linking/ELLinkBuilder.java | 10 +- .../uri/internal/JerseyUriBuilderTest.java | 10 +- 8 files changed, 1317 insertions(+), 266 deletions(-) create mode 100644 core-common/src/main/java/org/glassfish/jersey/uri/internal/TemplateVariable.java create mode 100644 core-common/src/main/java/org/glassfish/jersey/uri/internal/UriPart.java diff --git a/core-common/src/main/java/org/glassfish/jersey/uri/UriComponent.java b/core-common/src/main/java/org/glassfish/jersey/uri/UriComponent.java index 4c846f36d3..3f051615b1 100644 --- a/core-common/src/main/java/org/glassfish/jersey/uri/UriComponent.java +++ b/core-common/src/main/java/org/glassfish/jersey/uri/UriComponent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -404,7 +404,7 @@ private static boolean[][] initEncodingTables() { tables[Type.QUERY_PARAM_SPACE_ENCODED.ordinal()] = tables[Type.QUERY_PARAM.ordinal()]; - tables[Type.FRAGMENT.ordinal()] = tables[Type.QUERY.ordinal()]; + tables[Type.FRAGMENT.ordinal()] = tables[Type.PATH.ordinal()]; return tables; } diff --git a/core-common/src/main/java/org/glassfish/jersey/uri/UriTemplate.java b/core-common/src/main/java/org/glassfish/jersey/uri/UriTemplate.java index 1ed213b7c0..bbd1240661 100644 --- a/core-common/src/main/java/org/glassfish/jersey/uri/UriTemplate.java +++ b/core-common/src/main/java/org/glassfish/jersey/uri/UriTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -17,6 +17,7 @@ package org.glassfish.jersey.uri; import java.net.URI; +import java.net.URLEncoder; import java.util.ArrayDeque; import java.util.Collections; import java.util.Comparator; @@ -24,11 +25,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; import org.glassfish.jersey.internal.guava.Preconditions; +import org.glassfish.jersey.uri.internal.UriPart; import org.glassfish.jersey.uri.internal.UriTemplateParser; /** @@ -124,7 +126,7 @@ private static interface TemplateValueStrategy { * @throws java.lang.IllegalArgumentException in case no value has been found and the strategy * does not support {@code null} values. */ - public String valueFor(String templateVariable, String matchedGroup); + public String valueFor(UriPart templateVariable, String matchedGroup); } /** @@ -156,7 +158,12 @@ private static interface TemplateValueStrategy { /** * The template variables in the URI template. */ - private final List templateVariables; + private final List templateVariables; + + /** + * Get all UriParts, not only the variables + */ + private final List uriParts; /** * The number of explicit regular expressions declared for template * variables. @@ -182,6 +189,7 @@ private UriTemplate() { this.pattern = PatternWithGroups.EMPTY; this.endsWithSlash = false; this.templateVariables = Collections.emptyList(); + this.uriParts = Collections.emptyList(); this.numOfExplicitRegexes = this.numOfCharacters = this.numOfRegexGroups = 0; } @@ -240,6 +248,8 @@ protected UriTemplate(UriTemplateParser templateParser) throws PatternSyntaxExce this.endsWithSlash = template.charAt(template.length() - 1) == '/'; this.templateVariables = Collections.unmodifiableList(templateParser.getNames()); + + this.uriParts = templateParser.getUriParts(); } /** @@ -426,7 +436,7 @@ public final boolean endsWithSlash() { * @return the list of template variables. */ public final List getTemplateVariables() { - return templateVariables; + return templateVariables.stream().map(UriPart::getPart).collect(Collectors.toList()); } /** @@ -438,8 +448,8 @@ public final List getTemplateVariables() { */ @SuppressWarnings("UnusedDeclaration") public final boolean isTemplateVariablePresent(String name) { - for (String s : templateVariables) { - if (s.equals(name)) { + for (UriPart tv : templateVariables) { + if (tv.getPart().equals(name)) { return true; } } @@ -507,7 +517,7 @@ public final boolean match(CharSequence uri, Map templateVariabl throw new IllegalArgumentException(); } - return pattern.match(uri, templateVariables, templateVariableToValue); + return pattern.match(uri, getTemplateVariables(), templateVariableToValue); } /** @@ -547,10 +557,14 @@ public final boolean match(CharSequence uri, List groupValues) throws */ public final String createURI(final Map values) { final StringBuilder sb = new StringBuilder(); - resolveTemplate(normalizedTemplate, sb, new TemplateValueStrategy() { + resolveTemplate(sb, new TemplateValueStrategy() { @Override - public String valueFor(String templateVariable, String matchedGroup) { - return values.get(templateVariable); + public String valueFor(UriPart templateVariable, String matchedGroup) { + String value = values.get(templateVariable.getPart()); + if (value == null) { + return ""; + } + return templateVariable.resolve(value, null, false); } }); return sb.toString(); @@ -592,16 +606,16 @@ public final String createURI(final String[] values, final int offset, final int private final Map mapValues = new HashMap(); @Override - public String valueFor(String templateVariable, String matchedGroup) { + public String valueFor(UriPart templateVariable, String matchedGroup) { // Check if a template variable has already occurred // If so use the value to ensure that two or more declarations of // a template variable have the same value - String tValue = mapValues.get(templateVariable); + String tValue = mapValues.get(templateVariable.getPart()); if (tValue == null) { if (v < lengthPlusOffset) { tValue = values[v++]; if (tValue != null) { - mapValues.put(templateVariable, tValue); + mapValues.put(templateVariable.getPart(), tValue); } } } @@ -611,84 +625,24 @@ public String valueFor(String templateVariable, String matchedGroup) { }; final StringBuilder sb = new StringBuilder(); - resolveTemplate(normalizedTemplate, sb, ns); + resolveTemplate(sb, ns); return sb.toString(); } /** * Build a URI based on the parameters provided by the variable name strategy. * - * @param normalizedTemplate normalized URI template. A normalized template is a template without any explicit regular - * expressions. * @param builder URI string builder to be used. * @param valueStrategy The template value producer strategy to use. */ - private static void resolveTemplate( - String normalizedTemplate, - StringBuilder builder, - TemplateValueStrategy valueStrategy) { - // Find all template variables - Matcher m = TEMPLATE_NAMES_PATTERN.matcher(normalizedTemplate); - - int i = 0; - while (m.find()) { - builder.append(normalizedTemplate, i, m.start()); - String variableName = m.group(1); - // TODO matrix - char firstChar = variableName.charAt(0); - if (firstChar == '?' || firstChar == ';') { - final char prefix; - final char separator; - final String emptyValueAssignment; - if (firstChar == '?') { - // query - prefix = '?'; - separator = '&'; - emptyValueAssignment = "="; - } else { - // matrix - prefix = ';'; - separator = ';'; - emptyValueAssignment = ""; - } - - int index = builder.length(); - String[] variables = variableName.substring(1).split(", ?"); - for (String variable : variables) { - try { - String value = valueStrategy.valueFor(variable, m.group()); - if (value != null) { - if (index != builder.length()) { - builder.append(separator); - } - - builder.append(variable); - if (value.isEmpty()) { - builder.append(emptyValueAssignment); - } else { - builder.append('='); - builder.append(value); - } - } - } catch (IllegalArgumentException ex) { - // no value found => ignore the variable - } - } - - if (index != builder.length() && (index == 0 || builder.charAt(index - 1) != prefix)) { - builder.insert(index, prefix); - } + private void resolveTemplate(StringBuilder builder, TemplateValueStrategy valueStrategy) { + for (UriPart uriPart : uriParts) { + if (uriPart.isTemplate()) { + builder.append(valueStrategy.valueFor(uriPart, uriPart.getGroup())); } else { - String value = valueStrategy.valueFor(variableName, m.group()); - - if (value != null) { - builder.append(value); - } + builder.append(uriPart.getPart()); } - - i = m.end(); } - builder.append(normalizedTemplate, i, normalizedTemplate.length()); } @Override @@ -756,16 +710,9 @@ public static String createURI( final String path, final String query, final String fragment, final Map values, final boolean encode, final boolean encodeSlashInPath) { - Map stringValues = new HashMap(); - for (Map.Entry e : values.entrySet()) { - if (e.getValue() != null) { - stringValues.put(e.getKey(), e.getValue().toString()); - } - } - - return createURIWithStringValues(scheme, authority, + return createURI(scheme, authority, userInfo, host, port, path, query, fragment, - stringValues, encode, encodeSlashInPath); + new Object[] {}, encode, encodeSlashInPath, values); } /** @@ -800,7 +747,7 @@ public static String createURIWithStringValues( final String path, final String query, final String fragment, final Map values, final boolean encode, final boolean encodeSlashInPath) { - return createURIWithStringValues( + return createURI( scheme, authority, userInfo, host, port, path, query, fragment, EMPTY_VALUES, encode, encodeSlashInPath, values); } @@ -837,17 +784,10 @@ public static String createURI( final String path, final String query, final String fragment, final Object[] values, final boolean encode, final boolean encodeSlashInPath) { - String[] stringValues = new String[values.length]; - for (int i = 0; i < values.length; i++) { - if (values[i] != null) { - stringValues[i] = values[i].toString(); - } - } - - return createURIWithStringValues( + return createURI( scheme, authority, userInfo, host, port, path, query, fragment, - stringValues, encode, encodeSlashInPath); + values, encode, encodeSlashInPath, new HashMap()); } /** @@ -879,13 +819,13 @@ public static String createURIWithStringValues( final String[] values, final boolean encode, final boolean encodeSlashInPath) { final Map mapValues = new HashMap(); - return createURIWithStringValues( + return createURI( scheme, authority, userInfo, host, port, path, query, fragment, values, encode, encodeSlashInPath, mapValues); } - private static String createURIWithStringValues( + private static String createURI( final String scheme, final String authority, final String userInfo, final String host, final String port, - final String path, final String query, final String fragment, final String[] values, final boolean encode, + final String path, final String query, final String fragment, final Object[] values, final boolean encode, final boolean encodeSlashInPath, final Map mapValues) { final StringBuilder sb = new StringBuilder(); @@ -942,9 +882,15 @@ private static String createURIWithStringValues( } if (notEmpty(query)) { - sb.append('?'); + int sbLength = sb.length(); offset = createUriComponent(UriComponent.Type.QUERY_PARAM, query, values, offset, encode, mapValues, sb); + if (sb.length() > sbLength) { + char firstQuery = sb.charAt(sbLength); + if (firstQuery != '?' && firstQuery != '&') { + sb.insert(sbLength, '?'); + } + } } if (notEmpty(fragment)) { @@ -963,7 +909,7 @@ private static boolean notEmpty(String string) { @SuppressWarnings("unchecked") private static int createUriComponent(final UriComponent.Type componentType, String template, - final String[] values, + final Object[] values, final int valueOffset, final boolean encode, final Map _mapValues, @@ -977,33 +923,28 @@ private static int createUriComponent(final UriComponent.Type componentType, } // Find all template variables - template = new UriTemplateParser(template).getNormalizedTemplate(); - + UriTemplateParser templateParser = new UriTemplateParser(template); class ValuesFromArrayStrategy implements TemplateValueStrategy { private int offset = valueOffset; @Override - public String valueFor(String templateVariable, String matchedGroup) { + public String valueFor(UriPart templateVariable, String matchedGroup) { - Object value = mapValues.get(templateVariable); + Object value = mapValues.get(templateVariable.getPart()); if (value == null && offset < values.length) { value = values[offset++]; - mapValues.put(templateVariable, value); + mapValues.put(templateVariable.getPart(), value); } - if (value == null) { + if (value == null && templateVariable.throwWhenNoTemplateArg()) { throw new IllegalArgumentException( - String.format("The template variable '%s' has no value", templateVariable)); - } - if (encode) { - return UriComponent.encode(value.toString(), componentType); - } else { - return UriComponent.contextualEncode(value.toString(), componentType); + String.format("The template variable '%s' has no value", templateVariable.getPart())); } + return templateVariable.resolve(value, componentType, encode); } } ValuesFromArrayStrategy cs = new ValuesFromArrayStrategy(); - resolveTemplate(template, b, cs); + new UriTemplate(templateParser).resolveTemplate(b, cs); return cs.offset; } @@ -1033,25 +974,18 @@ public static String resolveTemplateValues(final UriComponent.Type type, final Map mapValues = (Map) _mapValues; - // Find all template variables - template = new UriTemplateParser(template).getNormalizedTemplate(); - StringBuilder sb = new StringBuilder(); - resolveTemplate(template, sb, new TemplateValueStrategy() { + // Find all template variables + new UriTemplate(new UriTemplateParser(template)).resolveTemplate(sb, new TemplateValueStrategy() { @Override - public String valueFor(String templateVariable, String matchedGroup) { + public String valueFor(UriPart templateVariable, String matchedGroup) { - Object value = mapValues.get(templateVariable); + Object value = mapValues.get(templateVariable.getPart()); if (value != null) { - if (encode) { - value = UriComponent.encode(value.toString(), type); - } else { - value = UriComponent.contextualEncode(value.toString(), type); - } - return value.toString(); + return templateVariable.resolve(value.toString(), type, encode); } else { - if (mapValues.containsKey(templateVariable)) { + if (mapValues.containsKey(templateVariable.getPart())) { throw new IllegalArgumentException( String.format("The value associated of the template value map for key '%s' is 'null'.", templateVariable) diff --git a/core-common/src/main/java/org/glassfish/jersey/uri/internal/TemplateVariable.java b/core-common/src/main/java/org/glassfish/jersey/uri/internal/TemplateVariable.java new file mode 100644 index 0000000000..c5032506f5 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/uri/internal/TemplateVariable.java @@ -0,0 +1,400 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.uri.internal; + +import org.glassfish.jersey.uri.UriComponent; + +import java.util.Collection; +import java.util.Map; + +/** + * The Reserved Expansion template variable representation as per RFC6570. + */ +/* package */ class TemplateVariable extends UriPart { + + protected final Position position; + protected int len = -1; // unlimited + protected boolean star = false; + + TemplateVariable(String part, Position position) { + super(part); + this.position = position; + } + + /** + * Choose the template variable type. The + * @param type Type of the template + * @param part the template content + * @param position the position of the variable in the template. + * @return Subclass of Templatevariable to represent the variable and allowing expansion based on the type of the variable + */ + static TemplateVariable createTemplateVariable(char type, String part, Position position) { + TemplateVariable newType; + switch (type) { + case '+': + newType = new TemplateVariable(part, position); + break; + case '-': // Not supported by RFC + newType = new MinusTemplateVariable(part, position); + break; + case '#': + newType = new HashTemplateVariable(part, position); + break; + case '.': + newType = new DotTemplateVariable(part, position); + break; + case '/': + newType = new SlashTemplateVariable(part, position); + break; + case ';': + newType = new MatrixTemplateVariable(part, position); + break; + case '?': + newType = new QueryTemplateVariable(part, position); + break; + case '&': + newType = new QueryContinuationTemplateVariable(part, position); + break; + default: + //'p' + newType = new PathTemplateVariable(part, position); + break; + } + return newType; + } + + @Override + public boolean isTemplate() { + return true; + } + + @Override + public String getGroup() { + StringBuilder sb = new StringBuilder(); + if (position.isFirst()) { + sb.append('{'); + } else { + sb.append(','); + } + sb.append(getPart()); + if (position.isLast()) { + sb.append('}'); + } + return sb.toString(); + } + + @Override + public String resolve(Object value, UriComponent.Type type, boolean encode) { + if (value == null) { + return ""; + } + return position.isFirst() + ? plainResolve(value, type, encode) + : separator() + plainResolve(value, type, encode); + } + + protected char separator() { + return ','; + } + + protected char keyValueSeparator() { + return star ? '=' : ','; + } + + protected String plainResolve(Object value, UriComponent.Type componentType, boolean encode) { + if (Collection.class.isInstance(value)) { + return ((Collection) value).stream() + .map(a -> plainResolve(a, componentType, encode)) + .reduce("", (a, b) -> a + (a.isEmpty() ? b : separator() + b)); + } else if (Map.class.isInstance(value)) { + return ((Map) value).entrySet().stream() + .map(e -> plainResolve(e.getKey(), componentType, encode) + + keyValueSeparator() + + plainResolve(e.getValue(), componentType, encode)) + .reduce("", (a, b) -> a + (a.isEmpty() ? b : separator() + b)); + } else { + return plainResolve(value.toString(), componentType, encode); + } + } + + protected String plainResolve(String value, UriComponent.Type componentType, boolean encode) { + String val = len == -1 ? value : value.substring(0, Math.min(value.length(), len)); + return encode(val, componentType, encode); + } + + protected String encode(String toEncode, UriComponent.Type componentType, boolean encode) { + if (componentType == null) { + componentType = getDefaultType(); + } + return UriPart.percentEncode(toEncode, componentType, encode); + } + + protected UriComponent.Type getDefaultType() { + return UriComponent.Type.PATH; + } + + void setLength(int len) { + this.len = len; + } + + void setStar(boolean b) { + star = b; + } + + /** + * The default UriBuilder template + */ + private static class PathTemplateVariable extends TemplateVariable { + protected PathTemplateVariable(String part, Position position) { + super(part, position); + } + + @Override + public boolean throwWhenNoTemplateArg() { + return true; // The default UriBuilder behaviour + } + + @Override + protected UriComponent.Type getDefaultType() { + return UriComponent.Type.PATH; + } + } + + /** + * The template that works according to RFC 6570, Section 3.2.2. + * The default Path works as described in Section 3.2.3, as described by RFC 3986. + */ + private static class MinusTemplateVariable extends TemplateVariable { + protected MinusTemplateVariable(String part, Position position) { + super(part, position); + } + + @Override + protected String encode(String toEncode, UriComponent.Type componentType, boolean encode) { + return super.encode(toEncode, UriComponent.Type.QUERY, encode); //Query has the same encoding as Section 3.2.3 + } + + @Override + protected UriComponent.Type getDefaultType() { + return UriComponent.Type.QUERY; + } + } + + + /** + * Section 3.2.5 + */ + private static class DotTemplateVariable extends MinusTemplateVariable { + protected DotTemplateVariable(String part, Position position) { + super(part, position); + } + + @Override + public String resolve(Object value, UriComponent.Type type, boolean encode) { + if (value == null) { + return ""; + } + return '.' + plainResolve(value, type, encode); + } + + @Override + protected char separator() { + return star ? '.' : super.separator(); + } + } + + /** + * Section 3.2.6 + */ + private static class SlashTemplateVariable extends MinusTemplateVariable { + protected SlashTemplateVariable(String part, Position position) { + super(part, position); + } + + @Override + public String resolve(Object value, UriComponent.Type type, boolean encode) { + if (value == null) { + return ""; + } + return '/' + plainResolve(value, type, encode); + } + + @Override + protected char separator() { + return star ? '/' : super.separator(); + } + } + + /** + * Section 3.2.4 + */ + private static class HashTemplateVariable extends TemplateVariable { + protected HashTemplateVariable(String part, Position position) { + super(part, position); + } + + @Override + public String resolve(Object value, UriComponent.Type type, boolean encode) { + return (value == null || !position.isFirst() ? "" : "#") + super.resolve(value, type, encode); + } + + @Override + protected UriComponent.Type getDefaultType() { + return UriComponent.Type.PATH; + } + } + + + private abstract static class ExtendedVariable extends TemplateVariable { + + private final Character firstSymbol; + private final char separator; + protected final boolean appendEmpty; + + protected ExtendedVariable(String part, Position position, Character firstSymbol, char separator, boolean appendEmpty) { + super(part, position); + this.firstSymbol = firstSymbol; + this.separator = separator; + this.appendEmpty = appendEmpty; + } + + @Override + public String resolve(Object value, UriComponent.Type componentType, boolean encode) { + if (value == null) { // RFC 6570 + return ""; + } + String sValue = super.plainResolve(value, componentType, encode); + StringBuilder sb = new StringBuilder(); + + if (position.isFirst()) { + sb.append(firstSymbol); + } else { + sb.append(separator); + } + + if (!star) { + sb.append(getPart()); + if (appendEmpty || !sValue.isEmpty()) { + sb.append('=').append(sValue); + } + } else if (!Map.class.isInstance(value)) { + String[] split = sValue.split(String.valueOf(separator())); + for (int i = 0; i != split.length; i++) { + sb.append(getPart()); + sb.append('=').append(split[i]); + if (i != split.length - 1) { + sb.append(separator); + } + } + } else if (Map.class.isInstance(value)) { + sb.append(sValue); + } + return sb.toString(); + } + + @Override + protected char separator() { + return star ? separator : super.separator(); + } + } + + /** + * Section 3.2.7 + */ + private static class MatrixTemplateVariable extends ExtendedVariable { + protected MatrixTemplateVariable(String part, Position position) { + super(part, position, ';', ';', false); + } + + @Override + protected UriComponent.Type getDefaultType() { + return UriComponent.Type.QUERY; // For matrix, use query encoding per 6570 + } + + @Override + public String resolve(Object value, UriComponent.Type componentType, boolean encode) { + return super.resolve(value, getDefaultType(), encode); + } + } + + /** + * Section 3.2.8 + */ + private static class QueryTemplateVariable extends ExtendedVariable { + protected QueryTemplateVariable(String part, Position position) { + super(part, position, '?', '&', true); + } + } + + /** + * Section 3.2.9 + */ + private static class QueryContinuationTemplateVariable extends ExtendedVariable { + protected QueryContinuationTemplateVariable(String part, Position position) { + super(part, position, '&', '&', true); + } + + @Override + protected UriComponent.Type getDefaultType() { + return UriComponent.Type.QUERY; + } + + @Override + public String resolve(Object value, UriComponent.Type componentType, boolean encode) { + return super.resolve(value, getDefaultType(), encode); + } + } + + /** + *

+ * Position of the template variable. For instance, template {@code {first, middle, last}} would have three arguments, on + * {@link Position#FIRST}, {@link Position#MIDDLE}, and {@link Position#LAST} positions. + * If only a single argument is in template (most common) e.g. {@code {single}}, the position is {@link Position#SINGLE}. + *

+ *

+ * {@link Position#SINGLE} is first (see {@link Position#isFirst()}) and last (see {@link Position#isLast()}) at the same time. + *

+ */ + + /* package */ static enum Position { + FIRST((byte) 0b1100), + MIDDLE((byte) 0b1010), + LAST((byte) 0b1001), + SINGLE((byte) 0b1111); + + final byte val; + + Position(byte val) { + this.val = val; + } + + /** + * Informs whether the position of the argument is the last in the argument group. + * @return true when the argument is the last. + */ + boolean isLast() { + return (val & LAST.val) == LAST.val; + } + + /** + * Informs whether the position of the argument is the first in the argument group. + * @return true when the argument is the first. + */ + boolean isFirst() { + return (val & FIRST.val) == FIRST.val; + } + } +} diff --git a/core-common/src/main/java/org/glassfish/jersey/uri/internal/UriPart.java b/core-common/src/main/java/org/glassfish/jersey/uri/internal/UriPart.java new file mode 100644 index 0000000000..e808a61622 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/uri/internal/UriPart.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.uri.internal; + +import org.glassfish.jersey.uri.UriComponent; + +/** + *

+ * This class represents a part of the uri as parsed by the UriTemplateParser. + *

+ *

+ * The UriTemplate parser can produce multiple UriParts, each representing a part of the Uri. One part can represent either + * a static uri part without a template or a template with a single variable. The template with multiple variables generates + * multiple UriParts, each for a single variable. + *

+ */ +public class UriPart { + private final String part; + + UriPart(String part) { + this.part = part; + } + + /** + * Return the string value representing this UriPart. It can either be static content or a template. + * @return string value representing this UriPart + */ + public String getPart() { + return part; + } + + /** + * Return the matching group of the template represented by this {@link UriPart} + * @return the matching group + */ + public String getGroup() { + return part; + } + + /** + * Returns true when this {@link UriPart} is a template with a variable + * @return true when a template + */ + public boolean isTemplate() { + return false; + } + + /** + * Returns the resolved template variable when the value object is passed + * @param value the value object to be used to resolve the template variable + * @param componentType the component type that can be used to determine the encoding os special characters + * @param encode the hint whether to encode or not + * @return the resolved template + */ + public String resolve(Object value, UriComponent.Type componentType, boolean encode) { + return part; + } + + /** + * Informs whether throw {@link IllegalArgumentException} when no object value matches the template argument + * @return {@code true} when when no object value matches the template argument and + * {@link IllegalArgumentException} is to be thrown + */ + public boolean throwWhenNoTemplateArg() { + return false; + } + + /** + * Percent encode the given text + * @param toEncode the given text to encode + * @param componentType the component type to encode + * @param encode toEncode or contextualEncode + * @return the encoded text + */ + public static String percentEncode(String toEncode, UriComponent.Type componentType, boolean encode) { + if (encode) { + toEncode = UriComponent.encode(toEncode, componentType); + } else { + toEncode = UriComponent.contextualEncode(toEncode, componentType); + } + return toEncode; + } +} diff --git a/core-common/src/main/java/org/glassfish/jersey/uri/internal/UriTemplateParser.java b/core-common/src/main/java/org/glassfish/jersey/uri/internal/UriTemplateParser.java index 8ddcb49e39..b438023bf2 100644 --- a/core-common/src/main/java/org/glassfish/jersey/uri/internal/UriTemplateParser.java +++ b/core-common/src/main/java/org/glassfish/jersey/uri/internal/UriTemplateParser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -61,13 +61,16 @@ private static Set initReserved() { * Default URI template value regexp pattern. */ public static final Pattern TEMPLATE_VALUE_PATTERN = Pattern.compile("[^/]+"); + public static final Pattern TEMPLATE_VALUE_PATTERN_MULTI = Pattern.compile("[^,/]+"); + public static final Pattern MATCH_NUMBER_OF_MAX_LENGTH_4 = Pattern.compile("[1-9][0-9]{0,3}"); private final String template; private final StringBuffer regex = new StringBuffer(); private final StringBuffer normalizedTemplate = new StringBuffer(); private final StringBuffer literalCharactersBuffer = new StringBuffer(); private final Pattern pattern; - private final List names = new ArrayList(); + private final List names = new ArrayList<>(); + private final List parts = new ArrayList<>(); private final List groupCounts = new ArrayList(); private final Map nameToPattern = new HashMap(); private int numOfExplicitRegexes; @@ -143,10 +146,21 @@ public final Map getNameToPattern() { * * @return the list of template names. */ - public final List getNames() { + public final List getNames() { return names; } + /** + * Get a collection of uri parts (static strings and dynamic arguments) as parsed by the parser. + * Can be used to compose the uri. This collection is usually a superset of {@link #getNames() names} + * and other parts that do not have a template. + * + * @return List of parts of the uri. + */ + public List getUriParts() { + return parts; + } + /** * Get the capturing group counts for each template variable. * @@ -248,6 +262,7 @@ private void processLiteralCharacters() { String s = encodeLiteralCharacters(literalCharactersBuffer.toString()); normalizedTemplate.append(s); + parts.add(new UriPart(s)); // Escape if reserved regex character for (int i = 0; i < s.length(); i++) { @@ -289,90 +304,71 @@ private static String[] initHexToUpperCaseRegex() { } private int parseName(final CharacterIterator ci, int skipGroup) { - char c = consumeWhiteSpace(ci); - - char paramType = 'p'; // Normal path param unless otherwise stated - StringBuilder nameBuffer = new StringBuilder(); - - // Look for query or matrix types - if (c == '?' || c == ';') { - paramType = c; - c = ci.next(); - } - - if (Character.isLetterOrDigit(c) || c == '_') { - // Template name character - nameBuffer.append(c); - } else { - throw new IllegalArgumentException(LocalizationMessages.ERROR_TEMPLATE_PARSER_ILLEGAL_CHAR_START_NAME(c, ci.pos(), - template)); - } + Variables variables = new Variables(); + variables.parse(ci, template); - String nameRegexString = ""; - while (true) { - c = ci.next(); - // "\\{(\\w[-\\w\\.]*) - if (Character.isLetterOrDigit(c) || c == '_' || c == '-' || c == '.') { - // Template name character - nameBuffer.append(c); - } else if (c == ',' && paramType != 'p') { - // separator allowed for non-path parameter names - nameBuffer.append(c); - } else if (c == ':' && paramType == 'p') { - nameRegexString = parseRegex(ci); - break; - } else if (c == '}') { - break; - } else if (c == ' ') { - c = consumeWhiteSpace(ci); - - if (c == ':') { - nameRegexString = parseRegex(ci); - break; - } else if (c == '}') { - break; - } else { - // Error - throw new IllegalArgumentException( - LocalizationMessages.ERROR_TEMPLATE_PARSER_ILLEGAL_CHAR_AFTER_NAME(c, ci.pos(), template)); - } - } else { - throw new IllegalArgumentException( - LocalizationMessages.ERROR_TEMPLATE_PARSER_ILLEGAL_CHAR_PART_OF_NAME(c, ci.pos(), template)); - } - } - - String name = nameBuffer.toString(); Pattern namePattern; + // Make sure we display something useful + String name = variables.getName(); + int argIndex = 0; try { - if (paramType == '?' || paramType == ';') { - String[] subNames = name.split(",\\s?"); - + switch (variables.paramType) { + case '?': + case ';': + case '&': // Build up the regex for each of these properties - StringBuilder regexBuilder = new StringBuilder(paramType == '?' ? "\\?" : ";"); - String separator = paramType == '?' ? "\\&" : ";/\\?"; + StringBuilder regexBuilder = new StringBuilder(); + String separator = null; + switch (variables.paramType) { + case '?': + separator = "\\&"; + regexBuilder.append("\\?"); // first symbol + break; + case '&': + separator = "\\&"; + regexBuilder.append("\\&"); // first symbol + break; + case ';': + separator = ";/\\?"; + regexBuilder.append(";"); // first symbol + break; + } + // Start a group because each parameter could repeat // names.add("__" + (paramType == '?' ? "query" : "matrix")); - boolean first = true; + regexBuilder.append('('); + for (String subName : variables.names) { + + TemplateVariable.Position position = determinePosition(variables.separatorCount, argIndex); + TemplateVariable templateVariable = + TemplateVariable.createTemplateVariable(variables.paramType, subName, position); + templateVariable.setStar(variables.explodes(argIndex)); - regexBuilder.append("("); - for (String subName : subNames) { regexBuilder.append("(&?"); regexBuilder.append(subName); regexBuilder.append("(=([^"); regexBuilder.append(separator); - regexBuilder.append("]*))?"); - regexBuilder.append(")"); - if (!first) { - regexBuilder.append("|"); + regexBuilder.append(']'); + if (variables.hasLength(argIndex)) { + regexBuilder.append('{').append(variables.getLength(argIndex)).append('}'); + templateVariable.setLength(variables.getLength(argIndex)); + } else { + regexBuilder.append('*'); } + regexBuilder.append("))?"); + regexBuilder.append(')'); + if (argIndex != 0) { + regexBuilder.append('|'); + } + + names.add(templateVariable); + parts.add(templateVariable); - names.add(subName); groupCounts.add( - first ? 5 : 3); - first = false; + argIndex == 0 ? 5 : 3); + argIndex++; } // groupCounts.add(1); @@ -384,30 +380,96 @@ private int parseName(final CharacterIterator ci, int skipGroup) { namePattern = Pattern.compile(regexBuilder.toString()); // Make sure we display something useful - name = paramType + name; - } else { - names.add(name); - // groupCounts.add(1 + skipGroup); + break; + default: + if (variables.separatorCount == 0) { + if (variables.hasRegexp(0)) { + numOfExplicitRegexes++; + } - if (!nameRegexString.isEmpty()) { - numOfExplicitRegexes++; - } - namePattern = (nameRegexString.isEmpty()) - ? TEMPLATE_VALUE_PATTERN : Pattern.compile(nameRegexString); - if (nameToPattern.containsKey(name)) { - if (!nameToPattern.get(name).equals(namePattern)) { - throw new IllegalArgumentException( - LocalizationMessages.ERROR_TEMPLATE_PARSER_NAME_MORE_THAN_ONCE(name, template)); + TemplateVariable templateVariable = TemplateVariable + .createTemplateVariable(variables.paramType, variables.getName(0), TemplateVariable.Position.SINGLE); + templateVariable.setStar(variables.explodes(0)); + names.add(templateVariable); + parts.add(templateVariable); + // groupCounts.add(1 + skipGroup); + + if (variables.hasLength(0)) { + int len = TEMPLATE_VALUE_PATTERN.pattern().length() - 1; + String pattern = TEMPLATE_VALUE_PATTERN.pattern().substring(0, len) + '{' + variables.getLength(0) + '}'; + namePattern = Pattern.compile(pattern); + templateVariable.setLength(variables.getLength(0)); + } else { + namePattern = (!variables.hasRegexp(0)) + ? TEMPLATE_VALUE_PATTERN : Pattern.compile(variables.regexp(0)); } + if (nameToPattern.containsKey(name)) { + if (!nameToPattern.get(name).equals(namePattern)) { + throw new IllegalArgumentException( + LocalizationMessages.ERROR_TEMPLATE_PARSER_NAME_MORE_THAN_ONCE(name, template)); + } + } else { + nameToPattern.put(name, namePattern); + } + + // Determine group count of pattern + Matcher m = namePattern.matcher(""); + int g = m.groupCount(); + groupCounts.add(1 + skipGroup); + skipGroup = g; } else { - nameToPattern.put(name, namePattern); + argIndex = 0; + regexBuilder = new StringBuilder(); + + for (String subName : variables.names) { + if (argIndex != 0) { + regexBuilder + .append('(') + .append(','); + } + TemplateVariable.Position position = determinePosition(variables.separatorCount, argIndex); + TemplateVariable templateVariable + = TemplateVariable.createTemplateVariable(variables.paramType, subName, position); + templateVariable.setStar(variables.explodes(argIndex)); + names.add(templateVariable); + parts.add(templateVariable); + + if (variables.hasLength(argIndex)) { + int len = TEMPLATE_VALUE_PATTERN_MULTI.pattern().length() - 1; + String pattern = TEMPLATE_VALUE_PATTERN_MULTI.pattern() + .substring(0, len) + '{' + variables.getLength(argIndex) + '}'; + namePattern = Pattern.compile(pattern); + templateVariable.setLength(variables.getLength(argIndex)); + } else { + namePattern = (!variables.hasRegexp(argIndex)) + ? TEMPLATE_VALUE_PATTERN_MULTI : Pattern.compile(variables.regexp(argIndex)); + } +// TODO breaks RFC 6570 --backward compatibility with default pattern + if (nameToPattern.containsKey(subName) && variables.paramType == 'p') { + if (!nameToPattern.get(subName).equals(namePattern)) { + throw new IllegalArgumentException( + LocalizationMessages.ERROR_TEMPLATE_PARSER_NAME_MORE_THAN_ONCE(name, template)); + } + } else { + nameToPattern.put(subName, namePattern); + } + + regexBuilder + .append('(') + .append(namePattern) + .append(')'); + + if (argIndex != 0) { + regexBuilder.append(")"); + } + regexBuilder.append("{0,1}"); + + argIndex++; + groupCounts.add(2); + } + namePattern = Pattern.compile(regexBuilder.toString()); } - - // Determine group count of pattern - Matcher m = namePattern.matcher(""); - int g = m.groupCount(); - groupCounts.add(1 + skipGroup); - skipGroup = g; + break; } regex.append('(') @@ -418,40 +480,312 @@ private int parseName(final CharacterIterator ci, int skipGroup) { .append(name) .append('}'); } catch (PatternSyntaxException ex) { - throw new IllegalArgumentException( - LocalizationMessages.ERROR_TEMPLATE_PARSER_INVALID_SYNTAX(nameRegexString, name, template), ex); + throw new IllegalArgumentException(LocalizationMessages + .ERROR_TEMPLATE_PARSER_INVALID_SYNTAX(variables.regexp(argIndex), variables.name, template), ex); } // Tell the next time through the loop how many to skip return skipGroup; } - private String parseRegex(final CharacterIterator ci) { - StringBuilder regexBuffer = new StringBuilder(); - - int braceCount = 1; - while (true) { - char c = ci.next(); - if (c == '{') { - braceCount++; - } else if (c == '}') { - braceCount--; - if (braceCount == 0) { - break; + private static TemplateVariable.Position determinePosition(int separatorCount, int argIndex) { + TemplateVariable.Position position = separatorCount == 0 + ? TemplateVariable.Position.SINGLE + : argIndex == 0 + ? TemplateVariable.Position.FIRST + : argIndex == separatorCount ? TemplateVariable.Position.LAST : TemplateVariable.Position.MIDDLE; + return position; + } + + private static class Variables { + private char paramType = 'p'; + private List names = new ArrayList<>(); // names + private List explodes = new ArrayList<>(); // * + private List regexps = new ArrayList<>(); // : regexp + private List lengths = new ArrayList<>(); // :1-9999 + private int separatorCount = 0; + private StringBuilder name = new StringBuilder(); + + private int getCount() { + return names.size(); + } + + private boolean explodes(int index) { + return !explodes.isEmpty() && explodes.get(index); + } + + private boolean hasRegexp(int index) { + return !regexps.isEmpty() && regexps.get(index) != null; + } + + private String regexp(int index) { + return regexps.get(index); + } + + private boolean hasLength(int index) { + return !lengths.isEmpty() && lengths.get(index) != null; + } + + private Integer getLength(int index) { + return lengths.get(index); + } + + private char getParamType() { + return paramType; + } + + private int getSeparatorCount() { + return separatorCount; + } + + private String getName() { + return name.toString(); + } + + private String getName(int index) { + return names.get(index); + } + + private void parse(CharacterIterator ci, String template) { + name.append('{'); + + char c = consumeWhiteSpace(ci); + + StringBuilder nameBuilder = new StringBuilder(); + + // Look for query or matrix types + if (c == '?' || c == ';' || c == '.' || c == '+' || c == '#' || c == '/' || c == '&') { + paramType = c; + c = ci.next(); + name.append(paramType); + } + + if (Character.isLetterOrDigit(c) || c == '_') { + // Template name character + nameBuilder.append(c); + name.append(c); + } else { + throw new IllegalArgumentException(LocalizationMessages.ERROR_TEMPLATE_PARSER_ILLEGAL_CHAR_START_NAME(c, ci.pos(), + template)); + } + + StringBuilder regexBuilder = new StringBuilder(); + State state = State.TEMPLATE; + boolean star = false; + boolean whiteSpace = false; + boolean ignoredLastComma = false; + int bracketDepth = 1; // { + int regExpBracket = 0; // [ + int regExpRound = 0; // ( + boolean reqExpSlash = false; // \ + while ((state.value & (State.ERROR.value | State.EXIT.value)) == 0) { + c = ci.next(); + // "\\{(\\w[-\\w\\.]*) + if (Character.isLetterOrDigit(c)) { + // Template name character + append(c, state, nameBuilder, regexBuilder); + state = state.transition(State.TEMPLATE.value | State.REGEXP.value); + } else switch (c) { + case '_': + case '-': + case '.': + // Template name character + append(c, state, nameBuilder, regexBuilder); + state = state.transition(State.TEMPLATE.value | State.REGEXP.value); + break; + case ',': + switch (state) { + case REGEXP: + if (bracketDepth == 1 && !reqExpSlash && regExpBracket == 0 && regExpRound == 0) { + state = State.COMMA; + } else { + regexBuilder.append(c); + } + break; + case TEMPLATE: + case STAR: + state = State.COMMA; + break; + } + separatorCount++; + break; + case ':': + if (state == State.REGEXP) { + regexBuilder.append(c); + } + state = state.transition(State.TEMPLATE.value | State.REGEXP.value | State.STAR.value, State.REGEXP); + break; + case '*': + state = state.transition(State.TEMPLATE.value | State.REGEXP.value); + if (state == State.TEMPLATE) { + star = true; + state = State.STAR; + } else if (state == State.REGEXP){ + regexBuilder.append(c); + } + break; + case '}': + bracketDepth--; + if (bracketDepth == 0) { + state = State.BRACKET; + } else { + regexBuilder.append(c); + } + break; + case '{': + if (state == State.REGEXP) { + bracketDepth++; + regexBuilder.append(c); + } else { + state = State.ERROR; // Error multiple parenthesis + } + break; + default: + if (!Character.isWhitespace(c)) { + if (state != State.REGEXP) { + state = State.ERROR; // Error - unknown symbol + } else { + switch (c) { + case '(' : + regExpRound++; + break; + case ')': + regExpRound--; + break; + case '[': + regExpBracket++; + break; + case ']': + regExpBracket--; + break; + } + if (c == '\\') { + reqExpSlash = true; + } else { + reqExpSlash = false; + } + regexBuilder.append(c); + } + } + whiteSpace = true; + break; + } + + // Store parsed name, and associated star, regexp, and length + switch (state) { + case COMMA: + case BRACKET: + if (nameBuilder.length() == 0 && regexBuilder.length() == 0 && !star + && name.charAt(name.length() - 1) == ',' /* ignore last comma */) { + if (ignoredLastComma) { // Do not ignore twice + state = State.ERROR; + } else { + name.setLength(name.length() - 1); + ignoredLastComma = true; + } + break; + } + if (regexBuilder.length() != 0) { + String regex = regexBuilder.toString(); + Matcher matcher = MATCH_NUMBER_OF_MAX_LENGTH_4.matcher(regex); + if (matcher.matches()) { + lengths.add(Integer.parseInt(regex)); + regexps.add(null); + } else { + if (paramType != 'p') { + state = State.ERROR; // regular expressions allowed just on path by the REST spec + c = regex.charAt(0); // display proper error values + ci.setPosition(ci.pos() - regex.length()); + break; + } + lengths.add(null); + regexps.add(regex); + } + } else { + regexps.add(null); + lengths.add(null); + } + + names.add(nameBuilder.toString()); + explodes.add(star); + + nameBuilder.setLength(0); + regexBuilder.setLength(0); + star = false; + ignoredLastComma = false; + break; + } + + if (!whiteSpace) { + name.append(c); + } + whiteSpace = false; + + // switch state back or exit + switch (state) { + case COMMA: + state = State.TEMPLATE; + break; + case BRACKET: + state = State.EXIT; + break; } } - regexBuffer.append(c); + + if (state == State.ERROR) { + throw new IllegalArgumentException( + LocalizationMessages.ERROR_TEMPLATE_PARSER_ILLEGAL_CHAR_AFTER_NAME(c, ci.pos(), template)); + } } - return regexBuffer.toString().trim(); - } + private static void append(char c, State state, StringBuilder templateSb, StringBuilder regexpSb) { + if (state == State.TEMPLATE) { + templateSb.append(c); + } else { // REGEXP + regexpSb.append(c); + } + } + + private static char consumeWhiteSpace(final CharacterIterator ci) { + char c; + do { + c = ci.next(); + } while (Character.isWhitespace(c)); + + return c; + } - private char consumeWhiteSpace(final CharacterIterator ci) { - char c; - do { - c = ci.next(); - } while (Character.isWhitespace(c)); + private enum State { + TEMPLATE/**/(0b000000001), // Template name, before '*', ':', ',' or '}' + REGEXP/* */(0b000000010), // Regular expression inside template, after : + STAR/* */(0b000000100), // * + COMMA/* */(0b000001000), // , + BRACKET/* */(0b000010000), // } + EXIT/* */(0b001000000), // quit parsing + ERROR/* */(0b100000000); // error when parsing + private final int value; + State(int value) { + this.value = value; + } + + /** + * Return error state when in not any of allowed states represented by their combined values + * @param allowed The combined values of states (state1.value | state2.value) not to return error level + * @return this state if in allowed state or {@link State#ERROR} if not + */ + State transition(int allowed) { + return ((value & allowed) != 0) ? this : State.ERROR; + } - return c; + /** + * Return error state when in not any of allowed states represented by their combined values + * @param allowed The combined values of states (state1.value | state2.value) not to return error level + * @param next the next state to transition + * @return next state if in allowed state or {@link State#ERROR} if not + */ + State transition(int allowed, State next) { + return ((value & allowed) != 0) ? next : State.ERROR; + } + } } } diff --git a/core-common/src/test/java/org/glassfish/jersey/uri/UriTemplateTest.java b/core-common/src/test/java/org/glassfish/jersey/uri/UriTemplateTest.java index 7826506379..7846c113af 100644 --- a/core-common/src/test/java/org/glassfish/jersey/uri/UriTemplateTest.java +++ b/core-common/src/test/java/org/glassfish/jersey/uri/UriTemplateTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -22,12 +22,14 @@ import java.util.Collections; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.MatchResult; import org.glassfish.jersey.uri.internal.UriTemplateParser; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; @@ -284,7 +286,9 @@ void _testMatching(final String template, final String uri, final String... valu assertEquals(uri.length(), mr.end(0)); for (int i = 0; i < mr.groupCount(); i++) { assertEquals(values[i], mr.group(i + 1)); - assertEquals(values[i], uri.substring(mr.start(i + 1), mr.end(i + 1))); + int start = mr.start(i + 1); + int end = mr.end(i + 1); + assertEquals(values[i], start == -1 ? null : uri.substring(start, end)); } } @@ -432,8 +436,8 @@ public void testSubstitutionMap() { _testSubstitutionMap("http://example.com/order/{c}/{c}/{c}/", "http://example.com/order/cheeseburger/cheeseburger/cheeseburger/", "c", "cheeseburger"); - _testSubstitutionMap("http://example.com/{q}", - "http://example.com/hullo#world", + _testSubstitutionMap("http://example.com/{q}/z", + "http://example.com/hullo%23world/z", "q", "hullo#world"); _testSubstitutionMap("http://example.com/{e}/", "http://example.com//", @@ -656,7 +660,7 @@ public void testNotSettingMatrixParameter() throws Exception { private static final String base = "http://example.com/home/"; private static final String path = "/foo/bar"; private static final List list = Arrays.asList("red", "green", "blue"); - private static final Map keys = new HashMap() {{ + private static final Map keys = new LinkedHashMap() {{ put("semi", ";"); put("dot", "."); put("comma", ","); @@ -690,11 +694,42 @@ public void testRfc6570QueryTemplateExamples() { assertEncodedQueryTemplateExpansion("?x=1024&y=768&empty=", "{?x,y,empty}", x, y, empty); assertEncodedQueryTemplateExpansion("?x=1024&y=768", "{?x,y,undef}", x, y); - // TODO assertEncodedQueryTemplateExpansion("?var=val", "{?var:3}", var); - // TODO assertEncodedQueryTemplateExpansion("?list=red,green,blue", "{?list}", list); - // TODO assertEncodedQueryTemplateExpansion("?list=red&list=green&list=blue", "{?list*}", list); - // TODO assertEncodedQueryTemplateExpansion("?keys=semi,%3B,dot,.,comma,%2C", "{?keys}", keys); - // TODO assertEncodedQueryTemplateExpansion("?semi=%3B&dot=.&comma=%2C", "{?keys*}", keys); + assertEncodedQueryTemplateExpansion("?var=val", "{?var:3}", var); + assertEncodedQueryTemplateExpansion("?list=red,green,blue", "{?list}", list); + assertEncodedQueryTemplateExpansion("?list=red&list=green&list=blue", "{?list*}", list); + assertEncodedQueryTemplateExpansion("?keys=semi,%3B,dot,.,comma,%2C", "{?keys}", new Object[]{keys}); + assertEncodedQueryTemplateExpansion("?semi=%3B&dot=.&comma=%2C", "{?keys*}", new Object[]{keys}); + } + + @Test + public void testRfc6570QueryContinuationTemplateExamples() { + /* + RFC 6570, section 3.2.9: + + {&who} &who=fred + {&half} &half=50%25 + ?fixed=yes{&x} ?fixed=yes&x=1024 + {&x,y,empty} &x=1024&y=768&empty= + {&x,y,undef} &x=1024&y=768 + + {&var:3} &var=val + {&list} &list=red,green,blue + {&list*} &list=red&list=green&list=blue + {&keys} &keys=semi,%3B,dot,.,comma,%2C + {&keys*} &semi=%3B&dot=.&comma=%2C + */ + + assertEncodedQueryTemplateExpansion("&who=fred", "{ &who}", who); + assertEncodedQueryTemplateExpansion("&half=50%25", "{&half}", half); + assertEncodedQueryTemplateExpansion("?fixed=yes&x=1024", "?fixed=yes{&x}", x, y); + assertEncodedQueryTemplateExpansion("&x=1024&y=768&empty=", "{&x,y,empty}", x, y, empty); + assertEncodedQueryTemplateExpansion("&x=1024&y=768", "{&x,y,undef}", x, y); + + assertEncodedQueryTemplateExpansion("&var=val", "{&var:3}", var); + assertEncodedQueryTemplateExpansion("&list=red,green,blue", "{&list}", list); + assertEncodedQueryTemplateExpansion("&list=red&list=green&list=blue", "{&list*}", list); + assertEncodedQueryTemplateExpansion("&keys=semi,%3B,dot,.,comma,%2C", "{&keys}", new Object[]{keys}); + assertEncodedQueryTemplateExpansion("&semi=%3B&dot=.&comma=%2C", "{&keys*}", new Object[]{keys}); } private void assertEncodedQueryTemplateExpansion(final String expectedExpansion, @@ -743,11 +778,252 @@ public void testRfc6570MatrixTemplateExamples() { assertEncodedPathTemplateExpansion(";x=1024;y=768", "{;x,y}", x, y); assertEncodedPathTemplateExpansion(";x=1024;y=768;empty", "{;x,y,empty}", x, y, empty); assertEncodedPathTemplateExpansion(";x=1024;y=768", "{;x,y,undef}", x, y); - // TODO assertEncodedPathTemplateExpansion(";hello=Hello", "{;hello:5}", hello); - // TODO assertEncodedPathTemplateExpansion(";list=red,green,blue", "{;list}", list); - // TODO assertEncodedPathTemplateExpansion(";list=red;list=green;list=blue", "{;list*}", list); - // TODO assertEncodedPathTemplateExpansion(";keys=semi,%3B,dot,.,comma,%2C", "{;keys}", keys); - // TODO assertEncodedPathTemplateExpansion(";semi=%3B;dot=.;comma=%2C", "{;keys*}", keys); + assertEncodedPathTemplateExpansion(";hello=Hello", "{;hello:5}", hello); + assertEncodedPathTemplateExpansion(";list=red,green,blue", "{;list}", list); + assertEncodedPathTemplateExpansion(";list=red;list=green;list=blue", "{;list*}", list); + assertEncodedPathTemplateExpansion(";keys=semi,%3B,dot,.,comma,%2C", "{;keys}", new Object[]{keys}); + assertEncodedPathTemplateExpansion(";semi=%3B;dot=.;comma=%2C", "{;keys*}", new Object[]{keys}); + } + + @Test + void testRfc6570DefaultTemplateExamples() { + /* + RFC 6570, section 3.2.2 + {var} value + {hello} Hello%20World%21 + {half} 50%25 + O{empty}X OX + O{undef}X OX + {x,y} 1024,768 + {x,hello,y} 1024,Hello%20World%21,768 + ?{x,empty} ?1024, + ?{x,undef} ?1024 + ?{undef,y} ?768 + {var:3} val + {var:30} value + {list} red,green,blue + {list*} red,green,blue + {keys} semi,%3B,dot,.,comma,%2C + {keys*} semi=%3B,dot=.,comma=%2C + */ + + // TODO assertEncodedPathTemplateExpansion("Hello%20World%21", "{hello}", hello); // conflicts with rfc3986 Path + assertEncodedPathTemplateExpansion("50%25", "{half}", half); + assertEncodedPathTemplateExpansion("0X", "0{empty}X", empty); + // TODO assertEncodedPathTemplateExpansion("0X", "0{undef}X"); // conflicts with UriBuilder + // TODO assertEncodedPathTemplateExpansion("1024,Hello%20World%21,768", "{x,hello,y}", x, hello, y); //Path is {+} + assertEncodedPathTemplateExpansion("?1024,", "?{x,empty}", x, empty); + // TODO assertEncodedPathTemplateExpansion("?1024", "?{x,undef}", x); // conflicts with UriBuilder + assertEncodedPathTemplateExpansion("val", "{var:3}", var); + assertEncodedPathTemplateExpansion("value", "{var:30}", var); + assertEncodedPathTemplateExpansion("red,green,blue", "{list}", list); + // TODO assertEncodedPathTemplateExpansion("semi,%3B,dot,.,comma,%2C", "{keys}", keys); + // TODO assertEncodedPathTemplateExpansion("semi=%3B,dot=.,comma=%2C", "{keys*}", keys); + + // TODO Proprietary minus template +// assertEncodedPathTemplateExpansion("Hello%20World%21", "{-hello}", hello); +// assertEncodedPathTemplateExpansion("50%25", "{-half}", half); +// assertEncodedPathTemplateExpansion("0X", "0{-empty}X", empty); +// assertEncodedPathTemplateExpansion("0X", "0{-undef}X"); +// assertEncodedPathTemplateExpansion("1024,Hello%20World%21,768", "{-x,hello,y}", x, hello, y); +// assertEncodedPathTemplateExpansion("?1024,", "?{-x,empty}", x, empty); +// assertEncodedPathTemplateExpansion("?1024", "?{-x,undef}", x); +// assertEncodedPathTemplateExpansion("val", "{-var:3}", var); +// assertEncodedPathTemplateExpansion("value", "{-var:30}", var); +// assertEncodedPathTemplateExpansion("red,green,blue", "{-list}", list); +// assertEncodedPathTemplateExpansion("semi,%3B,dot,.,comma,%2C", "{-keys}", new Object[]{keys}); +// assertEncodedPathTemplateExpansion("semi=%3B,dot=.,comma=%2C", "{-keys*}", new Object[]{keys}); + } + + @Test + void testRfc6570PlusTemplateExamples() { + /* + RFC 6570, section 3.2.3 + {+var} value + {+hello} Hello%20World! + {+half} 50%25 + + {base}index http%3A%2F%2Fexample.com%2Fhome%2Findex + {+base}index http://example.com/home/index + O{+empty}X OX + O{+undef}X OX + + {+path}/here /foo/bar/here + here?ref={+path} here?ref=/foo/bar + up{+path}{var}/here up/foo/barvalue/here + {+x,hello,y} 1024,Hello%20World!,768 + {+path,x}/here /foo/bar,1024/here + + {+path:6}/here /foo/b/here + {+list} red,green,blue + {+list*} red,green,blue + {+keys} semi,;,dot,.,comma,, + {+keys*} semi=;,dot=.,comma=, + */ + assertEncodedPathTemplateExpansion("Hello%20World!", "{+hello}", hello); + assertEncodedPathTemplateExpansion("50%25", "{+half}", half); + assertEncodedPathTemplateExpansion("50%25", "{+half}", half); +// assertEncodedPathTemplateExpansion("http%3A%2F%2Fexample.com%2Fhome%2Findex", "{-base}index", base); + assertEncodedPathTemplateExpansion("http://example.com/home/index", "{+base}index", base); + assertEncodedPathTemplateExpansion("/foo/bar/here", "{+path}/here", path); + assertEncodedPathTemplateExpansion("here?ref=/foo/bar", "here?ref={+path}", path); + assertEncodedPathTemplateExpansion("up/foo/barvalue/here", "up{+path}{var}/here", path, var); + assertEncodedPathTemplateExpansion("1024,Hello%20World!,768", "{+x,hello,y}", x, hello, y); + assertEncodedPathTemplateExpansion("/foo/bar,1024/here", "{+path,x}/here", path, x); + assertEncodedPathTemplateExpansion("/foo/b/here", "{+path:6}/here", path); + assertEncodedPathTemplateExpansion("red,green,blue", "{+list}", list); + assertEncodedPathTemplateExpansion("red,green,blue", "{+list*}", list); + assertEncodedPathTemplateExpansion("semi,;,dot,.,comma,,", "{+keys}", new Object[]{keys}); + assertEncodedPathTemplateExpansion("semi=;,dot=.,comma=,", "{+keys*}", new Object[]{keys}); + } + + @Test + void testRfc6570HashTemplateExamples() { + /* + RFC 6570, section 3.2.4 + {#var} #value + {#hello} #Hello%20World! + {#half} #50%25 + foo{#empty} foo# + foo{#undef} foo + {#x,hello,y} #1024,Hello%20World!,768 + {#path,x}/here #/foo/bar,1024/here + {#path:6}/here #/foo/b/here + {#list} #red,green,blue + {#list*} #red,green,blue + {#keys} #semi,;,dot,.,comma,, + {#keys*} #semi=;,dot=.,comma=, + */ + assertEncodedPathTemplateExpansion("#Hello%20World!", "{#hello}", hello); + assertEncodedPathTemplateExpansion("#50%25", "{#half}", half); + assertEncodedPathTemplateExpansion("0#X", "0{#empty}X", empty); + assertEncodedPathTemplateExpansion("0X", "0{#undef}X"); + assertEncodedPathTemplateExpansion("#1024,Hello%20World!,768", "{#x,hello,y}", x, hello, y); + assertEncodedPathTemplateExpansion("#/foo/bar,1024/here", "{#path,x}/here", path, x); + assertEncodedPathTemplateExpansion("#/foo/b/here", "{#path:6}/here", path); + assertEncodedPathTemplateExpansion("#red,green,blue", "{#list}", list); + assertEncodedPathTemplateExpansion("#red,green,blue", "{#list*}", list); + assertEncodedPathTemplateExpansion("#semi,;,dot,.,comma,,", "{#keys}", new Object[]{keys}); + assertEncodedPathTemplateExpansion("#semi=;,dot=.,comma=,", "{#keys*}", new Object[]{keys}); + } + + @Test + void testRfc6570DotTemplateExamples() { + /* + RFC 6570, section 3.2.5 + {.who} .fred + {.who,who} .fred.fred + {.half,who} .50%25.fred + www{.dom*} www.example.com + X{.var} X.value + X{.empty} X. + X{.undef} X + X{.var:3} X.val + X{.list} X.red,green,blue + X{.list*} X.red.green.blue + X{.keys} X.semi,%3B,dot,.,comma,%2C + X{.keys*} X.semi=%3B.dot=..comma=%2C + X{.empty_keys} X + X{.empty_keys*} X + */ + assertEncodedPathTemplateExpansion(".fred", "{.who}", who); + assertEncodedPathTemplateExpansion(".fred.fred", "{.who,who}", who); + assertEncodedPathTemplateExpansion(".50%25.fred", "{.half,who}", half, who); + assertEncodedPathTemplateExpansion("www.example.com", "www{.dom*}", dom); + assertEncodedPathTemplateExpansion("X.value", "X{.var}", var); + assertEncodedPathTemplateExpansion("X.", "X{.empty}", empty); + assertEncodedPathTemplateExpansion("X", "X{.undef}"); + assertEncodedPathTemplateExpansion("X.val", "X{.var:3}", var); + assertEncodedPathTemplateExpansion("X.red,green,blue", "X{.list}", list); + assertEncodedPathTemplateExpansion("X.red.green.blue", "X{.list*}", list); + assertEncodedPathTemplateExpansion("X.semi,%3B,dot,.,comma,%2C", "X{.keys}", new Object[]{keys}); + assertEncodedPathTemplateExpansion("X.semi=%3B.dot=..comma=%2C", "X{.keys*}", new Object[]{keys}); + assertEncodedPathTemplateExpansion("X", "X{.empty_keys}", emptyKeys); + assertEncodedPathTemplateExpansion("X", "X{.empty_keys*}", emptyKeys); + } + + @Test + void testRfc6570SlashTemplateExamples() { + /* + RFC 6570, section 3.2.6 + + {/who} /fred + {/who,who} /fred/fred + {/half,who} /50%25/fred + {/who,dub} /fred/me%2Ftoo + {/var} /value + {/var,empty} /value/ + {/var,undef} /value + {/var,x}/here /value/1024/here + {/var:1,var} /v/value + {/list} /red,green,blue + {/list*} /red/green/blue + {/list*,path:4} /red/green/blue/%2Ffoo + {/keys} /semi,%3B,dot,.,comma,%2C + {/keys*} /semi=%3B/dot=./comma=%2C + */ + assertEncodedPathTemplateExpansion("/fred", "{/who}", who); + assertEncodedPathTemplateExpansion("/fred/fred", "{/who,who}", who); + assertEncodedPathTemplateExpansion("/50%25/fred", "{/half,who}", half, who); + assertEncodedPathTemplateExpansion("/fred/me%2Ftoo", "{/who,dub}", who, dub); + assertEncodedPathTemplateExpansion("/value", "{/var}", var); + assertEncodedPathTemplateExpansion("/value/", "{/var,empty}", var, empty); + assertEncodedPathTemplateExpansion("/value", "{/var,undef}", var); + assertEncodedPathTemplateExpansion("/v/value", "{/var:1,var}", var); + assertEncodedPathTemplateExpansion("/red,green,blue", "{/list}", list); + assertEncodedPathTemplateExpansion("/red/green/blue", "{/list*}", list); + assertEncodedPathTemplateExpansion("/red/green/blue/%2Ffoo", "{/list*,path:4}", list, path); + assertEncodedPathTemplateExpansion("/semi,%3B,dot,.,comma,%2C", "{/keys}", new Object[]{keys}); + assertEncodedPathTemplateExpansion("/semi=%3B/dot=./comma=%2C", "{/keys*}", new Object[]{keys}); + } + + @Test + void testRfc6570MultiplePathArgs() { + _testTemplateNames("/{a,b,c}", "a", "b", "c"); + _testMatching("/uri/{a}", "/uri/hello", "hello"); + _testMatching("/uri/{a,b}", "/uri/hello,world", "hello", "world"); + _testMatching("/uri{?a,b}", "/uri?a=hello&b=world", "hello", "world"); + _testMatching("/uri/{a,b,c}", "/uri/hello,world,!", "hello", "world", "!"); + _testMatching("/uri/{a,b,c}", "/uri/hello,world", "hello", "world", null); + _testMatching("/uri/{a,b,c}", "/uri/hello", "hello", null, null); + _testMatching("/uri/{a,b,c}", "/uri/", null, null, null); + } + + @Test + void testRfc6570PathLength() { + _testMatching("/uri/{a:5}", "/uri/hello", "hello"); + _testMatching("/uri/{a:5,b:6}", "/uri/hello,world!", "hello", "world!"); + assertEncodedPathTemplateExpansion("102,7", "{x:3,y:1}", x, y); + } + + @Test + void testInvalidRegexp() { + _assertMatchingThrowsIAE("/uri/{a**}"); + _assertMatchingThrowsIAE("/uri/{a*a}"); + _assertMatchingThrowsIAE("/uri/{a{"); + _assertMatchingThrowsIAE("/uri/{*}"); + _assertMatchingThrowsIAE("/uri/{}}"); + _assertMatchingThrowsIAE("/uri/{?a:12345}"); //Query knows just length, but the length must be less than 10000 + _assertMatchingThrowsIAE("/uri/{?a:0}"); + _assertMatchingThrowsIAE("/uri/{?a:-1}"); + _assertMatchingThrowsIAE("/uri/{??a}"); + _assertMatchingThrowsIAE("/uri/{--a}"); + _assertMatchingThrowsIAE("/uri/{++a}"); + } + + @Test + public void ignoreLastComma() { + UriTemplateParser parser = new UriTemplateParser("/{a,b,}"); + Assertions.assertEquals(2, parser.getNames().size()); + } + + void _assertMatchingThrowsIAE(String uri) { + try { + _testMatching(uri, "/uri/hello", "hello"); + throw new IllegalStateException("IllegalArgumentException checking incorrect uri " + uri + " has not been thrown"); + } catch (IllegalArgumentException e) { + // expected + } } private void assertEncodedPathTemplateExpansion(final String expectedExpansion, diff --git a/incubator/declarative-linking/src/main/java/org/glassfish/jersey/linking/ELLinkBuilder.java b/incubator/declarative-linking/src/main/java/org/glassfish/jersey/linking/ELLinkBuilder.java index 8e777be394..787ebcc157 100644 --- a/incubator/declarative-linking/src/main/java/org/glassfish/jersey/linking/ELLinkBuilder.java +++ b/incubator/declarative-linking/src/main/java/org/glassfish/jersey/linking/ELLinkBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -28,6 +28,7 @@ import javax.el.ValueExpression; import org.glassfish.jersey.linking.mapping.ResourceMappingContext; +import org.glassfish.jersey.uri.internal.UriPart; import org.glassfish.jersey.uri.internal.UriTemplateParser; /** @@ -97,7 +98,7 @@ static URI buildURI(InjectLinkDescriptor link, // now process any embedded URI template parameters UriBuilder ub = applyLinkStyle(template, link.getLinkStyle(), uriInfo); UriTemplateParser parser = new UriTemplateParser(template); - List parameterNames = parser.getNames(); + List parameterNames = parser.getNames(); Map valueMap = getParameterValues(parameterNames, link, context, uriInfo); return ub.buildFromMap(valueMap); } @@ -119,12 +120,13 @@ private static UriBuilder applyLinkStyle(String template, InjectLink.Style style return ub; } - private static Map getParameterValues(List parameterNames, + private static Map getParameterValues(List parameterNames, InjectLinkDescriptor linkField, LinkELContext context, UriInfo uriInfo) { Map values = new HashMap<>(); - for (String name : parameterNames) { + for (UriPart param : parameterNames) { + String name = param.getPart(); String elExpression = linkField.getBinding(name); if (elExpression == null) { String value = uriInfo.getPathParameters().getFirst(name); diff --git a/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/uri/internal/JerseyUriBuilderTest.java b/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/uri/internal/JerseyUriBuilderTest.java index d9a391b6b9..7233c8d29d 100644 --- a/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/uri/internal/JerseyUriBuilderTest.java +++ b/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/uri/internal/JerseyUriBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -22,6 +22,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.net.URLEncoder; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -1657,6 +1658,13 @@ public void testQueryParamStyleMultiPairs() { "key1=val1&key1=val2&key2=val1&key1=val3"); } + @Test + void testFragment2569() throws URISyntaxException { + final URI uri = new URI("http://www.example.org/foo.xml#xpointer(//Rube)").normalize(); + Assertions.assertEquals(uri, UriBuilder.fromUri(uri).build()); // prints "http://www.example.org/foo.xml#xpointer(//Rube)" + Assertions.assertEquals(uri, UriBuilder.fromUri(uri).fragment("xpointer(//{type})").build("Rube")); + } + private void checkQueryFormat(String fromUri, JerseyQueryParamStyle queryParamStyle, String expected) { final URI uri = ((JerseyUriBuilder) UriBuilder.fromUri(fromUri)) .setQueryParamStyle(queryParamStyle)