NPE caused by ambiguous keys fixed.

This commit is contained in:
Kris De Volder
2016-12-16 17:56:22 -08:00
parent d867f71592
commit 05bd1dfcd6
7 changed files with 129 additions and 70 deletions

View File

@@ -91,7 +91,8 @@ public abstract class AbstractYamlAssistContext implements YamlAssistContext {
}
protected SNode getContextNode() throws Exception {
return contextPath.traverse((SNode)getContextRoot(getDocument()));
SNode root = (SNode)getContextRoot(getDocument());
return contextPath.traverse(root);
}
protected SDocNode getContextRoot(YamlDocument file) throws Exception {

View File

@@ -167,7 +167,7 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
return contextWith(s, typeUtil.getDomainType(type));
}
String key = s.toPropString();
SNode contextNode = getContextNode();
SNode contextNode = getContextNode();
DynamicSchemaContext dynamicCtxt = new SNodeDynamicSchemaContext(contextNode);
Map<String, YTypedProperty> subproperties = typeUtil.getPropertiesMap(type, dynamicCtxt);
if (subproperties!=null) {

View File

@@ -10,13 +10,42 @@
*******************************************************************************/
package org.springframework.ide.vscode.commons.yaml.path;
import java.util.stream.Stream;
import org.springframework.ide.vscode.commons.util.Log;
import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SNode;
/**
* Different types of things (e.g. {@link ApplicationYamlAssistContext}, {@link SNode} ...) can
* be traversed interpeting {@link YamlPath} as 'navigation operations'. To facilitate
* 'reusable' traversal code, they can implement this interface.
* <p>
* WARNING: both methods in this interface have default implementation. However at least one
* of them must be implemented explicitly otherwise they will call eachother in an infinite recursion!
*/
public interface YamlNavigable<T> {
T traverse(YamlPathSegment s) throws Exception;
/**
* Traversal which silently ignores ambiguity by picking the first valid target
* returned by traverseAmbiguously.
*/
default T traverse(YamlPathSegment s) throws Exception {
return traverseAmbiguously(s).findFirst().orElse(null);
}
/**
* To support traversal in the face of ambiguous steps (i.e. when a step may lead to multiple valid targets),
* implement this method. For convenience a default implementation is provided which calls `traverse`.
* Obviously, this implementation doesn't truly support ambiguity but it is sufficient for YamlNavigables
* where there is no ambiguity, or if you don't care about it.
*/
default Stream<T> traverseAmbiguously(YamlPathSegment s) {
try {
T it = traverse(s);
return it == null ? Stream.empty() : Stream.of(it);
} catch (Exception e) {
Log.log(e);
return Stream.empty();
}
}
}

View File

@@ -13,6 +13,7 @@ package org.springframework.ide.vscode.commons.yaml.path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import org.springframework.ide.vscode.commons.yaml.ast.NodeRef;
import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil;
@@ -121,18 +122,18 @@ public class YamlPath {
}
public <T extends YamlNavigable<T>> T traverse(T startNode) {
try {
T node = startNode;
return traverseAmbiguously(startNode).findFirst().orElse(null);
}
public <T extends YamlNavigable<T>> Stream<T> traverseAmbiguously(T startNode) {
if (startNode!=null) {
Stream<T> result = Stream.of(startNode);
for (YamlPathSegment s : segments) {
if (node==null) {
return null;
}
node = node.traverse(s);
result = result.flatMap((node) -> node.traverseAmbiguously(s));
}
return node;
} catch (Exception e) {
return null;
return result;
}
return Stream.empty();
}
public YamlPath dropFirst(int dropCount) {
@@ -265,4 +266,5 @@ public class YamlPath {
return new YamlPath(common);
}
}

View File

@@ -3,22 +3,35 @@ package org.springframework.ide.vscode.commons.yaml.structure;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.springframework.ide.vscode.commons.util.Assert;
import org.springframework.ide.vscode.commons.util.CollectionUtil;
import org.springframework.ide.vscode.commons.util.Log;
import org.springframework.ide.vscode.commons.util.StringUtil;
import org.springframework.ide.vscode.commons.util.text.IRegion;
import org.springframework.ide.vscode.commons.yaml.path.KeyAliases;
import org.springframework.ide.vscode.commons.yaml.path.YamlNavigable;
import org.springframework.ide.vscode.commons.yaml.path.YamlPath;
import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment;
import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SKeyNode;
import org.springframework.ide.vscode.commons.yaml.util.YamlIndentUtil;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
/**
* A robust, coarse-grained parser that guesses the structure of a
* yml document based on indentation levels.
@@ -251,34 +264,56 @@ public class YamlStructureParser {
* Default implementation, doesn't support any type of traversal operation.
* Subclasses must override and implement where appropriate.
*/
@Override
public SNode traverse(YamlPathSegment s) throws Exception {
return null;
}
protected abstract void dump(Writer out, int indent) throws Exception;
public YamlPath getPath() throws Exception {
ArrayList<YamlPathSegment> segments = new ArrayList<YamlPathSegment>();
buildPath(this, segments);
return new YamlPath(segments);
public ImmutableList<SNode> getPathNodes() throws Exception {
ImmutableList.Builder<SNode> nodes = ImmutableList.builder();
buildPath(this, nodes);
return nodes.build();
}
private static void buildPath(SNode node, ArrayList<YamlPathSegment> segments) throws Exception {
private static void buildPath(SNode node, Builder<SNode> nodes) {
if (node!=null) {
buildPath(node.getParent(), nodes);
nodes.add(node);
}
}
public YamlPath getPath() throws Exception {
List<YamlPathSegment> path = new ArrayList<>();
for (SNode node : getPathNodes()) {
YamlPathSegment segment = getSegment(node);
if (segment!=null) {
path.add(segment);
}
}
return new YamlPath(path);
}
/**
* Determine a YamlPathSegment that corresponds to given node. This may be
* null because not all SNodes can be interpreted as 'step' in the yml
* structure (e.g. raw nodes will return null, as will the 'root' node).
*/
private YamlPathSegment getSegment(SNode node) throws Exception {
if (node!=null) {
buildPath(node.getParent(), segments);
SNodeType nodeType = node.getNodeType();
if (nodeType==SNodeType.KEY) {
String key = ((SKeyNode)node).getKey();
segments.add(YamlPathSegment.valueAt(key));
return YamlPathSegment.valueAt(key);
} else if (nodeType==SNodeType.SEQ) {
int index = ((SSeqNode)node).getIndex();
segments.add(YamlPathSegment.valueAt(index));
return YamlPathSegment.valueAt(index);
} else if (nodeType==SNodeType.DOC) {
int index = ((SDocNode)node).getIndex();
segments.add(YamlPathSegment.valueAt(index));
return YamlPathSegment.valueAt(index);
}
}
return null;
}
public SRootNode getRoot() {
@@ -360,7 +395,7 @@ public class YamlStructureParser {
public abstract class SChildBearingNode extends SNode {
private List<SNode> children = null;
private Map<String, SKeyNode> keyMap = null; //lazily constructed index of children children.
private Multimap<String, SNode> keyMap = null; //lazily constructed index of children.
public SChildBearingNode(SChildBearingNode parent, YamlDocument doc, int indent, int start, int end) {
super(parent, doc, indent, start, end);
@@ -422,14 +457,14 @@ public class YamlStructureParser {
}
@Override
public SNode traverse(YamlPathSegment s) throws Exception {
public Stream<SNode> traverseAmbiguously(YamlPathSegment s) {
switch (s.getType()) {
case VAL_AT_KEY:
return this.getChildWithKey(s.toPropString());
return this.getChildrenWithKey(s.toPropString());
case VAL_AT_INDEX:
return this.getSeqChildWithIndex(s.toIndex());
return Stream.of(this.getSeqChildWithIndex(s.toIndex()));
default:
return null;
return Stream.empty();
}
}
@@ -446,36 +481,43 @@ public class YamlStructureParser {
return null;
}
public SKeyNode getChildWithKey(String key) throws Exception {
public Stream<SNode> getChildrenWithKey(String key) {
if (CollectionUtil.hasElements(children)) {
SKeyNode child = keyMap().get(key);
if (child==null) {
Iterable<String> keyAliases = getKeyAliases(key);
if (keyAliases!=null) {
for (String keyAlias : keyAliases) {
child = keyMap().get(keyAlias);
if (child!=null) {
return child;
}
Stream<SNode> allChildren = Stream.empty();
Collection<SNode> plainChildren = keyMap().get(key);
if (CollectionUtil.hasElements(plainChildren)) {
allChildren = plainChildren.stream();
}
Iterable<String> keyAliases = getKeyAliases(key);
if (keyAliases!=null) {
for (String keyAlias : keyAliases) {
Collection<SNode> aliasChildren = keyMap().get(keyAlias);
if (CollectionUtil.hasElements(aliasChildren)) {
allChildren = Stream.concat(allChildren, aliasChildren.stream());
}
}
}
return child;
return allChildren;
}
return null;
return Stream.empty();
}
public SKeyNode getChildWithKey(String key) {
return (SKeyNode)getChildrenWithKey(key).findFirst().orElse(null);
}
private Map<String, SKeyNode> keyMap() throws Exception {
private Multimap<String, SNode> keyMap() {
if (keyMap==null) {
HashMap<String, SKeyNode> index = new HashMap<String, SKeyNode>();
ListMultimap<String, SNode> index = MultimapBuilder.hashKeys().arrayListValues().build();
for (SNode node: getChildren()) {
if (node.getNodeType()==SNodeType.KEY) {
SKeyNode keyNode = (SKeyNode)node;
String key = ((SKeyNode)node).getKey();
SKeyNode existing = index.get(key);
if (existing==null) {
try {
if (node.getNodeType()==SNodeType.KEY) {
SKeyNode keyNode = (SKeyNode)node;
String key = ((SKeyNode)node).getKey();
index.put(key, keyNode);
}
} catch (Exception e) {
Log.log(e);
}
}
keyMap = index;

View File

@@ -17,6 +17,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import org.springframework.boot.configurationmetadata.Deprecation;
import org.springframework.ide.vscode.boot.common.InformationTemplates;
@@ -125,27 +126,6 @@ public abstract class ApplicationYamlAssistContext extends AbstractYamlAssistCon
return new IndexContext(doc, documentSelector, YamlPath.EMPTY, IndexNavigator.with(index), completionFactory, typeUtil, conf);
}
public static YamlAssistContext forPath(YamlDocument doc, YamlPath contextPath, FuzzyMap<PropertyInfo> index, PropertyCompletionFactory completionFactory, TypeUtil typeUtil, RelaxedNameConfig conf) {
try {
YamlPathSegment documentSelector = contextPath.getSegment(0);
if (documentSelector!=null) {
contextPath = contextPath.dropFirst(1);
YamlAssistContext context = ApplicationYamlAssistContext.subdocument(doc, documentSelector.toIndex(), index, completionFactory, typeUtil, conf);
for (YamlPathSegment s : contextPath.getSegments()) {
if (context==null) return null;
context = context.traverse(s);
}
return context;
}
} catch (Exception e) {
Log.log(e);
}
return null;
}
@Override
abstract public YamlAssistContext traverse(YamlPathSegment s) throws Exception;
private static class TypeContext extends ApplicationYamlAssistContext {
private PropertyCompletionFactory completionFactory;
@@ -415,7 +395,7 @@ public abstract class ApplicationYamlAssistContext extends AbstractYamlAssistCon
if (!matchingProps.isEmpty()) {
ArrayList<ICompletionProposal> completions = new ArrayList<ICompletionProposal>();
for (Match<PropertyInfo> match : matchingProps) {
DocumentEdits edits = createEdits(doc, offset, query, match);
DocumentEdits edits = createEdits(doc, node, offset, query, match);
ScoreableProposal completion = completionFactory.property(
doc.getDocument(), edits, match, typeUtil
);
@@ -430,7 +410,7 @@ public abstract class ApplicationYamlAssistContext extends AbstractYamlAssistCon
}
protected DocumentEdits createEdits(final YamlDocument file,
final int offset, final String query, final Match<PropertyInfo> match)
SNode node, final int offset, final String query, final Match<PropertyInfo> match)
throws Exception {
//Edits created lazyly as they are somwehat expensive to compute and mostly
// we need only the edits for the one proposal that user picks.

View File

@@ -3311,7 +3311,12 @@ public class ApplicationYamlEditorTest extends AbstractPropsEditorTest {
" activemq:\n" +
" broker-u<*>"
, // ==>
"fill it in later"
"spring:\n" +
" application:\n" +
" name: my-app\n" +
"spring:\n" +
" activemq:\n" +
" broker-url: <*>"
);
}