Merge application-properties into boot-properties project
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
// }
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user