diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshDeploymentManifestSchema.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshDeploymentManifestSchema.java index 271b8a949..3ba0128fe 100644 --- a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshDeploymentManifestSchema.java +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshDeploymentManifestSchema.java @@ -14,11 +14,13 @@ import java.util.Collection; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.function.Predicate; import org.apache.commons.lang3.tuple.Pair; import org.springframework.ide.vscode.bosh.models.CachingModelProvider; import org.springframework.ide.vscode.bosh.models.CloudConfigModel; import org.springframework.ide.vscode.bosh.models.DynamicModelProvider; +import org.springframework.ide.vscode.bosh.models.StemcellData; import org.springframework.ide.vscode.bosh.models.StemcellModel; import org.springframework.ide.vscode.bosh.models.StemcellsModel; import org.springframework.ide.vscode.commons.util.Assert; @@ -33,6 +35,7 @@ import org.springframework.ide.vscode.commons.yaml.path.YamlPath; import org.springframework.ide.vscode.commons.yaml.reconcile.ASTTypeCache; import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems; import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; +import org.springframework.ide.vscode.commons.yaml.schema.SchemaContextAware; import org.springframework.ide.vscode.commons.yaml.schema.YType; import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory; import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.AbstractType; @@ -193,20 +196,40 @@ public class BoshDeploymentManifestSchema implements YamlSchema { YBeanType t_stemcell = f.ybean("Stemcell"); - YType t_stemcell_name_ref = f.yenumFromDynamicValues("StemcellName", (dc) -> stemcellsProvider.getModel(dc).getStemcellNames()); - YType t_stemcell_os_ref = f.yenumFromDynamicValues("StemcellOs", (dc) -> stemcellsProvider.getModel(dc).getStemcellOss()); - YType t_stemcell_version_ref = f.yenumFromDynamicValues("StemcellVersion", (dc) -> { - StemcellModel currentStemcell = getCurrentStemcell(dc); - if (currentStemcell!=null) { - return stemcellsProvider.getModel(dc).getStemcells().stream() - .filter(sc -> StringUtil.hasText(sc.getVersion())) - .filter(currentStemcell.createVersionFilter()) - .map(sc -> sc.getVersion()) - .collect(CollectorUtil.toImmutableSet()); + YType t_stemcell_name_ref = f.yenumFromDynamicValues("StemcellName", (dc) -> + stemcellsProvider.getModel(dc).getStemcellNames() + ); + YType t_stemcell_os_ref = f.yenumFromDynamicValues("StemcellOs", (dc) -> + stemcellsProvider.getModel(dc).getStemcellOss() + ); + YType t_stemcell_version_ref = f.contextAware("StemcellVersion", new SchemaContextAware() { + + YAtomicType baseType = f.yenumFromDynamicValues("StemcellVersion", (dc) -> stemcellsProvider.getModel(dc).getVersions()); + { + baseType.addHints("latest"); + baseType.alsoAccept("latest"); } - //Troubles determining the filter. So return all stemcells. - return stemcellsProvider.getModel(dc).getVersions(); - }); + + @Override + public YType withContext(DynamicSchemaContext dc) throws Exception { + StemcellModel currentStemcell = getCurrentStemcell(dc); + if (StringUtil.hasText(currentStemcell.getName())||StringUtil.hasText(currentStemcell.getOs())) { + Predicate filter = currentStemcell.createVersionFilter(); + YAtomicType filteredType = f.yenumFromDynamicValues("StemcellVersion["+filter+"]", (_dc) -> { + //Note: it doesn't really matter whether we use _dc or dc in code below as they should be the same. + return stemcellsProvider.getModel(dc).getStemcells().stream() + .filter(sc -> StringUtil.hasText(sc.getVersion())) + .filter(currentStemcell.createVersionFilter()) + .map(sc -> sc.getVersion()) + .collect(CollectorUtil.toImmutableSet()); + }); + filteredType.addHints("latest"); + filteredType.alsoAccept("latest"); + return filteredType; + } + return baseType; + } + }).treatAsAtomic(); addProp(t_stemcell, "alias", t_stemcell_alias_def).isRequired(true); addProp(t_stemcell, "version", t_stemcell_version_ref).isRequired(true); @@ -260,7 +283,7 @@ public class BoshDeploymentManifestSchema implements YamlSchema { YBeanType t_variable = f.ybean("Variable"); addProp(t_variable, "name", t_var_name_def).isPrimary(true); YType t_variable_type = f.yenum("VariableType", "certificate", "password", "rsa", "ssh") - .parseWith(ValueParsers.NE_STRING); //Overrid the parser -> no errors / warnings... in theory there could be other valid values. + .parseWith(ValueParsers.NE_STRING); //Override the parser -> no errors / warnings... in theory there could be other valid values. addProp(t_variable, "type", t_variable_type).isRequired(true); addProp(t_variable, "options", t_params); addProp(v2Schema, "variables", f.yseq(t_variable)); diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/Main.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/Main.java index ebcbe0bbc..911376510 100644 --- a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/Main.java +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/Main.java @@ -13,13 +13,14 @@ package org.springframework.ide.vscode.bosh; import java.io.IOException; import org.springframework.ide.vscode.bosh.models.BoshCommandCloudConfigProvider; +import org.springframework.ide.vscode.bosh.models.BoshCommandStemcellsProvider; import org.springframework.ide.vscode.commons.languageserver.LaunguageServerApp; public class Main { public static void main(String[] args) throws IOException, InterruptedException { LaunguageServerApp.start(() -> new BoshLanguageServer( new BoshCommandCloudConfigProvider(), - (dc) -> null //TODO: real model provider here! + new BoshCommandStemcellsProvider() )); } } diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/StemcellModel.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/StemcellModel.java index 6cc174c2e..47aad85f1 100644 --- a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/StemcellModel.java +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/StemcellModel.java @@ -45,17 +45,50 @@ public class StemcellModel { if (StringUtil.hasText(name) && StringUtil.hasText(os)) { //User shouldn't really specify both of these... but if they do... then // let's do our best to generate proposals for the both of them. - return (sc) -> - name.equals(sc.getName()) || os.equals(sc.getOs()); + return new Predicate() { + @Override + public boolean test(StemcellData sc) { + return name.equals(sc.getName()) || os.equals(sc.getOs()); + } + + @Override + public String toString() { + return "name="+name+", os="+os; + } + }; } else if (StringUtil.hasText(name)) { - return (sc) -> - name.equals(sc.getName()); + return new Predicate() { + @Override + public boolean test(StemcellData sc) { + return name.equals(sc.getName()); + } + @Override + public String toString() { + return "name="+name; + } + }; } else if (StringUtil.hasText(os)) { - return (sc) -> - os.equals(sc.getOs()); + return new Predicate() { + @Override + public boolean test(StemcellData sc) { + return os.equals(sc.getOs()); + } + @Override + public String toString() { + return "os="+os; + } + }; } else { - return (sc) -> - true; + return new Predicate() { + @Override + public boolean test(StemcellData sc) { + return true; + } + @Override + public String toString() { + return "true"; + } + }; } } diff --git a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshEditorTest.java b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshEditorTest.java index 0012df26b..15ceb599a 100644 --- a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshEditorTest.java +++ b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/bosh/BoshEditorTest.java @@ -954,6 +954,22 @@ public class BoshEditorTest { ); } + @SuppressWarnings("unchecked") + @Test public void contentAssistStemcellVersionNoDirector() throws Exception { + Editor editor; + stemcellsProvider = mock(DynamicModelProvider.class); + when(stemcellsProvider.getModel(any())).thenThrow(new IOException("Couldn't connect to bosh")); + editor = harness.newEditor( + "stemcells:\n" + + "- alias: good\n" + + " name: centos-agent\n" + + " version: <*>\n" + ); + editor.assertContextualCompletions("<*>", + "latest<*>" + ); + } + @Test public void contentAssistStemcellVersionFromDirector() throws Exception { Editor editor; stemcellsProvider = provideStemcellsFrom( @@ -970,7 +986,7 @@ public class BoshEditorTest { " version: <*>\n" ); editor.assertContextualCompletions("<*>", - "222.2<*>", "333.3<*>" + "222.2<*>", "333.3<*>", "latest<*>" ); editor = harness.newEditor( @@ -980,7 +996,7 @@ public class BoshEditorTest { " version: <*>\n" ); editor.assertContextualCompletions("<*>", - "123.4<*>", "222.2<*>" + "123.4<*>", "222.2<*>", "latest<*>" ); editor = harness.newEditor( @@ -989,10 +1005,10 @@ public class BoshEditorTest { " version: <*>\n" ); editor.assertContextualCompletions("<*>", - "123.4<*>", "222.2<*>", "333.3<*>" + "123.4<*>", "222.2<*>", "333.3<*>", "latest<*>" ); - //when os or name are 'bogus' at least suggest proposals based on other prop + //when os and name are 'bogus' at least suggest proposals based on other prop editor = harness.newEditor( "stemcells:\n" + "- alias: good\n" + @@ -1001,7 +1017,7 @@ public class BoshEditorTest { " version: <*>\n" ); editor.assertContextualCompletions("<*>", - "222.2<*>", "333.3<*>" + "222.2<*>", "333.3<*>", "latest<*>" ); editor = harness.newEditor( @@ -1012,7 +1028,7 @@ public class BoshEditorTest { " version: <*>\n" ); editor.assertContextualCompletions("<*>", - "222.2<*>", "333.3<*>" + "222.2<*>", "333.3<*>", "latest<*>" ); //when the os and name disagree, merge the proposals for both: @@ -1024,10 +1040,34 @@ public class BoshEditorTest { " version: <*>\n" ); editor.assertContextualCompletions("<*>", - "123.4<*>", "222.2<*>", "333.3<*>" + "123.4<*>", "222.2<*>", "333.3<*>", "latest<*>" ); } + @SuppressWarnings("unchecked") + @Test public void reconcileStemcellVersionNoDirector() throws Exception { + Editor editor; + stemcellsProvider = mock(DynamicModelProvider.class); + when(stemcellsProvider.getModel(any())).thenThrow(new IOException("Couldn't connect to bosh")); + editor = harness.newEditor( + "stemcells:\n" + + "- alias: aaa\n" + + " name: centos-agent\n" + + " version: 222.2\n" + + "- alias: bbb\n" + + " name: centos-agent\n" + + " version: 123.4\n" + + "- alias: ddd\n" + + " os: ubuntu\n" + + " version: 333.3\n" + + "- alias: eee\n" + + " os: ubuntu\n" + + " version: latest\n" + ); + editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY); + editor.assertProblems(/*NONE*/); + } + @Test public void reconcileStemcellVersionFromDirector() throws Exception { Editor editor; stemcellsProvider = provideStemcellsFrom( @@ -1047,12 +1087,15 @@ public class BoshEditorTest { " version: 123.4\n" + "- alias: ddd\n" + " os: ubuntu\n" + - " version: 333.3\n" + " version: 333.3\n" + + "- alias: eee\n" + + " os: ubuntu\n" + + " version: latest\n" ); editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY); editor.assertProblems( - "123.4|unknown 'StemcellVersion'. Valid values are: [222.2, 333.3]", - "333.3|unknown 'StemcellVersion'. Valid values are: [123.4, 222.2]" + "123.4|unknown 'StemcellVersion[name=centos-agent]'. Valid values are: [222.2, 333.3]", + "333.3|unknown 'StemcellVersion[os=ubuntu]'. Valid values are: [123.4, 222.2]" ); } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java index 0c4455fbb..eff8b1eb2 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java @@ -11,6 +11,7 @@ package org.springframework.ide.vscode.commons.yaml.schema; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -24,12 +25,12 @@ import java.util.TreeSet; import java.util.concurrent.Callable; import java.util.function.BiFunction; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileException; import org.springframework.ide.vscode.commons.languageserver.reconcile.ReplacementQuickfix; import org.springframework.ide.vscode.commons.util.Assert; import org.springframework.ide.vscode.commons.util.EnumValueParser; +import org.springframework.ide.vscode.commons.util.Log; import org.springframework.ide.vscode.commons.util.Renderable; import org.springframework.ide.vscode.commons.util.Renderables; import org.springframework.ide.vscode.commons.util.ValueParser; @@ -291,19 +292,30 @@ public class YTypeFactory { return null; } - public AbstractType addHintProvider(Callable> hintProvider) { - addHintProvider((DynamicSchemaContext dc) -> hintProvider); + public AbstractType setHintProvider(Callable> hintProvider) { + setHintProvider((DynamicSchemaContext dc) -> hintProvider); return this; } - public AbstractType addHintProvider(SchemaContextAware>> hintProvider) { + public AbstractType setHintProvider(SchemaContextAware>> hintProvider) { //TODO: SchemaContextAware now allows throwing exceptions so should be able to simplify the above to SchemaContextAware> this.hintProvider = hintProvider; return this; } public YValueHint[] getHintValues(DynamicSchemaContext dc) throws Exception { - Collection providerHints=getProviderHints(dc); + Collection providerHints = null; + try { + providerHints=getProviderHints(dc); + } catch (Exception e) { + if (!hints.isEmpty()) { + Log.log(e); + //Recover from error returning just the static hints. + return hints.toArray(new YValueHint[hints.size()]); + } else { + throw e; + } + } if (providerHints == null || providerHints.isEmpty()) { return hints.toArray(new YValueHint[hints.size()]); @@ -324,6 +336,9 @@ public class YTypeFactory { } } + /** + * Prevents adding additional hints. + */ public void sealHints() { hints = ImmutableList.copyOf(hints); } @@ -382,23 +397,16 @@ public class YTypeFactory { addProperty(new YTypedPropertyImpl(name, type)); } public AbstractType addHints(String... strings) { - if (strings != null) { - for (String value : strings) { - BasicYValueHint hint = new BasicYValueHint(value); - if (!hints.contains(hint)) { - hints.add(hint); - } - } - } - return this; + return addHints(Arrays.stream(strings).map(s -> new BasicYValueHint(s)).toArray(BasicYValueHint[]::new)); } - public void addHints(YValueHint... extraHints) { + public AbstractType addHints(YValueHint... extraHints) { for (YValueHint h : extraHints) { if (!hints.contains(h)) { hints.add(h); } } + return this; } public void parseWith(SchemaContextAware parser) { @@ -413,6 +421,29 @@ public class YTypeFactory { return parser; } + /** + * Modifies currently installed parser so it is guaranteed to accept at least given values. + * @return + */ + public AbstractType alsoAccept(String... _values) { + if (parser!=null) { + ImmutableSet values = ImmutableSet.copyOf(_values); + final SchemaContextAware oldParserProvider = parser; + parser = (dc) -> (s) -> { + if (values.contains(s)) { + return s; + } else { + ValueParser oldParser = oldParserProvider.safeWithContext(dc).orElse(null); + if (oldParser!=null) { + return oldParser.parse(s); + } + return s; + } + }; + } + return this; + } + public AbstractType require(Constraint dynamicConstraint) { this.constraints.add(dynamicConstraint); return this; @@ -849,7 +880,7 @@ public class YTypeFactory { public YAtomicType yenumFromHints(String name, BiFunction, String> errorMessageFormatter, SchemaContextAware> values) { YAtomicType t = yatomic(name); - t.addHintProvider((dc) -> () -> values.withContext(dc)); + t.setHintProvider((dc) -> () -> values.withContext(dc)); t.parseWith((DynamicSchemaContext dc) -> { Collection strings = YTypeFactory.values(values.withContext(dc)); return new EnumValueParser(name, strings) { @@ -867,9 +898,7 @@ public class YTypeFactory { //Error message formatter: (parseString, validValues) -> "'"+parseString+"' is an unknown '"+name+"'. Valid values are: "+validValues, //Hints provider: - (dc) -> - hints(values.withContext(dc) - ) + (dc) -> hints(values.withContext(dc)) ); } @@ -879,7 +908,7 @@ public class YTypeFactory { public YAtomicType yenum(String name, BiFunction, String> errorMessageFormatter, SchemaContextAware> values) { YAtomicType t = yatomic(name); - t.addHintProvider((dc) -> { + t.setHintProvider((dc) -> { Collection strings = values.withContext(dc); return strings==null ? null diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java index 5a08a955e..f14a9c75e 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java @@ -118,7 +118,7 @@ public class PipelineYmlSchema implements YamlSchema { public final YType t_location = f.yatomic("Location") //Note: we could have used f.yenum here too. But it saves memory if we don't keep the large set of ValueHints in memory. // That's why we attach custom hint provider and parser here that do essentially the same thing. - .addHintProvider(() -> { + .setHintProvider(() -> { return ZoneId.getAvailableZoneIds().stream() .map(BasicYValueHint::new) .collect(Collectors.toList()); @@ -187,7 +187,7 @@ public class PipelineYmlSchema implements YamlSchema { } ); t_maybe_resource_name = f.yatomic("ResourceName | TaskOutput"); - t_maybe_resource_name.addHintProvider((DynamicSchemaContext dc) -> { + t_maybe_resource_name.setHintProvider((DynamicSchemaContext dc) -> { //Putting the Callable into a local variable is strange, but the compiler doesn't like it if // we return it directly. Too much complexity for Java type-inference? Callable> callable = () -> YTypeFactory.hints(models.getResourceNames(dc)); diff --git a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java index 7c85119fa..6f81233c8 100644 --- a/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java +++ b/headless-services/manifest-yaml-language-server/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java @@ -122,25 +122,25 @@ public final class ManifestYmlSchema implements YamlSchema { YAtomicType t_buildpack = f.yatomic("Buildpack"); if (buildpackProvider != null) { - t_buildpack.addHintProvider(buildpackProvider); + t_buildpack.setHintProvider(buildpackProvider); // t_buildpack.parseWith(ManifestYmlValueParsers.fromHints(t_buildpack.toString(), buildpackProvider)); } YAtomicType t_stack = f.yatomic("Stack"); if (stacksProvider!=null) { - t_stack.addHintProvider(stacksProvider); + t_stack.setHintProvider(stacksProvider); t_stack.parseWith(ManifestYmlValueParsers.fromValueHints(stacksProvider, t_stack, ManifestYamlSchemaProblemsTypes.UNKNOWN_STACK_PROBLEM)); } YAtomicType t_domain = f.yatomic("Domain"); if (domainsProvider != null) { - t_domain.addHintProvider(domainsProvider); + t_domain.setHintProvider(domainsProvider); t_domain.parseWith(ManifestYmlValueParsers.fromValueHints(domainsProvider, t_domain, ManifestYamlSchemaProblemsTypes.UNKNOWN_DOMAIN_PROBLEM)); } YAtomicType t_service = f.yatomic("Service"); if (servicesProvider != null) { - t_service.addHintProvider(servicesProvider); + t_service.setHintProvider(servicesProvider); t_service.parseWith(new CFServicesValueParser(t_service.toString(), YTypeFactory.valuesFromHintProvider(servicesProvider))); }