Merge application-properties into boot-properties project

This commit is contained in:
Kris De Volder
2016-11-30 15:26:55 -08:00
parent df78d3756e
commit 63b3472561
36 changed files with 66 additions and 559 deletions

View File

@@ -10,24 +10,20 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot;
import static org.springframework.ide.vscode.commons.util.Providers.lazy;
import javax.inject.Provider;
import org.eclipse.lsp4j.CompletionOptions;
import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.TextDocumentSyncKind;
import org.springframework.ide.vscode.application.properties.completions.SpringPropertiesCompletionEngine;
import org.springframework.ide.vscode.application.properties.hover.PropertiesHoverInfoProvider;
import org.springframework.ide.vscode.application.properties.metadata.PropertyInfo;
import org.springframework.ide.vscode.application.properties.metadata.SpringPropertyIndexProvider;
import org.springframework.ide.vscode.application.properties.metadata.completions.PropertyCompletionFactory;
import org.springframework.ide.vscode.application.properties.metadata.completions.RelaxedNameConfig;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtilProvider;
import org.springframework.ide.vscode.application.properties.metadata.util.FuzzyMap;
import org.springframework.ide.vscode.application.properties.reconcile.SpringPropertiesReconcileEngine;
import org.springframework.ide.vscode.application.yaml.completions.ApplicationYamlAssistContext;
import org.springframework.ide.vscode.application.yaml.reconcile.ApplicationYamlReconcileEngine;
import org.springframework.ide.vscode.boot.properties.completions.SpringPropertiesCompletionEngine;
import org.springframework.ide.vscode.boot.properties.hover.PropertiesHoverInfoProvider;
import org.springframework.ide.vscode.boot.properties.reconcile.SpringPropertiesReconcileEngine;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionEngine;
import org.springframework.ide.vscode.commons.languageserver.completion.VscodeCompletionEngineAdapter;
import org.springframework.ide.vscode.commons.languageserver.hover.HoverInfoProvider;

View File

@@ -0,0 +1,97 @@
/*******************************************************************************
* Copyright (c) 2016 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.boot.properties;
import org.eclipse.lsp4j.CompletionOptions;
import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.TextDocumentSyncKind;
import org.springframework.ide.vscode.application.properties.metadata.SpringPropertyIndexProvider;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtilProvider;
import org.springframework.ide.vscode.boot.properties.completions.SpringPropertiesCompletionEngine;
import org.springframework.ide.vscode.boot.properties.hover.PropertiesHoverInfoProvider;
import org.springframework.ide.vscode.boot.properties.reconcile.SpringPropertiesReconcileEngine;
import org.springframework.ide.vscode.commons.languageserver.completion.VscodeCompletionEngineAdapter;
import org.springframework.ide.vscode.commons.languageserver.hover.VscodeHoverEngineAdapter;
import org.springframework.ide.vscode.commons.languageserver.hover.VscodeHoverEngineAdapter.HoverType;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService;
import org.springframework.ide.vscode.commons.languageserver.util.TextDocument;
/**
* Language Server for Spring Boot Application Properties files
*
* @author Alex Boyko
*
*/
public class ApplicationPropertiesLanguageServer extends SimpleLanguageServer {
private SpringPropertyIndexProvider indexProvider;
private TypeUtilProvider typeUtilProvider;
private VscodeCompletionEngineAdapter completionEngine;
private SpringPropertiesReconcileEngine reconcileEngine;
private VscodeHoverEngineAdapter hoverEngine;
public ApplicationPropertiesLanguageServer(SpringPropertyIndexProvider indexProvider, TypeUtilProvider typeUtilProvider, JavaProjectFinder javaProjectFinder) {
this.indexProvider = indexProvider;
this.typeUtilProvider = typeUtilProvider;
SimpleTextDocumentService documents = getTextDocumentService();
reconcileEngine = getReconcileEngine();
documents.onDidChangeContent(params -> {
TextDocument doc = params.getDocument();
validateWith(doc, reconcileEngine);
});
SpringPropertiesCompletionEngine propertiesCompletionEngine = new SpringPropertiesCompletionEngine(
indexProvider,
typeUtilProvider,
javaProjectFinder
);
completionEngine = new VscodeCompletionEngineAdapter(this, propertiesCompletionEngine);
completionEngine.setMaxCompletionsNumber(100);
documents.onCompletion(completionEngine::getCompletions);
documents.onCompletionResolve(completionEngine::resolveCompletion);
PropertiesHoverInfoProvider hoverInfoProvider = new PropertiesHoverInfoProvider(indexProvider, typeUtilProvider, javaProjectFinder);
hoverEngine = new VscodeHoverEngineAdapter(this, hoverInfoProvider);
documents.onHover(hoverEngine::getHover);
}
public void setMaxCompletionsNumber(int number) {
completionEngine.setMaxCompletionsNumber(number);
}
public void setHoverType(HoverType type) {
hoverEngine.setHoverType(type);
}
@Override
protected ServerCapabilities getServerCapabilities() {
ServerCapabilities c = new ServerCapabilities();
c.setTextDocumentSync(TextDocumentSyncKind.Full);
CompletionOptions completionProvider = new CompletionOptions();
completionProvider.setResolveProvider(false);
c.setCompletionProvider(completionProvider);
c.setHoverProvider(true);
return c;
}
protected SpringPropertiesReconcileEngine getReconcileEngine() {
return new SpringPropertiesReconcileEngine(indexProvider, typeUtilProvider);
}
}

View File

