From f0f3d6c3a89e01fdebf3d6367427767bef21ed49 Mon Sep 17 00:00:00 2001 From: Francesco Nigro Date: Sun, 27 Aug 2023 17:18:24 +0200 Subject: [PATCH 1/5] Reduce String/StringBuilder allocations on ConfigMappings::mapConfiguration --- .../config/common/utils/StringUtil.java | 54 ++++++- .../config/ConfigMappingProvider.java | 133 ++++++++++-------- .../java/io/smallrye/config/NameIterator.java | 60 ++++++-- 3 files changed, 175 insertions(+), 72 deletions(-) diff --git a/common/src/main/java/io/smallrye/config/common/utils/StringUtil.java b/common/src/main/java/io/smallrye/config/common/utils/StringUtil.java index a66064013..1d9695fb7 100644 --- a/common/src/main/java/io/smallrye/config/common/utils/StringUtil.java +++ b/common/src/main/java/io/smallrye/config/common/utils/StringUtil.java @@ -88,14 +88,62 @@ public static String[] split(String text) { return list.toArray(NO_STRINGS); } + private static boolean isAsciiLetterOrDigit(char c) { + return 'a' <= c && c <= 'z' || + 'A' <= c && c <= 'Z' || + '0' <= c && c <= '9'; + } + + private static boolean isAsciiUpperCase(char c) { + return c >= 'A' && c <= 'Z'; + } + + private static char toAsciiLowerCase(char c) { + return isAsciiUpperCase(c) ? (char) (c + 32) : c; + } + + public static boolean equalsIgnoreCaseReplacingNonAlphanumericByUnderscores(final String property, + CharSequence mappedProperty) { + int length = mappedProperty.length(); + if (property.length() != mappedProperty.length()) { + // special-case/slow-path + if (property.length() != mappedProperty.length() + 1) { + return false; + } + if (mappedProperty.charAt(length - 1) == '"' && + property.charAt(length - 1) == '_' && property.charAt(length) == '_') { + length = mappedProperty.length() - 1; + } else { + return false; + } + } + for (int i = 0; i < length; i++) { + char ch = mappedProperty.charAt(i); + if (!isAsciiLetterOrDigit(ch)) { + if (property.charAt(i) != '_') { + return false; + } + continue; + } + final char pCh = property.charAt(i); + // in theory property should be ascii too, but better play safe + if (pCh < 128) { + if (toAsciiLowerCase(pCh) != toAsciiLowerCase(ch)) { + return false; + } + } else if (Character.toLowerCase(property.charAt(i)) != Character.toLowerCase(ch)) { + return false; + } + } + return true; + } + public static String replaceNonAlphanumericByUnderscores(final String name) { int length = name.length(); StringBuilder sb = new StringBuilder(length); for (int i = 0; i < length; i++) { char c = name.charAt(i); - if ('a' <= c && c <= 'z' || - 'A' <= c && c <= 'Z' || - '0' <= c && c <= '9') { + if (isAsciiLetterOrDigit(c)) { sb.append(c); } else { sb.append('_'); diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java index c51dfe294..8eeeb02f6 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java @@ -9,6 +9,7 @@ import static io.smallrye.config.ConfigMappingLoader.getConfigMapping; import static io.smallrye.config.ConfigMappingLoader.getConfigMappingClass; import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN; +import static io.smallrye.config.common.utils.StringUtil.equalsIgnoreCaseReplacingNonAlphanumericByUnderscores; import static io.smallrye.config.common.utils.StringUtil.replaceNonAlphanumericByUnderscores; import static java.lang.Integer.parseInt; @@ -67,14 +68,16 @@ final class ConfigMappingProvider implements Serializable { this.validateUnknown = builder.validateUnknown; final ArrayDeque currentPath = new ArrayDeque<>(); + final NameIterator nameIterator = NameIterator.empty(); for (Map.Entry>> entry : roots.entrySet()) { - NameIterator rootNi = new NameIterator(entry.getKey()); - while (rootNi.hasNext()) { - final String nextSegment = rootNi.getNextSegment(); - if (!nextSegment.isEmpty()) { - currentPath.add(nextSegment); + try (NameIterator rootNi = nameIterator.with(entry.getKey())) { + while (rootNi.hasNext()) { + final String nextSegment = rootNi.getNextSegment(); + if (!nextSegment.isEmpty()) { + currentPath.add(nextSegment); + } + rootNi.next(); } - rootNi.next(); } List> roots = entry.getValue(); for (Class root : roots) { @@ -1064,63 +1067,60 @@ private static Set additionalMappedProperties(final Set mappedPr Set additionalMappedProperties = new HashSet<>(); // Look for unmatched properties if we can find one in the Env ones and add it + NameIterator nameIterator = NameIterator.empty(); + StringBuilder sb = null; for (String mappedProperty : mappedProperties) { Set matchedEnvProperties = new HashSet<>(); - String endMappedProperty = replaceNonAlphanumericByUnderscores(mappedProperty); for (String envProperty : envProperties) { - if (envProperty.equalsIgnoreCase(endMappedProperty)) { + if (equalsIgnoreCaseReplacingNonAlphanumericByUnderscores(envProperty, mappedProperty)) { additionalMappedProperties.add(mappedProperty); matchedEnvProperties.add(envProperty); break; } - NameIterator ni = new NameIterator(mappedProperty); - StringBuilder sb = new StringBuilder(); - while (ni.hasNext()) { - String propertySegment = ni.getNextSegment(); - if (isIndexed(propertySegment)) { - // A mapped index property is represented as foo.bar[*] or foo.bar[*].baz - // The env property is represented as FOO_BAR_0_ or FOO_BAR_0__BAZ - // We need to match these somehow - int position = ni.getPosition(); - int indexStart = propertySegment.indexOf("[") + position + 1; - // If the segment is indexed, we try to match all previous segments with the env candidates - if (envProperty.length() >= indexStart - && envProperty.toLowerCase().startsWith(replaceNonAlphanumericByUnderscores( - sb + propertySegment.substring(0, indexStart - position - 1) + "_"))) { - - // Search for the ending _ to retrieve the possible index - int indexEnd = -1; - for (int i = indexStart + 1; i < envProperty.length(); i++) { - if (envProperty.charAt(i) == '_') { - indexEnd = i; - break; - } - } - - // Extract the index from the env property - // We don't care if this is numeric, it will be validated on the mapping retrieval - String index = envProperty.substring(indexStart + 1, indexEnd); - sb.append(propertySegment, 0, propertySegment.indexOf("[") + 1) - .append(index) - .append("]"); - } + try (NameIterator ni = nameIterator.with(mappedProperty)) { + if (sb == null) { + sb = new StringBuilder(); } else { - sb.append(propertySegment); + sb.setLength(0); } + while (ni.hasNext()) { + int initialLength = sb.length(); + // append the propertySegment to it + if (isIndexed(ni.getNextSegment(sb), initialLength)) { + String propertySegment = sb.substring(initialLength); + // rollback sb to the initialLength + sb.setLength(initialLength); + // A mapped index property is represented as foo.bar[*] or foo.bar[*].baz + // The env property is represented as FOO_BAR_0_ or FOO_BAR_0__BAZ + // We need to match these somehow + int position = ni.getPosition(); + int firstSquareBracket = propertySegment.indexOf("["); + int indexStart = firstSquareBracket + position + 1; + // If the segment is indexed, we try to match all previous segments with the env candidates + if (envProperty.length() >= indexStart + && envProperty.toLowerCase().startsWith(replaceNonAlphanumericByUnderscores( + sb + propertySegment.substring(0, indexStart - position - 1) + "_"))) { + // Search for the ending _ to retrieve the possible index + int indexEnd = envProperty.indexOf('_', indexStart + 1); + // Extract the index from the env property + // We don't care if this is numeric, it will be validated on the mapping retrieval + sb.append(propertySegment, 0, firstSquareBracket + 1) + .append(envProperty, indexStart + 1, indexEnd) + .append(']'); + } + } - ni.next(); + ni.next(); - if (ni.hasNext()) { - sb.append("."); + if (ni.hasNext()) { + sb.append("."); + } + } + if (equalsIgnoreCaseReplacingNonAlphanumericByUnderscores(envProperty, sb)) { + additionalMappedProperties.add(sb.toString()); + matchedEnvProperties.add(envProperty); } - } - - String mappedPropertyToMatch = sb.toString(); - if (envProperty.equalsIgnoreCase(replaceNonAlphanumericByUnderscores(mappedPropertyToMatch))) { - additionalMappedProperties.add(mappedPropertyToMatch); - matchedEnvProperties.add(envProperty); - // We cannot break here because if there are indexed properties they may match multiple envs } } envProperties.removeAll(matchedEnvProperties); @@ -1151,16 +1151,29 @@ private static boolean isIndexed(final String propertyName) { int indexStart = propertyName.indexOf("["); int indexEnd = propertyName.indexOf("]"); if (indexStart != -1 && indexEnd != -1) { - String index = propertyName.substring(indexStart + 1, indexEnd); - if (index.equals("*")) { - return true; - } - try { - Integer.parseInt(index); - return true; - } catch (NumberFormatException e) { - return false; - } + return isIndexed0(propertyName, indexEnd, indexStart); + } + return false; + } + + private static boolean isIndexed0(CharSequence propertyName, int indexEnd, int indexStart) { + int indexLength = indexEnd - (indexStart + 1); + if (indexLength == 1 && propertyName.charAt(indexStart + 1) == '*') { + return true; + } + try { + Integer.parseInt(propertyName, indexStart + 1, indexEnd, 10); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + private static boolean isIndexed(final StringBuilder propertyName, int start) { + int indexStart = propertyName.indexOf("[", start); + int indexEnd = propertyName.indexOf("]", start); + if (indexStart != -1 && indexEnd != -1) { + return isIndexed0(propertyName, indexEnd, indexStart); } return false; } diff --git a/implementation/src/main/java/io/smallrye/config/NameIterator.java b/implementation/src/main/java/io/smallrye/config/NameIterator.java index 0e33218bd..8016b049a 100644 --- a/implementation/src/main/java/io/smallrye/config/NameIterator.java +++ b/implementation/src/main/java/io/smallrye/config/NameIterator.java @@ -7,7 +7,7 @@ /** * An iterator for property name strings. */ -public final class NameIterator { +public final class NameIterator implements AutoCloseable { /** * Configuration key maximum allowed length. */ @@ -16,18 +16,55 @@ public final class NameIterator { private static final int POS_BITS = 12; private static final int SE_SHIFT = 32 - POS_BITS; - private final String name; + private String name; private int pos; + private StringBuilder tmp; + + private NameIterator() { + } public NameIterator(final String name) { - this(name, false); + with(name); } public NameIterator(final String name, final boolean startAtEnd) { - this(name, startAtEnd ? name.length() : -1); + with(name, startAtEnd); } public NameIterator(final String name, final int pos) { + with(name, pos); + } + + private StringBuilder acquireTmpBuilder() { + if (tmp == null) { + tmp = new StringBuilder(); + } else { + tmp.setLength(0); + } + return tmp; + } + + @Override + public void close() { + name = null; + } + + public static NameIterator empty() { + return new NameIterator(); + } + + public NameIterator with(String name, boolean startAtEnd) { + return with(name, startAtEnd ? name.length() : -1); + } + + public NameIterator with(String name) { + return with(name, false); + } + + public NameIterator with(String name, int pos) { + if (this.name != null) { + throw new IllegalStateException("this iterator must be closed first"); + } Assert.checkNotNullParam("name", name); if (name.length() > MAX_LENGTH) throw new IllegalArgumentException("Name is too long"); @@ -37,6 +74,7 @@ public NameIterator(final String name, final int pos) { throw new IllegalArgumentException("Position is not located at a delimiter"); this.name = name; this.pos = pos; + return this; } public void goToEnd() { @@ -94,15 +132,15 @@ private int initIteration() { return this.pos & POS_MASK; } - private int cookieOf(int state, int pos) { + private static int cookieOf(int state, int pos) { return state << POS_BITS | pos & POS_MASK; } - private int getPosition(int cookie) { + private static int getPosition(int cookie) { return (cookie & POS_MASK) << SE_SHIFT >> SE_SHIFT; } - private int getState(int cookie) { + private static int getState(int cookie) { return cookie >> POS_BITS; } @@ -119,6 +157,7 @@ private int nextPos(int cookie) { } int state = getState(cookie); int ch; + final String name = this.name; for (;;) { pos++; if (pos == name.length()) { @@ -253,12 +292,15 @@ public boolean nextSegmentEquals(String other, int offs, int len) { } public String getNextSegment() { - final StringBuilder b = new StringBuilder(); + return getNextSegment(acquireTmpBuilder()).toString(); + } + + public StringBuilder getNextSegment(StringBuilder b) { int cookie = initIteration(); for (;;) { cookie = nextPos(cookie); if (isSegmentDelimiter(cookie)) { - return b.toString(); + return b; } b.append((char) charAt(cookie)); } From b55374486ab3ed2afc14d3f40fee9a4c514e6afe Mon Sep 17 00:00:00 2001 From: Francesco Nigro Date: Mon, 28 Aug 2023 12:36:36 +0200 Subject: [PATCH 2/5] Address corner case 0 length strings --- .../main/java/io/smallrye/config/common/utils/StringUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/io/smallrye/config/common/utils/StringUtil.java b/common/src/main/java/io/smallrye/config/common/utils/StringUtil.java index 1d9695fb7..3aac48a87 100644 --- a/common/src/main/java/io/smallrye/config/common/utils/StringUtil.java +++ b/common/src/main/java/io/smallrye/config/common/utils/StringUtil.java @@ -107,7 +107,7 @@ public static boolean equalsIgnoreCaseReplacingNonAlphanumericByUnderscores(fina int length = mappedProperty.length(); if (property.length() != mappedProperty.length()) { // special-case/slow-path - if (property.length() != mappedProperty.length() + 1) { + if (length == 0 || property.length() != mappedProperty.length() + 1) { return false; } if (mappedProperty.charAt(length - 1) == '"' && From 2f9c226e9773fea3c8d34c9bf78b4c16ce4afaa3 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Mon, 28 Aug 2023 15:17:06 +0100 Subject: [PATCH 3/5] Filter environment variables to be mapped with mappings roots --- .../config/ConfigMappingProvider.java | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java index 8eeeb02f6..0cdac203d 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java @@ -957,8 +957,6 @@ ConfigMappingContext mapConfiguration(SmallRyeConfig config) throws ConfigValida defaultValuesConfigSource.registerDefaults(defaultValues); } } - - config.addPropertyNames(additionalMappedProperties(new HashSet<>(getProperties().keySet()), config)); return SecretKeys.doUnlocked(() -> mapConfigurationInternal(config)); } @@ -982,6 +980,8 @@ private ConfigMappingContext mapConfigurationInternal(SmallRyeConfig config) thr } } + config.addPropertyNames(additionalMappedProperties(new HashSet<>(getProperties().keySet()), roots.keySet(), config)); + // lazily sweep for (String name : config.getPropertyNames()) { NameIterator ni = new NameIterator(name); @@ -1053,7 +1053,11 @@ private boolean isPropertyInRoot(NameIterator propertyName) { return false; } - private static Set additionalMappedProperties(final Set mappedProperties, final SmallRyeConfig config) { + private static Set additionalMappedProperties( + final Set mappedProperties, + final Set roots, + final SmallRyeConfig config) { + // Collect EnvSource properties Set envProperties = new HashSet<>(); for (ConfigSource source : config.getConfigSources(EnvConfigSource.class)) { @@ -1065,6 +1069,32 @@ private static Set additionalMappedProperties(final Set mappedPr mappedProperties.remove(propertyName); } + Set envRoots = new HashSet<>(roots.size()); + for (String root : roots) { + envRoots.add(replaceNonAlphanumericByUnderscores(root)); + } + + // Ignore Env properties that don't belong to a root + Set envPropertiesUnmapped = new HashSet<>(); + for (String envProperty : envProperties) { + boolean matched = false; + for (String envRoot : envRoots) { + if (envProperty.length() < envRoot.length()) { + continue; + } + + if (envRoot.equalsIgnoreCase(envProperty.substring(0, envRoot.length()))) { + matched = true; + break; + } + } + + if (!matched) { + envPropertiesUnmapped.add(envProperty); + } + } + envProperties.removeAll(envPropertiesUnmapped); + Set additionalMappedProperties = new HashSet<>(); // Look for unmatched properties if we can find one in the Env ones and add it NameIterator nameIterator = NameIterator.empty(); From 4389e5bea03b760061c79597923d7277751d88ad Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Tue, 29 Aug 2023 18:01:38 +0100 Subject: [PATCH 4/5] Cache property names during mapping --- .../config/ConfigMappingProvider.java | 77 +++++++++---------- .../io/smallrye/config/SmallRyeConfig.java | 37 +++++++-- 2 files changed, 67 insertions(+), 47 deletions(-) diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java index 0cdac203d..84a5160c4 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingProvider.java @@ -957,6 +957,7 @@ ConfigMappingContext mapConfiguration(SmallRyeConfig config) throws ConfigValida defaultValuesConfigSource.registerDefaults(defaultValues); } } + config.addPropertyNames(additionalMappedProperties(new HashSet<>(getProperties().keySet()), roots.keySet(), config)); return SecretKeys.doUnlocked(() -> mapConfigurationInternal(config)); } @@ -969,6 +970,7 @@ private ConfigMappingContext mapConfigurationInternal(SmallRyeConfig config) thr } // eagerly populate roots + config.cachePropertyNames(true); for (Map.Entry>> entry : roots.entrySet()) { String path = entry.getKey(); List> roots = entry.getValue(); @@ -980,16 +982,9 @@ private ConfigMappingContext mapConfigurationInternal(SmallRyeConfig config) thr } } - config.addPropertyNames(additionalMappedProperties(new HashSet<>(getProperties().keySet()), roots.keySet(), config)); - // lazily sweep - for (String name : config.getPropertyNames()) { + for (String name : filterPropertiesInRoots(config.getPropertyNames(), roots.keySet())) { NameIterator ni = new NameIterator(name); - // filter properties in root - if (!isPropertyInRoot(ni)) { - continue; - } - BiConsumer action = matchActions.findRootValue(ni); if (action != null) { action.accept(context, ni); @@ -1006,51 +1001,49 @@ private ConfigMappingContext mapConfigurationInternal(SmallRyeConfig config) thr throw new ConfigValidationException(problems.toArray(ConfigValidationException.Problem.NO_PROBLEMS)); } context.fillInOptionals(); + config.cachePropertyNames(false); return context; } - private boolean isPropertyInRoot(NameIterator propertyName) { - final Set registeredRoots = roots.keySet(); - for (String registeredRoot : registeredRoots) { - // match everything - if (registeredRoot.length() == 0) { - return true; - } - - // A sub property from a namespace is always bigger in length - if (propertyName.getName().length() <= registeredRoot.length()) { - continue; - } + /** + * Filters the full list of properties names in Config to only the property names that can match any of the + * prefixes (namespaces) registered in mappings. + * + * @param properties the available property names in Config. + * @param roots the registered mapping roots. + * + * @return the property names that match to at least one root. + */ + private static Iterable filterPropertiesInRoots(final Iterable properties, final Set roots) { + if (roots.isEmpty()) { + return properties; + } - final NameIterator root = new NameIterator(registeredRoot); - // compare segments - while (root.hasNext()) { - String segment = root.getNextSegment(); - if (!propertyName.hasNext()) { - propertyName.goToStart(); - break; - } + // Will match everything, so no point in filtering + if (roots.contains("")) { + return properties; + } - final String nextSegment = propertyName.getNextSegment(); - if (!segment.equals(normalizeIfIndexed(nextSegment))) { - propertyName.goToStart(); - break; + List matchedProperties = new ArrayList<>(); + for (String property : properties) { + for (String root : roots) { + if (property.length() <= root.length()) { + continue; } - root.next(); - propertyName.next(); - - // root has no more segments and we reached this far so everything matched. - // on top, property still has more segments to do the mapping. - if (!root.hasNext() && propertyName.hasNext()) { - propertyName.goToStart(); - return true; + // foo.bar + // foo.bar."baz" + // foo.bar[0] + char c = property.charAt(root.length()); + if ((c == '.') || c == '[') { + if (property.startsWith(root)) { + matchedProperties.add(property); + } } } } - - return false; + return matchedProperties; } private static Set additionalMappedProperties( diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java index d207e3577..b2f0a1023 100644 --- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java +++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java @@ -518,6 +518,14 @@ void addPropertyNames(Set properties) { configSources.getPropertyNames().add(properties); } + void cachePropertyNames(boolean cache) { + if (cache) { + configSources.getPropertyNames().enableCache(); + } else { + configSources.getPropertyNames().disableCache(); + } + } + private static class ConfigSources implements Serializable { private static final long serialVersionUID = 3483018375584151712L; @@ -732,17 +740,36 @@ class PropertyNames implements Serializable { private final PropertyNamesConfigSourceInterceptor interceptor; + // TODO - Temporary cache to improve allocation. Mappings require multiple calls to getPropertyNames. + // TODO - Replace with a proper implementation to avoid recomputation of property names. + private final Set propertyNames = new HashSet<>(); + private boolean cached = false; + private PropertyNames(final PropertyNamesConfigSourceInterceptor propertyNamesInterceptor) { this.interceptor = propertyNamesInterceptor; } Iterable get() { - final HashSet names = new HashSet<>(); - final Iterator namesIterator = interceptorChain.iterateNames(); - while (namesIterator.hasNext()) { - names.add(namesIterator.next()); + if (cached) { + return propertyNames; + } else { + final HashSet names = new HashSet<>(); + final Iterator namesIterator = interceptorChain.iterateNames(); + while (namesIterator.hasNext()) { + names.add(namesIterator.next()); + } + return names; } - return names; + } + + void enableCache() { + propertyNames.addAll((Set) get()); + cached = true; + } + + void disableCache() { + propertyNames.clear(); + cached = false; } void add(final Set properties) { From eb734b5dc634a5721f37f675d675179e9289adf1 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Tue, 29 Aug 2023 18:02:18 +0100 Subject: [PATCH 5/5] Avoid exception allocation when checking for indexed segment --- .../config/common/utils/StringUtil.java | 42 ++++++++++++------- .../config/common/utils/StringUtilTest.java | 10 +++++ 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/common/src/main/java/io/smallrye/config/common/utils/StringUtil.java b/common/src/main/java/io/smallrye/config/common/utils/StringUtil.java index 3aac48a87..e501593a4 100644 --- a/common/src/main/java/io/smallrye/config/common/utils/StringUtil.java +++ b/common/src/main/java/io/smallrye/config/common/utils/StringUtil.java @@ -165,51 +165,48 @@ public static String toLowerCaseAndDotted(final String name) { if ('_' == c) { if (i == 0) { // leading _ can only mean a profile - sb.append("%"); + sb.append('%'); continue; } // Do not convert to index if the first segment is a number if (beginSegment > 0) { - try { - String segment = sb.substring(beginSegment, i); - Integer.parseInt(segment); - sb.replace(beginSegment - 1, beginSegment, "[").append("]"); + if (isNumeric(sb, beginSegment, i)) { + sb.setCharAt(beginSegment - 1, '['); + sb.append(']'); int j = i + 1; if (j < length) { if ('_' == name.charAt(j)) { - sb.append("."); + sb.append('.'); i = j; } } continue; - } catch (NumberFormatException e) { - // Ignore, it is not an indexed number } } int j = i + 1; if (j < length) { if ('_' == name.charAt(j) && !quotesOpen) { - sb.append("."); - sb.append("\""); + sb.append('.'); + sb.append('\"'); i = j; quotesOpen = true; } else if ('_' == name.charAt(j) && quotesOpen) { - sb.append("\""); + sb.append('\"'); // Ending if (j + 1 < length) { - sb.append("."); + sb.append('.'); } i = j; quotesOpen = false; } else { - sb.append("."); + sb.append('.'); } } else { - sb.append("."); + sb.append('.'); } beginSegment = j; } else { @@ -219,6 +216,23 @@ public static String toLowerCaseAndDotted(final String name) { return sb.toString(); } + public static boolean isNumeric(CharSequence digits) { + return isNumeric(digits, 0, digits.length()); + } + + public static boolean isNumeric(CharSequence digits, int start, int end) { + if (digits.length() == 0) { + return false; + } + + for (int i = start; i < end; i++) { + if (!Character.isDigit(digits.charAt(i))) { + return false; + } + } + return true; + } + public static String skewer(String camelHumps) { return skewer(camelHumps, '-'); } diff --git a/common/src/test/java/io/smallrye/config/common/utils/StringUtilTest.java b/common/src/test/java/io/smallrye/config/common/utils/StringUtilTest.java index 583fc4902..f51bde3df 100644 --- a/common/src/test/java/io/smallrye/config/common/utils/StringUtilTest.java +++ b/common/src/test/java/io/smallrye/config/common/utils/StringUtilTest.java @@ -16,7 +16,9 @@ package io.smallrye.config.common.utils; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; @@ -122,4 +124,12 @@ void replaceNonAlphanumericByUnderscores() { void toLowerCaseAndDotted() { assertEquals("test.language.\"de.etr\"", StringUtil.toLowerCaseAndDotted("TEST_LANGUAGE__DE_ETR__")); } + + @Test + void isNumeric() { + assertTrue(StringUtil.isNumeric("0")); + assertFalse(StringUtil.isNumeric("false")); + assertTrue(StringUtil.isNumeric("foo[0]", 4, 5)); + assertTrue(StringUtil.isNumeric(new StringBuilder("foo[0]"), 4, 5)); + } }