This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/camel.git
commit 3b6a6f3bd27f526e4c994154abbb446d8931c22e Author: Claus Ibsen <claus.ib...@gmail.com> AuthorDate: Sun Feb 23 19:43:30 2020 +0100 CAMEL-14609: camel-core - optimize --- .../java/org/apache/camel/util/StringHelper.java | 5 +- .../java/org/apache/camel/util/URIScanner.java | 108 +++++++++++---------- .../java/org/apache/camel/util/URISupport.java | 80 ++++++++++----- .../camel/util/UnsafeUriCharactersEncoder.java | 55 +++++------ .../apache/camel/itest/jmh/NormalizeUriTest.java | 100 +++++++++++++++++++ 5 files changed, 242 insertions(+), 106 deletions(-) diff --git a/core/camel-util/src/main/java/org/apache/camel/util/StringHelper.java b/core/camel-util/src/main/java/org/apache/camel/util/StringHelper.java index f5e2669..d955e58 100644 --- a/core/camel-util/src/main/java/org/apache/camel/util/StringHelper.java +++ b/core/camel-util/src/main/java/org/apache/camel/util/StringHelper.java @@ -73,12 +73,13 @@ public final class StringHelper { * @return number of times char is located in the string */ public static int countChar(String s, char ch) { - if (ObjectHelper.isEmpty(s)) { + if (s == null || s.isEmpty()) { return 0; } int matches = 0; - for (int i = 0; i < s.length(); i++) { + int len = s.length(); + for (int i = 0; i < len; i++) { char c = s.charAt(i); if (ch == c) { matches++; diff --git a/core/camel-util/src/main/java/org/apache/camel/util/URIScanner.java b/core/camel-util/src/main/java/org/apache/camel/util/URIScanner.java index 2ad8f5f..014065b 100644 --- a/core/camel-util/src/main/java/org/apache/camel/util/URIScanner.java +++ b/core/camel-util/src/main/java/org/apache/camel/util/URIScanner.java @@ -37,41 +37,31 @@ class URIScanner { private static final String RAW_START_ONE = RAW_TOKEN_PREFIX + RAW_TOKEN_START[0]; private static final String RAW_START_TWO = RAW_TOKEN_PREFIX + RAW_TOKEN_START[1]; + // TODO: when upgrading to JDK11 as minimum then use java.nio.Charset + private static final String CHARSET = "UTF-8"; + private enum Mode { KEY, VALUE } private static final char END = '\u0000'; - private final String charset; private final StringBuilder key; private final StringBuilder value; private Mode mode; private boolean isRaw; private char rawTokenEnd; - public URIScanner(String charset) { - this.charset = charset; - key = new StringBuilder(); - value = new StringBuilder(); + public URIScanner() { + this.key = new StringBuilder(); + this.value = new StringBuilder(); } private void initState() { - mode = Mode.KEY; - key.setLength(0); - value.setLength(0); - isRaw = false; - } - - private String getDecodedKey() throws UnsupportedEncodingException { - return URLDecoder.decode(key.toString(), charset); - } - - private String getDecodedValue() throws UnsupportedEncodingException { - // need to replace % with %25 - String s = StringHelper.replaceAll(value.toString(), "%", "%25"); - String answer = URLDecoder.decode(s, charset); - return answer; + this.mode = Mode.KEY; + this.key.setLength(0); + this.value.setLength(0); + this.isRaw = false; } public Map<String, Object> parseQuery(String uri, boolean useRaw) throws URISyntaxException { @@ -85,12 +75,13 @@ class URIScanner { initState(); // parse the uri parameters char by char - for (int i = 0; i < uri.length(); i++) { + int len = uri.length(); + for (int i = 0; i < len; i++) { // current char char ch = uri.charAt(i); // look ahead of the next char char next; - if (i <= uri.length() - 2) { + if (i <= len - 2) { next = uri.charAt(i + 1); } else { next = END; @@ -165,13 +156,19 @@ class URIScanner { return false; } - String start = value.substring(0, 4); - if (start.startsWith(RAW_START_ONE)) { - rawTokenEnd = RAW_TOKEN_END[0]; - return true; - } else if (start.startsWith(RAW_START_TWO)) { - rawTokenEnd = RAW_TOKEN_END[1]; - return true; + // optimize to not create new objects + char char1 = value.charAt(0); + char char2 = value.charAt(1); + char char3 = value.charAt(2); + char char4 = value.charAt(3); + if (char1 == 'R' && char2 == 'A' && char3 == 'W') { + if (char4 == '(') { + rawTokenEnd = RAW_TOKEN_END[0]; + return true; + } else if (char4 == '{') { + rawTokenEnd = RAW_TOKEN_END[1]; + return true; + } } return false; @@ -183,8 +180,14 @@ class URIScanner { } private void addParameter(Map<String, Object> answer, boolean isRaw) throws UnsupportedEncodingException { - String name = getDecodedKey(); - String value = isRaw ? this.value.toString() : getDecodedValue(); + String name = URLDecoder.decode(key.toString(), CHARSET); + String text; + if (isRaw) { + text = value.toString(); + } else { + String s = StringHelper.replaceAll(value.toString(), "%", "%25"); + text = URLDecoder.decode(s, CHARSET); + } // does the key already exist? if (answer.containsKey(name)) { @@ -202,13 +205,14 @@ class URIScanner { list.add(s); } } - list.add(value); + list.add(text); answer.put(name, list); } else { - answer.put(name, value); + answer.put(name, text); } } + @SuppressWarnings("unchecked") public static List<Pair<Integer>> scanRaw(String str) { if (str == null || ObjectHelper.isEmpty(str)) { return Collections.EMPTY_LIST; @@ -224,7 +228,6 @@ class URIScanner { char tokenEnd = RAW_TOKEN_END[i]; if (str.startsWith(tokenStart, start)) { offset = scanRawToEnd(str, start, tokenStart, tokenEnd, answer); - continue; } } start = str.indexOf(RAW_TOKEN_PREFIX, offset); @@ -248,29 +251,32 @@ class URIScanner { return end + 1; } - public static boolean isRaw(int index, List<Pair<Integer>> pairs) { - for (Pair<Integer> pair : pairs) { - if (index < pair.getLeft()) { - return false; - } - if (index <= pair.getRight()) { - return true; - } - } - return false; - } - public static String resolveRaw(String str) { int len = str.length(); if (len <= 4) { return null; } - int last = len - 1; - // check for end is quicker than for start - if (str.charAt(last) == ')' && str.startsWith(RAW_START_ONE) - || str.charAt(last) == '}' && str.startsWith(RAW_START_TWO)) { - return str.substring(4, last); + int endPos = len - 1; + char last = str.charAt(endPos); + + // optimize to not create new objects + if (last == ')') { + char char1 = str.charAt(0); + char char2 = str.charAt(1); + char char3 = str.charAt(2); + char char4 = str.charAt(3); + if (char1 == 'R' && char2 == 'A' && char3 == 'W' && char4 == '(') { + return str.substring(4, endPos); + } + } else if (last == '}') { + char char1 = str.charAt(0); + char char2 = str.charAt(1); + char char3 = str.charAt(2); + char char4 = str.charAt(3); + if (char1 == 'R' && char2 == 'A' && char3 == 'W' && char4 == '{') { + return str.substring(4, endPos); + } } // not RAW value diff --git a/core/camel-util/src/main/java/org/apache/camel/util/URISupport.java b/core/camel-util/src/main/java/org/apache/camel/util/URISupport.java index a4798de..6b0eb19 100644 --- a/core/camel-util/src/main/java/org/apache/camel/util/URISupport.java +++ b/core/camel-util/src/main/java/org/apache/camel/util/URISupport.java @@ -21,10 +21,12 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.util.ArrayList; +import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.regex.Pattern; /** @@ -184,7 +186,7 @@ public final class URISupport { throw new URISyntaxException(uri, "Invalid uri syntax: Trailing & marker found. " + "Check the uri and remove the trailing & marker."); } - URIScanner scanner = new URIScanner(CHARSET); + URIScanner scanner = new URIScanner(); return scanner.parseQuery(uri, useRaw); } @@ -226,7 +228,19 @@ public final class URISupport { * @see #RAW_TOKEN_END */ public static boolean isRaw(int index, List<Pair<Integer>> pairs) { - return URIScanner.isRaw(index, pairs); + if (pairs == null || pairs.isEmpty()) { + return false; + } + + for (Pair<Integer> pair : pairs) { + if (index < pair.getLeft()) { + return false; + } + if (index <= pair.getRight()) { + return true; + } + } + return false; } /** @@ -237,22 +251,33 @@ public final class URISupport { * @throws URISyntaxException is thrown if uri has invalid syntax. */ public static Map<String, Object> parseParameters(URI uri) throws URISyntaxException { + String query = prepareQuery(uri); + if (query == null) { + // empty an empty map + return new LinkedHashMap<>(0); + } + return parseQuery(query); + } + + public static String prepareQuery(URI uri) { String query = uri.getQuery(); if (query == null) { String schemeSpecificPart = uri.getSchemeSpecificPart(); int idx = schemeSpecificPart.indexOf('?'); if (idx < 0) { - // return an empty map - return new LinkedHashMap<>(0); + return null; } else { query = schemeSpecificPart.substring(idx + 1); } - } else { - query = stripPrefix(query, "?"); + } else if (query.indexOf('?') == 0) { + // skip leading query + query = query.substring(1); } - return parseQuery(query); + return query; } + + /** * Traverses the given parameters, and resolve any parameter values which * uses the RAW token syntax: <tt>key=RAW(value)</tt>. This method will then @@ -380,11 +405,15 @@ public final class URISupport { */ @SuppressWarnings("unchecked") public static String createQueryString(Map<String, Object> options) throws URISyntaxException { + return createQueryString(options.keySet(), options); + } + + public static String createQueryString(Collection<String> sortedKeys, Map<String, Object> options) throws URISyntaxException { try { if (options.size() > 0) { StringBuilder rc = new StringBuilder(); boolean first = true; - for (Object o : options.keySet()) { + for (Object o : sortedKeys) { if (first) { first = false; } else { @@ -495,8 +524,8 @@ public final class URISupport { public static String normalizeUri(String uri) throws URISyntaxException, UnsupportedEncodingException { URI u = new URI(UnsafeUriCharactersEncoder.encode(uri, true)); - String path = u.getSchemeSpecificPart(); String scheme = u.getScheme(); + String path = u.getSchemeSpecificPart(); // not possible to normalize if (scheme == null || path == null) { @@ -513,7 +542,7 @@ public final class URISupport { path = path.substring(0, idx); } - if (u.getScheme().startsWith("http")) { + if (scheme.startsWith("http")) { path = UnsafeUriCharactersEncoder.encodeHttpURI(path); } else { path = UnsafeUriCharactersEncoder.encode(path); @@ -528,8 +557,9 @@ public final class URISupport { // this to work out of the box with Camel, and hence we need to fix it // for them String userInfoPath = path; - if (userInfoPath.contains("/")) { - userInfoPath = userInfoPath.substring(0, userInfoPath.indexOf("/")); + idx = userInfoPath.indexOf('/'); + if (idx != -1) { + userInfoPath = userInfoPath.substring(0, idx); } if (StringHelper.countChar(userInfoPath, '@') > 1) { int max = userInfoPath.lastIndexOf('@'); @@ -543,23 +573,25 @@ public final class URISupport { } // in case there are parameters we should reorder them - Map<String, Object> parameters = URISupport.parseParameters(u); - if (parameters.isEmpty()) { + String query = prepareQuery(u); + if (query == null) { // no parameters then just return return buildUri(scheme, path, null); } else { - // reorder parameters a..z - List<String> keys = new ArrayList<>(parameters.keySet()); - keys.sort(null); + Map<String, Object> parameters = URISupport.parseQuery(query, false, false); + if (parameters.size() == 1) { + // only 1 parameter need to create new query string + query = URISupport.createQueryString(parameters); + return buildUri(scheme, path, query); + } else { + // reorder parameters a..z + List<String> keys = new ArrayList<>(parameters.keySet()); + keys.sort(null); - Map<String, Object> sorted = new LinkedHashMap<>(parameters.size()); - for (String key : keys) { - sorted.put(key, parameters.get(key)); + // build uri object with sorted parameters + query = URISupport.createQueryString(keys, parameters); + return buildUri(scheme, path, query); } - - // build uri object with sorted parameters - String query = URISupport.createQueryString(sorted); - return buildUri(scheme, path, query); } } diff --git a/core/camel-util/src/main/java/org/apache/camel/util/UnsafeUriCharactersEncoder.java b/core/camel-util/src/main/java/org/apache/camel/util/UnsafeUriCharactersEncoder.java index d5030f2..d02de20 100644 --- a/core/camel-util/src/main/java/org/apache/camel/util/UnsafeUriCharactersEncoder.java +++ b/core/camel-util/src/main/java/org/apache/camel/util/UnsafeUriCharactersEncoder.java @@ -16,7 +16,6 @@ */ package org.apache.camel.util; -import java.util.ArrayList; import java.util.BitSet; import java.util.List; @@ -93,42 +92,44 @@ public final class UnsafeUriCharactersEncoder { // Just skip the encode for isRAW part public static String encode(String s, BitSet unsafeCharacters, boolean checkRaw) { - List<Pair<Integer>> rawPairs; - if (checkRaw) { - rawPairs = URISupport.scanRaw(s); - } else { - rawPairs = new ArrayList<>(); - } - - int n = s == null ? 0 : s.length(); - if (n == 0) { + if (s == null || s.length() == 0) { return s; } - // First check whether we actually need to encode + // first check whether we actually need to encode char[] chars = s.toCharArray(); - for (int i = 0;;) { + int len = chars.length; + boolean safe = true; + for (char ch : chars) { // just deal with the ascii character - if (chars[i] > 0 && chars[i] < 128) { - if (unsafeCharacters.get(chars[i])) { - break; - } - } - if (++i >= chars.length) { - return s; + if (ch > 0 && ch < 128 && unsafeCharacters.get(ch)) { + safe = false; + break; } } + if (safe) { + return s; + } + + List<Pair<Integer>> rawPairs = null; + if (checkRaw) { + rawPairs = URISupport.scanRaw(s); + } + + + // add a bit of extra space as initial capacity + int initial = len + 8; // okay there are some unsafe characters so we do need to encode // see details at: http://en.wikipedia.org/wiki/Url_encode - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < chars.length; i++) { + StringBuilder sb = new StringBuilder(initial); + for (int i = 0; i < len; i++) { char ch = chars[i]; if (ch > 0 && ch < 128 && unsafeCharacters.get(ch)) { // special for % sign as it may be a decimal encoded value if (ch == '%') { - char next = i + 1 < chars.length ? chars[i + 1] : ' '; - char next2 = i + 2 < chars.length ? chars[i + 2] : ' '; + char next = i + 1 < len ? chars[i + 1] : ' '; + char next2 = i + 2 < len ? chars[i + 2] : ' '; if (isHexDigit(next) && isHexDigit(next2) && !URISupport.isRaw(i, rawPairs)) { // its already encoded (decimal encoded) so just append as is @@ -155,12 +156,8 @@ public final class UnsafeUriCharactersEncoder { } private static boolean isHexDigit(char ch) { - for (char hex : HEX_DIGITS) { - if (hex == ch) { - return true; - } - } - return false; + // 0..9 A..F a..f + return ch >= 48 && ch <= 57 || ch >= 65 && ch <= 70 || ch >= 97 && ch <= 102; } } diff --git a/tests/camel-jmh/src/test/java/org/apache/camel/itest/jmh/NormalizeUriTest.java b/tests/camel-jmh/src/test/java/org/apache/camel/itest/jmh/NormalizeUriTest.java new file mode 100644 index 0000000..8f9a6e2 --- /dev/null +++ b/tests/camel-jmh/src/test/java/org/apache/camel/itest/jmh/NormalizeUriTest.java @@ -0,0 +1,100 @@ +/* + * 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.camel.itest.jmh; + +import java.util.concurrent.TimeUnit; + +import org.apache.camel.util.URISupport; +import org.junit.Test; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.openjdk.jmh.runner.options.TimeValue; + +/** + * Tests the {@link org.apache.camel.util.URISupport#normalizeUri(String)}. + * <p/> + * Thanks to this SO answer: https://stackoverflow.com/questions/30485856/how-to-run-jmh-from-inside-junit-tests + */ +public class NormalizeUriTest { + + @Test + public void launchBenchmark() throws Exception { + Options opt = new OptionsBuilder() + // Specify which benchmarks to run. + // You can be more specific if you'd like to run only one benchmark per test. + .include(this.getClass().getName() + ".*") + // Set the following options as needed + .mode(Mode.All) + .timeUnit(TimeUnit.MICROSECONDS) + .warmupTime(TimeValue.seconds(1)) + .warmupIterations(2) + .measurementTime(TimeValue.seconds(1)) + .measurementIterations(2) + .threads(2) + .forks(1) + .shouldFailOnError(true) + .shouldDoGC(false) + .measurementBatchSize(100000) + .build(); + + new Runner(opt).run(); + } + + // The JMH samples are the best documentation for how to use it + // http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/ + @State(Scope.Thread) + public static class BenchmarkState { + @Setup(Level.Trial) + public void initialize() { + } + } + + @Benchmark + public void benchmark(ContainsIgnoreCaseTest.BenchmarkState state, Blackhole bh) throws Exception { + bh.consume(URISupport.normalizeUri("log:foo")); + bh.consume(URISupport.normalizeUri("log:foo?level=INFO&logMask=false&exchangeFormatter=#myFormatter")); + bh.consume(URISupport.normalizeUri("smtp://localhost?password=secret&username=davsclaus")); + bh.consume(URISupport.normalizeUri("seda:foo?concurrentConsumer=2")); + bh.consume(URISupport.normalizeUri("irc:someserver/#camel?user=davsclaus")); + bh.consume(URISupport.normalizeUri("http:www.google.com?q=Camel")); + bh.consume(URISupport.normalizeUri("http://www.google.com?q=S%C3%B8ren%20Hansen")); + bh.consume(URISupport.normalizeUri("smtp://localhost?to=foo&to=bar&from=me&from=you")); + bh.consume(URISupport.normalizeUri("ftp://us%40r:t%st@localhost:21000/tmp3/camel?foo=us@r")); + bh.consume(URISupport.normalizeUri("ftp://us%40r:t%25st@localhost:21000/tmp3/camel?foo=us@r")); + bh.consume(URISupport.normalizeUri("ftp://us@r:t%st@localhost:21000/tmp3/camel?foo=us@r")); + bh.consume(URISupport.normalizeUri("ftp://us@r:t%25st@localhost:21000/tmp3/camel?foo=us@r")); + bh.consume(URISupport.normalizeUri("xmpp://camel-user@localhost:123/test-user@localhost?password=secret&serviceName=someCoolChat")); + bh.consume(URISupport.normalizeUri("xmpp://camel-user@localhost:123/test-user@localhost?password=RAW(++?w0rd)&serviceName=some chat")); + bh.consume(URISupport.normalizeUri("xmpp://camel-user@localhost:123/test-user@localhost?password=RAW(foo %% bar)&serviceName=some chat")); + bh.consume(URISupport.normalizeUri("xmpp://camel-user@localhost:123/test-user@localhost?password=RAW{++?w0rd}&serviceName=some chat")); + bh.consume(URISupport.normalizeUri("xmpp://camel-user@localhost:123/test-user@localhost?password=RAW{foo %% bar}&serviceName=some chat")); + } + + @Benchmark + public void sorted(ContainsIgnoreCaseTest.BenchmarkState state, Blackhole bh) throws Exception { + bh.consume(URISupport.normalizeUri("log:foo?zzz=123&xxx=222&hhh=444&aaa=tru&d=yes&cc=no&Camel=awesome&foo.hey=bar&foo.bar=blah")); + } + +}