From 137264db49399230b9219d75da3fbb185ce1d10b Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Mon, 13 Feb 2023 12:42:28 +0000 Subject: [PATCH 01/13] Fix URI template expansion --- .../abstractions/gradle/dependencies.gradle | 2 +- .../microsoft/kiota/RequestInformation.java | 25 +++++++++++++------ .../kiota/RequestInformationTest.java | 24 ++++++++++++++++-- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/components/abstractions/gradle/dependencies.gradle b/components/abstractions/gradle/dependencies.gradle index efc7d740e..37c983d2e 100644 --- a/components/abstractions/gradle/dependencies.gradle +++ b/components/abstractions/gradle/dependencies.gradle @@ -9,7 +9,7 @@ dependencies { // This dependency is used internally, and not exposed to consumers on their own compile classpath. implementation 'com.google.guava:guava:31.1-jre' implementation 'org.javatuples:javatuples:1.2' - implementation 'com.github.hal4j:uritemplate:1.3.0' + implementation 'com.damnhandy:handy-uri-templates:2.1.7' implementation 'javax.annotation:javax.annotation-api:1.3.2' implementation 'io.opentelemetry:opentelemetry-api:1.22.0' implementation 'io.opentelemetry:opentelemetry-context:1.22.0' diff --git a/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java b/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java index 2aa62cf60..58ab3cb06 100644 --- a/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java +++ b/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java @@ -6,6 +6,7 @@ import java.io.InputStream; import java.lang.reflect.Field; import java.math.BigDecimal; +import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; @@ -23,15 +24,15 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import com.damnhandy.uri.template.UriTemplate; import com.microsoft.kiota.serialization.Parsable; import com.microsoft.kiota.serialization.SerializationWriter; -import com.github.hal4j.uritemplate.URITemplate; - import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Scope; import io.opentelemetry.api.GlobalOpenTelemetry; + /** This class represents an abstract HTTP request. */ public class RequestInformation { /** Creates a new instance of the request information class. */ @@ -44,6 +45,7 @@ public RequestInformation() { @Nullable public HashMap pathParameters = new HashMap<>(); private URI uri; + private static DateTimeFormatter RFC3339 = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"); /** Gets the URI of the request. * @throws URISyntaxException when the uri template is invalid. * @throws IllegalStateException when the baseurl template parameter is missing from the path parameters. @@ -63,11 +65,20 @@ public URI getUri() throws URISyntaxException,IllegalStateException{ if(!pathParameters.containsKey("baseurl") && urlTemplate.toLowerCase(Locale.ROOT).contains("{+baseurl}")) throw new IllegalStateException("PathParameters must contain a value for \"baseurl\" for the url to be built."); - final URITemplate template = new URITemplate(urlTemplate) - .expandOnly(new HashMap(queryParameters) {{ - putAll(pathParameters); - }}); - return template.toURI(); + Map params = new HashMap<>(pathParameters.size() + queryParameters.size()); + params.putAll(pathParameters); + params.putAll(queryParameters); + + for (String param: params.keySet()) { + Object value = params.get(param); + if (value instanceof OffsetDateTime) { + params.put(param, ((OffsetDateTime) value).format(RFC3339)); + } + } + + return new URI(UriTemplate.fromTemplate(urlTemplate) + .set(params) + .expand()); } } /** diff --git a/components/abstractions/src/test/java/com/microsoft/kiota/RequestInformationTest.java b/components/abstractions/src/test/java/com/microsoft/kiota/RequestInformationTest.java index ffb9246c4..de963c85f 100644 --- a/components/abstractions/src/test/java/com/microsoft/kiota/RequestInformationTest.java +++ b/components/abstractions/src/test/java/com/microsoft/kiota/RequestInformationTest.java @@ -64,8 +64,28 @@ public void SetsPathParametersOfDateTimeOffsetType() // Assert var uriResult = assertDoesNotThrow(() -> requestInfo.getUri()); - assertTrue(uriResult.toString().contains("fromDateTime='2022-08-01T00%3A00Z'")); - assertTrue(uriResult.toString().contains("toDateTime='2022-08-02T00%3A00Z'")); + assertTrue(uriResult.toString().contains("fromDateTime='2022-08-01T00%3A00%3A00Z'")); + assertTrue(uriResult.toString().contains("toDateTime='2022-08-02T00%3A00%3A00Z'")); + } + + @Test + public void ExpandQueryParametersAfterPathParams() + { + // Arrange as the request builders would + final RequestInformation requestInfo = new RequestInformation(); + requestInfo.httpMethod= HttpMethod.GET; + requestInfo.urlTemplate = "{+baseurl}/users/{id}/list{?async*,page*,size*,orderBy*,search*}"; + + // Act + requestInfo.pathParameters.put("baseurl", "http://localhost:9090"); + requestInfo.pathParameters.put("id", 1); + requestInfo.addQueryParameter("async", true); + requestInfo.addQueryParameter("size", 10); + + // Assert + var uriResult = assertDoesNotThrow(() -> requestInfo.getUri()); + assertTrue(uriResult.toString().contains("size=10")); + assertTrue(uriResult.toString().contains("async=true")); } @Test From 26048551263930166fafd27096d9c1f25a17ea36 Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Mon, 13 Feb 2023 12:44:15 +0000 Subject: [PATCH 02/13] changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce0370b9c..9afcdf263 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Fix #165 incorrect substitution of `queryParameters` + ## [0.2.0] - 2023-01-17 ### Changed From dd59bdd834b83dbee5e2e43fdbecc03ea35e2247 Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Mon, 13 Feb 2023 12:58:54 +0000 Subject: [PATCH 03/13] spotbugs fix --- .../main/java/com/microsoft/kiota/RequestInformation.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java b/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java index 58ab3cb06..b54628e72 100644 --- a/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java +++ b/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java @@ -69,10 +69,9 @@ public URI getUri() throws URISyntaxException,IllegalStateException{ params.putAll(pathParameters); params.putAll(queryParameters); - for (String param: params.keySet()) { - Object value = params.get(param); - if (value instanceof OffsetDateTime) { - params.put(param, ((OffsetDateTime) value).format(RFC3339)); + for (Map.Entry entry: params.entrySet()) { + if (entry.getValue() instanceof OffsetDateTime) { + params.put(entry.getKey(), ((OffsetDateTime) entry.getValue()).format(RFC3339)); } } From 31887c3b0904f068489cc4403c40a51c6e987959 Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Mon, 13 Feb 2023 16:21:16 +0000 Subject: [PATCH 04/13] use a port of micronaut implementation --- .../abstractions/gradle/dependencies.gradle | 1 - .../microsoft/kiota/RequestInformation.java | 16 +- .../java/com/microsoft/kiota/UriTemplate.java | 1126 +++++++++++++++++ 3 files changed, 1132 insertions(+), 11 deletions(-) create mode 100644 components/abstractions/src/main/java/com/microsoft/kiota/UriTemplate.java diff --git a/components/abstractions/gradle/dependencies.gradle b/components/abstractions/gradle/dependencies.gradle index 37c983d2e..8188d8204 100644 --- a/components/abstractions/gradle/dependencies.gradle +++ b/components/abstractions/gradle/dependencies.gradle @@ -9,7 +9,6 @@ dependencies { // This dependency is used internally, and not exposed to consumers on their own compile classpath. implementation 'com.google.guava:guava:31.1-jre' implementation 'org.javatuples:javatuples:1.2' - implementation 'com.damnhandy:handy-uri-templates:2.1.7' implementation 'javax.annotation:javax.annotation-api:1.3.2' implementation 'io.opentelemetry:opentelemetry-api:1.22.0' implementation 'io.opentelemetry:opentelemetry-context:1.22.0' diff --git a/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java b/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java index b54628e72..959c451af 100644 --- a/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java +++ b/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java @@ -24,7 +24,6 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; -import com.damnhandy.uri.template.UriTemplate; import com.microsoft.kiota.serialization.Parsable; import com.microsoft.kiota.serialization.SerializationWriter; @@ -69,15 +68,12 @@ public URI getUri() throws URISyntaxException,IllegalStateException{ params.putAll(pathParameters); params.putAll(queryParameters); - for (Map.Entry entry: params.entrySet()) { - if (entry.getValue() instanceof OffsetDateTime) { - params.put(entry.getKey(), ((OffsetDateTime) entry.getValue()).format(RFC3339)); - } - } - - return new URI(UriTemplate.fromTemplate(urlTemplate) - .set(params) - .expand()); +// for (Map.Entry entry: params.entrySet()) { +// if (entry.getValue() instanceof OffsetDateTime) { +// params.put(entry.getKey(), ((OffsetDateTime) entry.getValue()).format(RFC3339)); +// } +// } + return new URI(new UriTemplate(urlTemplate).expand(params)); } } /** diff --git a/components/abstractions/src/main/java/com/microsoft/kiota/UriTemplate.java b/components/abstractions/src/main/java/com/microsoft/kiota/UriTemplate.java new file mode 100644 index 000000000..e0bbbd908 --- /dev/null +++ b/components/abstractions/src/main/java/com/microsoft/kiota/UriTemplate.java @@ -0,0 +1,1126 @@ +// Original implementation from: https://github.com/micronaut-projects/micronaut-core/blob/02992a905cf9a2279b7fe8e49927ff080cb937d5/http/src/main/java/io/micronaut/http/uri/UriTemplate.java#L1 +/* + * 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 com.microsoft.kiota; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.StringJoiner; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + *

A Fast Implementation of URI Template specification. See rfc6570 and + * URI.js.

+ * + *

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

+ * + * @author Graeme Rocher + * @since 1.0 + */ +class UriTemplate implements Comparable { + + 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 = '.'; + private static final DateTimeFormatter RFC3339 = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"); + + // 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 + ")?"); + + protected final String templateString; + final List segments = new ArrayList<>(); + + /** + * Construct a new URI template for the given template. + * + * @param templateString The template string + */ + protected UriTemplate(CharSequence templateString) { + this(templateString, new Object[0]); + } + + /** + * 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) { + createParser(scheme + "://", parserArguments).parse(segments); + } + 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); + } + } + + /** + * @param templateString The template + * @param segments The list of segments + */ + protected UriTemplate(String templateString, List segments) { + this.templateString = templateString; + this.segments.addAll(segments); + } + + /** + * @return The number of segments that are variable + */ + public long getVariableSegmentCount() { + return segments.stream().filter(PathSegment::isVariable).count(); + } + + /** + * @return The number of path segments that are variable + */ + public long getPathVariableSegmentCount() { + return segments.stream().filter(PathSegment::isVariable).filter(s -> !s.isQuerySegment()).count(); + } + + /** + * @return The number of segments that are raw + */ + public long getRawSegmentCount() { + return segments.stream().filter(segment -> !segment.isVariable()).count(); + } + + /** + * @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. + * + * @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. + * + * @param parameters The parameters + * @return The expanded URI + */ + public String expand(Map 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(); + } + + @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 {@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. + * + * @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 newSegments = buildNestedSegments(uriTemplate, len, parserArguments); + return newUriTemplate(uriTemplate, newSegments); + } + + /** + * @param uriTemplate The URI template + * @param newSegments The new segments + * @return The new {@link UriTemplate} + */ + protected UriTemplate newUriTemplate(CharSequence uriTemplate, List newSegments) { + return new UriTemplate(normalizeNested(this.templateString, uriTemplate), newSegments); + } + + /** + * 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 (nested.length() <= 0 || nested.toString().trim().isEmpty()) { + 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; + } + } + } + + /** + * @param uriTemplate The URI template + * @param len The lenght + * @param parserArguments The parsed arguments + * @return A list of path segments + */ + protected List buildNestedSegments(CharSequence uriTemplate, int len, Object... parserArguments) { + List newSegments = new ArrayList<>(); + List 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. + * + * @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. + * + * @param filter The filter to test segments + * @return The template as a string + */ + protected String toString(Predicate 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. + */ + protected interface PathSegment extends CharSequence { + + /** + * @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. + * + * @return The variable name if present + */ + default Optional getVariable() { + return Optional.empty(); + } + + /** + * @return True if this is a variable segment + */ + default boolean isVariable() { + return getVariable().isPresent(); + } + + /** + * 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 parameters, boolean previousHasContent, boolean anyPreviousHasOperator); + } + + /** + * 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; + + /** + * @param templateText The template + */ + UriTemplateParser(String templateText) { + this.templateText = templateText; + } + + /** + * Parse a list of segments. + * + * @param segments The list of segments + */ + protected void parse(List 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; + } + // fall through + 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; + // fall through + 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; + // fall through + case '+': + case DOT_OPERATOR: + case SLASH_OPERATOR: + operator = c; + state = STATE_VAR_CONTENT; + continue; + default: + state = STATE_VAR_CONTENT; + buff.append(c); + continue; + } + default: + // no-op + } + } + + if (state == STATE_TEXT && buff.length() > 0) { + String val = buff.toString(); + addRawContentSegment(segments, val, isQuerySegment); + } + } + + /** + * Adds a raw content segment. + * + * @param segments The segments + * @param value The value + * @param isQuerySegment Whether is a query segment + */ + protected void addRawContentSegment(List segments, String value, boolean isQuerySegment) { + segments.add(new RawPathSegment(isQuerySegment, value)); + } + + /** + * 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 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. + */ + 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 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() { + return Objects.hash(isQuerySegment, value); + } + + @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. + */ + 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 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 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 map = (Map) found; + 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) -> { + if (some == null) { + return; + } + 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); + } + } + }); + if (joiner.length() == 0) { + // only null entries + return ""; + } else { + result = joiner.toString(); + } + } else if (found instanceof OffsetDateTime) { + String str = ((OffsetDateTime) found).format(RFC3339); + str = applyModifier(modifierStr, modifierChar, str, str.length()); + result = encode ? encode(str, isQuery) : escape(str); + } 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; + } + // fall through + 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) { + System.out.println("EXPAND POJO!!!"); + // 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 || found.getClass().getCanonicalName().startsWith("java.lang")) { + return found; + } + // This is not a complete implementation, just give up ... + throw new RuntimeException("Unsupported type " + found.getClass()); + } + } + } + +} \ No newline at end of file From 647ba640b2edbfec0d6c3c9951da049ebe3e1c67 Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Mon, 13 Feb 2023 18:28:36 +0000 Subject: [PATCH 05/13] minor --- components/abstractions/spotBugsExcludeFilter.xml | 4 ++++ .../src/main/java/com/microsoft/kiota/UriTemplate.java | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/components/abstractions/spotBugsExcludeFilter.xml b/components/abstractions/spotBugsExcludeFilter.xml index ebca482dc..bfe85928b 100644 --- a/components/abstractions/spotBugsExcludeFilter.xml +++ b/components/abstractions/spotBugsExcludeFilter.xml @@ -3,4 +3,8 @@ xmlns="https://github.com/spotbugs/filter/3.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubusercontent.com/spotbugs/spotbugs/3.1.0/spotbugs/etc/findbugsfilter.xsd"> + + + + \ No newline at end of file diff --git a/components/abstractions/src/main/java/com/microsoft/kiota/UriTemplate.java b/components/abstractions/src/main/java/com/microsoft/kiota/UriTemplate.java index e0bbbd908..21e34fb0f 100644 --- a/components/abstractions/src/main/java/com/microsoft/kiota/UriTemplate.java +++ b/components/abstractions/src/main/java/com/microsoft/kiota/UriTemplate.java @@ -873,7 +873,7 @@ public String toString() { /** * Variable path segment implementation. */ - private class VariablePathSegment implements PathSegment { + private static class VariablePathSegment implements PathSegment { private final boolean isQuerySegment; private final String variable; From 633d4c80512fa1a93988b90ba560a815db992d5e Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Mon, 13 Feb 2023 18:32:40 +0000 Subject: [PATCH 06/13] cleanup --- .../main/java/com/microsoft/kiota/RequestInformation.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java b/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java index 959c451af..ff080fd08 100644 --- a/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java +++ b/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java @@ -68,11 +68,6 @@ public URI getUri() throws URISyntaxException,IllegalStateException{ params.putAll(pathParameters); params.putAll(queryParameters); -// for (Map.Entry entry: params.entrySet()) { -// if (entry.getValue() instanceof OffsetDateTime) { -// params.put(entry.getKey(), ((OffsetDateTime) entry.getValue()).format(RFC3339)); -// } -// } return new URI(new UriTemplate(urlTemplate).expand(params)); } } From 7369958445c5960b7884b1fffd35a1f195dbbf15 Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Mon, 13 Feb 2023 18:33:12 +0000 Subject: [PATCH 07/13] more cleanup --- .../src/main/java/com/microsoft/kiota/RequestInformation.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java b/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java index ff080fd08..5a2261ca6 100644 --- a/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java +++ b/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java @@ -6,7 +6,6 @@ import java.io.InputStream; import java.lang.reflect.Field; import java.math.BigDecimal; -import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; @@ -44,7 +43,6 @@ public RequestInformation() { @Nullable public HashMap pathParameters = new HashMap<>(); private URI uri; - private static DateTimeFormatter RFC3339 = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"); /** Gets the URI of the request. * @throws URISyntaxException when the uri template is invalid. * @throws IllegalStateException when the baseurl template parameter is missing from the path parameters. From a10a86c50a4dd8572c0d56e4a5f8e4a0d8c39eff Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Mon, 13 Feb 2023 18:52:38 +0000 Subject: [PATCH 08/13] cleanup and UriTemplate tests --- .../abstractions/gradle/dependencies.gradle | 1 + .../java/com/microsoft/kiota/UriTemplate.java | 5 ++- .../com/microsoft/kiota/UriTemplateTest.java | 35 +++++++++++++++++++ .../abstractions/src/test/resources/data.csv | 8 +++++ 4 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 components/abstractions/src/test/java/com/microsoft/kiota/UriTemplateTest.java create mode 100644 components/abstractions/src/test/resources/data.csv diff --git a/components/abstractions/gradle/dependencies.gradle b/components/abstractions/gradle/dependencies.gradle index 8188d8204..5581a7bb2 100644 --- a/components/abstractions/gradle/dependencies.gradle +++ b/components/abstractions/gradle/dependencies.gradle @@ -1,6 +1,7 @@ dependencies { // Use JUnit Jupiter API for testing. testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.2' testImplementation 'org.mockito:mockito-inline:5.1.1' // Use JUnit Jupiter Engine for testing. diff --git a/components/abstractions/src/main/java/com/microsoft/kiota/UriTemplate.java b/components/abstractions/src/main/java/com/microsoft/kiota/UriTemplate.java index 21e34fb0f..af67d6219 100644 --- a/components/abstractions/src/main/java/com/microsoft/kiota/UriTemplate.java +++ b/components/abstractions/src/main/java/com/microsoft/kiota/UriTemplate.java @@ -1108,7 +1108,6 @@ private String encode(String str, boolean query) { } private Object expandPOJO(Object found) { - System.out.println("EXPAND POJO!!!"); // Check for common expanded types, such as list or Map if (found instanceof Iterable || found instanceof Map) { return found; @@ -1117,6 +1116,10 @@ private Object expandPOJO(Object found) { if (found == null || found.getClass().getCanonicalName().startsWith("java.lang")) { return found; } + // Handle OffsetDateTime + if (found instanceof OffsetDateTime) { + return ((OffsetDateTime) found).format(RFC3339); + } // This is not a complete implementation, just give up ... throw new RuntimeException("Unsupported type " + found.getClass()); } diff --git a/components/abstractions/src/test/java/com/microsoft/kiota/UriTemplateTest.java b/components/abstractions/src/test/java/com/microsoft/kiota/UriTemplateTest.java new file mode 100644 index 000000000..6612f1597 --- /dev/null +++ b/components/abstractions/src/test/java/com/microsoft/kiota/UriTemplateTest.java @@ -0,0 +1,35 @@ +package com.microsoft.kiota; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvFileSource; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class UriTemplateTest { + + Map params = Map.of( + "baseurl", "http://localhost:8080", + "id", 1, + "name", "foo", + "async", true, + "page", 10, + "date", OffsetDateTime.of(2023, 2, 13, 18, 49, 00, 00, ZoneOffset.UTC)); + + // Tests from: https://github.com/micronaut-projects/micronaut-core/blob/02992a905cf9a2279b7fe8e49927ff080cb937d5/http/src/test/groovy/io/micronaut/http/uri/UriTemplateSpec.groovy + @ParameterizedTest + @CsvFileSource(resources = "/data.csv", numLinesToSkip = 1, delimiter = ';') + void shouldProduceExpectedOutput(String template, String expected) { + // Arrange + UriTemplate uriTemplate = new UriTemplate(template); + + // Act + String result = uriTemplate.expand(params); + + // Assert + assertEquals(expected, result); + } +} diff --git a/components/abstractions/src/test/resources/data.csv b/components/abstractions/src/test/resources/data.csv new file mode 100644 index 000000000..1d73a014f --- /dev/null +++ b/components/abstractions/src/test/resources/data.csv @@ -0,0 +1,8 @@ +template, result +{+baseurl}/users; http://localhost:8080/users +{+baseurl}/users/{id}/list; http://localhost:8080/users/1/list +{+baseurl}/users/{id}/list{?async*}; http://localhost:8080/users/1/list?async=true +{+baseurl}/users/{id}/list{?async*,unmatched*}; http://localhost:8080/users/1/list?async=true +{+baseurl}/users/{id}/list{?async*,unmatched*,page*}; http://localhost:8080/users/1/list?async=true&page=10 +{+baseurl}/users/{id}/name/{name}/list{?async*,unmatched*,page*}; http://localhost:8080/users/1/name/foo/list?async=true&page=10 +{+baseurl}/users/{id}/list{?async*,unmatched*,page*,date*}; http://localhost:8080/users/1/list?async=true&page=10&date=2023-02-13T18%3A49%3A00Z From b91982e06a25c9c9b3fe6dcd5864e347f8ec465c Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Mon, 13 Feb 2023 18:53:31 +0000 Subject: [PATCH 09/13] minor --- .../src/test/java/com/microsoft/kiota/UriTemplateTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/abstractions/src/test/java/com/microsoft/kiota/UriTemplateTest.java b/components/abstractions/src/test/java/com/microsoft/kiota/UriTemplateTest.java index 6612f1597..ffdc1090b 100644 --- a/components/abstractions/src/test/java/com/microsoft/kiota/UriTemplateTest.java +++ b/components/abstractions/src/test/java/com/microsoft/kiota/UriTemplateTest.java @@ -19,7 +19,7 @@ public class UriTemplateTest { "page", 10, "date", OffsetDateTime.of(2023, 2, 13, 18, 49, 00, 00, ZoneOffset.UTC)); - // Tests from: https://github.com/micronaut-projects/micronaut-core/blob/02992a905cf9a2279b7fe8e49927ff080cb937d5/http/src/test/groovy/io/micronaut/http/uri/UriTemplateSpec.groovy + // Tests inspired from: https://github.com/micronaut-projects/micronaut-core/blob/02992a905cf9a2279b7fe8e49927ff080cb937d5/http/src/test/groovy/io/micronaut/http/uri/UriTemplateSpec.groovy @ParameterizedTest @CsvFileSource(resources = "/data.csv", numLinesToSkip = 1, delimiter = ';') void shouldProduceExpectedOutput(String template, String expected) { From 1c71e758237d111ab320b9f290760092e50ddb7c Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Mon, 13 Feb 2023 19:02:54 +0000 Subject: [PATCH 10/13] minor --- .../src/main/java/com/microsoft/kiota/UriTemplate.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/abstractions/src/main/java/com/microsoft/kiota/UriTemplate.java b/components/abstractions/src/main/java/com/microsoft/kiota/UriTemplate.java index af67d6219..be7e146f8 100644 --- a/components/abstractions/src/main/java/com/microsoft/kiota/UriTemplate.java +++ b/components/abstractions/src/main/java/com/microsoft/kiota/UriTemplate.java @@ -217,7 +217,7 @@ public String expand(Map parameters) { } if (segment instanceof UriTemplateParser.VariablePathSegment) { UriTemplateParser.VariablePathSegment varPathSegment = (UriTemplateParser.VariablePathSegment) segment; - if (varPathSegment.isQuerySegment && ! queryParameter) { + if (varPathSegment.isQuerySegment() && ! queryParameter) { // reset anyPrevious* when we reach query parameters queryParameter = true; anyPreviousHasContent = false; From 2a6554070bac8a5844197f33caacac8824085ba6 Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Mon, 13 Feb 2023 19:10:40 +0000 Subject: [PATCH 11/13] fix android linting issues --- .../java/com/microsoft/kiota/UriTemplate.java | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/components/abstractions/src/main/java/com/microsoft/kiota/UriTemplate.java b/components/abstractions/src/main/java/com/microsoft/kiota/UriTemplate.java index be7e146f8..5fa3d26d1 100644 --- a/components/abstractions/src/main/java/com/microsoft/kiota/UriTemplate.java +++ b/components/abstractions/src/main/java/com/microsoft/kiota/UriTemplate.java @@ -63,7 +63,8 @@ class UriTemplate implements Comparable { private static final char AND_OPERATOR = '&'; private static final String SLASH_STRING = "/"; private static final char DOT_OPERATOR = '.'; - private static final DateTimeFormatter RFC3339 = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"); + + static final DateTimeFormatter RFC3339 = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"); // Regex patterns that matches URIs. See RFC 3986, appendix B static final Pattern PATTERN_SCHEME = Pattern.compile("^" + STRING_PATTERN_SCHEME + "//.*"); @@ -423,7 +424,7 @@ protected List buildNestedSegments(CharSequence uriTemplate, int le } else { PathSegment last = segments.get(segments.size() - 1); if (last instanceof UriTemplateParser.RawPathSegment) { - String v = ((UriTemplateParser.RawPathSegment) last).value; + String v = ((UriTemplateParser.RawPathSegment) last).getValue(); if (v.endsWith(SLASH_STRING)) { templateString = templateString.substring(1); } else { @@ -465,12 +466,12 @@ protected String toString(Predicate filter) { 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); + if (varSeg.getOperator() == previousVariable.getOperator() && varSeg.getModifierChar() != EXPAND_MODIFIER) { + builder.append(varSeg.getDelimiter()); } else { builder.append(VAR_END); builder.append(VAR_START); - char op = varSeg.operator; + char op = varSeg.getOperator(); if (OPERATOR_NONE != op) { builder.append(op); } @@ -481,7 +482,7 @@ protected String toString(Predicate filter) { if (isVar) { previousVariable = (UriTemplateParser.VariablePathSegment) segment; builder.append(VAR_START); - char op = previousVariable.operator; + char op = previousVariable.getOperator(); if (OPERATOR_NONE != op) { builder.append(op); } @@ -817,6 +818,10 @@ public RawPathSegment(boolean isQuerySegment, String value) { this.value = value; } + public String getValue() { + return value; + } + @Override public boolean isQuerySegment() { return isQuerySegment; @@ -908,6 +913,14 @@ public char getOperator() { return this.operator; } + public char getModifierChar() { + return this.modifierChar; + } + + public String getDelimiter() { + return this.delimiter; + } + @Override public boolean isQuerySegment() { return isQuerySegment; From 4102753e07182615d2b18bdffa56ba47384015d8 Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Mon, 13 Feb 2023 19:12:50 +0000 Subject: [PATCH 12/13] update CHANGELOG --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9afcdf263..850e31ee5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Fix #165 incorrect substitution of `queryParameters` +## [0.2.1] - 2023-01-13 + +### Changed + +- Fix #165 incorrect/missing substitutions of `queryParameters` after `pathParameters` and other edge cases ## [0.2.0] - 2023-01-17 From b9fb9339f78f321e485bc6f4d108cee688a70d58 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 13 Feb 2023 14:36:02 -0500 Subject: [PATCH 13/13] - bumps minor version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 60f24c5fc..2756e73b4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,7 +26,7 @@ org.gradle.caching=true mavenGroupId = com.microsoft.kiota mavenMajorVersion = 0 mavenMinorVersion = 2 -mavenPatchVersion = 0 +mavenPatchVersion = 1 mavenArtifactSuffix = #These values are used to run functional tests