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 e6bf358ec..271b8a949 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 @@ -19,14 +19,20 @@ 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.StemcellModel; import org.springframework.ide.vscode.bosh.models.StemcellsModel; import org.springframework.ide.vscode.commons.util.Assert; import org.springframework.ide.vscode.commons.util.CollectorUtil; import org.springframework.ide.vscode.commons.util.Renderable; import org.springframework.ide.vscode.commons.util.Renderables; +import org.springframework.ide.vscode.commons.util.StringUtil; import org.springframework.ide.vscode.commons.util.ValueParsers; +import org.springframework.ide.vscode.commons.yaml.ast.YamlAstCache; +import org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST; +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.YType; import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory; import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.AbstractType; @@ -83,16 +89,18 @@ public class BoshDeploymentManifestSchema implements YamlSchema { private YType t_release_name_ref; private YType t_instance_group_name_def; private YType t_var_name_def; + private final YamlAstCache asts; private final ASTTypeCache astTypes; private DynamicModelProvider cloudConfigProvider; private DynamicModelProvider stemcellsProvider; private List> defAndRefTypes; public BoshDeploymentManifestSchema( - ASTTypeCache astTypes, + YamlAstCache asts, ASTTypeCache astTypes, DynamicModelProvider cloudConfigProvider, DynamicModelProvider stemcellsProvider ) { + this.asts = asts; this.astTypes = astTypes; this.cloudConfigProvider = new CachingModelProvider<>(cloudConfigProvider); this.stemcellsProvider = new CachingModelProvider<>(stemcellsProvider); @@ -144,9 +152,6 @@ public class BoshDeploymentManifestSchema implements YamlSchema { .parseWith(ValueParsers.NE_STRING); - 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()); - YAtomicType t_ip_address = f.yatomic("IPAddress"); //TODO: some kind of checking? t_ip_address.parseWith(ValueParsers.NE_STRING); @@ -187,8 +192,24 @@ public class BoshDeploymentManifestSchema implements YamlSchema { t_release.require(BoshConstraints.SHA1_REQUIRED_FOR_HTTP_URL); 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()); + } + //Troubles determining the filter. So return all stemcells. + return stemcellsProvider.getModel(dc).getVersions(); + }); + addProp(t_stemcell, "alias", t_stemcell_alias_def).isRequired(true); - addProp(t_stemcell, "version", t_ne_string).isRequired(true); + addProp(t_stemcell, "version", t_stemcell_version_ref).isRequired(true); addProp(t_stemcell, "name", t_stemcell_name_ref); addProp(t_stemcell, "os", t_stemcell_os_ref); t_stemcell.requireOneOf("name", "os"); @@ -256,6 +277,12 @@ public class BoshDeploymentManifestSchema implements YamlSchema { return v2Schema; } + private StemcellModel getCurrentStemcell(DynamicSchemaContext dc) throws Exception { + YamlPath path = dc.getPath(); + YamlFileAST ast = asts.getAst(dc.getDocument(), true); + return new StemcellModel(path.dropLast().traverseToNode(ast)); + } + @Override public YType getTopLevelType() { return TOPLEVEL_TYPE; diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshLanguageServer.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshLanguageServer.java index bb062ed63..9f92d41b6 100644 --- a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshLanguageServer.java +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshLanguageServer.java @@ -44,7 +44,7 @@ public class BoshLanguageServer extends SimpleLanguageServer { SimpleTextDocumentService documents = getTextDocumentService(); ASTTypeCache astTypeCache = new ASTTypeCache(); - BoshDeploymentManifestSchema schema = new BoshDeploymentManifestSchema(astTypeCache, cloudConfigProvider, stemcellsProvider); + BoshDeploymentManifestSchema schema = new BoshDeploymentManifestSchema(asts, astTypeCache, cloudConfigProvider, stemcellsProvider); YamlStructureProvider structureProvider = YamlStructureProvider.DEFAULT; YamlAssistContextProvider contextProvider = new SchemaBasedYamlAssistContextProvider(schema); @@ -66,7 +66,6 @@ public class BoshLanguageServer extends SimpleLanguageServer { documents.onDefinition(new BoshDefintionFinder(this, schema, asts, astTypeCache)); } - private void validateOnDocumentChange(IReconcileEngine engine, TextDocument doc) { if (LanguageId.BOSH_DEPLOYMENT.equals(doc.getLanguageId())) { validateWith(doc.getId(), engine); 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 new file mode 100644 index 000000000..6cc174c2e --- /dev/null +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/models/StemcellModel.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.bosh.models; + +import java.util.function.Predicate; + +import org.springframework.ide.vscode.commons.util.StringUtil; +import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil; +import org.yaml.snakeyaml.nodes.Node; + +/** + * A 'view' on a Yaml AST node interpreting it as a Stemcell declaration. + */ +public class StemcellModel { + + private Node node; + + public StemcellModel(Node node) { + this.node = node; + } + + public String getName() { + return NodeUtil.getScalarProperty(node, "name"); + } + + public String getOs() { + return NodeUtil.getScalarProperty(node, "os"); + } + + /** + * Create an appropriate filter to limit the candidate stemcells for + * providing a version based on 'name' or 'os' values already defined in this stemcell. + */ + public Predicate createVersionFilter() { + String name = getName(); + String os = getOs(); + 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()); + } else if (StringUtil.hasText(name)) { + return (sc) -> + name.equals(sc.getName()); + } else if (StringUtil.hasText(os)) { + return (sc) -> + os.equals(sc.getOs()); + } else { + return (sc) -> + 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 e4a7c8e2f..0012df26b 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 @@ -16,8 +16,11 @@ import static org.springframework.ide.vscode.languageserver.testharness.TestAsse import static org.mockito.Mockito.*; import java.io.IOException; +import java.util.Collection; import java.util.concurrent.TimeoutException; +import javax.management.modelmbean.ModelMBeanAttributeInfo; + import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.DiagnosticSeverity; @@ -27,12 +30,17 @@ import org.mockito.Mockito; import org.springframework.ide.vscode.bosh.mocks.MockCloudConfigProvider; import org.springframework.ide.vscode.bosh.models.BoshCommandStemcellsProvider; import org.springframework.ide.vscode.bosh.models.DynamicModelProvider; +import org.springframework.ide.vscode.bosh.models.StemcellData; import org.springframework.ide.vscode.bosh.models.StemcellsModel; +import org.springframework.ide.vscode.commons.util.ExternalCommand; import org.springframework.ide.vscode.commons.util.text.LanguageId; import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems; +import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; import org.springframework.ide.vscode.languageserver.testharness.Editor; import org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableMultiset; import com.google.common.collect.ImmutableSet; @@ -45,7 +53,7 @@ public class BoshEditorTest { @Before public void setup() throws Exception { harness = new LanguageServerHarness(() -> { - return new BoshLanguageServer(cloudConfigProvider, stemcellsProvider) + return new BoshLanguageServer(cloudConfigProvider, (dc) -> stemcellsProvider.getModel(dc)) .setMaxCompletions(100); }, LanguageId.BOSH_DEPLOYMENT @@ -946,6 +954,124 @@ public class BoshEditorTest { ); } + @Test public void contentAssistStemcellVersionFromDirector() throws Exception { + Editor editor; + stemcellsProvider = provideStemcellsFrom( + new StemcellData("ubuntu-agent", "123.4", "ubuntu"), + new StemcellData("ubuntu-agent", "222.2", "ubuntu"), + new StemcellData("centos-agent", "222.2", "centos"), + new StemcellData("centos-agent", "333.3", "centos") + ); + + editor = harness.newEditor( + "stemcells:\n" + + "- alias: good\n" + + " name: centos-agent\n" + + " version: <*>\n" + ); + editor.assertContextualCompletions("<*>", + "222.2<*>", "333.3<*>" + ); + + editor = harness.newEditor( + "stemcells:\n" + + "- alias: good\n" + + " os: ubuntu\n" + + " version: <*>\n" + ); + editor.assertContextualCompletions("<*>", + "123.4<*>", "222.2<*>" + ); + + editor = harness.newEditor( + "stemcells:\n" + + "- alias: good\n" + + " version: <*>\n" + ); + editor.assertContextualCompletions("<*>", + "123.4<*>", "222.2<*>", "333.3<*>" + ); + + //when os or name are 'bogus' at least suggest proposals based on other prop + editor = harness.newEditor( + "stemcells:\n" + + "- alias: good\n" + + " name: centos-agent\n" + + " os: bogus\n" + + " version: <*>\n" + ); + editor.assertContextualCompletions("<*>", + "222.2<*>", "333.3<*>" + ); + + editor = harness.newEditor( + "stemcells:\n" + + "- alias: good\n" + + " os: centos\n" + + " name: bogus\n" + + " version: <*>\n" + ); + editor.assertContextualCompletions("<*>", + "222.2<*>", "333.3<*>" + ); + + //when the os and name disagree, merge the proposals for both: + editor = harness.newEditor( + "stemcells:\n" + + "- alias: good\n" + + " os: centos\n" + //Contradicts the name + " name: ubuntu-agent\n" + //Contradicts the os + " version: <*>\n" + ); + editor.assertContextualCompletions("<*>", + "123.4<*>", "222.2<*>", "333.3<*>" + ); + } + + @Test public void reconcileStemcellVersionFromDirector() throws Exception { + Editor editor; + stemcellsProvider = provideStemcellsFrom( + new StemcellData("ubuntu-agent", "123.4", "ubuntu"), + new StemcellData("ubuntu-agent", "222.2", "ubuntu"), + new StemcellData("centos-agent", "222.2", "centos"), + new StemcellData("centos-agent", "333.3", "centos") + ); + + 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" + ); + 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]" + ); + } + + private DynamicModelProvider provideStemcellsFrom(StemcellData... stemcellData) { + return new BoshCommandStemcellsProvider() { + @Override + protected String executeCommand(ExternalCommand command) throws Exception { + String rows = mapper.writeValueAsString(stemcellData); + return "{\n" + + " \"Tables\": [\n" + + " {\n" + + " \"Rows\": " + rows + + " }\n" + + " ]\n" + + "}"; + } + }; + } + @Test public void contentAssistVMtype() throws Exception { Editor editor = harness.newEditor( "name: foo\n" + diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/ASTTypeCache.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/ASTTypeCache.java index 6e31fe3bc..e7dc85c16 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/ASTTypeCache.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/ASTTypeCache.java @@ -10,7 +10,6 @@ *******************************************************************************/ package org.springframework.ide.vscode.commons.yaml.reconcile; -import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -30,9 +29,7 @@ import org.yaml.snakeyaml.nodes.Node; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; -import com.google.common.collect.ImmutableMultiset; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.ImmutableSet.Builder; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Multimap;