From 146ae7ee610af9a96a8948bc4b1450ce85ce525b Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Fri, 28 Oct 2016 17:17:39 -0400 Subject: [PATCH] Integrate properties parser into boot properties extension --- .../DefaultSpringPropertyIndexProvider.java | 6 +- .../metadata/SpringPropertyIndex.java | 28 ++ .../languageserver/util/DocumentRegion.java | 250 ++++++++++++++ .../properties/antlr/parser/AntlrParser.java | 15 +- .../parser/PropertiesFileEscapes.java | 317 ++++++++++++++++++ .../test/harness/PropertyIndexHarness.java | 1 - .../ApplicationPropertiesLanguageServer.java | 81 +++-- .../vscode/application/properties/Main.java | 12 +- .../ReplaceDeprecatedPropertyQuickfix.java | 155 +++++++++ .../ApplicationPropertiesProblemType.java | 3 +- .../reconcile/DelimitedListReconciler.java | 52 +++ .../reconcile/DuplicateNameChecker.java | 57 ++++ .../reconcile/PropertyNavigator.java | 254 ++++++++++++++ .../SpringPropertiesReconcileEngine.java | 247 ++++++++++++++ .../reconcile/SpringPropertyProblem.java | 46 +++ .../test/ApplicationPropertiesEditorTest.java | 21 +- ...plicationPropertiesLanguageServerTest.java | 12 +- .../ide/vscode/application/yaml/Main.java | 1 + 18 files changed, 1498 insertions(+), 60 deletions(-) rename vscode-extensions/{vscode-application-yaml/src/main/java/org/springframework/ide/vscode/application/yaml => commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/application/properties/metadata}/DefaultSpringPropertyIndexProvider.java (64%) create mode 100644 vscode-extensions/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/DocumentRegion.java create mode 100644 vscode-extensions/commons/java-properties/src/main/java/org/springframework/ide/vscode/java/properties/parser/PropertiesFileEscapes.java create mode 100644 vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/quickfix/ReplaceDeprecatedPropertyQuickfix.java create mode 100644 vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/reconcile/DelimitedListReconciler.java create mode 100644 vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/reconcile/DuplicateNameChecker.java create mode 100644 vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/reconcile/PropertyNavigator.java create mode 100644 vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/reconcile/SpringPropertiesReconcileEngine.java create mode 100644 vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/reconcile/SpringPropertyProblem.java diff --git a/vscode-extensions/vscode-application-yaml/src/main/java/org/springframework/ide/vscode/application/yaml/DefaultSpringPropertyIndexProvider.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/application/properties/metadata/DefaultSpringPropertyIndexProvider.java similarity index 64% rename from vscode-extensions/vscode-application-yaml/src/main/java/org/springframework/ide/vscode/application/yaml/DefaultSpringPropertyIndexProvider.java rename to vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/application/properties/metadata/DefaultSpringPropertyIndexProvider.java index 7724078ba..c5507a29e 100644 --- a/vscode-extensions/vscode-application-yaml/src/main/java/org/springframework/ide/vscode/application/yaml/DefaultSpringPropertyIndexProvider.java +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/application/properties/metadata/DefaultSpringPropertyIndexProvider.java @@ -1,9 +1,5 @@ -package org.springframework.ide.vscode.application.yaml; +package org.springframework.ide.vscode.application.properties.metadata; -import org.springframework.ide.vscode.application.properties.metadata.PropertyInfo; -import org.springframework.ide.vscode.application.properties.metadata.SpringPropertiesIndexManager; -import org.springframework.ide.vscode.application.properties.metadata.SpringPropertyIndexProvider; -import org.springframework.ide.vscode.application.properties.metadata.ValueProviderRegistry; 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.java.JavaProjectFinder; diff --git a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/application/properties/metadata/SpringPropertyIndex.java b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/application/properties/metadata/SpringPropertyIndex.java index f4335826a..4f0c2394e 100644 --- a/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/application/properties/metadata/SpringPropertyIndex.java +++ b/vscode-extensions/commons/application-properties-metadata/src/main/java/org/springframework/ide/vscode/application/properties/metadata/SpringPropertyIndex.java @@ -19,6 +19,7 @@ import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepos import org.springframework.boot.configurationmetadata.ConfigurationMetadataSource; import org.springframework.ide.vscode.application.properties.metadata.util.FuzzyMap; import org.springframework.ide.vscode.commons.java.IClasspath; +import org.springframework.ide.vscode.commons.util.StringUtil; public class SpringPropertyIndex extends FuzzyMap { @@ -125,4 +126,31 @@ public class SpringPropertyIndex extends FuzzyMap { protected String getKey(PropertyInfo entry) { return entry.getId(); } + + /** + * 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 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 = StringUtil.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; + } + } diff --git a/vscode-extensions/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/DocumentRegion.java b/vscode-extensions/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/DocumentRegion.java new file mode 100644 index 000000000..8b84f3435 --- /dev/null +++ b/vscode-extensions/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/DocumentRegion.java @@ -0,0 +1,250 @@ +package org.springframework.ide.vscode.commons.languageserver.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.ide.vscode.commons.util.Assert; + +/** + * A non-sucky alternative to {@link IRegion}. Represents a region of text in a document. + *

+ * Caution: assumes the underlying document is not mutated during the lifetime of the + * region object (otherwise start/end positions may no longer be valid). + *

+ * Implements {@link CharSequence} for convenience (e.g you can use {@link DocumentRegion} as + * input to a {@link Pattern} and other standard JRE functions which expect a {@link CharSequence}. + * + * @author Kris De Volder + */ +public class DocumentRegion implements CharSequence { + final IDocument doc; + final int start; + final int end; + public DocumentRegion(IDocument doc, IRegion r) { + this(doc, + r.getOffset(), + r.getOffset()+r.getLength() + ); + } + + /** + * Constructs a {@link DocumentRegion} on a given document. Tries its + * best to behave sensibly when passed 'strange' coordinates by + * adjusting them logically rather than throw an Exception. + *

+ * A position before the start of the document is moved to be the start + * of the document. + *

+ * A position after the end of the document is moved to the end + * of the document. + *

+ * If 'end' position is before the start position it is moved be + * exactly at the start position (this avoids region with + * negative length). + */ + public DocumentRegion(IDocument doc, int start, int end) { + this.doc = doc; + this.start = limitRange(start, 0, doc.getLength()); + this.end = limitRange(end, start, doc.getLength()); + } + + private int limitRange(int offset, int min, int max) { + if (offsetmax) { + return max; + } + return offset; + } + + @Override + public String toString() { + return DocumentUtil.textBetween(doc, start, end); + } + + public DocumentRegion trim() { + return trimEnd().trimStart(); + } + + public DocumentRegion trimStart() { + int howMany = 0; + int len = length(); + while (howMany0) { + return subSequence(0, len-howMany); + } + return this; + } + + /** + * Gets character from the region, offset from the start of the region + * @return the character from the document (char)0 if the offset is outside the region. + */ + @Override + public char charAt(int offset) { + if (offset<0 || offset>=length()) { + throw new IndexOutOfBoundsException(""+offset); + } + try { + return doc.getChar(start+offset); + } catch (BadLocationException e) { + throw new IndexOutOfBoundsException(""+offset); + } + } + + @Override + public int length() { + return end-start; + } + + @Override + public DocumentRegion subSequence(int start, int end) { + int len = length(); + Assert.isLegal(start>=0); + Assert.isLegal(end<=len); + if (start==0 && end==len) { + return this; + } + return new DocumentRegion(doc, this.start+start, this.start+end); + } + + public boolean isEmpty() { + return length()==0; + } + + public DocumentRegion subSequence(int start) { + return subSequence(start, length()); + } + + public IRegion asRegion() { + return new Region(start, end-start); + } + + public int indexOf(char ch, int fromIndex) { + while (fromIndex < length()) { + if (charAt(fromIndex)==ch) { + return fromIndex; + } + fromIndex++; + } + return -1; + } + + public DocumentRegion[] split(char c) { + List pieces = new ArrayList<>(); + int start = 0; + int end; + while ((end=indexOf(c, start))>=0) { + pieces.add(subSequence(start, end)); + start = end+1; + } + // Do not forget the last piece! + pieces.add(subSequence(start, length())); + return pieces.toArray(new DocumentRegion[pieces.size()]); + } + + public DocumentRegion[] split(Pattern delimiter) { + List pieces = new ArrayList<>(); + int start = 0; + Matcher matcher = delimiter.matcher(this); + while (matcher.find(start)) { + int end = matcher.start(); + pieces.add(subSequence(start, end)); + start = matcher.end(); + } + // Do not forget the last piece! + pieces.add(subSequence(start, length())); + return pieces.toArray(new DocumentRegion[pieces.size()]); + } + + /** + * Removes a single occurrence of pat from the start of this region. + */ + public DocumentRegion trimStart(Pattern pat) { + pat = Pattern.compile("^("+pat.pattern()+")"); + Matcher matcher = pat.matcher(this); + if (matcher.find()) { + return subSequence(matcher.end()); + } + return this; + } + + /** + * Removes a single occurrence of pat from the end of this region. + */ + public DocumentRegion trimEnd(Pattern pat) { + pat = Pattern.compile("("+pat.pattern()+")$"); + Matcher matcher = pat.matcher(this); + if (matcher.find()) { + return subSequence(0, matcher.start()); + } + return this; + } + + /** + * Get the region after this one with a given lenght. + *

+ * If the document is too short to provide the requested lenght + * then the region is truncated to end of the document. + */ + public DocumentRegion textAfter(int len) { + Assert.isLegal(len>=0); + return new DocumentRegion(doc, end, end+len); + } + + /** + * Get the region before this one with a given lenght. + *

+ * If the requested region extends before the start of the document, + * then the region is shortened so its start coincides with document start. + */ + public DocumentRegion textBefore(int len) { + Assert.isLegal(len>=0); + return new DocumentRegion(doc, start-len, start); + } + + public IDocument getDocument() { + return doc; + } + + /** + * Get the start of this region in 'absolute' terms (i.e. relative to the document). + */ + public int getStart() { + return start; + } + + /** + * Get the end of this region in 'absolute' terms (i.e. relative to the document). + */ + public int getEnd() { + return end; + } + + /** + * Convert the given document offset into an offset relative to this region. + */ + public int toRelative(int offset) { + return offset-start; + } + + public int getLength() { + return getEnd() - getStart(); + } + +} \ No newline at end of file diff --git a/vscode-extensions/commons/java-properties/src/main/java/org/springframework/ide/vscode/java/properties/antlr/parser/AntlrParser.java b/vscode-extensions/commons/java-properties/src/main/java/org/springframework/ide/vscode/java/properties/antlr/parser/AntlrParser.java index d157a2e54..c380c636d 100644 --- a/vscode-extensions/commons/java-properties/src/main/java/org/springframework/ide/vscode/java/properties/antlr/parser/AntlrParser.java +++ b/vscode-extensions/commons/java-properties/src/main/java/org/springframework/ide/vscode/java/properties/antlr/parser/AntlrParser.java @@ -31,6 +31,7 @@ import org.springframework.ide.vscode.java.properties.parser.Parser; import org.springframework.ide.vscode.java.properties.parser.Problem; import org.springframework.ide.vscode.java.properties.parser.ProblemCodes; import org.springframework.ide.vscode.java.properties.parser.PropertiesAst; +import org.springframework.ide.vscode.java.properties.parser.PropertiesFileEscapes; import com.google.common.collect.ImmutableList; @@ -230,7 +231,12 @@ public class AntlrParser implements Parser { @Override public String decode() { - return context.getText().replace("\\:", ":").replace("\\=", "="); +// return context.getText().replace("\\:", ":").replace("\\=", "="); + try { + return PropertiesFileEscapes.unescape(context.getText()); + } catch (Exception e) { + return context.getText().replace("\\:", ":").replace("\\=", "="); + } } } @@ -249,8 +255,13 @@ public class AntlrParser implements Parser { private void init() { // Remove the separator, if it exists value = context.getText().replaceAll("^\\s*[:=]?\\s*", ""); - // Remove all escaped line breaks with trailing spaces + // Remove all escaped line breaks with trailing spaces decoded = value.replaceAll("\\\\(\r?\n|\r)[ \t\f]*", ""); + try { + decoded = PropertiesFileEscapes.unescape(decoded); + } catch (Exception e) { + // ignore + } } @Override diff --git a/vscode-extensions/commons/java-properties/src/main/java/org/springframework/ide/vscode/java/properties/parser/PropertiesFileEscapes.java b/vscode-extensions/commons/java-properties/src/main/java/org/springframework/ide/vscode/java/properties/parser/PropertiesFileEscapes.java new file mode 100644 index 000000000..7a78f4a58 --- /dev/null +++ b/vscode-extensions/commons/java-properties/src/main/java/org/springframework/ide/vscode/java/properties/parser/PropertiesFileEscapes.java @@ -0,0 +1,317 @@ +package org.springframework.ide.vscode.java.properties.parser; + +/** + * Helper class to convert between Java chars and the escaped form that must be used in .properties + * files. + * + * @since 3.7 + */ +public class PropertiesFileEscapes { + + private static final char[] HEX_DIGITS= { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + + private static char toHex(int halfByte) { + return HEX_DIGITS[(halfByte & 0xF)]; + } + + /** + * Returns the decimal value of the Hex digit, or -1 if the digit is not a valid Hex digit. + * + * @param digit the Hex digit + * @return the decimal value of digit, or -1 if digit is not a valid Hex digit. + */ + private static int getHexDigitValue(char digit) { + switch (digit) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return digit - '0'; + case 'a': + case 'b': + case 'c': + case 'd': + case 'e': + case 'f': + return 10 + digit - 'a'; + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + return 10 + digit - 'A'; + default: + return -1; + } + } + + /** + * Convert a Java char to the escaped form that must be used in .properties files. + * + * @param c the Java char + * @return escaped string + */ + public static String escape(char c) { + return escape(c, true, true, true); + } + + /** + * Convert characters in a Java string to the escaped form that must be used in .properties + * files. + * + * @param s the Java string + * @param escapeWhitespaceChars if true, escape whitespace characters + * @param escapeBackslash if true, escape backslash characters + * @param escapeUnicodeChars if true, escape unicode characters + * @return escaped string + */ + public static String escape(String s, boolean escapeWhitespaceChars, boolean escapeBackslash, boolean escapeUnicodeChars) { + StringBuffer sb= new StringBuffer(s.length()); + int length= s.length(); + for (int i= 0; i < length; i++) { + char c= s.charAt(i); + sb.append(escape(c, escapeWhitespaceChars, escapeBackslash, escapeUnicodeChars)); + } + return sb.toString(); + } + + /** + * Convert a Java char to the escaped form that must be used in .properties files. + * + * @param c the Java char + * @param escapeWhitespaceChars if true, escape whitespace characters + * @param escapeBackslash if true, escape backslash characters + * @param escapeUnicodeChars if true, escape unicode characters + * @return escaped string + */ + public static String escape(char c, boolean escapeWhitespaceChars, boolean escapeBackslash, boolean escapeUnicodeChars) { + switch (c) { + case '\t': + return escapeWhitespaceChars ? "\\t" : "\t"; //$NON-NLS-1$//$NON-NLS-2$ + case '\n': + return escapeWhitespaceChars ? "\\n" : "\n"; //$NON-NLS-1$//$NON-NLS-2$ + case '\f': + return escapeWhitespaceChars ? "\\f" : "\r"; //$NON-NLS-1$//$NON-NLS-2$ + case '\r': + return escapeWhitespaceChars ? "\\r" : "\r"; //$NON-NLS-1$//$NON-NLS-2$ + case '\\': + return escapeBackslash ? "\\\\" : "\\"; //$NON-NLS-1$ //$NON-NLS-2$ + default: + if (escapeUnicodeChars && ((c < 0x0020) || (c > 0x007e && c <= 0x00a0) || (c > 0x00ff))) { + //NBSP (0x00a0) is escaped to differentiate from normal space character + return new StringBuffer() + .append('\\') + .append('u') + .append(toHex((c >> 12) & 0xF)) + .append(toHex((c >> 8) & 0xF)) + .append(toHex((c >> 4) & 0xF)) + .append(toHex(c & 0xF)).toString(); + + } else + return String.valueOf(c); + } + } + + /** + * Convert an escaped string to a string composed of Java characters. + * + * @param s the escaped string + * @return string composed of Java characters + * @throws CoreException if the escaped string has a malformed \\uxxx sequence + */ + public static String unescape(String s) throws Exception { + boolean isValidEscapedString= true; + if (s == null) + return null; + + char aChar; + int len= s.length(); + StringBuffer outBuffer= new StringBuffer(len); + + for (int x= 0; x < len;) { + aChar= s.charAt(x++); + if (aChar == '\\') { + if (x > len - 1) { + return outBuffer.toString(); // silently ignore the \ + } + aChar= s.charAt(x++); + if (aChar == 'u') { + // Read the xxxx + int value= 0; + if (x > len - 4) { + throw new Exception("Malformed encoding for properties file"); + } + StringBuffer buf= new StringBuffer("\\u"); //$NON-NLS-1$ + int digit= 0; + for (int i= 0; i < 4; i++) { + aChar= s.charAt(x++); + digit= getHexDigitValue(aChar); + if (digit == -1) { + isValidEscapedString= false; + x--; + break; + } + value= (value << 4) + digit; + buf.append(aChar); + } + outBuffer.append(digit == -1 ? buf.toString() : String.valueOf((char)value)); + } else if (aChar == 't') { + outBuffer.append('\t'); + } else if (aChar == 'n') { + outBuffer.append('\n'); + } else if (aChar == 'f') { + outBuffer.append('\f'); + } else if (aChar == 'r') { + outBuffer.append('\r'); + } else { + outBuffer.append(aChar); // silently ignore the \ + } + } else + outBuffer.append(aChar); + } + if (isValidEscapedString) { + return outBuffer.toString(); + } else { + throw new Exception("Malformed encoding for properties file"); + } + } + + /** + * Unescape backslash characters in a string. + * + * @param s the escaped string + * @return string with backslash characters unescaped + */ + public static String unescapeBackslashes(String s) { + if (s == null) + return null; + + char c; + int length= s.length(); + StringBuffer outBuffer= new StringBuffer(length); + + for (int i= 0; i < length;) { + c= s.charAt(i++); + if (c == '\\') { + c= s.charAt(i++); + } + outBuffer.append(c); + } + + return outBuffer.toString(); + } + + /** + * Tests if the given text contains any invalid escape sequence. + * + * @param text the text + * @return true if text contains an invalid escape sequence, false + * otherwise + */ + public static boolean containsInvalidEscapeSequence(String text) { + try { + //check for invalid unicode escapes + unescape(text); + } catch (Exception e) { + return true; + } + + int length= text.length(); + for (int i= 0; i < length; i++) { + char c= text.charAt(i); + if (c == '\\') { + if (i < length - 1) { + char nextC= text.charAt(i + 1); + switch (nextC) { + case 't': + case 'n': + case 'f': + case 'r': + case 'u': + case '\n': + case '\r': + case '=': + case ':': + break; + case '\\': + i++; + break; + default: + return true; + } + } else { + return true; + } + } + } + return false; + } + + /** + * Tests if the given text contains an unescaped backslash character. + * + * @param text the text + * @return true if text contains an unescaped backslash character, + * false otherwise + */ + public static boolean containsUnescapedBackslash(String text) { + int length= text.length(); + for (int i= 0; i < length; i++) { + char c= text.charAt(i); + if (c == '\\') { + if (i < length - 1) { + char nextC= text.charAt(i + 1); + switch (nextC) { + case '\\': + i++; + break; + default: + return true; + } + } else { + return true; + } + } + } + return false; + } + + /** + * Tests if the given text contains only escaped backslash characters and no unescaped backslash + * character. + * + * @param text the text + * @return true if text contains only escaped backslash characters, + * false otherwise + */ + public static boolean containsEscapedBackslashes(String text) { + boolean result= false; + int length= text.length(); + for (int i= 0; i < length; i++) { + char c= text.charAt(i); + if (c == '\\') { + if (i < length - 1) { + char nextC= text.charAt(i + 1); + switch (nextC) { + case '\\': + i++; + result= true; + break; + default: + return false; + } + } else { + return false; + } + } + } + return result; + } +} diff --git a/vscode-extensions/commons/properties-editor-test-harness/src/main/java/org/springframework/ide/vscode/properties/editor/test/harness/PropertyIndexHarness.java b/vscode-extensions/commons/properties-editor-test-harness/src/main/java/org/springframework/ide/vscode/properties/editor/test/harness/PropertyIndexHarness.java index 279df8099..c2b89403c 100644 --- a/vscode-extensions/commons/properties-editor-test-harness/src/main/java/org/springframework/ide/vscode/properties/editor/test/harness/PropertyIndexHarness.java +++ b/vscode-extensions/commons/properties-editor-test-harness/src/main/java/org/springframework/ide/vscode/properties/editor/test/harness/PropertyIndexHarness.java @@ -1,6 +1,5 @@ package org.springframework.ide.vscode.properties.editor.test.harness; -import java.nio.file.Path; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; diff --git a/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/ApplicationPropertiesLanguageServer.java b/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/ApplicationPropertiesLanguageServer.java index 316afa643..d788c0a09 100644 --- a/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/ApplicationPropertiesLanguageServer.java +++ b/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/ApplicationPropertiesLanguageServer.java @@ -10,8 +10,11 @@ *******************************************************************************/ package org.springframework.ide.vscode.application.properties; -import java.util.stream.Collectors; - +import org.springframework.ide.vscode.application.properties.metadata.SpringPropertyIndexProvider; +import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtilProvider; +import org.springframework.ide.vscode.application.properties.reconcile.SpringPropertiesReconcileEngine; +import org.springframework.ide.vscode.commons.languageserver.reconcile.BadWordReconcileEngine; +import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine; 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; @@ -19,11 +22,7 @@ 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 io.typefox.lsapi.Diagnostic; -import io.typefox.lsapi.DiagnosticSeverity; -import io.typefox.lsapi.ServerCapabilities; import io.typefox.lsapi.TextDocumentSyncKind; -import io.typefox.lsapi.impl.DiagnosticImpl; import io.typefox.lsapi.impl.ServerCapabilitiesImpl; /** @@ -40,41 +39,53 @@ public class ApplicationPropertiesLanguageServer extends SimpleLanguageServer { private ParseResults parseResults; private Parser parser; + + private SpringPropertyIndexProvider indexProvider; + private TypeUtilProvider typeUtilProvider; - public ApplicationPropertiesLanguageServer() { + + public ApplicationPropertiesLanguageServer(SpringPropertyIndexProvider indexProvider, TypeUtilProvider typeUtilProvider) { + this.indexProvider = indexProvider; + this.typeUtilProvider = typeUtilProvider; this.parser = new AntlrParser(); SimpleTextDocumentService documents = getTextDocumentService(); - + + IReconcileEngine reconcileEngine = getReconcileEngine(); documents.onDidChangeContent(params -> { - System.out.println("Document changed: "+params); TextDocument doc = params.getDocument(); - parseResults = parser.parse(doc.getText()); - validateDocument(documents, doc); + validateWith(doc, reconcileEngine); }); + +// documents.onDidChangeContent(params -> { +// System.out.println("Document changed: "+params); +// TextDocument doc = params.getDocument(); +// parseResults = parser.parse(doc.getText()); +// validateDocument(documents, doc); +// }); } - private void validateDocument(SimpleTextDocumentService documents, TextDocument doc) { - documents.publishDiagnostics(doc, parseResults.syntaxErrors.stream().map(problem -> { - DiagnosticImpl diagnostic = new DiagnosticImpl(); - diagnostic.setMessage(createSyntaxErrorMessage(problem.getMessage())); - diagnostic.setCode(problem.getCode()); - diagnostic.setSeverity(DiagnosticSeverity.Error); - diagnostic.setSource("java-properties"); - diagnostic.setRange(doc.toRange(problem.getOffset(), problem.getLength())); - return diagnostic; - }).collect(Collectors.toList())); - } - - private static String createSyntaxErrorMessage(String parserMessage) { - String message = parserMessage; - if (parserMessage.contains("extraneous input '\\n' expecting")) { - message = SYNTAX_ERROR_MSG__UNEXPECTED_END_OF_LINE; - } else if (parserMessage.contains("mismatched input '' expecting")) { - message = YNTAX_ERROR_MSG__UNEXPECTED_END_OF_INPUT; - } - return SYNTAX_ERROR_HEADER_MSG + message; - } +// private void validateDocument(SimpleTextDocumentService documents, TextDocument doc) { +// documents.publishDiagnostics(doc, parseResults.syntaxErrors.stream().map(problem -> { +// DiagnosticImpl diagnostic = new DiagnosticImpl(); +// diagnostic.setMessage(createSyntaxErrorMessage(problem.getMessage())); +// diagnostic.setCode(problem.getCode()); +// diagnostic.setSeverity(DiagnosticSeverity.Error); +// diagnostic.setSource("java-properties"); +// diagnostic.setRange(doc.toRange(problem.getOffset(), problem.getLength())); +// return diagnostic; +// }).collect(Collectors.toList())); +// } +// +// private static String createSyntaxErrorMessage(String parserMessage) { +// String message = parserMessage; +// if (parserMessage.contains("extraneous input '\\n' expecting")) { +// message = SYNTAX_ERROR_MSG__UNEXPECTED_END_OF_LINE; +// } else if (parserMessage.contains("mismatched input '' expecting")) { +// message = YNTAX_ERROR_MSG__UNEXPECTED_END_OF_INPUT; +// } +// return SYNTAX_ERROR_HEADER_MSG + message; +// } @Override protected ServerCapabilitiesImpl getServerCapabilities() { @@ -84,4 +95,10 @@ public class ApplicationPropertiesLanguageServer extends SimpleLanguageServer { return c; } + + protected IReconcileEngine getReconcileEngine() { + return new SpringPropertiesReconcileEngine(indexProvider, typeUtilProvider); + } + + } diff --git a/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/Main.java b/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/Main.java index d14a78514..f26f2ae5f 100644 --- a/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/Main.java +++ b/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/Main.java @@ -20,6 +20,11 @@ import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; +import org.springframework.ide.vscode.application.properties.metadata.DefaultSpringPropertyIndexProvider; +import org.springframework.ide.vscode.application.properties.metadata.SpringPropertyIndexProvider; +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.commons.languageserver.util.IDocument; import org.springframework.ide.vscode.commons.languageserver.util.LoggingFormat; import io.typefox.lsapi.services.json.LoggingJsonAdapter; @@ -125,7 +130,12 @@ public class Main { * When the request stream is closed, wait for 5s for all outstanding responses to compute, then return. */ public static void run(Connection connection) { - ApplicationPropertiesLanguageServer server = new ApplicationPropertiesLanguageServer(); + SpringPropertyIndexProvider indexProvider = new DefaultSpringPropertyIndexProvider(); + TypeUtil typeUtil = new TypeUtil(null); + TypeUtilProvider typeUtilProvider = (IDocument doc) -> typeUtil; + + ApplicationPropertiesLanguageServer server = new ApplicationPropertiesLanguageServer(indexProvider, typeUtilProvider); + LoggingJsonAdapter jsonServer = new LoggingJsonAdapter(server); jsonServer.setMessageLog(new PrintWriter(System.out)); diff --git a/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/quickfix/ReplaceDeprecatedPropertyQuickfix.java b/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/quickfix/ReplaceDeprecatedPropertyQuickfix.java new file mode 100644 index 000000000..b55ca8af1 --- /dev/null +++ b/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/quickfix/ReplaceDeprecatedPropertyQuickfix.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * 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.application.properties.quickfix; + +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; + +import io.typefox.lsapi.CompletionItemKind; + +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; +// } + +} diff --git a/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/reconcile/ApplicationPropertiesProblemType.java b/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/reconcile/ApplicationPropertiesProblemType.java index a07535687..d8f051dbf 100644 --- a/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/reconcile/ApplicationPropertiesProblemType.java +++ b/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/reconcile/ApplicationPropertiesProblemType.java @@ -30,7 +30,8 @@ public enum ApplicationPropertiesProblemType implements ProblemType { 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_DUPLICATE_KEY("Multiple assignments to the same property value"), + PROP_SYNTAX_ERROR("Syntax Error"); private final ProblemSeverity defaultSeverity; private String description; diff --git a/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/reconcile/DelimitedListReconciler.java b/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/reconcile/DelimitedListReconciler.java new file mode 100644 index 000000000..f2ed9b5f7 --- /dev/null +++ b/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/reconcile/DelimitedListReconciler.java @@ -0,0 +1,52 @@ +package org.springframework.ide.vscode.application.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; + } + +} diff --git a/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/reconcile/DuplicateNameChecker.java b/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/reconcile/DuplicateNameChecker.java new file mode 100644 index 000000000..c6a896763 --- /dev/null +++ b/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/reconcile/DuplicateNameChecker.java @@ -0,0 +1,57 @@ +package org.springframework.ide.vscode.application.properties.reconcile; + +import static org.springframework.ide.vscode.application.properties.reconcile.ApplicationPropertiesProblemType.PROP_DUPLICATE_KEY; +import static org.springframework.ide.vscode.application.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.java.properties.parser.PropertiesAst.Key; + +/** + * 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. + *

+ * This is used so that the first occurrence can still be reported retroactively + * when the second occurrence is encountered. + */ + private Map seen = new HashMap<>(); + + IProblemCollector problems; + + public DuplicateNameChecker(IProblemCollector problems) { + this.problems = problems; + } + + public void check(Key nameRegion) { + String name = nameRegion.decode(); + if (!name.isEmpty()) { + if (seen.containsKey(name)) { + Key pending = seen.get(name); + if (pending!=null) { + reportDuplicate(pending); + seen.put(name, null); + } + reportDuplicate(nameRegion); + } else { + seen.put(name, nameRegion); + } + } + } + + private void reportDuplicate(Key nameRegion) { + String decodedKey = nameRegion.decode(); + problems.accept(problem(PROP_DUPLICATE_KEY, + "Duplicate property '"+decodedKey+"'", nameRegion)); + } + +} diff --git a/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/reconcile/PropertyNavigator.java b/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/reconcile/PropertyNavigator.java new file mode 100644 index 000000000..34cf3617f --- /dev/null +++ b/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/reconcile/PropertyNavigator.java @@ -0,0 +1,254 @@ +package org.springframework.ide.vscode.application.properties.reconcile; + +import static org.springframework.ide.vscode.application.properties.reconcile.SpringPropertyProblem.problem; +import static org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil.isBracketable; + +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.IDocument; +import org.springframework.ide.vscode.commons.util.StringUtil; +import org.springframework.ide.vscode.commons.util.ValueParser; +import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Node; + +/** + * Helper class for {@link SpringPropertiesReconcileEngine} and {@link SpringPropertiesCompletionEngine}. + *

+ * 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 Node region; + + private String regionText; + + public PropertyNavigator(IDocument doc, IProblemCollector problemCollector, TypeUtil typeUtil, Node region) throws BadLocationException { + this.doc = doc; + this.problemCollector = problemCollector==null?IProblemCollector.NULL:problemCollector; + this.typeUtil = typeUtil; + this.region = region; + this.regionText = doc.get(region.getOffset(), 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 (offsetstart) { + return doc.get(start, end-start); + } + } catch (BadLocationException e) { + //ignore + } + return ""; + } + + private int indexOf(char c, int from) { + int offset = region.getOffset(); + 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.getOffset(), 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 = getEnd(region); + } + String key = StringUtil.camelCaseToHyphens(textBetween(keyStart, keyEnd)); + + List 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 = getEnd(region); + 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; + } + } + + private int getEnd(Node region) { + return region.getOffset()+region.getLength(); + } +} diff --git a/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/reconcile/SpringPropertiesReconcileEngine.java b/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/reconcile/SpringPropertiesReconcileEngine.java new file mode 100644 index 000000000..647eca2cf --- /dev/null +++ b/vscode-extensions/vscode-application-properties/src/main/java/org/springframework/ide/vscode/application/properties/reconcile/SpringPropertiesReconcileEngine.java @@ -0,0 +1,247 @@ +/******************************************************************************* + * 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.application.properties.reconcile; + +//import static org.springframework.ide.eclipse.boot.properties.editor.SpringPropertiesCompletionEngine.isAssign; +import static org.springframework.ide.vscode.application.properties.reconcile.ApplicationPropertiesProblemType.PROP_DEPRECATED; +import static org.springframework.ide.vscode.application.properties.reconcile.ApplicationPropertiesProblemType.PROP_SYNTAX_ERROR; +import static org.springframework.ide.vscode.application.properties.reconcile.ApplicationPropertiesProblemType.PROP_UNKNOWN_PROPERTY; +import static org.springframework.ide.vscode.application.properties.reconcile.SpringPropertyProblem.problem; +import static org.springframework.ide.vscode.commons.util.StringUtil.commonPrefix; + +import java.util.regex.Pattern; + +import javax.inject.Provider; + +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.application.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.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.Key; +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}. + *

+ * 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 + */ +@SuppressWarnings("restriction") +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)*" +// ); + + /** + * Regexp that matches a whitespace, including escaped whitespace + */ +// private static final Pattern ASSIGN = SpringPropertiesCompletionEngine.ASSIGN; + + 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 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 -> { + Key fullName = pair.getKey(); + String keyName = fullName.decode(); + duplicateNameChecker.check(fullName); + PropertyInfo validProperty = SpringPropertyIndex.findLongestValidProperty(index, keyName); + try { + 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(fullName, validProperty)); + } + int offset = validProperty.getId().length() + fullName.getOffset(); + PropertyNavigator navigator = new PropertyNavigator(doc, problemCollector, typeUtilProvider.getTypeUtil(doc), fullName); + 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(fullName.toString()); + CharSequence validPrefix = commonPrefix(similarEntry.getId(), keyName); + problemCollector.accept(problemUnkownProperty(createRegion(doc, fullName), similarEntry, validPrefix)); + } //end: validProperty==null + } catch (Exception e) { + Log.log(e); + } + }); + } catch (Throwable e2) { + Log.log(e2); + } finally { + problemCollector.endCollecting(); + } + } + + protected SpringPropertyProblem problemDeprecated(Key key, PropertyInfo property) { + SpringPropertyProblem p = problem(PROP_DEPRECATED, + TypeUtil.deprecatedPropertyMessage( + property.getId(), null, + property.getDeprecationReplacement(), + property.getDeprecationReason() + ), + key + ); + 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) { +// DocumentRegion escapedValue = getAssignedValue(doc, regions, i); +// if (escapedValue==null) { +// int charPos = DocumentUtil.lastNonWhitespaceCharOfRegion(doc, regions[i]); +// if (charPos>=0) { +// problems.accept(problem(SpringPropertiesProblemType.PROP_VALUE_TYPE_MISMATCH, +// "Expecting '"+typeUtil.niceTypeName(expectType)+"'", +// charPos, 1)); +// } +// } else { +// reconcileType(escapedValue, expectType, problems); +// } + reconcileType(createRegion(doc, value), expectType, + problems); + } + + private DocumentRegion createRegion(IDocument doc, Node value) { + return new DocumentRegion(doc, value.getOffset(), value.getOffset() + value.getLength()); + } + + 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 DocumentRegion getAssignedValue(IDocument doc, ITypedRegion[] regions, int i) { +// int valueRegionIndex = i+1; +// if (valueRegionIndex f = () -> new ApplicationPropertiesLanguageServer((d) -> null, (d) -> null); + return new LanguageServerHarness(f); + } + @Test public void createAndInitializeServerWithWorkspace() throws Exception { - LanguageServerHarness harness = new LanguageServerHarness(ApplicationPropertiesLanguageServer::new); + LanguageServerHarness harness = newHarness(); File workspaceRoot = getTestResource("/workspace/"); assertExpectedInitResult(harness.intialize(workspaceRoot)); } @@ -46,7 +52,7 @@ public class ApplicationPropertiesLanguageServerTest { @Test public void createAndInitializeServerWithoutWorkspace() throws Exception { File workspaceRoot = null; - LanguageServerHarness harness = new LanguageServerHarness(ApplicationPropertiesLanguageServer::new); + LanguageServerHarness harness = newHarness(); assertExpectedInitResult(harness.intialize(workspaceRoot)); } diff --git a/vscode-extensions/vscode-application-yaml/src/main/java/org/springframework/ide/vscode/application/yaml/Main.java b/vscode-extensions/vscode-application-yaml/src/main/java/org/springframework/ide/vscode/application/yaml/Main.java index 657620c9c..970377472 100644 --- a/vscode-extensions/vscode-application-yaml/src/main/java/org/springframework/ide/vscode/application/yaml/Main.java +++ b/vscode-extensions/vscode-application-yaml/src/main/java/org/springframework/ide/vscode/application/yaml/Main.java @@ -9,6 +9,7 @@ import java.net.Socket; import java.util.logging.Level; import java.util.logging.Logger; +import org.springframework.ide.vscode.application.properties.metadata.DefaultSpringPropertyIndexProvider; import org.springframework.ide.vscode.application.properties.metadata.SpringPropertyIndexProvider; import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil; import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtilProvider;