diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000000..aa4d88504dc --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,37 @@ +name: Build +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] +jobs: + build: + name: Build + runs-on: ubuntu-latest + timeout-minutes: 2880 + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + - name: Cache SonarCloud packages + uses: actions/cache@v1 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Maven packages + uses: actions/cache@v1 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + - name: Build and analyze + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: mvn -B -fn verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -DskipTests diff --git a/core/src/main/java/org/apache/cxf/annotations/Logging.java b/core/src/main/java/org/apache/cxf/annotations/Logging.java index 2d3dd50fe77..55f61eda05a 100644 --- a/core/src/main/java/org/apache/cxf/annotations/Logging.java +++ b/core/src/main/java/org/apache/cxf/annotations/Logging.java @@ -56,6 +56,11 @@ */ boolean pretty() default false; + /** + * For XML content, turn on regex printing in the logs + */ + boolean regex() default false; + /** * Ignore binary payloads by default */ diff --git a/core/src/main/java/org/apache/cxf/feature/LoggingFeature.java b/core/src/main/java/org/apache/cxf/feature/LoggingFeature.java index 6d06623e067..839f9e86b74 100644 --- a/core/src/main/java/org/apache/cxf/feature/LoggingFeature.java +++ b/core/src/main/java/org/apache/cxf/feature/LoggingFeature.java @@ -67,8 +67,12 @@ public LoggingFeature(String in, String out, int lim, boolean p) { super(new Portable(in, out, lim, p)); } - public LoggingFeature(String in, String out, int lim, boolean p, boolean showBinary) { - super(new Portable(in, out, lim, p, showBinary)); + public LoggingFeature(String in, String out, int lim, boolean p, boolean r) { + super(new Portable(in, out, lim, p, r)); + } + + public LoggingFeature(String in, String out, int lim, boolean p, boolean r, boolean showBinary) { + super(new Portable(in, out, lim, p, r, showBinary)); } public LoggingFeature(Logging annotation) { @@ -87,10 +91,18 @@ public boolean isPrettyLogging() { return delegate.isPrettyLogging(); } + public boolean isRegexLogging() { + return delegate.isRegexLogging(); + } + public void setPrettyLogging(boolean prettyLogging) { delegate.setPrettyLogging(prettyLogging); } + public void setRegexLogging(boolean regexLogging) { + delegate.setRegexLogging(regexLogging); + } + public static class Portable implements AbstractPortableFeature { private static final int DEFAULT_LIMIT = AbstractLoggingInterceptor.DEFAULT_LIMIT; private static final LoggingInInterceptor IN = new LoggingInInterceptor(DEFAULT_LIMIT); @@ -100,6 +112,7 @@ public static class Portable implements AbstractPortableFeature { String inLocation; String outLocation; boolean prettyLogging; + boolean regexLogging; boolean showBinary; int limit = DEFAULT_LIMIT; @@ -127,8 +140,16 @@ public Portable(String in, String out, int lim, boolean p) { prettyLogging = p; } - public Portable(String in, String out, int lim, boolean p, boolean showBinary) { - this(in, out, lim, p); + public Portable(String in, String out, int lim, boolean p, boolean r) { + inLocation = in; + outLocation = out; + limit = lim; + prettyLogging = p; + regexLogging = r; + } + + public Portable(String in, String out, int lim, boolean p, boolean r, boolean showBinary) { + this(in, out, lim, p, r); this.showBinary = showBinary; } @@ -137,13 +158,14 @@ public Portable(Logging annotation) { outLocation = annotation.outLocation(); limit = annotation.limit(); prettyLogging = annotation.pretty(); + regexLogging = annotation.regex(); showBinary = annotation.showBinary(); } @Override public void doInitializeProvider(InterceptorProvider provider, Bus bus) { if (limit == DEFAULT_LIMIT && inLocation == null - && outLocation == null && !prettyLogging) { + && outLocation == null && !prettyLogging && !regexLogging) { provider.getInInterceptors().add(IN); provider.getInFaultInterceptors().add(IN); provider.getOutInterceptors().add(OUT); @@ -152,10 +174,12 @@ public void doInitializeProvider(InterceptorProvider provider, Bus bus) { LoggingInInterceptor in = new LoggingInInterceptor(limit); in.setOutputLocation(inLocation); in.setPrettyLogging(prettyLogging); + in.setRegexLogging(regexLogging); in.setShowBinaryContent(showBinary); LoggingOutInterceptor out = new LoggingOutInterceptor(limit); out.setOutputLocation(outLocation); out.setPrettyLogging(prettyLogging); + out.setRegexLogging(regexLogging); out.setShowBinaryContent(showBinary); provider.getInInterceptors().add(in); @@ -185,6 +209,13 @@ public int getLimit() { public boolean isPrettyLogging() { return prettyLogging; } + + /** + */ + public boolean isRegexLogging() { + return regexLogging; + } + /** * Turn pretty logging of XML content on/off * @param prettyLogging @@ -192,5 +223,13 @@ public boolean isPrettyLogging() { public void setPrettyLogging(boolean prettyLogging) { this.prettyLogging = prettyLogging; } + + /** + * Turn regex logging on/off + * @param regexLogging + */ + public void setRegexLogging(boolean regexLogging) { + this.regexLogging = regexLogging; + } } } diff --git a/core/src/main/java/org/apache/cxf/interceptor/AbstractLoggingInterceptor.java b/core/src/main/java/org/apache/cxf/interceptor/AbstractLoggingInterceptor.java index d36e11c1274..bddbc9c1b8b 100644 --- a/core/src/main/java/org/apache/cxf/interceptor/AbstractLoggingInterceptor.java +++ b/core/src/main/java/org/apache/cxf/interceptor/AbstractLoggingInterceptor.java @@ -66,6 +66,7 @@ public abstract class AbstractLoggingInterceptor extends AbstractPhaseIntercepto protected long threshold = -1; protected PrintWriter writer; protected boolean prettyLogging; + protected boolean regexLogging; private boolean showBinaryContent; private boolean showMultipartContent = true; private List binaryContentMediaTypes = BINARY_CONTENT_MEDIA_TYPES; @@ -148,10 +149,18 @@ public void setPrettyLogging(boolean flag) { prettyLogging = flag; } + public void setRegexLogging(boolean flag) { + regexLogging = flag; + } + public boolean isPrettyLogging() { return prettyLogging; } + public boolean isRegexLogging() { + return regexLogging; + } + public void setInMemThreshold(long t) { threshold = t; } @@ -164,7 +173,7 @@ protected void writePayload(StringBuilder builder, CachedOutputStream cos, String encoding, String contentType, boolean truncated) throws Exception { // Just transform the XML message when the cos has content - if (!truncated && isPrettyLogging() && contentType != null && contentType.contains("xml") + if (!truncated && isPrettyLogging() && isRegexLogging() && contentType != null && contentType.contains("xml") && !contentType.toLowerCase().contains("multipart/related") && cos.size() > 0) { StringWriter swriter = new StringWriter(); @@ -205,6 +214,7 @@ protected void writePayload(StringBuilder builder, String contentType) throws Exception { if (isPrettyLogging() + && isRegexLogging() && contentType != null && contentType.contains("xml") && stringWriter.getBuffer().length() > 0) { diff --git a/pom.xml b/pom.xml index 8bd0b7eca86..967bf009aed 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,9 @@ UTF-8 scp://people.apache.org/www/cxf.apache.org/maven-site 6.0.4 + ayojava_Software-Engineering-Methodologies + software-engrg-methodologies + https://sonarcloud.io @@ -477,6 +480,9 @@ org.apache.maven.plugins maven-surefire-plugin 3.0.0-M5 + + true + org.apache.maven.plugins @@ -543,11 +549,6 @@ maven-deploy-plugin 2.8.2 - - org.apache.maven.plugins - maven-surefire-report-plugin - 3.0.0-M5 - org.apache.maven.plugins maven-project-info-reports-plugin @@ -738,4 +739,4 @@ - + \ No newline at end of file diff --git a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/AbstractLoggingInterceptor.java b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/AbstractLoggingInterceptor.java index e12cc9a0dd5..38da8b0bd7d 100644 --- a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/AbstractLoggingInterceptor.java +++ b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/AbstractLoggingInterceptor.java @@ -27,6 +27,7 @@ import org.apache.cxf.ext.logging.event.LogEvent; import org.apache.cxf.ext.logging.event.LogEventSender; import org.apache.cxf.ext.logging.event.PrettyLoggingFilter; +import org.apache.cxf.ext.logging.event.RegexLoggingFilter; import org.apache.cxf.interceptor.Fault; import org.apache.cxf.message.Exchange; import org.apache.cxf.message.Message; @@ -93,6 +94,12 @@ public void setPrettyLogging(boolean prettyLogging) { } } + public void setRegexLogging(boolean regexLogging) { + if (sender instanceof RegexLoggingFilter) { + ((RegexLoggingFilter)this.sender).setRegexLogging(regexLogging); + } + } + protected boolean shouldLogContent(LogEvent event) { return event.isBinaryContent() && logBinary || event.isMultipartContent() && logMultipart diff --git a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/Logging.java b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/Logging.java index 50e46c76dce..2b419ad8c54 100644 --- a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/Logging.java +++ b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/Logging.java @@ -50,6 +50,11 @@ */ boolean pretty() default false; + /** + * For XML content, turn on regex printing in the logs + */ + boolean regex() default false; + /** * Log binary payloads by default */ diff --git a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/LoggingBusLifecycleListener.java b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/LoggingBusLifecycleListener.java index 9f47c306e4e..9f3ecae0765 100644 --- a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/LoggingBusLifecycleListener.java +++ b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/LoggingBusLifecycleListener.java @@ -30,14 +30,23 @@ public class LoggingBusLifecycleListener implements BusLifeCycleListener { static final boolean FORCE_LOGGING; static final boolean FORCE_PRETTY; + static final boolean FORCE_REGEX; static { boolean b = false; boolean pretty = false; + boolean reg = false; try { String prop = System.getProperty("org.apache.cxf.logging.enabled", "false"); - if ("pretty".equals(prop)) { + if (("pretty".equals(prop)) && ("regex".equals(prop))) { b = true; pretty = true; + reg = true; + } else if ("pretty".equals(prop)) { + b = true; + pretty = true; + } else if ("regex".equals(prop)) { + b = true; + reg = true; } else { b = Boolean.parseBoolean(prop); //treat these all the same @@ -51,6 +60,7 @@ public class LoggingBusLifecycleListener implements BusLifeCycleListener { } FORCE_LOGGING = b; FORCE_PRETTY = pretty; + FORCE_REGEX = reg; } private final Bus bus; @@ -65,6 +75,7 @@ public void initComplete() { if (FORCE_LOGGING) { LoggingFeature feature = new LoggingFeature(); feature.setPrettyLogging(FORCE_PRETTY); + feature.setRegexLogging(FORCE_REGEX); bus.getFeatures().add(feature); feature.initialize(bus); } diff --git a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/LoggingFactoryBeanListener.java b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/LoggingFactoryBeanListener.java index fd03de08a4c..115ad250376 100644 --- a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/LoggingFactoryBeanListener.java +++ b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/LoggingFactoryBeanListener.java @@ -64,6 +64,7 @@ private void addLoggingSupport(Endpoint endpoint, Bus bus, Logging annotation) { LoggingFeature lf = new LoggingFeature(); lf.setInMemThreshold(annotation.inMemThresHold()); lf.setPrettyLogging(annotation.pretty()); + lf.setRegexLogging(annotation.regex()); lf.setLimit(annotation.limit()); lf.setLogBinary(annotation.logBinary()); lf.setLogMultipart(annotation.logMultipart()); diff --git a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/LoggingFeature.java b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/LoggingFeature.java index dff3e88bed6..f65716e5e67 100644 --- a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/LoggingFeature.java +++ b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/LoggingFeature.java @@ -26,6 +26,7 @@ import org.apache.cxf.common.injection.NoJSR250Annotations; import org.apache.cxf.ext.logging.event.LogEventSender; import org.apache.cxf.ext.logging.event.PrettyLoggingFilter; +import org.apache.cxf.ext.logging.event.RegexLoggingFilter; import org.apache.cxf.ext.logging.slf4j.Slf4jEventSender; import org.apache.cxf.ext.logging.slf4j.Slf4jVerboseEventSender; import org.apache.cxf.feature.AbstractPortableFeature; @@ -78,6 +79,10 @@ public void setPrettyLogging(boolean prettyLogging) { delegate.setPrettyLogging(prettyLogging); } + public void setRegexLogging(boolean regexLogging) { + delegate.setRegexLogging(regexLogging); + } + public void setLogBinary(boolean logBinary) { delegate.setLogBinary(logBinary); } @@ -172,13 +177,18 @@ public static class Portable implements AbstractPortableFeature { private LoggingOutInterceptor out; private PrettyLoggingFilter inPrettyFilter; private PrettyLoggingFilter outPrettyFilter; + private RegexLoggingFilter inRegexFilter; + private RegexLoggingFilter outRegexFilter; + public Portable() { LogEventSender sender = new Slf4jVerboseEventSender(); inPrettyFilter = new PrettyLoggingFilter(sender); outPrettyFilter = new PrettyLoggingFilter(sender); - in = new LoggingInInterceptor(inPrettyFilter); - out = new LoggingOutInterceptor(outPrettyFilter); + inRegexFilter = new RegexLoggingFilter(inPrettyFilter); + outRegexFilter = new RegexLoggingFilter(outPrettyFilter); + in = new LoggingInInterceptor(inRegexFilter); + out = new LoggingOutInterceptor(outRegexFilter); } @Override @@ -217,6 +227,11 @@ public void setPrettyLogging(boolean prettyLogging) { this.outPrettyFilter.setPrettyLogging(prettyLogging); } + public void setRegexLogging(boolean regexLogging) { + this.inRegexFilter.setRegexLogging(regexLogging); + this.outRegexFilter.setRegexLogging(regexLogging); + } + /** * Log binary content? * @param logBinary defaults to false diff --git a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/event/RegexLoggingFilter.java b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/event/RegexLoggingFilter.java new file mode 100644 index 00000000000..4ff90427f40 --- /dev/null +++ b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/event/RegexLoggingFilter.java @@ -0,0 +1,89 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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 org.apache.cxf.ext.logging.event; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Removes regex matches from the payload. + * Matches the complete payload, including element tags. + */ +public class RegexLoggingFilter implements LogEventSender { + private LogEventSender next; + private boolean filterLogging; + private Pattern regexPattern; + private String replacer; + + public RegexLoggingFilter(LogEventSender next, Pattern regexPattern, String replacer) { + this.next = next; + this.replacer = replacer; + this.regexPattern = regexPattern; + } + + public RegexLoggingFilter(LogEventSender next, Pattern regexPattern) { + this.next = next; + this.regexPattern = regexPattern; + this.replacer = ""; + } + + public RegexLoggingFilter(LogEventSender next) { + this.next = next; + this.regexPattern = null; + this.replacer = ""; + } + + @Override + public void send(LogEvent event) { + if (shouldFilter(event)) { + event.setPayload(getRegexFilteredMessage(event)); + } + next.send(event); + } + + private boolean shouldFilter(LogEvent event) { + String contentType = event.getContentType(); + return filterLogging + && regexPattern != null + && contentType != null + && contentType.indexOf("xml") >= 0 + && contentType.toLowerCase().indexOf("multipart/related") < 0 + && event.getPayload() != null + && event.getPayload().length() > 0; + } + + public String getRegexFilteredMessage(LogEvent event) { + String payload = event.getPayload(); + Matcher regexMatcher = regexPattern.matcher(payload); + + while (regexMatcher.find()) { + String matchString = regexMatcher.group(); + payload = payload.replace(matchString, replacer); + } + return payload; + } + + public void setNext(LogEventSender next) { + this.next = next; + } + + public void setRegexLogging(boolean filterLoggingEnabled) { + this.filterLogging = filterLoggingEnabled; + } +} diff --git a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/osgi/Activator.java b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/osgi/Activator.java index 5845180addd..3f4f7889b00 100644 --- a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/osgi/Activator.java +++ b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/osgi/Activator.java @@ -67,6 +67,7 @@ public void updated(Dictionary config) throws ConfigurationException { LOG.info("CXF message logging feature " + (enabled ? "enabled" : "disabled")); Integer limit = Integer.valueOf(getValue(config, "limit", "65536")); Boolean pretty = Boolean.valueOf(getValue(config, "pretty", "false")); + Boolean regex = Boolean.valueOf(getValue(config, "regex", "false")); Boolean verbose = Boolean.valueOf(getValue(config, "verbose", "true")); Long inMemThreshold = Long.valueOf(getValue(config, "inMemThresHold", "-1")); Boolean logMultipart = Boolean.valueOf(getValue(config, "logMultipart", "true")); @@ -81,6 +82,9 @@ public void updated(Dictionary config) throws ConfigurationException { if (pretty != null) { logging.setPrettyLogging(pretty); } + if (regex != null) { + logging.setRegexLogging(pretty); + } if (verbose != null) { logging.setVerbose(verbose); diff --git a/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/TestRegexLoggingFilter.java b/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/TestRegexLoggingFilter.java new file mode 100644 index 00000000000..6e278f31917 --- /dev/null +++ b/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/TestRegexLoggingFilter.java @@ -0,0 +1,109 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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 org.apache.cxf.ext.logging; + +import java.util.regex.Pattern; + +import org.apache.cxf.ext.logging.event.LogEvent; +import org.apache.cxf.ext.logging.event.LogEventSender; +import org.apache.cxf.ext.logging.event.RegexLoggingFilter; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + + +/** + * + * Test {@linkplain RegexLoggingFilter}} with well-formed and non-well-formed XML payloads. + * + */ + +public class TestRegexLoggingFilter { + + @Test + public void testWellformedXMLMessage() { + String message = "aa123456aa55555aa7777777"; + String expected = "aaaa55555aa7"; + Pattern regex = Pattern.compile("\\d{6}"); + filter(message, expected, regex, false); + } + + @Test + public void testInvalidXMLMessageUnexpectedEndTag() { + String message = "text"; + Pattern regex = Pattern.compile(""); + filter(message, message, regex, false); + } + + @Test + public void testInvalidXMLMessageMissingEndTag() { + String message = "text"; + Pattern regex = Pattern.compile(""); + filter(message, message, regex, false); + } + + @Test + public void testInvalidXMLMessageGarbageStartTag() { + String message = "text"; + Pattern regex = Pattern.compile(""); + filter(message, message, regex, false); + } + + @Test + public void testInvalidButTruncatedXMLMessageWithMissingEndTag() { + String message = "123text12"; + String expected = "text12"; + Pattern regex = Pattern.compile("\\d{3}"); + filter(message, expected, regex, true); + } + + @Test + public void testHtmlEntityTruncated() { + String message = "a&n"; + String expected = ""; + Pattern regex = Pattern.compile("a&n"); + filter(message, expected, regex, true); + } + + @Test + public void testPatternSyntaxException() { + String message = "message"; + Pattern regex = Pattern.compile("["); + filter(message, null, regex, false); + } + + private void filter(String payload, String expected, Pattern regex, boolean truncated) { + LogEvent logEvent = new LogEvent(); + logEvent.setPayload(payload); + logEvent.setContentType("text/xml"); + logEvent.setTruncated(truncated); + + LogEventSender dummy = new LogEventSender() { + public void send(LogEvent event) { + } + }; + + RegexLoggingFilter regexFilter = new RegexLoggingFilter(dummy, regex); + regexFilter.setRegexLogging(true); + regexFilter.send(logEvent); + assertEquals(expected, logEvent.getPayload()); + } + +} diff --git a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/UriBuilderImpl.java b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/UriBuilderImpl.java index 78ec9756382..9b594db28f3 100644 --- a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/UriBuilderImpl.java +++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/UriBuilderImpl.java @@ -1,1101 +1,1107 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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 - * - * http://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 org.apache.cxf.jaxrs.impl; - -import java.lang.reflect.Method; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import javax.ws.rs.Path; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.PathSegment; -import javax.ws.rs.core.UriBuilder; -import javax.ws.rs.core.UriBuilderException; - -import org.apache.cxf.common.util.CollectionUtils; -import org.apache.cxf.common.util.PropertyUtils; -import org.apache.cxf.common.util.StringUtils; -import org.apache.cxf.jaxrs.model.URITemplate; -import org.apache.cxf.jaxrs.utils.HttpUtils; -import org.apache.cxf.jaxrs.utils.JAXRSUtils; - -public class UriBuilderImpl extends UriBuilder implements Cloneable { - private static final String EXPAND_QUERY_VALUE_AS_COLLECTION = "expand.query.value.as.collection"; - private static final String USE_ARRAY_SYNTAX_FOR_QUERY_VALUES = "use.array.syntax.for.query.values"; - - private String scheme; - private String userInfo; - private int port = -1; - private String host; - private List paths = new ArrayList<>(); - private boolean originalPathEmpty; - private boolean leadingSlash; - private String fragment; - private String schemeSpecificPart; - private MultivaluedMap query = new MetadataMap<>(); - private MultivaluedMap matrix = new MetadataMap<>(); - - private Map resolvedTemplates; - private Map resolvedTemplatesPathEnc; - private Map resolvedEncodedTemplates; - - private boolean queryValueIsCollection; - private boolean useArraySyntaxForQueryParams; - - /** - * Creates builder with empty URI. - */ - public UriBuilderImpl() { - } - - /** - * Creates builder with empty URI and properties - */ - public UriBuilderImpl(Map properties) { - queryValueIsCollection = PropertyUtils.isTrue(properties, EXPAND_QUERY_VALUE_AS_COLLECTION); - useArraySyntaxForQueryParams = PropertyUtils.isTrue(properties, USE_ARRAY_SYNTAX_FOR_QUERY_VALUES); - } - - /** - * Creates builder initialized with given URI. - * - * @param uri initial value for builder - * @throws IllegalArgumentException when uri is null - */ - public UriBuilderImpl(URI uri) throws IllegalArgumentException { - setUriParts(uri); - } - - @Override - public URI build(Object... values) throws IllegalArgumentException, UriBuilderException { - return doBuild(false, true, values); - } - - private static Map getResolvedTemplates(Map rtemplates) { - return rtemplates == null - ? Collections.emptyMap() : new LinkedHashMap(rtemplates); - } - - private URI doBuild(boolean fromEncoded, boolean encodePathSlash, Object... values) { - if (values == null) { - throw new IllegalArgumentException("Template parameter values are set to null"); - } - for (int i = 0; i < values.length; i++) { - if (values[i] == null) { - throw new IllegalArgumentException("Template parameter value at position " + i + " is set to null"); - } - } - - UriParts parts = doBuildUriParts(fromEncoded, encodePathSlash, false, values); - try { - return buildURI(fromEncoded, parts.path, parts.query, parts.fragment); - } catch (URISyntaxException ex) { - throw new UriBuilderException("URI can not be built", ex); - } - } - - private UriParts doBuildUriParts(boolean fromEncoded, boolean encodePathSlash, - boolean allowUnresolved, Object... values) { - - Map alreadyResolvedTs = getResolvedTemplates(resolvedTemplates); - Map alreadyResolvedTsPathEnc = getResolvedTemplates(resolvedTemplatesPathEnc); - Map alreadyResolvedEncTs = getResolvedTemplates(resolvedEncodedTemplates); - final int resolvedTsSize = alreadyResolvedTs.size() - + alreadyResolvedEncTs.size() - + alreadyResolvedTsPathEnc.size(); - - String thePath = buildPath(); - URITemplate pathTempl = URITemplate.createExactTemplate(thePath); - thePath = substituteVarargs(pathTempl, alreadyResolvedTs, alreadyResolvedTsPathEnc, - alreadyResolvedEncTs, values, 0, false, fromEncoded, - allowUnresolved, encodePathSlash); - int pathTemplateVarsSize = pathTempl.getVariables().size(); - - String theQuery = buildQuery(); - int queryTemplateVarsSize = 0; - if (theQuery != null) { - URITemplate queryTempl = URITemplate.createExactTemplate(theQuery); - queryTemplateVarsSize = queryTempl.getVariables().size(); - if (queryTemplateVarsSize > 0) { - int lengthDiff = values.length + resolvedTsSize - - alreadyResolvedTs.size() - alreadyResolvedTsPathEnc.size() - alreadyResolvedEncTs.size() - - pathTemplateVarsSize; - theQuery = substituteVarargs(queryTempl, alreadyResolvedTs, alreadyResolvedTsPathEnc, - alreadyResolvedEncTs, values, values.length - lengthDiff, - true, fromEncoded, allowUnresolved, false); - } - } - - String theFragment = fragment; - if (theFragment != null) { - URITemplate fragmentTempl = URITemplate.createExactTemplate(theFragment); - if (fragmentTempl.getVariables().size() > 0) { - int lengthDiff = values.length + resolvedTsSize - - alreadyResolvedTs.size() - alreadyResolvedTsPathEnc.size() - alreadyResolvedEncTs.size() - - pathTemplateVarsSize - queryTemplateVarsSize; - theFragment = substituteVarargs(fragmentTempl, alreadyResolvedTs, alreadyResolvedTsPathEnc, - alreadyResolvedEncTs, values, values.length - lengthDiff, - true, fromEncoded, allowUnresolved, false); - } - } - - return new UriParts(thePath, theQuery, theFragment); - } - - private URI buildURI(boolean fromEncoded, String thePath, String theQuery, String theFragment) - throws URISyntaxException { - if (fromEncoded) { - return buildURIFromEncoded(thePath, theQuery, theFragment); - } else if (!isSchemeOpaque()) { - if ((scheme != null || host != null || userInfo != null) - && thePath.length() != 0 && !(thePath.startsWith("/") || thePath.startsWith(";"))) { - thePath = "/" + thePath; - } - try { - return buildURIFromEncoded(thePath, theQuery, theFragment); - } catch (Exception ex) { - // lets try the option below - } - URI uri = new URI(scheme, userInfo, host, port, - thePath, theQuery, theFragment); - if (thePath.contains("%2F")) { - // TODO: the bogus case of segments containing encoded '/' - // Not sure if we have a cleaner solution though. - String realPath = uri.getRawPath().replace("%252F", "%2F"); - uri = buildURIFromEncoded(realPath, uri.getRawQuery(), uri.getRawFragment()); - } - return uri; - } else { - return new URI(scheme, schemeSpecificPart, theFragment); - } - } - - private URI buildURIFromEncoded(String thePath, String theQuery, String theFragment) - throws URISyntaxException { - return new URI(buildUriString(thePath, theQuery, theFragment)); - } - - private String buildUriString(String thePath, String theQuery, String theFragment) { - StringBuilder b = new StringBuilder(); - if (scheme != null) { - b.append(scheme).append(':'); - } - if (!isSchemeOpaque()) { - if (scheme != null) { - b.append("//"); - } - if (userInfo != null) { - b.append(userInfo).append('@'); - } - if (host != null) { - b.append(host); - } - if (port != -1) { - b.append(':').append(port); - } - if (thePath != null && thePath.length() > 0) { - b.append(thePath.startsWith("/") || b.length() == 0 || originalPathEmpty - ? thePath : '/' + thePath); - } - if (theQuery != null && theQuery.length() != 0) { - b.append('?').append(theQuery); - } - } else { - b.append(schemeSpecificPart); - } - if (theFragment != null) { - b.append('#').append(theFragment); - } - return b.toString(); - } - - private boolean isSchemeOpaque() { - return schemeSpecificPart != null; - } - - @Override - public URI buildFromEncoded(Object... values) throws IllegalArgumentException, UriBuilderException { - return doBuild(true, false, values); - } - - @Override - public URI buildFromMap(Map map) throws IllegalArgumentException, - UriBuilderException { - return doBuildFromMap(map, false, true); - } - - private URI doBuildFromMap(Map map, boolean fromEncoded, - boolean encodePathSlash) - throws IllegalArgumentException, UriBuilderException { - try { - Map alreadyResolvedTs = getResolvedTemplates(resolvedTemplates); - Map alreadyResolvedTsPathEnc = getResolvedTemplates(resolvedTemplatesPathEnc); - Map alreadyResolvedEncTs = getResolvedTemplates(resolvedEncodedTemplates); - - String thePath = buildPath(); - thePath = substituteMapped(thePath, map, alreadyResolvedTs, alreadyResolvedTsPathEnc, - alreadyResolvedEncTs, false, fromEncoded, encodePathSlash); - - String theQuery = buildQuery(); - if (theQuery != null) { - theQuery = substituteMapped(theQuery, map, alreadyResolvedTs, alreadyResolvedTsPathEnc, - alreadyResolvedEncTs, true, fromEncoded, false); - } - - String theFragment = fragment == null - ? null : substituteMapped(fragment, map, alreadyResolvedTs, alreadyResolvedTsPathEnc, - alreadyResolvedEncTs, true, fromEncoded, encodePathSlash); - - return buildURI(fromEncoded, thePath, theQuery, theFragment); - } catch (URISyntaxException ex) { - throw new UriBuilderException("URI can not be built", ex); - } - } - //CHECKSTYLE:OFF - private String substituteVarargs(URITemplate templ, //NOPMD - Map alreadyResolvedTs, - Map alreadyResolvedTsPathEnc, - Map alreadyResolvedTsEnc, - Object[] values, - int ind, - boolean isQuery, - boolean fromEncoded, - boolean allowUnresolved, - boolean encodePathSlash) { - - //CHECKSTYLE:ON - Map varValueMap = new HashMap<>(); - - // vars in set are properly ordered due to linking in hash set - Set uniqueVars = new LinkedHashSet<>(templ.getVariables()); - if (!allowUnresolved && values.length + alreadyResolvedTs.size() + alreadyResolvedTsEnc.size() - + alreadyResolvedTsPathEnc.size() < uniqueVars.size()) { - throw new IllegalArgumentException("Unresolved variables; only " + values.length - + " value(s) given for " + uniqueVars.size() - + " unique variable(s)"); - } - int idx = ind; - Set pathEncodeVars = alreadyResolvedTsPathEnc.isEmpty() && !encodePathSlash - ? Collections.emptySet() : new HashSet<>(); - for (String var : uniqueVars) { - - boolean resolvedPathVarHasToBeEncoded = alreadyResolvedTsPathEnc.containsKey(var); - boolean varValueHasToBeEncoded = resolvedPathVarHasToBeEncoded || alreadyResolvedTs.containsKey(var); - - Map resolved = !varValueHasToBeEncoded ? alreadyResolvedTsEnc - : resolvedPathVarHasToBeEncoded ? alreadyResolvedTsPathEnc : alreadyResolvedTs; - Object oval = resolved.isEmpty() ? null : resolved.remove(var); - boolean valueFromEncodedMap = false; - if (oval == null) { - if (allowUnresolved) { - continue; - } - oval = values[idx++]; - } else { - valueFromEncodedMap = resolved == alreadyResolvedTsEnc; - } - - if (oval == null) { - throw new IllegalArgumentException("No object for " + var); - } - String value = oval.toString(); - if (fromEncoded || valueFromEncodedMap) { - value = HttpUtils.encodePartiallyEncoded(value, isQuery); - } else { - value = isQuery ? HttpUtils.queryEncode(value) : HttpUtils.pathEncode(value); - } - - varValueMap.put(var, value); - - if (!isQuery && (resolvedPathVarHasToBeEncoded - || encodePathSlash && !varValueHasToBeEncoded)) { - pathEncodeVars.add(var); - } - - } - return templ.substitute(varValueMap, pathEncodeVars, allowUnresolved); - } - - //CHECKSTYLE:OFF - private String substituteMapped(String path, - Map varValueMap, - Map alreadyResolvedTs, - Map alreadyResolvedTsPathEnc, - Map alreadyResolvedTsEnc, - boolean isQuery, - boolean fromEncoded, - boolean encodePathSlash) { - //CHECKSTYLE:ON - URITemplate templ = URITemplate.createExactTemplate(path); - - Set uniqueVars = new HashSet<>(templ.getVariables()); - if (varValueMap.size() + alreadyResolvedTs.size() + alreadyResolvedTsEnc.size() - + alreadyResolvedTsPathEnc.size() < uniqueVars.size()) { - throw new IllegalArgumentException("Unresolved variables; only " + varValueMap.size() - + " value(s) given for " + uniqueVars.size() - + " unique variable(s)"); - } - - Set pathEncodeVars = alreadyResolvedTsPathEnc.isEmpty() && !encodePathSlash - ? Collections.emptySet() : new HashSet<>(); - - Map theMap = new LinkedHashMap<>(); - for (String var : uniqueVars) { - boolean isPathEncVar = !isQuery && alreadyResolvedTsPathEnc.containsKey(var); - - boolean isVarEncoded = isPathEncVar || alreadyResolvedTs.containsKey(var) ? false : true; - Map resolved = isVarEncoded ? alreadyResolvedTsEnc - : isPathEncVar ? alreadyResolvedTsPathEnc : alreadyResolvedTs; - Object oval = resolved.isEmpty() ? null : resolved.remove(var); - if (oval == null) { - oval = varValueMap.get(var); - } - if (oval == null) { - throw new IllegalArgumentException("No object for " + var); - } - if (fromEncoded) { - oval = HttpUtils.encodePartiallyEncoded(oval.toString(), isQuery); - } else { - oval = isQuery ? HttpUtils.queryEncode(oval.toString()) : HttpUtils.pathEncode(oval.toString()); - } - theMap.put(var, oval); - if (!isQuery && (isPathEncVar || encodePathSlash)) { - pathEncodeVars.add(var); - } - } - return templ.substitute(theMap, pathEncodeVars, false); - } - - @Override - public URI buildFromEncodedMap(Map map) throws IllegalArgumentException, - UriBuilderException { - - Map decodedMap = new HashMap<>(map.size()); - for (Map.Entry entry : map.entrySet()) { - if (entry.getValue() == null) { - throw new IllegalArgumentException("Value is null"); - } - String theValue = entry.getValue().toString(); - if (theValue.contains("/")) { - // protecting '/' from being encoded here assumes that a given value may constitute multiple - // path segments - very questionable especially given that queries and fragments may also - // contain template vars - technically this can be covered by checking where a given template - // var is coming from and act accordingly. Confusing nonetheless. - StringBuilder buf = new StringBuilder(); - String[] values = theValue.split("/"); - for (int i = 0; i < values.length; i++) { - buf.append(HttpUtils.encodePartiallyEncoded(values[i], false)); - if (i + 1 < values.length) { - buf.append('/'); - } - } - decodedMap.put(entry.getKey(), buf.toString()); - } else { - decodedMap.put(entry.getKey(), HttpUtils.encodePartiallyEncoded(theValue, false)); - } - - } - return doBuildFromMap(decodedMap, true, false); - } - - // CHECKSTYLE:OFF - @Override - public UriBuilder clone() { //NOPMD - UriBuilderImpl builder = new UriBuilderImpl(); - builder.scheme = scheme; - builder.userInfo = userInfo; - builder.port = port; - builder.host = host; - builder.paths = new ArrayList<>(paths); - builder.fragment = fragment; - builder.query = new MetadataMap<>(query); - builder.matrix = new MetadataMap<>(matrix); - builder.schemeSpecificPart = schemeSpecificPart; - builder.leadingSlash = leadingSlash; - builder.originalPathEmpty = originalPathEmpty; - builder.queryValueIsCollection = queryValueIsCollection; - builder.resolvedEncodedTemplates = - resolvedEncodedTemplates == null ? null : new HashMap(resolvedEncodedTemplates); - builder.resolvedTemplates = - resolvedTemplates == null ? null : new HashMap(resolvedTemplates); - builder.resolvedTemplatesPathEnc = - resolvedTemplatesPathEnc == null ? null : new HashMap(resolvedTemplatesPathEnc); - builder.useArraySyntaxForQueryParams = useArraySyntaxForQueryParams; - return builder; - } - // CHECKSTYLE:ON - - @Override - public UriBuilder fragment(String theFragment) throws IllegalArgumentException { - this.fragment = theFragment; - return this; - } - - @Override - public UriBuilder host(String theHost) throws IllegalArgumentException { - if ("".equals(theHost)) { - throw new IllegalArgumentException("Host cannot be empty"); - } - this.host = theHost; - return this; - } - - @Override - public UriBuilder path(@SuppressWarnings("rawtypes") Class resource) throws IllegalArgumentException { - if (resource == null) { - throw new IllegalArgumentException("resource is null"); - } - Class cls = resource; - Path ann = cls.getAnnotation(Path.class); - if (ann == null) { - throw new IllegalArgumentException("Class '" + resource.getCanonicalName() - + "' is not annotated with Path"); - } - // path(String) decomposes multi-segment path when necessary - return path(ann.value()); - } - - @Override - public UriBuilder path(@SuppressWarnings("rawtypes") Class resource, String method) - throws IllegalArgumentException { - if (resource == null) { - throw new IllegalArgumentException("resource is null"); - } - if (method == null) { - throw new IllegalArgumentException("method is null"); - } - Path foundAnn = null; - for (Method meth : resource.getMethods()) { - if (meth.getName().equals(method)) { - Path ann = meth.getAnnotation(Path.class); - if (foundAnn != null && ann != null) { - throw new IllegalArgumentException("Multiple Path annotations for '" + method - + "' overloaded method"); - } - foundAnn = ann; - } - } - if (foundAnn == null) { - throw new IllegalArgumentException("No Path annotation for '" + method + "' method"); - } - // path(String) decomposes multi-segment path when necessary - return path(foundAnn.value()); - } - - - @Override - public UriBuilder path(Method method) throws IllegalArgumentException { - if (method == null) { - throw new IllegalArgumentException("method is null"); - } - Path ann = method.getAnnotation(Path.class); - if (ann == null) { - throw new IllegalArgumentException("Method '" + method.getDeclaringClass().getCanonicalName() + "." - + method.getName() + "' is not annotated with Path"); - } - // path(String) decomposes multi-segment path when necessary - return path(ann.value()); - } - - @Override - public UriBuilder path(String path) throws IllegalArgumentException { - return doPath(path, true); - } - - private UriBuilder doPath(String path, boolean checkSegments) { - if (path == null) { - throw new IllegalArgumentException("path is null"); - } - if (isAbsoluteUriPath(path)) { - try { - URI uri = URI.create(path); - this.originalPathEmpty = StringUtils.isEmpty(uri.getPath()); - uri(uri); - } catch (IllegalArgumentException ex) { - if (!URITemplate.createExactTemplate(path).getVariables().isEmpty()) { - return uriAsTemplate(path); - } - String pathEncoded = HttpUtils.pathEncode(path); - // Bad hack to bypass the TCK usage of bogus URI with empty paths containing matrix parameters, - // which even URI class chokes upon; cheaper to do the following than try to challenge, - // given that URI RFC mentions the possibility of empty paths, though no word on the possibility of - // such empty paths having matrix parameters... - int schemeIndex = pathEncoded.indexOf("//"); - if (schemeIndex != -1) { - int pathComponentStart = pathEncoded.indexOf("/", schemeIndex + 2); - if (pathComponentStart == -1) { - this.originalPathEmpty = true; - pathComponentStart = pathEncoded.indexOf(';'); - if (pathComponentStart != -1) { - pathEncoded = pathEncoded.substring(0, pathComponentStart) - + "/" + pathEncoded.substring(pathComponentStart); - } - } - } - setUriParts(URI.create(pathEncoded)); - } - return this; - } - - if (paths.isEmpty()) { - leadingSlash = path.startsWith("/"); - } - - List segments; - if (checkSegments) { - segments = JAXRSUtils.getPathSegments(path, false, false); - } else { - segments = new ArrayList<>(); - path = path.replaceAll("/", "%2F"); - segments.add(new PathSegmentImpl(path, false)); - } - if (!paths.isEmpty() && !matrix.isEmpty()) { - PathSegment ps = paths.remove(paths.size() - 1); - paths.add(replacePathSegment(ps)); - } - paths.addAll(segments); - matrix.clear(); - if (!paths.isEmpty()) { - matrix = paths.get(paths.size() - 1).getMatrixParameters(); - } - return this; - } - - @Override - public UriBuilder port(int thePort) throws IllegalArgumentException { - if (thePort < 0 && thePort != -1) { - throw new IllegalArgumentException("Port cannot be negative"); - } - this.port = thePort; - return this; - } - - @Override - public UriBuilder scheme(String s) throws IllegalArgumentException { - scheme = s; - return this; - } - - @Override - public UriBuilder schemeSpecificPart(String ssp) throws IllegalArgumentException { - // scheme-specific part is whatever after ":" of URI - // see: http://en.wikipedia.org/wiki/URI_scheme - try { - if (scheme == null) { - scheme = "http"; - } - URI uri = new URI(scheme, ssp, fragment); - setUriParts(uri); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Wrong syntax of scheme-specific part", e); - } - return this; - } - - @Override - public UriBuilder uri(URI uri) throws IllegalArgumentException { - setUriParts(uri); - return this; - } - - @Override - public UriBuilder userInfo(String ui) throws IllegalArgumentException { - this.userInfo = ui; - return this; - } - - private void setUriParts(URI uri) { - if (uri == null) { - throw new IllegalArgumentException("uri is null"); - } - String theScheme = uri.getScheme(); - if (theScheme != null) { - scheme = theScheme; - } - String rawPath = uri.getRawPath(); - if (!uri.isOpaque() && schemeSpecificPart == null - && (theScheme != null || rawPath != null)) { - port = uri.getPort(); - host = uri.getHost(); - if (rawPath != null) { - setPathAndMatrix(rawPath); - } - String rawQuery = uri.getRawQuery(); - if (rawQuery != null) { - query = JAXRSUtils.getStructuredParams(rawQuery, "&", false, true); - } - userInfo = uri.getUserInfo(); - schemeSpecificPart = null; - } else { - schemeSpecificPart = uri.getSchemeSpecificPart(); - } - if (scheme != null && host == null && port == -1 && userInfo == null - && CollectionUtils.isEmpty(query) - && uri.getSchemeSpecificPart() != null - && !schemeSpecificPartMatchesUriPath(uri)) { - schemeSpecificPart = uri.getSchemeSpecificPart(); - } - String theFragment = uri.getFragment(); - if (theFragment != null) { - fragment = theFragment; - } - } - - private boolean schemeSpecificPartMatchesUriPath(final URI uri) { - return uri.getRawSchemeSpecificPart() != null - && uri.getPath() != null - && uri.getRawSchemeSpecificPart().equals("//" + uri.getPath()); - } - - private void setPathAndMatrix(String path) { - leadingSlash = !originalPathEmpty && path.startsWith("/"); - paths = JAXRSUtils.getPathSegments(path, false, false); - if (!paths.isEmpty()) { - matrix = paths.get(paths.size() - 1).getMatrixParameters(); - } else { - matrix.clear(); - } - } - - private String buildPath() { - StringBuilder sb = new StringBuilder(); - Iterator iter = paths.iterator(); - while (iter.hasNext()) { - PathSegment ps = iter.next(); - String p = ps.getPath(); - if (p.length() != 0 || !iter.hasNext()) { - p = URITemplate.createExactTemplate(p).encodeLiteralCharacters(false); - if (sb.length() == 0 && leadingSlash) { - sb.append('/'); - } else if (!p.startsWith("/") && sb.length() > 0) { - sb.append('/'); - } - sb.append(p); - if (iter.hasNext()) { - buildMatrix(sb, ps.getMatrixParameters()); - } - } - } - buildMatrix(sb, matrix); - return sb.toString(); - } - - private String buildQuery() { - return buildParams(query, '&'); - } - - @Override - public UriBuilder matrixParam(String name, Object... values) throws IllegalArgumentException { - if (name == null || values == null) { - throw new IllegalArgumentException("name or values is null"); - } - List list = matrix.get(name); - if (list == null) { - matrix.put(name, toStringList(true, values)); - } else { - list.addAll(toStringList(true, values)); - } - return this; - } - - @Override - public UriBuilder queryParam(String name, Object... values) throws IllegalArgumentException { - if (name == null || values == null) { - throw new IllegalArgumentException("name or values is null"); - } - List list = query.get(name); - if (list == null) { - query.put(name, toStringList(false, values)); - } else { - list.addAll(toStringList(false, values)); - } - return this; - } - - @Override - public UriBuilder replaceMatrix(String matrixValues) throws IllegalArgumentException { - String encodedMatrixValues = matrixValues != null ? HttpUtils.pathEncode(matrixValues) : null; - this.matrix = JAXRSUtils.getStructuredParams(encodedMatrixValues, ";", true, false); - return this; - } - - @Override - public UriBuilder replaceMatrixParam(String name, Object... values) throws IllegalArgumentException { - if (name == null) { - throw new IllegalArgumentException("name is null"); - } - if (values != null && values.length >= 1 && values[0] != null) { - matrix.put(name, toStringList(true, values)); - } else { - matrix.remove(name); - } - return this; - } - - @Override - public UriBuilder replacePath(String path) { - if (path == null) { - clearPathAndMatrix(); - } else if (isAbsoluteUriPath(path)) { - clearPathAndMatrix(); - uri(URI.create(path)); - } else { - setPathAndMatrix(path); - } - return this; - } - - private void clearPathAndMatrix() { - paths.clear(); - matrix.clear(); - } - - private boolean isAbsoluteUriPath(String path) { - // This is the cheapest way to figure out if a given path is an absolute - // URI with the http(s) scheme, more expensive way is to always convert - // a path to URI and check if it starts from some scheme or not - - // Given that the list of schemes can be open-ended it is recommended that - // UriBuilder.fromUri is called instead for schemes like 'file', 'jms', etc - // be supported though the use of non-http schemes for *building* new URIs - // is pretty limited in the context of working with JAX-RS services - - return path.startsWith("http:") || path.startsWith("https:"); - } - - @Override - public UriBuilder replaceQuery(String queryValue) throws IllegalArgumentException { - if (queryValue != null) { - // workaround to do with a conflicting and confusing requirement where spaces - // passed as part of replaceQuery are encoded as %20 while those passed as part - // of quertyParam are encoded as '+' - queryValue = queryValue.replace(" ", "%20"); - } - query = JAXRSUtils.getStructuredParams(queryValue, "&", false, true); - return this; - } - - @Override - public UriBuilder replaceQueryParam(String name, Object... values) throws IllegalArgumentException { - if (name == null) { - throw new IllegalArgumentException("name is null"); - } - if (values != null && values.length >= 1 && values[0] != null) { - query.put(name, toStringList(false, values)); - } else { - query.remove(name); - } - return this; - } - - @Override - public UriBuilder segment(String... segments) throws IllegalArgumentException { - if (segments == null) { - throw new IllegalArgumentException("Segments should not be null"); - } - for (String segment : segments) { - doPath(segment, false); - } - return this; - } - - /** - * Query or matrix params convertion from object values vararg to list of strings. No encoding is - * provided. - * - * @param values entry vararg values - * @return list of strings - * @throws IllegalArgumentException when one of values is null - */ - private List toStringList(boolean encodeSlash, Object... values) throws IllegalArgumentException { - List list = new ArrayList<>(); - if (values != null && values.length > 0) { - for (int i = 0; i < values.length; i++) { - Object value = values[i]; - if (value == null) { - throw new IllegalArgumentException("Null value on " + i + " position"); - } - String strValue = value.toString(); - if (encodeSlash) { - strValue = strValue.replaceAll("/", "%2F"); - } - list.add(strValue); - } - } else { - list.add(null); - } - return list; - } - - /** - * Builds param string for query part or matrix part of URI. - * - * @param map query or matrix multivalued map - * @param separator params separator, '&' for query ';' for matrix - * @return stringified params. - */ - private String buildParams(MultivaluedMap map, char separator) { - boolean isQuery = separator == '&'; - StringBuilder b = new StringBuilder(); - for (Iterator>> it = map.entrySet().iterator(); it.hasNext();) { - Map.Entry> entry = it.next(); - - // Expand query parameter as "name=v1,v2,v3" - if (isQuery && queryValueIsCollection) { - b.append(entry.getKey()); - if (useArraySyntaxForQueryParams) { - b.append("[]"); - } - b.append('='); - - for (Iterator sit = entry.getValue().iterator(); sit.hasNext();) { - String val = sit.next(); - - if (val != null) { - boolean templateValue = val.startsWith("{") && val.endsWith("}"); - if (!templateValue) { - val = HttpUtils.encodePartiallyEncoded(val, isQuery); - if (!isQuery) { - val = val.replaceAll("/", "%2F"); - } - } else { - val = URITemplate.createExactTemplate(val).encodeLiteralCharacters(isQuery); - } - - if (!val.isEmpty()) { - b.append(val); - } - } - if (sit.hasNext()) { - b.append(','); - } - } - - if (it.hasNext()) { - b.append(separator); - } - } else { - // Expand query parameter as "name=v1&name=v2&name=v3", or use dedicated - // separator for matrix parameters - for (Iterator sit = entry.getValue().iterator(); sit.hasNext();) { - String val = sit.next(); - b.append(entry.getKey()); - if (useArraySyntaxForQueryParams) { - b.append("[]"); - } - if (val != null) { - boolean templateValue = val.startsWith("{") && val.endsWith("}"); - if (!templateValue) { - val = HttpUtils.encodePartiallyEncoded(val, isQuery); - if (!isQuery) { - val = val.replaceAll("/", "%2F"); - } - } else { - val = URITemplate.createExactTemplate(val).encodeLiteralCharacters(isQuery); - } - b.append('='); - if (!val.isEmpty()) { - b.append(val); - } - } - if (sit.hasNext() || it.hasNext()) { - b.append(separator); - } - } - } - } - return b.length() > 0 ? b.toString() : null; - } - - /** - * Builds param string for matrix part of URI. - * - * @param sb buffer to add the matrix part to, will get ';' added if map is not empty - * @param map matrix multivalued map - */ - private void buildMatrix(StringBuilder sb, MultivaluedMap map) { - if (!map.isEmpty()) { - sb.append(';'); - sb.append(buildParams(map, ';')); - } - } - - private PathSegment replacePathSegment(PathSegment ps) { - StringBuilder sb = new StringBuilder(); - sb.append(ps.getPath()); - buildMatrix(sb, matrix); - return new PathSegmentImpl(sb.toString()); - } - - - public UriBuilder uri(String uriTemplate) throws IllegalArgumentException { - if (StringUtils.isEmpty(uriTemplate)) { - throw new IllegalArgumentException(); - } - try { - return uri(URI.create(uriTemplate)); - } catch (Exception ex) { - if (URITemplate.createExactTemplate(uriTemplate).getVariables().isEmpty()) { - throw new IllegalArgumentException(ex); - } - return uriAsTemplate(uriTemplate); - - } - } - - public UriBuilder uriAsTemplate(String uri) { - // This can be a start of replacing URI class Parser completely - // but it can be too complicated, the following code is needed for now - // to deal with URIs containing template variables. - int index = uri.indexOf(':'); - if (index != -1) { - this.scheme = uri.substring(0, index); - uri = uri.substring(index + 1); - if (uri.indexOf("//") == 0) { - uri = uri.substring(2); - index = uri.indexOf('/'); - if (index != -1) { - String[] schemePair = uri.substring(0, index).split(":"); - this.host = schemePair[0]; - this.port = schemePair.length == 2 ? Integer.parseInt(schemePair[1]) : -1; - - uri = uri.substring(index); - } - } - - } - String rawQuery = null; - index = uri.indexOf('?'); - if (index != -1) { - rawQuery = uri.substring(index + 1); - uri = uri.substring(0, index); - } - setPathAndMatrix(uri); - if (rawQuery != null) { - query = JAXRSUtils.getStructuredParams(rawQuery, "&", false, true); - } - - return this; - } - - //the clarified rules for encoding values of uri templates are: - // - encode each value contextually based on the URI component containing the template - // - in path templates, by default, encode also slashes (i.e. treat all path templates as - // part of a single path segment, to be consistent with @Path annotation templates) - // - for special cases when the slash encoding in path templates is not desired, - // users may use the newly added build methods to override the default behavior - - @Override - public URI build(Object[] vars, boolean encodePathSlash) throws IllegalArgumentException, UriBuilderException { - return doBuild(false, encodePathSlash, vars); - } - - @Override - public URI buildFromMap(Map map, boolean encodePathSlash) throws IllegalArgumentException, - UriBuilderException { - return doBuildFromMap(map, false, encodePathSlash); - } - - - @Override - public String toTemplate() { - UriParts parts = doBuildUriParts(false, false, true); - return buildUriString(parts.path, parts.query, parts.fragment); - } - - @Override - public UriBuilder resolveTemplate(String name, Object value) throws IllegalArgumentException { - return resolveTemplate(name, value, true); - } - - @Override - public UriBuilder resolveTemplate(String name, Object value, boolean encodePathSlash) - throws IllegalArgumentException { - return resolveTemplates(Collections.singletonMap(name, value), encodePathSlash); - } - - @Override - public UriBuilder resolveTemplates(Map values) throws IllegalArgumentException { - return resolveTemplates(values, true); - } - - @Override - public UriBuilder resolveTemplates(Map values, boolean encodePathSlash) - throws IllegalArgumentException { - if (encodePathSlash) { - resolvedTemplatesPathEnc = fillInResolveTemplates(resolvedTemplatesPathEnc, values); - } else { - resolvedTemplates = fillInResolveTemplates(resolvedTemplates, values); - } - return this; - } - - @Override - public UriBuilder resolveTemplateFromEncoded(String name, Object value) throws IllegalArgumentException { - return resolveTemplatesFromEncoded(Collections.singletonMap(name, value)); - } - - @Override - public UriBuilder resolveTemplatesFromEncoded(Map values) - throws IllegalArgumentException { - resolvedEncodedTemplates = fillInResolveTemplates(resolvedEncodedTemplates, values); - return this; - } - - private static Map fillInResolveTemplates(Map map, Map values) - throws IllegalArgumentException { - if (values == null) { - throw new IllegalArgumentException(); - } - if (map == null) { - map = new LinkedHashMap<>(); - } - - for (Map.Entry entry : values.entrySet()) { - if (entry.getKey() == null || entry.getValue() == null) { - throw new IllegalArgumentException(); - } - map.put(entry.getKey(), entry.getValue()); - } - return map; - } - - private static class UriParts { - String path; - String query; - String fragment; - - UriParts(String path, String query, String fragment) { - this.path = path; - this.query = query; - this.fragment = fragment; - } - } -} +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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 org.apache.cxf.jaxrs.impl; + +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Method; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.ws.rs.Path; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.PathSegment; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriBuilderException; + +import org.apache.cxf.common.util.CollectionUtils; +import org.apache.cxf.common.util.PropertyUtils; +import org.apache.cxf.common.util.StringUtils; +import org.apache.cxf.jaxrs.model.URITemplate; +import org.apache.cxf.jaxrs.utils.HttpUtils; +import org.apache.cxf.jaxrs.utils.JAXRSUtils; + +public class UriBuilderImpl extends UriBuilder implements Cloneable { + private static final String EXPAND_QUERY_VALUE_AS_COLLECTION = "expand.query.value.as.collection"; + private static final String USE_ARRAY_SYNTAX_FOR_QUERY_VALUES = "use.array.syntax.for.query.values"; + + private String scheme; + private String userInfo; + private int port = -1; + private String host; + private List paths = new ArrayList<>(); + private boolean originalPathEmpty; + private boolean leadingSlash; + private String fragment; + private String schemeSpecificPart; + private MultivaluedMap query = new MetadataMap<>(); + private MultivaluedMap matrix = new MetadataMap<>(); + + private Map resolvedTemplates; + private Map resolvedTemplatesPathEnc; + private Map resolvedEncodedTemplates; + + private boolean queryValueIsCollection; + private boolean useArraySyntaxForQueryParams; + + /** + * Creates builder with empty URI. + */ + public UriBuilderImpl() { + } + + /** + * Creates builder with empty URI and properties + */ + public UriBuilderImpl(Map properties) { + queryValueIsCollection = PropertyUtils.isTrue(properties, EXPAND_QUERY_VALUE_AS_COLLECTION); + useArraySyntaxForQueryParams = PropertyUtils.isTrue(properties, USE_ARRAY_SYNTAX_FOR_QUERY_VALUES); + } + + /** + * Creates builder initialized with given URI. + * + * @param uri initial value for builder + * @throws IllegalArgumentException when uri is null + */ + public UriBuilderImpl(URI uri) throws IllegalArgumentException { + setUriParts(uri); + } + + @Override + public URI build(Object... values) throws IllegalArgumentException, UriBuilderException { + return doBuild(false, true, values); + } + + private static Map getResolvedTemplates(Map rtemplates) { + return rtemplates == null + ? Collections.emptyMap() : new LinkedHashMap(rtemplates); + } + + private URI doBuild(boolean fromEncoded, boolean encodePathSlash, Object... values) { + if (values == null) { + throw new IllegalArgumentException("Template parameter values are set to null"); + } + for (int i = 0; i < values.length; i++) { + if (values[i] == null) { + throw new IllegalArgumentException("Template parameter value at position " + i + " is set to null"); + } + } + + UriParts parts = doBuildUriParts(fromEncoded, encodePathSlash, false, values); + try { + return buildURI(fromEncoded, parts.path, parts.query, parts.fragment); + } catch (URISyntaxException ex) { + throw new UriBuilderException("URI can not be built", ex); + } + } + + private UriParts doBuildUriParts(boolean fromEncoded, boolean encodePathSlash, + boolean allowUnresolved, Object... values) { + + Map alreadyResolvedTs = getResolvedTemplates(resolvedTemplates); + Map alreadyResolvedTsPathEnc = getResolvedTemplates(resolvedTemplatesPathEnc); + Map alreadyResolvedEncTs = getResolvedTemplates(resolvedEncodedTemplates); + final int resolvedTsSize = alreadyResolvedTs.size() + + alreadyResolvedEncTs.size() + + alreadyResolvedTsPathEnc.size(); + + String thePath = buildPath(); + URITemplate pathTempl = URITemplate.createExactTemplate(thePath); + thePath = substituteVarargs(pathTempl, alreadyResolvedTs, alreadyResolvedTsPathEnc, + alreadyResolvedEncTs, values, 0, false, fromEncoded, + allowUnresolved, encodePathSlash); + int pathTemplateVarsSize = pathTempl.getVariables().size(); + + String theQuery = buildQuery(); + int queryTemplateVarsSize = 0; + if (theQuery != null) { + URITemplate queryTempl = URITemplate.createExactTemplate(theQuery); + queryTemplateVarsSize = queryTempl.getVariables().size(); + if (queryTemplateVarsSize > 0) { + int lengthDiff = values.length + resolvedTsSize + - alreadyResolvedTs.size() - alreadyResolvedTsPathEnc.size() - alreadyResolvedEncTs.size() + - pathTemplateVarsSize; + theQuery = substituteVarargs(queryTempl, alreadyResolvedTs, alreadyResolvedTsPathEnc, + alreadyResolvedEncTs, values, values.length - lengthDiff, + true, fromEncoded, allowUnresolved, false); + } + } + + String theFragment = fragment; + if (theFragment != null) { + URITemplate fragmentTempl = URITemplate.createExactTemplate(theFragment); + if (fragmentTempl.getVariables().size() > 0) { + int lengthDiff = values.length + resolvedTsSize + - alreadyResolvedTs.size() - alreadyResolvedTsPathEnc.size() - alreadyResolvedEncTs.size() + - pathTemplateVarsSize - queryTemplateVarsSize; + theFragment = substituteVarargs(fragmentTempl, alreadyResolvedTs, alreadyResolvedTsPathEnc, + alreadyResolvedEncTs, values, values.length - lengthDiff, + true, fromEncoded, allowUnresolved, false); + } + } + + return new UriParts(thePath, theQuery, theFragment); + } + + private URI buildURI(boolean fromEncoded, String thePath, String theQuery, String theFragment) + throws URISyntaxException { + if (fromEncoded) { + return buildURIFromEncoded(thePath, theQuery, theFragment); + } else if (!isSchemeOpaque()) { + if ((scheme != null || host != null || userInfo != null) + && thePath.length() != 0 && !(thePath.startsWith("/") || thePath.startsWith(";"))) { + thePath = "/" + thePath; + } + try { + return buildURIFromEncoded(thePath, theQuery, theFragment); + } catch (Exception ex) { + // lets try the option below + } + URI uri = new URI(scheme, userInfo, host, port, + thePath, theQuery, theFragment); + if (thePath.contains("%2F")) { + // TODO: the bogus case of segments containing encoded '/' + // Not sure if we have a cleaner solution though. + String realPath = uri.getRawPath().replace("%252F", "%2F"); + uri = buildURIFromEncoded(realPath, uri.getRawQuery(), uri.getRawFragment()); + } + return uri; + } else { + return new URI(scheme, schemeSpecificPart, theFragment); + } + } + + private URI buildURIFromEncoded(String thePath, String theQuery, String theFragment) + throws URISyntaxException { + return new URI(buildUriString(thePath, theQuery, theFragment)); + } + + private String buildUriString(String thePath, String theQuery, String theFragment) { + StringBuilder b = new StringBuilder(); + if (scheme != null) { + b.append(scheme).append(':'); + } + if (!isSchemeOpaque()) { + if (scheme != null) { + b.append("//"); + } + if (userInfo != null) { + b.append(userInfo).append('@'); + } + if (host != null) { + b.append(host); + } + if (port != -1) { + b.append(':').append(port); + } + if (thePath != null && thePath.length() > 0) { + b.append(thePath.startsWith("/") || b.length() == 0 || originalPathEmpty + ? thePath : '/' + thePath); + } + if (theQuery != null && theQuery.length() != 0) { + b.append('?').append(theQuery); + } + } else { + b.append(schemeSpecificPart); + } + if (theFragment != null) { + b.append('#').append(theFragment); + } + return b.toString(); + } + + private boolean isSchemeOpaque() { + return schemeSpecificPart != null; + } + + @Override + public URI buildFromEncoded(Object... values) throws IllegalArgumentException, UriBuilderException { + return doBuild(true, false, values); + } + + @Override + public URI buildFromMap(Map map) throws IllegalArgumentException, + UriBuilderException { + return doBuildFromMap(map, false, true); + } + + private URI doBuildFromMap(Map map, boolean fromEncoded, + boolean encodePathSlash) + throws IllegalArgumentException, UriBuilderException { + try { + Map alreadyResolvedTs = getResolvedTemplates(resolvedTemplates); + Map alreadyResolvedTsPathEnc = getResolvedTemplates(resolvedTemplatesPathEnc); + Map alreadyResolvedEncTs = getResolvedTemplates(resolvedEncodedTemplates); + + String thePath = buildPath(); + thePath = substituteMapped(thePath, map, alreadyResolvedTs, alreadyResolvedTsPathEnc, + alreadyResolvedEncTs, false, fromEncoded, encodePathSlash); + + String theQuery = buildQuery(); + if (theQuery != null) { + theQuery = substituteMapped(theQuery, map, alreadyResolvedTs, alreadyResolvedTsPathEnc, + alreadyResolvedEncTs, true, fromEncoded, false); + } + + String theFragment = fragment == null + ? null : substituteMapped(fragment, map, alreadyResolvedTs, alreadyResolvedTsPathEnc, + alreadyResolvedEncTs, true, fromEncoded, encodePathSlash); + + return buildURI(fromEncoded, thePath, theQuery, theFragment); + } catch (URISyntaxException ex) { + throw new UriBuilderException("URI can not be built", ex); + } + } + //CHECKSTYLE:OFF + private String substituteVarargs(URITemplate templ, //NOPMD + Map alreadyResolvedTs, + Map alreadyResolvedTsPathEnc, + Map alreadyResolvedTsEnc, + Object[] values, + int ind, + boolean isQuery, + boolean fromEncoded, + boolean allowUnresolved, + boolean encodePathSlash) { + + //CHECKSTYLE:ON + Map varValueMap = new HashMap<>(); + + // vars in set are properly ordered due to linking in hash set + Set uniqueVars = new LinkedHashSet<>(templ.getVariables()); + if (!allowUnresolved && values.length + alreadyResolvedTs.size() + alreadyResolvedTsEnc.size() + + alreadyResolvedTsPathEnc.size() < uniqueVars.size()) { + throw new IllegalArgumentException("Unresolved variables; only " + values.length + + " value(s) given for " + uniqueVars.size() + + " unique variable(s)"); + } + int idx = ind; + Set pathEncodeVars = alreadyResolvedTsPathEnc.isEmpty() && !encodePathSlash + ? Collections.emptySet() : new HashSet<>(); + for (String var : uniqueVars) { + try { + var = URLEncoder.encode(var, "UTF-8"); + } catch (UnsupportedEncodingException ex) { + // ignore + } + boolean resolvedPathVarHasToBeEncoded = alreadyResolvedTsPathEnc.containsKey(var); + boolean varValueHasToBeEncoded = resolvedPathVarHasToBeEncoded || alreadyResolvedTs.containsKey(var); + + Map resolved = !varValueHasToBeEncoded ? alreadyResolvedTsEnc + : resolvedPathVarHasToBeEncoded ? alreadyResolvedTsPathEnc : alreadyResolvedTs; + Object oval = resolved.isEmpty() ? null : resolved.remove(var); + boolean valueFromEncodedMap = false; + if (oval == null) { + if (allowUnresolved) { + continue; + } + oval = values[idx++]; + } else { + valueFromEncodedMap = resolved == alreadyResolvedTsEnc; + } + + if (oval == null) { + throw new IllegalArgumentException("No object for " + var); + } + String value = oval.toString(); + if (fromEncoded || valueFromEncodedMap) { + value = HttpUtils.encodePartiallyEncoded(value, isQuery); + } else { + value = isQuery ? HttpUtils.queryEncode(value) : HttpUtils.pathEncode(value); + } + + varValueMap.put(var, value); + + if (!isQuery && (resolvedPathVarHasToBeEncoded + || encodePathSlash && !varValueHasToBeEncoded)) { + pathEncodeVars.add(var); + } + + } + return templ.substitute(varValueMap, pathEncodeVars, allowUnresolved); + } + + //CHECKSTYLE:OFF + private String substituteMapped(String path, + Map varValueMap, + Map alreadyResolvedTs, + Map alreadyResolvedTsPathEnc, + Map alreadyResolvedTsEnc, + boolean isQuery, + boolean fromEncoded, + boolean encodePathSlash) { + //CHECKSTYLE:ON + URITemplate templ = URITemplate.createExactTemplate(path); + + Set uniqueVars = new HashSet<>(templ.getVariables()); + if (varValueMap.size() + alreadyResolvedTs.size() + alreadyResolvedTsEnc.size() + + alreadyResolvedTsPathEnc.size() < uniqueVars.size()) { + throw new IllegalArgumentException("Unresolved variables; only " + varValueMap.size() + + " value(s) given for " + uniqueVars.size() + + " unique variable(s)"); + } + + Set pathEncodeVars = alreadyResolvedTsPathEnc.isEmpty() && !encodePathSlash + ? Collections.emptySet() : new HashSet<>(); + + Map theMap = new LinkedHashMap<>(); + for (String var : uniqueVars) { + boolean isPathEncVar = !isQuery && alreadyResolvedTsPathEnc.containsKey(var); + + boolean isVarEncoded = isPathEncVar || alreadyResolvedTs.containsKey(var) ? false : true; + Map resolved = isVarEncoded ? alreadyResolvedTsEnc + : isPathEncVar ? alreadyResolvedTsPathEnc : alreadyResolvedTs; + Object oval = resolved.isEmpty() ? null : resolved.remove(var); + if (oval == null) { + oval = varValueMap.get(var); + } + if (oval == null) { + throw new IllegalArgumentException("No object for " + var); + } + if (fromEncoded) { + oval = HttpUtils.encodePartiallyEncoded(oval.toString(), isQuery); + } else { + oval = isQuery ? HttpUtils.queryEncode(oval.toString()) : HttpUtils.pathEncode(oval.toString()); + } + theMap.put(var, oval); + if (!isQuery && (isPathEncVar || encodePathSlash)) { + pathEncodeVars.add(var); + } + } + return templ.substitute(theMap, pathEncodeVars, false); + } + + @Override + public URI buildFromEncodedMap(Map map) throws IllegalArgumentException, + UriBuilderException { + + Map decodedMap = new HashMap<>(map.size()); + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() == null) { + throw new IllegalArgumentException("Value is null"); + } + String theValue = entry.getValue().toString(); + if (theValue.contains("/")) { + // protecting '/' from being encoded here assumes that a given value may constitute multiple + // path segments - very questionable especially given that queries and fragments may also + // contain template vars - technically this can be covered by checking where a given template + // var is coming from and act accordingly. Confusing nonetheless. + StringBuilder buf = new StringBuilder(); + String[] values = theValue.split("/"); + for (int i = 0; i < values.length; i++) { + buf.append(HttpUtils.encodePartiallyEncoded(values[i], false)); + if (i + 1 < values.length) { + buf.append('/'); + } + } + decodedMap.put(entry.getKey(), buf.toString()); + } else { + decodedMap.put(entry.getKey(), HttpUtils.encodePartiallyEncoded(theValue, false)); + } + + } + return doBuildFromMap(decodedMap, true, false); + } + + // CHECKSTYLE:OFF + @Override + public UriBuilder clone() { //NOPMD + UriBuilderImpl builder = new UriBuilderImpl(); + builder.scheme = scheme; + builder.userInfo = userInfo; + builder.port = port; + builder.host = host; + builder.paths = new ArrayList<>(paths); + builder.fragment = fragment; + builder.query = new MetadataMap<>(query); + builder.matrix = new MetadataMap<>(matrix); + builder.schemeSpecificPart = schemeSpecificPart; + builder.leadingSlash = leadingSlash; + builder.originalPathEmpty = originalPathEmpty; + builder.queryValueIsCollection = queryValueIsCollection; + builder.resolvedEncodedTemplates = + resolvedEncodedTemplates == null ? null : new HashMap(resolvedEncodedTemplates); + builder.resolvedTemplates = + resolvedTemplates == null ? null : new HashMap(resolvedTemplates); + builder.resolvedTemplatesPathEnc = + resolvedTemplatesPathEnc == null ? null : new HashMap(resolvedTemplatesPathEnc); + builder.useArraySyntaxForQueryParams = useArraySyntaxForQueryParams; + return builder; + } + // CHECKSTYLE:ON + + @Override + public UriBuilder fragment(String theFragment) throws IllegalArgumentException { + this.fragment = theFragment; + return this; + } + + @Override + public UriBuilder host(String theHost) throws IllegalArgumentException { + if ("".equals(theHost)) { + throw new IllegalArgumentException("Host cannot be empty"); + } + this.host = theHost; + return this; + } + + @Override + public UriBuilder path(@SuppressWarnings("rawtypes") Class resource) throws IllegalArgumentException { + if (resource == null) { + throw new IllegalArgumentException("resource is null"); + } + Class cls = resource; + Path ann = cls.getAnnotation(Path.class); + if (ann == null) { + throw new IllegalArgumentException("Class '" + resource.getCanonicalName() + + "' is not annotated with Path"); + } + // path(String) decomposes multi-segment path when necessary + return path(ann.value()); + } + + @Override + public UriBuilder path(@SuppressWarnings("rawtypes") Class resource, String method) + throws IllegalArgumentException { + if (resource == null) { + throw new IllegalArgumentException("resource is null"); + } + if (method == null) { + throw new IllegalArgumentException("method is null"); + } + Path foundAnn = null; + for (Method meth : resource.getMethods()) { + if (meth.getName().equals(method)) { + Path ann = meth.getAnnotation(Path.class); + if (foundAnn != null && ann != null) { + throw new IllegalArgumentException("Multiple Path annotations for '" + method + + "' overloaded method"); + } + foundAnn = ann; + } + } + if (foundAnn == null) { + throw new IllegalArgumentException("No Path annotation for '" + method + "' method"); + } + // path(String) decomposes multi-segment path when necessary + return path(foundAnn.value()); + } + + + @Override + public UriBuilder path(Method method) throws IllegalArgumentException { + if (method == null) { + throw new IllegalArgumentException("method is null"); + } + Path ann = method.getAnnotation(Path.class); + if (ann == null) { + throw new IllegalArgumentException("Method '" + method.getDeclaringClass().getCanonicalName() + "." + + method.getName() + "' is not annotated with Path"); + } + // path(String) decomposes multi-segment path when necessary + return path(ann.value()); + } + + @Override + public UriBuilder path(String path) throws IllegalArgumentException { + return doPath(path, true); + } + + private UriBuilder doPath(String path, boolean checkSegments) { + if (path == null) { + throw new IllegalArgumentException("path is null"); + } + if (isAbsoluteUriPath(path)) { + try { + URI uri = URI.create(path); + this.originalPathEmpty = StringUtils.isEmpty(uri.getPath()); + uri(uri); + } catch (IllegalArgumentException ex) { + if (!URITemplate.createExactTemplate(path).getVariables().isEmpty()) { + return uriAsTemplate(path); + } + String pathEncoded = HttpUtils.pathEncode(path); + // Bad hack to bypass the TCK usage of bogus URI with empty paths containing matrix parameters, + // which even URI class chokes upon; cheaper to do the following than try to challenge, + // given that URI RFC mentions the possibility of empty paths, though no word on the possibility of + // such empty paths having matrix parameters... + int schemeIndex = pathEncoded.indexOf("//"); + if (schemeIndex != -1) { + int pathComponentStart = pathEncoded.indexOf("/", schemeIndex + 2); + if (pathComponentStart == -1) { + this.originalPathEmpty = true; + pathComponentStart = pathEncoded.indexOf(';'); + if (pathComponentStart != -1) { + pathEncoded = pathEncoded.substring(0, pathComponentStart) + + "/" + pathEncoded.substring(pathComponentStart); + } + } + } + setUriParts(URI.create(pathEncoded)); + } + return this; + } + + if (paths.isEmpty()) { + leadingSlash = path.startsWith("/"); + } + + List segments; + if (checkSegments) { + segments = JAXRSUtils.getPathSegments(path, false, false); + } else { + segments = new ArrayList<>(); + path = path.replaceAll("/", "%2F"); + segments.add(new PathSegmentImpl(path, false)); + } + if (!paths.isEmpty() && !matrix.isEmpty()) { + PathSegment ps = paths.remove(paths.size() - 1); + paths.add(replacePathSegment(ps)); + } + paths.addAll(segments); + matrix.clear(); + if (!paths.isEmpty()) { + matrix = paths.get(paths.size() - 1).getMatrixParameters(); + } + return this; + } + + @Override + public UriBuilder port(int thePort) throws IllegalArgumentException { + if (thePort < 0 && thePort != -1) { + throw new IllegalArgumentException("Port cannot be negative"); + } + this.port = thePort; + return this; + } + + @Override + public UriBuilder scheme(String s) throws IllegalArgumentException { + scheme = s; + return this; + } + + @Override + public UriBuilder schemeSpecificPart(String ssp) throws IllegalArgumentException { + // scheme-specific part is whatever after ":" of URI + // see: http://en.wikipedia.org/wiki/URI_scheme + try { + if (scheme == null) { + scheme = "http"; + } + URI uri = new URI(scheme, ssp, fragment); + setUriParts(uri); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Wrong syntax of scheme-specific part", e); + } + return this; + } + + @Override + public UriBuilder uri(URI uri) throws IllegalArgumentException { + setUriParts(uri); + return this; + } + + @Override + public UriBuilder userInfo(String ui) throws IllegalArgumentException { + this.userInfo = ui; + return this; + } + + private void setUriParts(URI uri) { + if (uri == null) { + throw new IllegalArgumentException("uri is null"); + } + String theScheme = uri.getScheme(); + if (theScheme != null) { + scheme = theScheme; + } + String rawPath = uri.getRawPath(); + if (!uri.isOpaque() && schemeSpecificPart == null + && (theScheme != null || rawPath != null)) { + port = uri.getPort(); + host = uri.getHost(); + if (rawPath != null) { + setPathAndMatrix(rawPath); + } + String rawQuery = uri.getRawQuery(); + if (rawQuery != null) { + query = JAXRSUtils.getStructuredParams(rawQuery, "&", false, true); + } + userInfo = uri.getUserInfo(); + schemeSpecificPart = null; + } else { + schemeSpecificPart = uri.getSchemeSpecificPart(); + } + if (scheme != null && host == null && port == -1 && userInfo == null + && CollectionUtils.isEmpty(query) + && uri.getSchemeSpecificPart() != null + && !schemeSpecificPartMatchesUriPath(uri)) { + schemeSpecificPart = uri.getSchemeSpecificPart(); + } + String theFragment = uri.getFragment(); + if (theFragment != null) { + fragment = theFragment; + } + } + + private boolean schemeSpecificPartMatchesUriPath(final URI uri) { + return uri.getRawSchemeSpecificPart() != null + && uri.getPath() != null + && uri.getRawSchemeSpecificPart().equals("//" + uri.getPath()); + } + + private void setPathAndMatrix(String path) { + leadingSlash = !originalPathEmpty && path.startsWith("/"); + paths = JAXRSUtils.getPathSegments(path, false, false); + if (!paths.isEmpty()) { + matrix = paths.get(paths.size() - 1).getMatrixParameters(); + } else { + matrix.clear(); + } + } + + private String buildPath() { + StringBuilder sb = new StringBuilder(); + Iterator iter = paths.iterator(); + while (iter.hasNext()) { + PathSegment ps = iter.next(); + String p = ps.getPath(); + if (p.length() != 0 || !iter.hasNext()) { + p = URITemplate.createExactTemplate(p).encodeLiteralCharacters(false); + if (sb.length() == 0 && leadingSlash) { + sb.append('/'); + } else if (!p.startsWith("/") && sb.length() > 0) { + sb.append('/'); + } + sb.append(p); + if (iter.hasNext()) { + buildMatrix(sb, ps.getMatrixParameters()); + } + } + } + buildMatrix(sb, matrix); + return sb.toString(); + } + + private String buildQuery() { + return buildParams(query, '&'); + } + + @Override + public UriBuilder matrixParam(String name, Object... values) throws IllegalArgumentException { + if (name == null || values == null) { + throw new IllegalArgumentException("name or values is null"); + } + List list = matrix.get(name); + if (list == null) { + matrix.put(name, toStringList(true, values)); + } else { + list.addAll(toStringList(true, values)); + } + return this; + } + + @Override + public UriBuilder queryParam(String name, Object... values) throws IllegalArgumentException { + if (name == null || values == null) { + throw new IllegalArgumentException("name or values is null"); + } + List list = query.get(name); + if (list == null) { + query.put(name, toStringList(false, values)); + } else { + list.addAll(toStringList(false, values)); + } + return this; + } + + @Override + public UriBuilder replaceMatrix(String matrixValues) throws IllegalArgumentException { + String encodedMatrixValues = matrixValues != null ? HttpUtils.pathEncode(matrixValues) : null; + this.matrix = JAXRSUtils.getStructuredParams(encodedMatrixValues, ";", true, false); + return this; + } + + @Override + public UriBuilder replaceMatrixParam(String name, Object... values) throws IllegalArgumentException { + if (name == null) { + throw new IllegalArgumentException("name is null"); + } + if (values != null && values.length >= 1 && values[0] != null) { + matrix.put(name, toStringList(true, values)); + } else { + matrix.remove(name); + } + return this; + } + + @Override + public UriBuilder replacePath(String path) { + if (path == null) { + clearPathAndMatrix(); + } else if (isAbsoluteUriPath(path)) { + clearPathAndMatrix(); + uri(URI.create(path)); + } else { + setPathAndMatrix(path); + } + return this; + } + + private void clearPathAndMatrix() { + paths.clear(); + matrix.clear(); + } + + private boolean isAbsoluteUriPath(String path) { + // This is the cheapest way to figure out if a given path is an absolute + // URI with the http(s) scheme, more expensive way is to always convert + // a path to URI and check if it starts from some scheme or not + + // Given that the list of schemes can be open-ended it is recommended that + // UriBuilder.fromUri is called instead for schemes like 'file', 'jms', etc + // be supported though the use of non-http schemes for *building* new URIs + // is pretty limited in the context of working with JAX-RS services + + return path.startsWith("http:") || path.startsWith("https:"); + } + + @Override + public UriBuilder replaceQuery(String queryValue) throws IllegalArgumentException { + if (queryValue != null) { + // workaround to do with a conflicting and confusing requirement where spaces + // passed as part of replaceQuery are encoded as %20 while those passed as part + // of quertyParam are encoded as '+' + queryValue = queryValue.replace(" ", "%20"); + } + query = JAXRSUtils.getStructuredParams(queryValue, "&", false, true); + return this; + } + + @Override + public UriBuilder replaceQueryParam(String name, Object... values) throws IllegalArgumentException { + if (name == null) { + throw new IllegalArgumentException("name is null"); + } + if (values != null && values.length >= 1 && values[0] != null) { + query.put(name, toStringList(false, values)); + } else { + query.remove(name); + } + return this; + } + + @Override + public UriBuilder segment(String... segments) throws IllegalArgumentException { + if (segments == null) { + throw new IllegalArgumentException("Segments should not be null"); + } + for (String segment : segments) { + doPath(segment, false); + } + return this; + } + + /** + * Query or matrix params convertion from object values vararg to list of strings. No encoding is + * provided. + * + * @param values entry vararg values + * @return list of strings + * @throws IllegalArgumentException when one of values is null + */ + private List toStringList(boolean encodeSlash, Object... values) throws IllegalArgumentException { + List list = new ArrayList<>(); + if (values != null && values.length > 0) { + for (int i = 0; i < values.length; i++) { + Object value = values[i]; + if (value == null) { + throw new IllegalArgumentException("Null value on " + i + " position"); + } + String strValue = value.toString(); + if (encodeSlash) { + strValue = strValue.replaceAll("/", "%2F"); + } + list.add(strValue); + } + } else { + list.add(null); + } + return list; + } + + /** + * Builds param string for query part or matrix part of URI. + * + * @param map query or matrix multivalued map + * @param separator params separator, '&' for query ';' for matrix + * @return stringified params. + */ + private String buildParams(MultivaluedMap map, char separator) { + boolean isQuery = separator == '&'; + StringBuilder b = new StringBuilder(); + for (Iterator>> it = map.entrySet().iterator(); it.hasNext();) { + Map.Entry> entry = it.next(); + + // Expand query parameter as "name=v1,v2,v3" + if (isQuery && queryValueIsCollection) { + b.append(entry.getKey()); + if (useArraySyntaxForQueryParams) { + b.append("[]"); + } + b.append('='); + + for (Iterator sit = entry.getValue().iterator(); sit.hasNext();) { + String val = sit.next(); + + if (val != null) { + boolean templateValue = val.startsWith("{") && val.endsWith("}"); + if (!templateValue) { + val = HttpUtils.encodePartiallyEncoded(val, isQuery); + if (!isQuery) { + val = val.replaceAll("/", "%2F"); + } + } else { + val = URITemplate.createExactTemplate(val).encodeLiteralCharacters(isQuery); + } + + if (!val.isEmpty()) { + b.append(val); + } + } + if (sit.hasNext()) { + b.append(','); + } + } + + if (it.hasNext()) { + b.append(separator); + } + } else { + // Expand query parameter as "name=v1&name=v2&name=v3", or use dedicated + // separator for matrix parameters + for (Iterator sit = entry.getValue().iterator(); sit.hasNext();) { + String val = sit.next(); + b.append(entry.getKey()); + if (useArraySyntaxForQueryParams) { + b.append("[]"); + } + if (val != null) { + boolean templateValue = val.startsWith("{") && val.endsWith("}"); + if (!templateValue) { + val = HttpUtils.encodePartiallyEncoded(val, isQuery); + if (!isQuery) { + val = val.replaceAll("/", "%2F"); + } + } else { + val = URITemplate.createExactTemplate(val).encodeLiteralCharacters(isQuery); + } + b.append('='); + if (!val.isEmpty()) { + b.append(val); + } + } + if (sit.hasNext() || it.hasNext()) { + b.append(separator); + } + } + } + } + return b.length() > 0 ? b.toString() : null; + } + + /** + * Builds param string for matrix part of URI. + * + * @param sb buffer to add the matrix part to, will get ';' added if map is not empty + * @param map matrix multivalued map + */ + private void buildMatrix(StringBuilder sb, MultivaluedMap map) { + if (!map.isEmpty()) { + sb.append(';'); + sb.append(buildParams(map, ';')); + } + } + + private PathSegment replacePathSegment(PathSegment ps) { + StringBuilder sb = new StringBuilder(); + sb.append(ps.getPath()); + buildMatrix(sb, matrix); + return new PathSegmentImpl(sb.toString()); + } + + + public UriBuilder uri(String uriTemplate) throws IllegalArgumentException { + if (StringUtils.isEmpty(uriTemplate)) { + throw new IllegalArgumentException(); + } + try { + return uri(URI.create(uriTemplate)); + } catch (Exception ex) { + if (URITemplate.createExactTemplate(uriTemplate).getVariables().isEmpty()) { + throw new IllegalArgumentException(ex); + } + return uriAsTemplate(uriTemplate); + + } + } + + public UriBuilder uriAsTemplate(String uri) { + // This can be a start of replacing URI class Parser completely + // but it can be too complicated, the following code is needed for now + // to deal with URIs containing template variables. + int index = uri.indexOf(':'); + if (index != -1) { + this.scheme = uri.substring(0, index); + uri = uri.substring(index + 1); + if (uri.indexOf("//") == 0) { + uri = uri.substring(2); + index = uri.indexOf('/'); + if (index != -1) { + String[] schemePair = uri.substring(0, index).split(":"); + this.host = schemePair[0]; + this.port = schemePair.length == 2 ? Integer.parseInt(schemePair[1]) : -1; + + uri = uri.substring(index); + } + } + + } + String rawQuery = null; + index = uri.indexOf('?'); + if (index != -1) { + rawQuery = uri.substring(index + 1); + uri = uri.substring(0, index); + } + setPathAndMatrix(uri); + if (rawQuery != null) { + query = JAXRSUtils.getStructuredParams(rawQuery, "&", false, true); + } + + return this; + } + + //the clarified rules for encoding values of uri templates are: + // - encode each value contextually based on the URI component containing the template + // - in path templates, by default, encode also slashes (i.e. treat all path templates as + // part of a single path segment, to be consistent with @Path annotation templates) + // - for special cases when the slash encoding in path templates is not desired, + // users may use the newly added build methods to override the default behavior + + @Override + public URI build(Object[] vars, boolean encodePathSlash) throws IllegalArgumentException, UriBuilderException { + return doBuild(false, encodePathSlash, vars); + } + + @Override + public URI buildFromMap(Map map, boolean encodePathSlash) throws IllegalArgumentException, + UriBuilderException { + return doBuildFromMap(map, false, encodePathSlash); + } + + + @Override + public String toTemplate() { + UriParts parts = doBuildUriParts(false, false, true); + return buildUriString(parts.path, parts.query, parts.fragment); + } + + @Override + public UriBuilder resolveTemplate(String name, Object value) throws IllegalArgumentException { + return resolveTemplate(name, value, true); + } + + @Override + public UriBuilder resolveTemplate(String name, Object value, boolean encodePathSlash) + throws IllegalArgumentException { + return resolveTemplates(Collections.singletonMap(name, value), encodePathSlash); + } + + @Override + public UriBuilder resolveTemplates(Map values) throws IllegalArgumentException { + return resolveTemplates(values, true); + } + + @Override + public UriBuilder resolveTemplates(Map values, boolean encodePathSlash) + throws IllegalArgumentException { + if (encodePathSlash) { + resolvedTemplatesPathEnc = fillInResolveTemplates(resolvedTemplatesPathEnc, values); + } else { + resolvedTemplates = fillInResolveTemplates(resolvedTemplates, values); + } + return this; + } + + @Override + public UriBuilder resolveTemplateFromEncoded(String name, Object value) throws IllegalArgumentException { + return resolveTemplatesFromEncoded(Collections.singletonMap(name, value)); + } + + @Override + public UriBuilder resolveTemplatesFromEncoded(Map values) + throws IllegalArgumentException { + resolvedEncodedTemplates = fillInResolveTemplates(resolvedEncodedTemplates, values); + return this; + } + + private static Map fillInResolveTemplates(Map map, Map values) + throws IllegalArgumentException { + if (values == null) { + throw new IllegalArgumentException(); + } + if (map == null) { + map = new LinkedHashMap<>(); + } + + for (Map.Entry entry : values.entrySet()) { + if (entry.getKey() == null || entry.getValue() == null) { + throw new IllegalArgumentException(); + } + map.put(entry.getKey(), entry.getValue()); + } + return map; + } + + private static class UriParts { + String path; + String query; + String fragment; + + UriParts(String path, String query, String fragment) { + this.path = path; + this.query = query; + this.fragment = fragment; + } + } +} diff --git a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/UriBuilderImplTest.java b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/UriBuilderImplTest.java index fb72e8048aa..f771ca6c6ed 100644 --- a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/UriBuilderImplTest.java +++ b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/UriBuilderImplTest.java @@ -1721,6 +1721,52 @@ public void testUseArraySyntaxForQueryParamsBuildFromEncodedNormalize() { assertEquals("foo[]=v1&foo[]=v2&foo[]=v3", uri.getQuery()); } + @Test + public void testURIWithSpecialCharacters() { + String expected = "http://localhost:8080/xy\""; + URI uri = UriBuilder.fromUri("http://localhost:8080").path("xy\"").build(); + assertEquals(expected, uri.toString()); + } + + @Test + public void testURIWithSpecialCharacters2() { + String expected = "http://localhost:8080/xy\t"; + URI uri = UriBuilder.fromUri("http://localhost:8080").path("xy\t").buildFromEncoded(); + assertEquals(expected, uri.toString()); + } + + @Test + public void testURIWithSpecialCharactersPreservePath() { + String expected = "http://localhost:8080/xy/\"/abc"; + URI uri = UriBuilder.fromPath("") + .replacePath("http://localhost:8080") + .path("/{a}/{b}") + .buildFromEncoded("xy", "\"", "abc"); + assertEquals(expected, uri.toString()); + } + + @Test + public void testURIWithSpecialCharactersPreservePath2() { + + String expected = "http://localhost:8080/xy/\t/abc"; + URI uri = UriBuilder.fromPath("") + .replacePath("http://localhost:8080") + .path("/{a}/{b}") + .buildFromEncoded("xy", "\t", "abc"); + assertEquals(expected, uri.toString()); + } + + @Test + public void testIllegalURI() { + String path = "invalidpath"; + try { + URI uri = UriBuilder.fromPath(path).build(); + assertEquals(null, uri.toString()); + } catch (Exception ex) { + //or exception Expected + } + } + @Path(value = "/TestPath") public static class TestPath {