@@ -0,0 +1,338 @@
package org.springframework.ide.vscode.boot.properties.completions;
import static org.springframework.ide.vscode.boot.properties.tools.CommonLanguageTools.*;
import static org.springframework.ide.vscode.commons.util.StringUtil.camelCaseToHyphens;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.springframework.ide.vscode.application.properties.metadata.PropertyInfo;
import org.springframework.ide.vscode.application.properties.metadata.completions.PropertyCompletionFactory;
import org.springframework.ide.vscode.application.properties.metadata.hints.HintProvider;
import org.springframework.ide.vscode.application.properties.metadata.hints.HintProviders;
import org.springframework.ide.vscode.application.properties.metadata.hints.StsValueHint;
import org.springframework.ide.vscode.application.properties.metadata.hints.ValueHintHoverInfo;
import org.springframework.ide.vscode.application.properties.metadata.types.Type;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeParser;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil.BeanPropertyNameMode;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil.EnumCaseMode;
import org.springframework.ide.vscode.application.properties.metadata.types.TypedProperty;
import org.springframework.ide.vscode.application.properties.metadata.util.FuzzyMap;
import org.springframework.ide.vscode.application.properties.metadata.util.FuzzyMap.Match;
import org.springframework.ide.vscode.boot.properties.reconcile.PropertyNavigator;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
import org.springframework.ide.vscode.commons.languageserver.completion.LazyProposalApplier;
import org.springframework.ide.vscode.commons.languageserver.util.BadLocationException;
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
import org.springframework.ide.vscode.commons.languageserver.util.IDocument;
import org.springframework.ide.vscode.commons.languageserver.util.PrefixFinder;
import org.springframework.ide.vscode.commons.util.CollectionUtil;
import org.springframework.ide.vscode.commons.util.FuzzyMatcher;
import org.springframework.ide.vscode.commons.util.Log;
import org.springframework.ide.vscode.java.properties.antlr.parser.AntlrParser;
import org.springframework.ide.vscode.java.properties.parser.ParseResults;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.EmptyLine;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Key;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Node;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Value;
import com.google.common.collect.ImmutableList;
public class PropertiesCompletionProposalsCalculator {
private static final PrefixFinder valuePrefixFinder = new PrefixFinder() {
protected boolean isPrefixChar(char c) {
return isValuePrefixChar(c);
}
};
private static final PrefixFinder fuzzySearchPrefix = new PrefixFinder() {
protected boolean isPrefixChar(char c) {
return !Character.isWhitespace(c);
}
};
private static final PrefixFinder navigationPrefixFinder = new PrefixFinder() {
public String getPrefix(IDocument doc, int offset) {
String prefix = super.getPrefix(doc, offset);
//Check if character before looks like 'navigation'.. otherwise don't
// return a navigationPrefix.
char charBefore = getCharBefore(doc, prefix, offset);
if (charBefore=='.' || charBefore==']') {
return prefix;
}
return null;
}
private char getCharBefore(IDocument doc, String prefix, int offset) {
try {
if (prefix!=null) {
int offsetBefore = offset-prefix.length()-1;
if (offsetBefore>=0) {
return doc.getChar(offsetBefore);
}
}
} catch (BadLocationException e) {
//ignore
}
return 0;
}
protected boolean isPrefixChar(char c) {
return !Character.isWhitespace(c) && c!=']' && c!=']' && c!='.';
}
};
private FuzzyMap<PropertyInfo> index;
private TypeUtil typeUtil;
private PropertyCompletionFactory completionFactory;
private IDocument doc;
private int offset;
private boolean preferLowerCaseEnums;
private AntlrParser parser;
public PropertiesCompletionProposalsCalculator(FuzzyMap<PropertyInfo> index, TypeUtil typeUtil, PropertyCompletionFactory completionFactory, IDocument doc, int offset, boolean preferLowerCaseEnums) {
this.index = index;
this.typeUtil = typeUtil;
this.completionFactory = completionFactory;
this.doc = doc;
this.offset = offset;
this.preferLowerCaseEnums = preferLowerCaseEnums;
this.parser = new AntlrParser();
}
/**
* Create completions proposals in the context of a properties text editor.
*/
public Collection<ICompletionProposal> calculate() throws BadLocationException {
ParseResults parseResults = parser.parse(doc.get());
Node node = parseResults.ast.findNode(offset);
if (node instanceof Value) {
return getValueCompletions((Value)node);
} else if (node instanceof Key || node instanceof EmptyLine || node == null) {
return getPropertyCompletions();
}
return Collections.emptyList();
}
private Collection<ICompletionProposal> getNavigationProposals() {
String navPrefix = navigationPrefixFinder.getPrefix(doc, offset);
try {
if (navPrefix!=null) {
int navOffset = offset-navPrefix.length()-1; //offset of 'nav' operator char (i.e. '.' or ']').
navPrefix = fuzzySearchPrefix.getPrefix(doc, navOffset);
if (navPrefix!=null && !navPrefix.isEmpty()) {
PropertyInfo prop = findLongestValidProperty(index, navPrefix);
if (prop!=null) {
int regionStart = navOffset-navPrefix.length();
Collection<ICompletionProposal> hintProposals = getKeyHintProposals(prop, navOffset);
if (CollectionUtil.hasElements(hintProposals)) {
return hintProposals;
}
PropertyNavigator navigator = new PropertyNavigator(doc, null, typeUtil, new DocumentRegion(doc, regionStart, navOffset));
Type type = navigator.navigate(regionStart+prop.getId().length(), TypeParser.parse(prop.getType()));
if (type!=null) {
return getNavigationProposals(type, navOffset);
}
}
}
}
} catch (Exception e) {
Log.log(e);
}
return Collections.emptyList();
}
private Collection<ICompletionProposal> getKeyHintProposals(PropertyInfo prop, int navOffset) {
HintProvider hintProvider = prop.getHints(typeUtil, false);
if (!HintProviders.isNull(hintProvider)) {
String query = textBetween(doc, navOffset+1, offset);
List<TypedProperty> hintProperties = hintProvider.getPropertyHints(query);
if (CollectionUtil.hasElements(hintProperties)) {
return createPropertyProposals(TypeParser.parse(prop.getType()), navOffset, query, hintProperties);
}
}
return ImmutableList.of();
}
private String textBetween(IDocument doc, int start, int end) {
if (end > doc.getLength()) {
end = doc.getLength();
}
if (start>doc.getLength()) {
start = doc.getLength();
}
if (start<0) {
start = 0;
}
if (end < 0) {
end = 0;
}
if (start<end) {
try {
return doc.get(start, end-start);
} catch (BadLocationException e) {
}
}
return "";
}
/**
* @param type Type of the expression leading upto the 'nav' operator
* @param navOffset Offset of the nav operator (either ']' or '.'
* @param offset Offset of the cursor where CA was requested.
*/
private Collection<ICompletionProposal> getNavigationProposals(Type type, int navOffset) {
try {
char navOp = doc.getChar(navOffset);
if (navOp=='.') {
String prefix = doc.get(navOffset+1, offset-(navOffset+1));
EnumCaseMode caseMode = caseMode(prefix);
List<TypedProperty> objectProperties = typeUtil.getProperties(type, caseMode, BeanPropertyNameMode.HYPHENATED);
//Note: properties editor itself deals with relaxed names. So it expects the properties here to be returned in hyphenated form only.
if (objectProperties!=null && !objectProperties.isEmpty()) {
return createPropertyProposals(type, navOffset, prefix, objectProperties);
}
} else {
//TODO: other cases ']' or '[' ?
}
} catch (Exception e) {
Log.log(e);
}
return Collections.emptyList();
}
protected Collection<ICompletionProposal> createPropertyProposals(Type type, int navOffset,
String prefix, List<TypedProperty> objectProperties) {
ArrayList<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>();
for (TypedProperty prop : objectProperties) {
double score = FuzzyMatcher.matchScore(prefix, prop.getName());
if (score!=0) {
Type valueType = prop.getType();
String postFix = propertyCompletionPostfix(typeUtil, valueType);
DocumentEdits edits = new DocumentEdits(doc);
edits.delete(navOffset+1, offset);
edits.insert(offset, prop.getName()+postFix);
proposals.add(
completionFactory.beanProperty(doc, null, type, prefix, prop, score, edits, typeUtil)
);
}
}
return proposals;
}
/**
* Determines the EnumCaseMode used to generate completion candidates based on prefix.
*/
protected EnumCaseMode caseMode(String prefix) {
EnumCaseMode caseMode;
if ("".equals(prefix)) {
caseMode = preferLowerCaseEnums?EnumCaseMode.LOWER_CASE:EnumCaseMode.ORIGNAL;
} else {
caseMode = Character.isLowerCase(prefix.charAt(0))?EnumCaseMode.LOWER_CASE:EnumCaseMode.ORIGNAL;
}
return caseMode;
}
protected static String propertyCompletionPostfix(TypeUtil typeUtil, Type type) {
String postfix = "";
if (type!=null) {
if (typeUtil.isAssignableType(type)) {
postfix = "=";
} else if (TypeUtil.isBracketable(type)) {
postfix = "[";
} else if (typeUtil.isDotable(type)) {
postfix = ".";
}
}
return postfix;
}
private Collection<ICompletionProposal> getValueCompletions(Value value) {
DocumentRegion valueRegion = createRegion(doc, value).trimStart(SPACES).trimEnd(SPACES);
String query = valuePrefixFinder.getPrefix(doc, offset, valueRegion.getStart());
int startOfValue = offset - query.length();
EnumCaseMode caseMode = caseMode(query);
// note: no need to skip whitespace backwards.
String propertyName = /*fuzzySearchPrefix.getPrefix(doc, pair.getOffset())*/value.getParent().getKey().decode();
// because value partition includes whitespace around the assignment
if (propertyName != null) {
Collection<StsValueHint> valueCompletions = getValueHints(index, typeUtil, query, propertyName, caseMode);
if (valueCompletions != null && !valueCompletions.isEmpty()) {
ArrayList<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>();
for (StsValueHint hint : valueCompletions) {
String valueCandidate = hint.getValue();
double score = FuzzyMatcher.matchScore(query, valueCandidate);
if (score != 0) {
DocumentEdits edits = new DocumentEdits(doc);
edits.delete(startOfValue, offset);
edits.insert(offset, valueCandidate);
proposals.add(completionFactory.valueProposal(valueCandidate, query, getValueType(index, typeUtil, propertyName),
score, edits, new ValueHintHoverInfo(hint))
// new ValueProposal(startOfValue, valuePrefix,
// valueCandidate, i)
);
}
}
return proposals;
}
}
return Collections.emptyList();
}
private DocumentRegion createRegion(IDocument doc, Node value) {
// Trim trailing spaces (there is no leading white space already)
int length = value.getLength();
try {
length = doc.get(value.getOffset(), value.getLength()).trim().length();
} catch (BadLocationException e) {
// ignore
}
return new DocumentRegion(doc, value.getOffset(), value.getOffset() + length);
}
private List<Match<PropertyInfo>> findMatches(String prefix) {
List<Match<PropertyInfo>> matches = index.find(camelCaseToHyphens(prefix));
return matches;
}
private Collection<ICompletionProposal> getPropertyCompletions() throws BadLocationException {
Collection<ICompletionProposal> navProposals = getNavigationProposals();
if (!navProposals.isEmpty()) {
return navProposals;
}
return getFuzzyCompletions();
}
protected Collection<ICompletionProposal> getFuzzyCompletions() {
final String prefix = fuzzySearchPrefix.getPrefix(doc, offset);
if (prefix != null) {
Collection<Match<PropertyInfo>> matches = findMatches(prefix);
if (matches!=null && !matches.isEmpty()) {
ArrayList<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(matches.size());
for (final Match<PropertyInfo> match : matches) {
DocumentEdits docEdits;
try {
docEdits = LazyProposalApplier.from(() -> {
Type type = TypeParser.parse(match.data.getType());
DocumentEdits edits = new DocumentEdits(doc);
edits.delete(offset-prefix.length(), offset);
edits.insert(offset, match.data.getId() + propertyCompletionPostfix(typeUtil, type));
return edits;
});
proposals.add(completionFactory.property(doc, docEdits, match, typeUtil));
} catch (Exception e) {
Log.log(e);
}
}
return proposals;
}
}
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,51 @@
package org.springframework.ide.vscode.boot.properties.completions;
import java.util.Collection;
import org.springframework.ide.vscode.application.properties.metadata.SpringPropertyIndexProvider;
import org.springframework.ide.vscode.application.properties.metadata.completions.PropertyCompletionFactory;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtilProvider;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionEngine;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.languageserver.util.BadLocationException;
import org.springframework.ide.vscode.commons.languageserver.util.IDocument;;
/**
* @author Kris De Volder
*/
public class SpringPropertiesCompletionEngine implements ICompletionEngine {
private boolean preferLowerCaseEnums = true; //might make sense to make this user configurable
private SpringPropertyIndexProvider indexProvider;
private TypeUtilProvider typeUtilProvider;
private PropertyCompletionFactory completionFactory = null;
/**
* Constructor used in 'production'. Wires up stuff properly for running inside a normal
* Eclipse runtime.
*/
public SpringPropertiesCompletionEngine(SpringPropertyIndexProvider indexProvider, TypeUtilProvider typeUtilProvider, JavaProjectFinder projectFinder) {
this.indexProvider = indexProvider;
this.typeUtilProvider = typeUtilProvider;
this.completionFactory = new PropertyCompletionFactory(projectFinder);
}
/**
* Create completions proposals in the context of a properties text editor.
*/
public Collection<ICompletionProposal> getCompletions(IDocument doc, int offset) throws BadLocationException {
return new PropertiesCompletionProposalsCalculator(indexProvider.getIndex(doc),
typeUtilProvider.getTypeUtil(doc), completionFactory, doc, offset, preferLowerCaseEnums).calculate();
}
public boolean getPreferLowerCaseEnums() {
return preferLowerCaseEnums;
}
public void setPreferLowerCaseEnums(boolean preferLowerCaseEnums) {
this.preferLowerCaseEnums = preferLowerCaseEnums;
}
}

View File

@@ -0,0 +1,170 @@
package org.springframework.ide.vscode.boot.properties.hover;
import static org.springframework.ide.vscode.boot.properties.tools.CommonLanguageTools.SPACES;
import static org.springframework.ide.vscode.boot.properties.tools.CommonLanguageTools.getValueHints;
import static org.springframework.ide.vscode.boot.properties.tools.CommonLanguageTools.getValueType;
import java.util.Collection;
import java.util.Optional;
import org.springframework.ide.vscode.application.properties.metadata.PropertyInfo;
import org.springframework.ide.vscode.application.properties.metadata.SpringPropertyIndex;
import org.springframework.ide.vscode.application.properties.metadata.hints.StsValueHint;
import org.springframework.ide.vscode.application.properties.metadata.hover.PropertyRenderableProvider;
import org.springframework.ide.vscode.application.properties.metadata.types.Type;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil.EnumCaseMode;
import org.springframework.ide.vscode.application.properties.metadata.util.FuzzyMap;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.util.BadLocationException;
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
import org.springframework.ide.vscode.commons.languageserver.util.IDocument;
import org.springframework.ide.vscode.commons.languageserver.util.IRegion;
import org.springframework.ide.vscode.commons.util.Renderable;
import org.springframework.ide.vscode.commons.util.Renderables;
import org.springframework.ide.vscode.java.properties.antlr.parser.AntlrParser;
import org.springframework.ide.vscode.java.properties.parser.ParseResults;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Key;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Node;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Value;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
class PropertiesHoverCalculator {
private FuzzyMap<PropertyInfo> index;
private TypeUtil typeUtil;
private IJavaProject project;
private IDocument doc;
private int offset;
private AntlrParser parser;
PropertiesHoverCalculator(FuzzyMap<PropertyInfo> index, TypeUtil typeUtil, IJavaProject project, IDocument doc, int offset) {
this.index = index;
this.typeUtil = typeUtil;
this.project = project;
this.doc = doc;
this.offset = offset;
this.parser = new AntlrParser();
}
Tuple2<Renderable, IRegion> calculate() {
ParseResults parseResults = parser.parse(doc.get());
Node node = parseResults.ast.findNode(offset);
if (node instanceof Value) {
return getValueHover((Value)node);
} else if (node instanceof Key) {
return getPropertyHover((Key)node);
}
return null;
}
private DocumentRegion createRegion(IDocument doc, Node value) {
// Trim trailing spaces (there is no leading white space already)
int length = value.getLength();
try {
length = doc.get(value.getOffset(), value.getLength()).length();
} catch (BadLocationException e) {
// ignore
}
return new DocumentRegion(doc, value.getOffset(), value.getOffset() + length);
}
private Tuple2<Renderable, IRegion> getPropertyHover(Key property) {
PropertyInfo best = findBestHoverMatch(property.decode());
if (best == null) {
return null;
} else {
Renderable renderable = new PropertyRenderableProvider(project, best).getRenderable();
DocumentRegion region = createRegion(doc, property);
return Tuples.of(renderable, region.asRegion());
}
}
private Tuple2<Renderable, IRegion> getValueHover(Value value) {
DocumentRegion valueRegion = createRegion(doc, value).trimStart(SPACES).trimEnd(SPACES);
if (valueRegion.getStart() <= offset && offset < valueRegion.getEnd()) {
String valueString = valueRegion.toString();
String propertyName = value.getParent().getKey().decode();
Type type = getValueType(index, typeUtil, propertyName);
if (TypeUtil.isArray(type) || TypeUtil.isList(type)) {
//It is useful to provide content assist for the values in the list when entering a list
type = TypeUtil.getDomainType(type);
}
if (TypeUtil.isClass(type)) {
//Special case. We want to provide hoverinfos more liberally than what's suggested for completions (i.e. even class names
//that are not suggested by the hints because they do not meet subtyping constraints should be hoverable and linkable!
StsValueHint hint = StsValueHint.className(valueString, typeUtil);
if (hint!=null) {
return Tuples.of(createRenderable(hint), valueRegion.asRegion());
}
}
//Hack: pretend to invoke content-assist at the end of the value text. This should provide hints applicable to that value
// then show hoverinfo based on that. That way we can avoid duplication a lot of similar logic to compute hoverinfos and hyperlinks.
Collection<StsValueHint> hints = getValueHints(index, typeUtil, valueString, propertyName, EnumCaseMode.ALIASED);
if (hints!=null) {
Optional<StsValueHint> hint = hints.stream().filter(h -> valueString.equals(h.getValue())).findFirst();
if (hint.isPresent()) {
return Tuples.of(createRenderable(hint.get()), valueRegion.asRegion());
}
}
}
return null;
}
private Renderable createRenderable(StsValueHint hint) {
return Renderables.htmlBlob((html) -> {
/*
* HACK: javadoc comment from HTML javadoc provider coming from
* generated HTML javadoc is very rich and decorating it further
* with some header like labels just makes it look worse
*/
String descriptionHtml = hint.getDescription().toHtml();
if (descriptionHtml.indexOf("<h4>") == -1) {
// Simple text like description without proper header
html.bold(hint.getValue());
html.raw("<p>");
html.raw(hint.getDescription().toHtml());
html.raw("</p>");
} else {
// Description is javadoc-like description from HTML javadoc
html.raw(hint.getDescription().toHtml());
}
});
}
/**
* Search known properties for the best 'match' to show as hover data.
*/
private PropertyInfo findBestHoverMatch(String propName) {
PropertyInfo propertyInfo = index.get(propName);
if (propertyInfo == null) {
propertyInfo = SpringPropertyIndex.findLongestValidProperty(index, propName);
}
return propertyInfo;
// //TODO: optimize, should be able to use index's treemap to find this without iterating all entries.
// PropertyInfo best = null;
// int bestCommonPrefixLen = 0; //We try to pick property with longest common prefix
// int bestExtraLen = Integer.MAX_VALUE;
// for (PropertyInfo candidate : index) {
// int commonPrefixLen = StringUtil.commonPrefixLength(propName, candidate.getId());
// int extraLen = candidate.getId().length()-commonPrefixLen;
// if (commonPrefixLen==propName.length() && extraLen==0) {
// //exact match found, can stop searching for better matches
// return candidate;
// }
// //candidate is better if...
// if (commonPrefixLen>bestCommonPrefixLen // it has a longer common prefix
// || commonPrefixLen==bestCommonPrefixLen && extraLen<bestExtraLen //or same common prefix but fewer extra chars
// ) {
// bestCommonPrefixLen = commonPrefixLen;
// bestExtraLen = extraLen;
// best = candidate;
// }
// }
// return best;
}
}

View File

@@ -0,0 +1,34 @@
package org.springframework.ide.vscode.boot.properties.hover;
import org.springframework.ide.vscode.application.properties.metadata.SpringPropertyIndexProvider;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtilProvider;
import org.springframework.ide.vscode.commons.languageserver.hover.HoverInfoProvider;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.languageserver.util.IDocument;
import org.springframework.ide.vscode.commons.languageserver.util.IRegion;
import org.springframework.ide.vscode.commons.util.Renderable;
import reactor.util.function.Tuple2;
public class PropertiesHoverInfoProvider implements HoverInfoProvider {
private SpringPropertyIndexProvider indexProvider;
private TypeUtilProvider typeUtilProvider;
private JavaProjectFinder projectFinder;
public PropertiesHoverInfoProvider(SpringPropertyIndexProvider indexProvider, TypeUtilProvider typeUtilProvider, JavaProjectFinder projectFinder) {
this.indexProvider = indexProvider;
this.typeUtilProvider = typeUtilProvider;
this.projectFinder = projectFinder;
}
@Override
public Tuple2<Renderable, IRegion> getHoverInfo(IDocument document, int offset) throws Exception {
return new PropertiesHoverCalculator(indexProvider.getIndex(document),
typeUtilProvider.getTypeUtil(document), projectFinder.find(document), document, offset).calculate();
}
}

View File

@@ -0,0 +1,154 @@
/*******************************************************************************
* Copyright (c) 2016 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.boot.properties.quickfix;
import org.eclipse.lsp4j.CompletionItemKind;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
import org.springframework.ide.vscode.commons.languageserver.quickfix.ProblemFixer;
public class ReplaceDeprecatedPropertyQuickfix implements ICompletionProposal {
public static ProblemFixer FIXER = (context, problem, proposals) -> {
throw new UnsupportedOperationException("Not yet implemented");
// PropertyInfo metadata = problem.getMetadata();
// if (metadata!=null) {
// String replacement = metadata.getDeprecationReplacement();
// if (replacement!=null) {
// //No need to check problem type... we only attach this fixer to problems of applicable type.
// proposals.add(new ReplaceDeprecatedYamlQuickfix(context, problem));
// }
// }
};
@Override
public ICompletionProposal deemphasize() {
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public String getLabel() {
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public CompletionItemKind getKind() {
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public DocumentEdits getTextEdit() {
throw new UnsupportedOperationException("Not yet implemented");
}
// private final QuickfixContext context;
// private final SpringPropertyProblem problem;
//
// private LazyProposalApplier applier = new LazyProposalApplier() {
// protected ProposalApplier create() throws Exception {
// String newName = problem.getMetadata().getDeprecationReplacement();
// String oldName = problem.getPropertyName();
// YamlPath newPath = YamlPath.fromProperty(newName);
// YamlPath oldPath = YamlPath.fromProperty(oldName);
// YamlPath prefix = newPath.commonPrefix(oldPath);
// if (prefix.size()==newPath.size()-1 && newPath.size()==oldPath.size()) {
// //only the last segment has changed. We can do a simple 'in-place' replace
// // of just the change segment.
// DocumentEdits edits = new DocumentEdits(context.getDocument());
// edits.replace(problem.getOffset(), problem.getEnd(), newPath.getLastSegment().toPropString());
// return edits;
// }
// YamlDocument doc = new YamlDocument(context.getDocument(), YamlStructureProvider.DEFAULT);
// SNode problemNode = doc.getStructure().find(problem.getOffset());
// if (problemNode.getNodeType()==SNodeType.KEY) {
// SKeyNode problemKey = (SKeyNode) problemNode;
// if (problemKey.isInKey(problem.getOffset())) {
// YamlPathEdits edits = new YamlPathEdits(doc);
//// print(doc, edits);
// String valueText = problemKey.getValueWithRelativeIndent();
// edits.deleteNode(problemKey);
// int maxParentDeletions = oldPath.size() - prefix.size() - 1; // don't delete bits of the common prefix!
// SChildBearingNode parent = problemNode.getParent();
// while (maxParentDeletions>0 && parent!=null && parent.getChildren().size()==1) {
// edits.deleteNode(parent);
// parent = parent.getParent();
// maxParentDeletions--;
// }
//// print(doc, edits);
// SDocNode docRoot = problemNode.getDocNode(); //edits should stay within the same 'document' for yaml file that has multiple documents inside of it.
// edits.createPath(docRoot, YamlPath.fromProperty(newName), valueText);
//// print(doc, edits);
// return edits;
// }
// }
// //Not sure what to do... case not covered... so do nothing but tell the user.
// context.getUI().error("Yaml file too complex",
// "Sorry, but the yaml file is too complex for this quickfix. " +
// "Please make the change manually."
// );
// return ProposalApplier.NULL;
// }
//
//// private void print(YamlDocument doc, YamlPathEdits edits) throws Exception {
//// Document workingCopy = new Document(doc.getDocument().get());
//// edits.apply(workingCopy);
//// System.out.println("==============");
//// System.out.println(workingCopy.get());
//// System.out.println("==============");
//// }
// };
//
// public ReplaceDeprecatedYamlQuickfix(QuickfixContext context, SpringPropertyProblem problem) {
// this.context = context;
// this.problem = problem;
// }
//
// @Override
// public void apply(IDocument doc) {
// try {
// applier.apply(doc);
// } catch (Exception e) {
// Log.log(e);
// }
// }
//
// private String getReplacementProperty() {
// return problem.getMetadata().getDeprecationReplacement();
// }
//
// @Override
// public Point getSelection(IDocument doc) {
// try {
// return applier.getSelection(doc);
// } catch (Exception e) {
// Log.log(e);
// return null;
// }
// }
//
// @Override
// public String getAdditionalProposalInfo() {
// return null;
// }
//
// @Override
// public String getDisplayString() {
// return "Change to '"+getReplacementProperty()+"'";
// }
//
// @Override
// public Image getImage() {
// return JavaPluginImages.get(JavaPluginImages.IMG_CORRECTION_CHANGE);
// }
//
// @Override
// public IContextInformation getContextInformation() {
// return null;
// }
}

View File

@@ -0,0 +1,79 @@
/*******************************************************************************
* Copyright (c) 2015 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.boot.properties.reconcile;
import static org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemSeverity.ERROR;
import static org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemSeverity.WARNING;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemSeverity;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
/**
* @author Kris De Volder
*/
public enum ApplicationPropertiesProblemType implements ProblemType {
PROP_INVALID_BEAN_NAVIGATION("Accessing a 'bean property' in a type that doesn't have properties (e.g. like String or Integer)"),
PROP_INVALID_INDEXED_NAVIGATION("Accessing a property using [] in a type that doesn't support that"),
PROP_EXPECTED_DOT_OR_LBRACK("Unexpected character found where a '.' or '[' was expected"),
PROP_NO_MATCHING_RBRACK("Found a '[' but no matching ']'"),
PROP_NON_INTEGER_IN_BRACKETS("Use of [..] navigation with non-integer value"),
PROP_VALUE_TYPE_MISMATCH("Expecting a value of a certain type, but value doesn't parse as such"),
PROP_INVALID_BEAN_PROPERTY("Accessing a named property in a type that doesn't provide a property accessor with that name"),
PROP_UNKNOWN_PROPERTY(WARNING, "Property-key not found in any configuration metadata on the project's classpath"),
PROP_DEPRECATED(WARNING, "Property is marked as Deprecated"),
PROP_DUPLICATE_KEY("Multiple assignments to the same property value"),
PROP_SYNTAX_ERROR("Syntax Error");
private final ProblemSeverity defaultSeverity;
private String description;
private String label;
private ApplicationPropertiesProblemType(ProblemSeverity defaultSeverity, String description, String label) {
this.description = description;
this.defaultSeverity = defaultSeverity;
this.label = label;
}
private ApplicationPropertiesProblemType(ProblemSeverity defaultSeverity, String description) {
this(defaultSeverity, description, null);
}
private ApplicationPropertiesProblemType(String description) {
this(ERROR, description);
}
public ProblemSeverity getDefaultSeverity() {
return defaultSeverity;
}
public String getLabel() {
if (label==null) {
label = createDefaultLabel();
}
return label;
}
public String getDescription() {
return description;
}
private String createDefaultLabel() {
String label = this.toString().substring(5).toLowerCase().replace('_', ' ');
return Character.toUpperCase(label.charAt(0)) + label.substring(1);
}
@Override
public String getCode() {
return name();
}
}

View File

@@ -0,0 +1,52 @@
package org.springframework.ide.vscode.boot.properties.reconcile;
import java.util.Arrays;
import java.util.regex.Pattern;
import org.springframework.ide.vscode.application.properties.metadata.types.Type;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
/**
* Helper class to reconcile text contained in a document region as a comma-separated list.
*
* @author Kris De Volder
*/
public class DelimitedListReconciler {
interface TypeBasedReconciler {
void reconcile(DocumentRegion region, Type expectType, IProblemCollector problems);
}
private final TypeBasedReconciler valueReconciler;
private final Pattern delimiter;
public DelimitedListReconciler(Pattern delimiter, TypeBasedReconciler valueReconciler) {
this.valueReconciler = valueReconciler;
this.delimiter = delimiter;
}
public void reconcile(DocumentRegion region, Type listType, IProblemCollector problems) {
Type elType = getElementType(listType);
//Its pointless to reconcile list of we can't determine value type.
if (elType!=null) {
Arrays.stream(region.split(delimiter)).forEach(entry -> {
valueReconciler.reconcile(entry, elType, problems);
});
}
}
private Type getElementType(Type listType) {
Type elType = TypeUtil.getDomainType(listType);
if (elType!=null) {
Type nestedElType = getElementType(elType);
if (nestedElType!=null) {
return nestedElType;
}
return elType;
}
return null;
}
}

View File

@@ -0,0 +1,58 @@
package org.springframework.ide.vscode.boot.properties.reconcile;
import static org.springframework.ide.vscode.boot.properties.reconcile.ApplicationPropertiesProblemType.PROP_DUPLICATE_KEY;
import static org.springframework.ide.vscode.boot.properties.reconcile.SpringPropertyProblem.problem;
import java.util.HashMap;
import java.util.Map;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
import org.springframework.ide.vscode.java.properties.parser.PropertiesFileEscapes;
/**
* Instance of this class is fed the regions of names in a properties file, checks them for duplicates and
* reports the duplicates to {@link IProblemCollector}.
*
* @author Kris De Volder
*/
public class DuplicateNameChecker {
/**
* Keep track of seen names. The value in the map entries is either null
* or the Region for the first time the name was seen.
* <p>
* This is used so that the first occurrence can still be reported retroactively
* when the second occurrence is encountered.
*/
private Map<String, DocumentRegion> seen = new HashMap<>();
IProblemCollector problems;
public DuplicateNameChecker(IProblemCollector problems) {
this.problems = problems;
}
public void check(DocumentRegion nameRegion) throws Exception {
String name = PropertiesFileEscapes.unescape(nameRegion.toString());
if (!name.isEmpty()) {
if (seen.containsKey(name)) {
DocumentRegion pending = seen.get(name);
if (pending!=null) {
reportDuplicate(pending);
seen.put(name, null);
}
reportDuplicate(nameRegion);
} else {
seen.put(name, nameRegion);
}
}
}
private void reportDuplicate(DocumentRegion nameRegion) throws Exception {
String decodedKey = PropertiesFileEscapes.unescape(nameRegion.toString());
problems.accept(problem(PROP_DUPLICATE_KEY,
"Duplicate property '"+decodedKey+"'", nameRegion));
}
}

View File

@@ -0,0 +1,251 @@
package org.springframework.ide.vscode.boot.properties.reconcile;
import static org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil.isBracketable;
import static org.springframework.ide.vscode.boot.properties.reconcile.SpringPropertyProblem.problem;
import java.util.List;
import org.springframework.ide.vscode.application.properties.metadata.types.Type;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil.BeanPropertyNameMode;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil.EnumCaseMode;
import org.springframework.ide.vscode.application.properties.metadata.types.TypedProperty;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem;
import org.springframework.ide.vscode.commons.languageserver.util.BadLocationException;
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
import org.springframework.ide.vscode.commons.languageserver.util.IDocument;
import org.springframework.ide.vscode.commons.util.StringUtil;
import org.springframework.ide.vscode.commons.util.ValueParser;
/**
* Helper class for {@link SpringPropertiesReconcileEngine} and {@link SpringPropertiesCompletionEngine}.
* <p>
* This class provides a means to 'navigate' a chain of bracket and dot navigation operations down
* from a typed property into its value type.
*
* @author Kris De Volder
*/
public class PropertyNavigator {
private static final char EOF = 0;
/**
* If problem collector is not null, then problems detected in the navigation chain are added to the
* collector.
*/
private IProblemCollector problemCollector;
/**
* Document in which navigation chain text is contained.
*/
private IDocument doc;
private TypeUtil typeUtil;
private DocumentRegion region;
private String regionText;
public PropertyNavigator(IDocument doc, IProblemCollector problemCollector, TypeUtil typeUtil, DocumentRegion region) throws BadLocationException {
this.doc = doc;
this.problemCollector = problemCollector==null?IProblemCollector.NULL:problemCollector;
this.typeUtil = typeUtil;
this.region = region;
this.regionText = doc.get(region.getStart(), region.getLength());
}
/**
* @param offset current position in the nav chain. Text before this offset is already 'processed'.
* @param type The type at the end of the already processed nav chain. The next nav op in the chain
* should go deeper into this type.
* @param r The entire region of the navchain, including both the already processed portion as well
* as the remaining text.
* @return Type at the end of the whole nav chain, or null if the type could not be determined.
*/
public Type navigate(int offset, Type type) {
if (type!=null) {
if (offset<region.getEnd()) {
char navOp = getChar(offset);
if (navOp=='.') {
if (typeUtil.isDotable(type)) {
return dotNavigate(offset, type);
} else {
problemCollector.accept(problem(ApplicationPropertiesProblemType.PROP_INVALID_BEAN_NAVIGATION,
"Can't use '.' navigation for property '"+textBetween(region.getStart(), offset)+"' of type "+type,
offset, region.getEnd()-offset));
}
} else if (navOp=='[') {
if (isBracketable(type)) {
return bracketNavigate(offset, type);
} else {
problemCollector.accept(problem(ApplicationPropertiesProblemType.PROP_INVALID_INDEXED_NAVIGATION,
"Can't use '[..]' navigation for property '"+textBetween(region.getStart(), offset)+"' of type "+type,
offset, region.getEnd()-offset));
}
} else {
problemCollector.accept(problem(ApplicationPropertiesProblemType.PROP_EXPECTED_DOT_OR_LBRACK, "Expecting either a '.' or '['", offset, region.getEnd()-offset));
}
} else {
//end of nav chain
return type;
}
}
//Something we can't handle...
return null;
}
private String textBetween(int start, int end) {
try {
if (end>start) {
return doc.get(start, end-start);
}
} catch (BadLocationException e) {
//ignore
}
return "";
}
private int indexOf(char c, int from) {
int offset = region.getStart();
int found = regionText.indexOf(c, from-offset);
if (found>=0) {
return found+offset;
}
return -1;
}
/**
* Handle bracket navigation into given type, after a bracket at
* was found at given offset. Assumes the type has already been checked to
* be 'bracketable'.
*/
private Type bracketNavigate(int offset, Type type) {
int lbrack = offset;
int rbrack = indexOf(']', lbrack);
if (rbrack<0) {
problemCollector.accept(problem(ApplicationPropertiesProblemType.PROP_NO_MATCHING_RBRACK,
"No matching ']'",
offset, 1));
} else {
String indexStr = textBetween(lbrack+1, rbrack);
if (!indexStr.contains("${")) {
try {
Integer.parseInt(indexStr);
} catch (Exception e) {
problemCollector.accept(problem(ApplicationPropertiesProblemType.PROP_NON_INTEGER_IN_BRACKETS,
"Expecting 'Integer' for '[...]' notation '"+textBetween(region.getStart(), lbrack)+"'",
lbrack+1, rbrack-lbrack-1
));
}
}
Type domainType = TypeUtil.getDomainType(type);
return navigate(rbrack+1, domainType);
}
return null;
}
/**
* Handle dot navigation into given type, after a '.' was
* was found at given offset. Assumes the type has already been
* checked to be 'dotable'.
*/
private Type dotNavigate(int offset, Type type) {
if (TypeUtil.isMap(type)) {
int keyStart = offset+1;
Type domainType = TypeUtil.getDomainType(type);
int keyEnd = -1;
if (typeUtil.isDotable(domainType)) {
//'.' should be interpreted as navigation.
keyEnd = nextNavOp(".[", offset+1);
} else {
//'.' should *not* be interpreted as navigation.
keyEnd = nextNavOp("[", offset+1);
}
String key = textBetween(keyStart, keyEnd);
Type keyType = typeUtil.getKeyType(type);
if (keyType!=null) {
ValueParser keyParser = typeUtil.getValueParser(keyType);
if (keyParser!=null) {
try {
keyParser.parse(key);
} catch (Exception e) {
problemCollector.accept(problem(ApplicationPropertiesProblemType.PROP_VALUE_TYPE_MISMATCH,
"Expecting "+typeUtil.niceTypeName(keyType),
keyStart, keyEnd-keyStart));
}
}
}
return navigate(keyEnd, domainType);
} else {
// dot navigation into object properties
int keyStart = offset+1;
int keyEnd = nextNavOp(".[", offset+1);
if (keyEnd<0) {
keyEnd = region.getEnd();
}
String key = StringUtil.camelCaseToHyphens(textBetween(keyStart, keyEnd));
List<TypedProperty> properties = typeUtil.getProperties(type, EnumCaseMode.ALIASED, BeanPropertyNameMode.ALIASED);
if (properties!=null) {
TypedProperty prop = null;
for (TypedProperty p : properties) {
if (p.getName().equals(key)) {
prop = p;
break;
}
}
if (prop==null) {
problemCollector.accept(problem(ApplicationPropertiesProblemType.PROP_INVALID_BEAN_PROPERTY,
"Type '"+typeUtil.niceTypeName(type)+"' has no property '"+key+"'",
keyStart, keyEnd-keyStart));
} else {
if (prop.isDeprecated()) {
problemCollector.accept(problemDeprecated(type, prop, keyStart, keyEnd-keyStart));
}
return navigate(keyEnd, prop.getType());
}
}
}
return null;
}
private ReconcileProblem problemDeprecated(Type contextType, TypedProperty prop, int offset, int len) {
SpringPropertyProblem p = problem(ApplicationPropertiesProblemType.PROP_DEPRECATED,
TypeUtil.deprecatedPropertyMessage(
prop.getName(), typeUtil.niceTypeName(contextType),
prop.getDeprecationReplacement(), prop.getDeprecationReason()
),
offset, len
);
p.setPropertyName(prop.getName());
return p;
}
/**
* Skip ahead from give position until reaching the next 'navigation' operator (or the end
* of the navigation chain region).
*
* @param navops Each character in this string is considered a 'navigation operator'.
* @param pos current position in the document.
* @return position of next navop if found, or the position at the end of the region if not found.
*/
private int nextNavOp(String navops, int pos) {
int end = region.getEnd();
while (pos < end && navops.indexOf(getChar(pos))<0) {
pos++;
}
return Math.min(pos, end); //ensure never past the end
}
private char getChar(int offset) {
try {
return doc.getChar(offset);
} catch (BadLocationException e) {
//outside doc, return something anyways.
return EOF;
}
}
}

View File

@@ -0,0 +1,204 @@
/*******************************************************************************
* Copyright (c) 2014-2016 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.boot.properties.reconcile;
import static org.springframework.ide.vscode.boot.properties.reconcile.ApplicationPropertiesProblemType.PROP_DEPRECATED;
import static org.springframework.ide.vscode.boot.properties.reconcile.ApplicationPropertiesProblemType.PROP_SYNTAX_ERROR;
import static org.springframework.ide.vscode.boot.properties.reconcile.ApplicationPropertiesProblemType.PROP_UNKNOWN_PROPERTY;
import static org.springframework.ide.vscode.boot.properties.reconcile.SpringPropertyProblem.problem;
import static org.springframework.ide.vscode.commons.util.StringUtil.commonPrefix;
import java.util.regex.Pattern;
import org.springframework.ide.vscode.application.properties.metadata.PropertyInfo;
import org.springframework.ide.vscode.application.properties.metadata.SpringPropertyIndex;
import org.springframework.ide.vscode.application.properties.metadata.SpringPropertyIndexProvider;
import org.springframework.ide.vscode.application.properties.metadata.types.Type;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeParser;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtilProvider;
import org.springframework.ide.vscode.application.properties.metadata.util.FuzzyMap;
import org.springframework.ide.vscode.boot.properties.quickfix.ReplaceDeprecatedPropertyQuickfix;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine;
import org.springframework.ide.vscode.commons.languageserver.util.BadLocationException;
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
import org.springframework.ide.vscode.commons.languageserver.util.IDocument;
import org.springframework.ide.vscode.commons.util.Log;
import org.springframework.ide.vscode.commons.util.ValueParser;
import org.springframework.ide.vscode.java.properties.antlr.parser.AntlrParser;
import org.springframework.ide.vscode.java.properties.parser.ParseResults;
import org.springframework.ide.vscode.java.properties.parser.Parser;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.KeyValuePair;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Node;
import org.springframework.ide.vscode.java.properties.parser.PropertiesFileEscapes;
/**
* Implements reconciling algorithm for {@link SpringPropertiesReconcileStrategy}.
* <p>
* The code in here could have been also part of the {@link SpringPropertiesReconcileStrategy}
* itself, however isolating it here allows it to me more easily unit tested (no dependencies
* on ISourceViewer which is difficult to 'mock' in testing harness.
*
* @author Kris De Volder
*/
public class SpringPropertiesReconcileEngine implements IReconcileEngine {
/**
* Regexp that matches a ',' surrounded by whitespace, including escaped whitespace / newlines
*/
private static final Pattern COMMA = Pattern.compile(
"(\\s|\\\\\\s)*,(\\s|\\\\\\s)*"
);
private static final Pattern SPACES = Pattern.compile(
"(\\s|\\\\\\s)*"
);
private SpringPropertyIndexProvider fIndexProvider;
private TypeUtilProvider typeUtilProvider;
private final DelimitedListReconciler commaListReconciler = new DelimitedListReconciler(COMMA, this::reconcileType);
private Parser parser = new AntlrParser();
public SpringPropertiesReconcileEngine(SpringPropertyIndexProvider provider, TypeUtilProvider typeUtilProvider) {
this.fIndexProvider = provider;
this.typeUtilProvider = typeUtilProvider;
}
public void reconcile(IDocument doc, IProblemCollector problemCollector) {
FuzzyMap<PropertyInfo> index = fIndexProvider.getIndex(doc);
problemCollector.beginCollecting();
try {
ParseResults results = parser.parse(doc.get());
DuplicateNameChecker duplicateNameChecker = new DuplicateNameChecker(problemCollector);
results.syntaxErrors.forEach(syntaxError -> {
problemCollector.accept(problem(PROP_SYNTAX_ERROR, syntaxError.getMessage(), syntaxError.getOffset(),
syntaxError.getLength()));
});
if (index==null || index.isEmpty()) {
//don't report errors when index is empty, simply don't check (otherwise we will just reprot
// all properties as errors, but this not really useful information since the cause is
// some problem putting information about properties into the index.
return;
}
results.ast.getNodes(KeyValuePair.class).forEach(pair -> {
try {
DocumentRegion propertyNameRegion = createRegion(doc, pair.getKey());
String keyName = PropertiesFileEscapes.unescape(propertyNameRegion.toString());
duplicateNameChecker.check(propertyNameRegion);
PropertyInfo validProperty = SpringPropertyIndex.findLongestValidProperty(index, keyName);
if (validProperty!=null) {
//TODO: Remove last remnants of 'IRegion trimmedRegion' here and replace
// it all with just passing around 'fullName' DocumentRegion. This may require changes
// in PropertyNavigator (probably these changes are also for the better making it simpler as well)
if (validProperty.isDeprecated()) {
problemCollector.accept(problemDeprecated(propertyNameRegion, validProperty));
}
int offset = validProperty.getId().length() + propertyNameRegion.getStart();
PropertyNavigator navigator = new PropertyNavigator(doc, problemCollector, typeUtilProvider.getTypeUtil(doc), propertyNameRegion);
Type valueType = navigator.navigate(offset, TypeParser.parse(validProperty.getType()));
if (valueType!=null) {
reconcileType(doc, valueType, pair.getValue(), problemCollector);
}
} else { //validProperty==null
//The name is invalid, with no 'prefix' of the name being a valid property name.
PropertyInfo similarEntry = index.findLongestCommonPrefixEntry(propertyNameRegion.toString());
CharSequence validPrefix = commonPrefix(similarEntry.getId(), keyName);
problemCollector.accept(problemUnkownProperty(propertyNameRegion, similarEntry, validPrefix));
} //end: validProperty==null
} catch (Exception e) {
Log.log(e);
}
});
} catch (Throwable e2) {
Log.log(e2);
} finally {
problemCollector.endCollecting();
}
}
protected SpringPropertyProblem problemDeprecated(DocumentRegion region, PropertyInfo property) {
SpringPropertyProblem p = problem(PROP_DEPRECATED,
TypeUtil.deprecatedPropertyMessage(
property.getId(), null,
property.getDeprecationReplacement(),
property.getDeprecationReason()
),
region
);
p.setPropertyName(property.getId());
p.setMetadata(property);
p.setProblemFixer(ReplaceDeprecatedPropertyQuickfix.FIXER);
return p;
}
protected SpringPropertyProblem problemUnkownProperty(DocumentRegion fullNameRegion,
PropertyInfo similarEntry, CharSequence validPrefix) {
String fullName = fullNameRegion.toString();
SpringPropertyProblem p = problem(PROP_UNKNOWN_PROPERTY,
"'"+fullName+"' is an unknown property."+suggestSimilar(similarEntry, validPrefix, fullName),
fullNameRegion.subSequence(validPrefix.length())
);
p.setPropertyName(fullName);
return p;
}
private void reconcileType(IDocument doc, Type expectType, Node value, IProblemCollector problems) {
// Trim start and end spaces from the value node
reconcileType(createRegion(doc, value).trimStart(SPACES).trimEnd(SPACES), expectType,
problems);
}
private DocumentRegion createRegion(IDocument doc, Node value) {
// Trim trailing spaces (there is no leading white space already)
int length = value.getLength();
try {
length = doc.get(value.getOffset(), value.getLength()).length();
} catch (BadLocationException e) {
// ignore
}
return new DocumentRegion(doc, value.getOffset(), value.getOffset() + length);
}
private void reconcileType(DocumentRegion region, Type expectType, IProblemCollector problems) {
TypeUtil typeUtil = typeUtilProvider.getTypeUtil(region.getDocument());
ValueParser parser = typeUtil.getValueParser(expectType);
if (parser!=null) {
try {
String valueStr = PropertiesFileEscapes.unescape(region.toString());
if (!valueStr.contains("${")) {
//Don't check strings that look like they use variable substitution.
parser.parse(valueStr);
}
} catch (Exception e) {
problems.accept(problem(ApplicationPropertiesProblemType.PROP_VALUE_TYPE_MISMATCH,
"Expecting '"+typeUtil.niceTypeName(expectType)+"'",
region));
}
} else if (TypeUtil.isList(expectType)||TypeUtil.isArray(expectType)) {
commaListReconciler.reconcile(region, expectType, problems);
}
}
private String suggestSimilar(PropertyInfo similarEntry, CharSequence validPrefix, CharSequence fullName) {
int matchedChars = validPrefix.length();
int wrongChars = fullName.length()-matchedChars;
if (wrongChars<matchedChars) {
return " Did you mean '"+similarEntry.getId()+"'?";
} else {
return "";
}
}
}

View File

@@ -0,0 +1,44 @@
package org.springframework.ide.vscode.boot.properties.reconcile;
import org.springframework.ide.vscode.application.properties.metadata.PropertyInfo;
import org.springframework.ide.vscode.commons.languageserver.quickfix.ProblemFixer;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl;
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
// TODO: Move to a common project shared between YAML and Properties
public class SpringPropertyProblem extends ReconcileProblemImpl {
private PropertyInfo property = null;
private ProblemFixer fixer;
private String propertyName;
public SpringPropertyProblem(ProblemType type, String msg, int offset, int len) {
super(type, msg, offset, len);
}
public static SpringPropertyProblem problem(ApplicationPropertiesProblemType type, String msg, int offset, int len) {
return new SpringPropertyProblem(type, msg, offset, len);
}
public static SpringPropertyProblem problem(ApplicationPropertiesProblemType type, String msg, DocumentRegion region) {
if (region.isEmpty()) {
region = makeVisible(region);
}
return new SpringPropertyProblem(type, msg, region.getStart(), region.getLength());
}
public void setMetadata(PropertyInfo property) {
this.property = property;
}
public void setProblemFixer(ProblemFixer fixer) {
this.fixer = fixer;
}
public void setPropertyName(String name) {
propertyName = name;
}
}

View File

@@ -0,0 +1,109 @@
package org.springframework.ide.vscode.boot.properties.tools;
import static org.springframework.ide.vscode.commons.util.StringUtil.camelCaseToHyphens;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;
import org.springframework.ide.vscode.application.properties.metadata.PropertyInfo;
import org.springframework.ide.vscode.application.properties.metadata.hints.HintProvider;
import org.springframework.ide.vscode.application.properties.metadata.hints.HintProviders;
import org.springframework.ide.vscode.application.properties.metadata.hints.StsValueHint;
import org.springframework.ide.vscode.application.properties.metadata.types.Type;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeParser;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil.EnumCaseMode;
import org.springframework.ide.vscode.application.properties.metadata.util.FuzzyMap;
import org.springframework.ide.vscode.boot.properties.reconcile.PropertyNavigator;
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
import org.springframework.ide.vscode.commons.languageserver.util.TextDocument;
import org.springframework.ide.vscode.commons.util.CollectionUtil;
import org.springframework.ide.vscode.commons.util.Log;
public class CommonLanguageTools {
public static final Pattern SPACES = Pattern.compile(
"(\\s|\\\\\\s)*"
);
public static boolean isValuePrefixChar(char c) {
return !Character.isWhitespace(c) && c!=',';
}
/**
* Determine the value type for a give propertyName.
*/
public static Type getValueType(FuzzyMap<PropertyInfo> index, TypeUtil typeUtil, String propertyName) {
try {
PropertyInfo prop = index.get(propertyName);
if (prop!=null) {
return TypeParser.parse(prop.getType());
} else {
prop = CommonLanguageTools.findLongestValidProperty(index, propertyName);
if (prop!=null) {
TextDocument doc = new TextDocument(null);
doc.setText(propertyName);
PropertyNavigator navigator = new PropertyNavigator(doc, null, typeUtil, new DocumentRegion(doc, 0, doc.getLength()));
return navigator.navigate(prop.getId().length(), TypeParser.parse(prop.getType()));
}
}
} catch (Exception e) {
Log.log(e);
}
return null;
}
public static Collection<StsValueHint> getValueHints(FuzzyMap<PropertyInfo> index, TypeUtil typeUtil, String query, String propertyName, EnumCaseMode caseMode) {
Type type = getValueType(index, typeUtil, propertyName);
if (TypeUtil.isArray(type) || TypeUtil.isList(type)) {
//It is useful to provide content assist for the values in the list when entering a list
type = TypeUtil.getDomainType(type);
}
List<StsValueHint> allHints = new ArrayList<>();
{
Collection<StsValueHint> hints = typeUtil.getHintValues(type, query, caseMode);
if (CollectionUtil.hasElements(hints)) {
allHints.addAll(hints);
}
}
{
PropertyInfo prop = index.findLongestCommonPrefixEntry(propertyName);
if (prop!=null) {
HintProvider hintProvider = prop.getHints(typeUtil, false);
if (!HintProviders.isNull(hintProvider)) {
allHints.addAll(hintProvider.getValueHints(query));
}
}
}
return allHints;
}
/**
* Find the longest known property that is a prefix of the given name. Here prefix does not mean
* 'string prefix' but a prefix in the sense of treating '.' as a kind of separators. So
* 'prefix' is not allowed to end in the middle of a 'segment'.
*/
public static PropertyInfo findLongestValidProperty(FuzzyMap<PropertyInfo> index, String name) {
int bracketPos = name.indexOf('[');
int endPos = bracketPos>=0?bracketPos:name.length();
PropertyInfo prop = null;
String prefix = null;
while (endPos>0 && prop==null) {
prefix = name.substring(0, endPos);
String canonicalPrefix = camelCaseToHyphens(prefix);
prop = index.get(canonicalPrefix);
if (prop==null) {
endPos = name.lastIndexOf('.', endPos-1);
}
}
if (prop!=null) {
//We should meet caller's expectation that matched properties returned by this method
// match the names exactly even if we found them using relaxed name matching.
return prop.withId(prefix);
}
return null;
}
}