From 654899974809730fd3938064d8de9b2bde8a8a5d Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Wed, 23 Jul 2025 15:49:50 -0700 Subject: [PATCH 1/3] Relax enum property value validation according to Boot core Signed-off-by: BoykoAlex --- .../vscode/commons/util/EnumValueParser.java | 35 +++++++++++++------ .../boot/test/ApplicationYamlEditorTest.java | 1 + 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/EnumValueParser.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/EnumValueParser.java index d8ba8c984c..9cdd9e6a5e 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/EnumValueParser.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/EnumValueParser.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2014-2017 Pivotal, Inc. + * Copyright (c) 2014, 2025 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -11,9 +11,11 @@ package org.springframework.ide.vscode.commons.util; import java.util.Collection; +import java.util.Set; import java.util.TreeSet; import java.util.concurrent.Callable; import java.util.function.Supplier; +import java.util.stream.Collectors; import com.google.common.collect.ImmutableSet; @@ -29,6 +31,7 @@ public class EnumValueParser implements ValueParser { private Supplier> values; private final boolean longRunning; + private transient Set _canonicalValues; public EnumValueParser(String typeName, String... values) { this(typeName, ImmutableSet.copyOf(values)); @@ -69,20 +72,15 @@ public Object parse(String str) throws Exception { throw errorOnBlank(createBlankTextErrorMessage()); } - PartialCollection values = this.values.get(); - - // If values is not fully known then just assume the str is acceptable. - if (values == null || !values.isComplete() || hasMatchingValue(str, values.getElements())) { + Set canonicalValues = getCanonicalValues(); + + if (canonicalValues == null || canonicalValues.contains(getCanonicalName(str))) { return str; } else { - throw errorOnParse(createErrorMessage(str, values.getElements())); + throw errorOnParse(createErrorMessage(str, this.values.get().getElements())); } } - protected boolean hasMatchingValue(String str, Collection values) { - return values.contains(str); - } - protected String createBlankTextErrorMessage() { return "'" + typeName + "'" + " cannot be blank."; } @@ -102,4 +100,21 @@ protected Exception errorOnBlank(String message) { public boolean longRunning() { return this.longRunning ; } + + private Set getCanonicalValues() { + PartialCollection partialCollection = this.values.get(); + if (partialCollection != null) { + _canonicalValues = partialCollection.getElements().stream().map(EnumValueParser::getCanonicalName).collect(Collectors.toSet()); + } + return _canonicalValues; + } + + static String getCanonicalName(String name) { + StringBuilder canonicalName = new StringBuilder(name.length()); + name.chars() + .filter(Character::isLetterOrDigit) + .map(Character::toLowerCase) + .forEach((c) -> canonicalName.append((char) c)); + return canonicalName.toString(); + } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationYamlEditorTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationYamlEditorTest.java index 69aa34d43d..414b7068b5 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationYamlEditorTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationYamlEditorTest.java @@ -5014,6 +5014,7 @@ private void doCollectionOfEnumReconcileTest(String collectionType) throws Excep "my:\n" + " colors:\n" + " - red\n" + + " - r-e-d\n" + // Canonical name is "red" from "r-e-d" hence it is okay in Boot as well " - green\n" + " - BLUE\n" + " - not-a-color\n" From fb6b5c7cf470af59fc30218a8895345153dabb04 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Wed, 23 Jul 2025 17:07:21 -0700 Subject: [PATCH 2/3] Introduce BootEnumValueParser Signed-off-by: BoykoAlex --- .../vscode/commons/util/EnumValueParser.java | 41 ++++++-------- .../vscode/boot/metadata/types/TypeUtil.java | 4 +- .../reconcile/BootEnumValueParser.java | 55 +++++++++++++++++++ 3 files changed, 73 insertions(+), 27 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/reconcile/BootEnumValueParser.java diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/EnumValueParser.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/EnumValueParser.java index 9cdd9e6a5e..d7f08dedf0 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/EnumValueParser.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/EnumValueParser.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2014, 2025 Pivotal, Inc. + * Copyright (c) 2014-2017 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -11,11 +11,10 @@ package org.springframework.ide.vscode.commons.util; import java.util.Collection; -import java.util.Set; +import java.util.Collections; import java.util.TreeSet; import java.util.concurrent.Callable; import java.util.function.Supplier; -import java.util.stream.Collectors; import com.google.common.collect.ImmutableSet; @@ -31,7 +30,6 @@ public class EnumValueParser implements ValueParser { private Supplier> values; private final boolean longRunning; - private transient Set _canonicalValues; public EnumValueParser(String typeName, String... values) { this(typeName, ImmutableSet.copyOf(values)); @@ -72,14 +70,24 @@ public Object parse(String str) throws Exception { throw errorOnBlank(createBlankTextErrorMessage()); } - Set canonicalValues = getCanonicalValues(); - - if (canonicalValues == null || canonicalValues.contains(getCanonicalName(str))) { + PartialCollection values = this.values.get(); + + // If values is not fully known then just assume the str is acceptable. + if (values == null || !values.isComplete() || hasMatchingValue(str, values.getElements())) { return str; } else { - throw errorOnParse(createErrorMessage(str, this.values.get().getElements())); + throw errorOnParse(createErrorMessage(str, values.getElements())); } } + + protected final Collection getAllKnownValues() { + PartialCollection partialCollection = this.values.get(); + return partialCollection == null ? Collections.emptyList() : this.values.get().getElements(); + } + + protected boolean hasMatchingValue(String str, Collection values) { + return values.contains(str); + } protected String createBlankTextErrorMessage() { return "'" + typeName + "'" + " cannot be blank."; @@ -100,21 +108,4 @@ protected Exception errorOnBlank(String message) { public boolean longRunning() { return this.longRunning ; } - - private Set getCanonicalValues() { - PartialCollection partialCollection = this.values.get(); - if (partialCollection != null) { - _canonicalValues = partialCollection.getElements().stream().map(EnumValueParser::getCanonicalName).collect(Collectors.toSet()); - } - return _canonicalValues; - } - - static String getCanonicalName(String name) { - StringBuilder canonicalName = new StringBuilder(name.length()); - name.chars() - .filter(Character::isLetterOrDigit) - .map(Character::toLowerCase) - .forEach((c) -> canonicalName.append((char) c)); - return canonicalName.toString(); - } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/types/TypeUtil.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/types/TypeUtil.java index 354a13888e..fe9d3a87b4 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/types/TypeUtil.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/types/TypeUtil.java @@ -44,6 +44,7 @@ import org.springframework.ide.vscode.boot.metadata.hints.StsValueHint; import org.springframework.ide.vscode.boot.metadata.util.DeprecationUtil; import org.springframework.ide.vscode.boot.metadata.util.PropertyDocUtils; +import org.springframework.ide.vscode.boot.properties.reconcile.BootEnumValueParser; import org.springframework.ide.vscode.commons.java.Flags; import org.springframework.ide.vscode.commons.java.IField; import org.springframework.ide.vscode.commons.java.IJavaElement; @@ -55,7 +56,6 @@ import org.springframework.ide.vscode.commons.util.ArrayUtils; import org.springframework.ide.vscode.commons.util.Assert; import org.springframework.ide.vscode.commons.util.CollectionUtil; -import org.springframework.ide.vscode.commons.util.EnumValueParser; import org.springframework.ide.vscode.commons.util.LazyProvider; import org.springframework.ide.vscode.commons.util.MimeTypes; import org.springframework.ide.vscode.commons.util.Renderables; @@ -272,7 +272,7 @@ public ValueParser getValueParser(Type type) { //Note, technically if 'enumValues is empty array' this means something different // from when it is null. An empty array means a type that has no values, so // assigning anything to it is an error. - return new EnumValueParser(niceTypeName(type), getBareValues(enumValues)); + return new BootEnumValueParser(niceTypeName(type), getBareValues(enumValues)); } if (isMap(type)) { //Trying to parse map types from scalars is not possible. Thus we diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/reconcile/BootEnumValueParser.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/reconcile/BootEnumValueParser.java new file mode 100644 index 0000000000..073fcab0e5 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/reconcile/BootEnumValueParser.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.properties.reconcile; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.ide.vscode.commons.util.EnumValueParser; +import org.springframework.ide.vscode.commons.util.StringUtil; + +public class BootEnumValueParser extends EnumValueParser { + + private Set canonicalValues; + + private static String getCanonicalName(String name) { + StringBuilder canonicalName = new StringBuilder(name.length()); + name.chars() + .filter(Character::isLetterOrDigit) + .map(Character::toLowerCase) + .forEach((c) -> canonicalName.append((char) c)); + return canonicalName.toString(); + } + + public BootEnumValueParser(String typeName, String[] values) { + super(typeName, values); + this.canonicalValues = Arrays.stream(values).map(BootEnumValueParser::getCanonicalName).collect(Collectors.toSet()); + } + + @Override + public Object parse(String str) throws Exception { + // IMPORTANT: check the text FIRST before fetching values + // from the hints provider, as the hints provider may be expensive when + // resolving values + if (!StringUtil.hasText(str)) { + throw errorOnBlank(createBlankTextErrorMessage()); + } + + // If values is not fully known then just assume the str is acceptable. + if (canonicalValues.contains(getCanonicalName(str))) { + return str; + } else { + throw errorOnParse(createErrorMessage(str, getAllKnownValues())); + } + } + +} From 48c1a650fe11e54ad5f40f1d7093fef5e6f79121 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Thu, 24 Jul 2025 11:33:38 -0700 Subject: [PATCH 3/3] Comments adjustments Signed-off-by: BoykoAlex --- .../boot/properties/reconcile/BootEnumValueParser.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/reconcile/BootEnumValueParser.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/reconcile/BootEnumValueParser.java index 073fcab0e5..bc9c901a2e 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/reconcile/BootEnumValueParser.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/reconcile/BootEnumValueParser.java @@ -17,6 +17,11 @@ import org.springframework.ide.vscode.commons.util.EnumValueParser; import org.springframework.ide.vscode.commons.util.StringUtil; +/** + * Boot property values allow relaxed parsing via canonical names. + * + * @author Alex Boyko + */ public class BootEnumValueParser extends EnumValueParser { private Set canonicalValues; @@ -44,7 +49,6 @@ public Object parse(String str) throws Exception { throw errorOnBlank(createBlankTextErrorMessage()); } - // If values is not fully known then just assume the str is acceptable. if (canonicalValues.contains(getCanonicalName(str))) { return str; } else {