diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpThingHandler.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpThingHandler.java index 8947852e11057..19bfbb9bfbb87 100644 --- a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpThingHandler.java +++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/HttpThingHandler.java @@ -369,7 +369,7 @@ private void sendHttpValue(String commandUrl, String command) { private void sendHttpValue(String commandUrl, String command, boolean isRetry) { try { // format URL - URI uri = Util.uriFromString(String.format(commandUrl, new Date(), command)); + URI uri = Util.uriFromString(Util.wrappedStringFormat(commandUrl, new Date(), command)); // build request rateLimitedHttpClient.newPriorityRequest(uri, config.commandMethod, command, config.contentType) diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/Util.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/Util.java index b0c6ff69e9a69..2c490ba5d5a71 100644 --- a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/Util.java +++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/Util.java @@ -18,6 +18,8 @@ import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -34,8 +36,10 @@ @NonNullByDefault public class Util { + public static final Pattern FORMAT_REPLACE_PATTERN = Pattern.compile("%\\d\\$[^%]+"); + /** - * create a log string from a {@link org.eclipse.jetty.client.api.Request} + * Create a log string from a {@link org.eclipse.jetty.client.api.Request} * * @param request the request to log * @return the string representing the request @@ -51,17 +55,33 @@ public static String requestToLogString(Request request) { } /** - * create an URI from a string, escaping all necessary characters + * Create a URI from a string, escaping all necessary characters * * @param s the URI as unescaped string * @return URI corresponding to the input string - * @throws MalformedURLException if parameter is not an URL - * @throws URISyntaxException if parameter could not be converted to an URI + * @throws MalformedURLException if parameter is not a URL + * @throws URISyntaxException if parameter could not be converted to a URI */ public static URI uriFromString(String s) throws MalformedURLException, URISyntaxException { URL url = new URL(s); URI uri = new URI(url.getProtocol(), url.getUserInfo(), IDN.toASCII(url.getHost()), url.getPort(), url.getPath(), url.getQuery(), url.getRef()); - return URI.create(uri.toASCIIString()); + return URI.create(uri.toASCIIString().replace("+", "%2B")); + } + + /** + * Format a string using {@link String#format(String, Object...)} but allow non-format percent characters + * + * The {@param inputString} is checked for format patterns ({@code %$}) and passes only those to the + * {@link String#format(String, Object...)} method. This avoids format errors due to other percent characters in the + * string. + * + * @param inputString the input string, potentially containing format instructions + * @param params an array of parameters to be passed to the splitted input string + * @return the formatted string + */ + public static String wrappedStringFormat(String inputString, Object... params) { + Matcher replaceMatcher = FORMAT_REPLACE_PATTERN.matcher(inputString); + return replaceMatcher.replaceAll(matchResult -> String.format(matchResult.group(), params)); } } diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/http/RefreshingUrlCache.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/http/RefreshingUrlCache.java index 2045f34cdfd6a..da2fbc264e3c6 100644 --- a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/http/RefreshingUrlCache.java +++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/http/RefreshingUrlCache.java @@ -108,7 +108,7 @@ private void refresh(boolean isRetry) { // format URL try { - URI uri = Util.uriFromString(String.format(this.url, new Date())); + URI uri = Util.uriFromString(Util.wrappedStringFormat(this.url, new Date())); logger.trace("Requesting refresh (retry={}) from '{}' with timeout {}ms", isRetry, uri, timeout); httpClient.newRequest(uri, httpMethod, httpContent, httpContentType).thenAccept(request -> { diff --git a/bundles/org.openhab.binding.http/src/test/java/org/openhab/binding/http/UtilTest.java b/bundles/org.openhab.binding.http/src/test/java/org/openhab/binding/http/UtilTest.java index b68e799a6a21f..b5a8d3e40a1bd 100644 --- a/bundles/org.openhab.binding.http/src/test/java/org/openhab/binding/http/UtilTest.java +++ b/bundles/org.openhab.binding.http/src/test/java/org/openhab/binding/http/UtilTest.java @@ -12,11 +12,14 @@ */ package org.openhab.binding.http; +import static org.junit.jupiter.api.Assertions.assertEquals; + import java.net.MalformedURLException; import java.net.URISyntaxException; +import java.time.Instant; +import java.util.Date; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.openhab.binding.http.internal.Util; @@ -31,30 +34,61 @@ public class UtilTest { @Test public void uriUTF8InHostnameEncodeTest() throws MalformedURLException, URISyntaxException { String s = "https://foöo.bar/zhu.html?str=zin&tzz=678"; - Assertions.assertEquals("https://xn--foo-tna.bar/zhu.html?str=zin&tzz=678", Util.uriFromString(s).toString()); + assertEquals("https://xn--foo-tna.bar/zhu.html?str=zin&tzz=678", Util.uriFromString(s).toString()); } @Test public void uriUTF8InPathEncodeTest() throws MalformedURLException, URISyntaxException { String s = "https://foo.bar/zül.html?str=zin"; - Assertions.assertEquals("https://foo.bar/z%C3%BCl.html?str=zin", Util.uriFromString(s).toString()); + assertEquals("https://foo.bar/z%C3%BCl.html?str=zin", Util.uriFromString(s).toString()); } @Test public void uriUTF8InQueryEncodeTest() throws MalformedURLException, URISyntaxException { String s = "https://foo.bar/zil.html?str=zän"; - Assertions.assertEquals("https://foo.bar/zil.html?str=z%C3%A4n", Util.uriFromString(s).toString()); + assertEquals("https://foo.bar/zil.html?str=z%C3%A4n", Util.uriFromString(s).toString()); } @Test public void uriSpaceInPathEncodeTest() throws MalformedURLException, URISyntaxException { String s = "https://foo.bar/z l.html?str=zun"; - Assertions.assertEquals("https://foo.bar/z%20l.html?str=zun", Util.uriFromString(s).toString()); + assertEquals("https://foo.bar/z%20l.html?str=zun", Util.uriFromString(s).toString()); } @Test public void uriSpaceInQueryEncodeTest() throws MalformedURLException, URISyntaxException { String s = "https://foo.bar/zzl.html?str=z n"; - Assertions.assertEquals("https://foo.bar/zzl.html?str=z%20n", Util.uriFromString(s).toString()); + assertEquals("https://foo.bar/zzl.html?str=z%20n", Util.uriFromString(s).toString()); + } + + @Test + public void uriPlusInQueryEncodeTest() throws MalformedURLException, URISyntaxException { + String s = "https://foo.bar/zzl.html?str=z+n"; + assertEquals("https://foo.bar/zzl.html?str=z%2Bn", Util.uriFromString(s).toString()); + } + + @Test + public void uriAlreadyPartlyEscapedTest() throws MalformedURLException, URISyntaxException { + String s = "https://foo.bar/zzl.html?p=field%2Bvalue&foostatus=This is a test String&date=2024- 07-01"; + assertEquals( + "https://foo.bar/zzl.html?p=field%252Bvalue&foostatus=This%20is%20a%20test%20String&date=2024-%20%2007-01", + Util.uriFromString(s).toString()); + } + + @Test + public void wrappedStringFormatDateTest() { + String formatString = "https://foo.bar/zzl.html?p=field%2Bvalue&date=%1$tY-%1$4tm-%1$td"; + Date testDate = Date.from(Instant.parse("2024-07-01T10:00:00.000Z")); + assertEquals("https://foo.bar/zzl.html?p=field%2Bvalue&date=2024- 07-01", + Util.wrappedStringFormat(formatString, testDate)); + } + + @Test + public void wrappedStringFormatDateAndCommandTest() { + String formatString = "https://foo.bar/zzl.html?p=field%2Bvalue&foostatus=%2$s&date=%1$tY-%1$4tm-%1$td"; + Date testDate = Date.from(Instant.parse("2024-07-01T10:00:00.000Z")); + String testCommand = "This is a test String"; + assertEquals("https://foo.bar/zzl.html?p=field%2Bvalue&foostatus=This is a test String&date=2024- 07-01", + Util.wrappedStringFormat(formatString, testDate, testCommand)); } }