diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java index 4d8e5f725..3c50d711d 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java @@ -21,6 +21,7 @@ import org.springframework.ide.vscode.boot.java.autowired.AutowiredHoverProvider import org.springframework.ide.vscode.boot.java.beans.BeansSymbolProvider; import org.springframework.ide.vscode.boot.java.beans.ComponentHoverProvider; import org.springframework.ide.vscode.boot.java.beans.ComponentSymbolProvider; +import org.springframework.ide.vscode.boot.java.conditionals.ConditionalsLiveHoverProvider; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeLensEngine; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCompletionEngine; import org.springframework.ide.vscode.boot.java.handlers.BootJavaDocumentSymbolHandler; @@ -263,6 +264,14 @@ public class BootJavaLanguageServer extends SimpleLanguageServer { providers.put(org.springframework.ide.vscode.boot.java.autowired.Constants.SPRING_AUTOWIRED, new AutowiredHoverProvider()); providers.put(org.springframework.ide.vscode.boot.java.beans.Constants.SPRING_COMPONENT, new ComponentHoverProvider()); + providers.put(org.springframework.ide.vscode.boot.java.conditionals.Constants.CONDITIONAL_ON_BEAN, new ConditionalsLiveHoverProvider()); + providers.put(org.springframework.ide.vscode.boot.java.conditionals.Constants.CONDITIONAL_ON_MISSING_BEAN, new ConditionalsLiveHoverProvider()); + providers.put(org.springframework.ide.vscode.boot.java.conditionals.Constants.CONDITIONAL_ON_PROPERTY, new ConditionalsLiveHoverProvider()); + providers.put(org.springframework.ide.vscode.boot.java.conditionals.Constants.CONDITIONAL_ON_RESOURCE, new ConditionalsLiveHoverProvider()); + providers.put(org.springframework.ide.vscode.boot.java.conditionals.Constants.CONDITIONAL_ON_CLASS, new ConditionalsLiveHoverProvider()); + providers.put(org.springframework.ide.vscode.boot.java.conditionals.Constants.CONDITIONAL_ON_CLOUD_PLATFORM, new ConditionalsLiveHoverProvider()); + providers.put(org.springframework.ide.vscode.boot.java.conditionals.Constants.CONDITIONAL_ON_WEB_APPLICATION, new ConditionalsLiveHoverProvider()); + return new BootJavaHoverProvider(this, javaProjectFinder, providers, runningAppProvider); } diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionals/ConditionalsLiveHoverProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionals/ConditionalsLiveHoverProvider.java new file mode 100644 index 000000000..021e210f9 --- /dev/null +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionals/ConditionalsLiveHoverProvider.java @@ -0,0 +1,221 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.conditionals; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.IMethodBinding; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.MarkedString; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.json.JSONArray; +import org.json.JSONObject; +import org.springframework.ide.vscode.boot.java.handlers.HoverProvider; +import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; +import org.springframework.ide.vscode.commons.util.BadLocationException; +import org.springframework.ide.vscode.commons.util.Log; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +/** + * + * Provides live hovers and hints for @ConditionalOn... Spring Boot annotations from running + * spring boot apps. + */ +public class ConditionalsLiveHoverProvider implements HoverProvider { + + @Override + public CompletableFuture provideHover(ASTNode node, Annotation annotation, ITypeBinding type, int offset, + TextDocument doc, SpringBootApp[] runningApps) { + return provideHover(annotation, doc, runningApps); + } + + @Override + public Range getLiveHoverHint(Annotation annotation, TextDocument doc, SpringBootApp[] runningApps) { + try { + if (runningApps.length > 0) { + ConditionalParserFromRunningApp parser = new ConditionalParserFromRunningApp(); + Optional> val = parser.parse(annotation, runningApps); + if (val.isPresent()) { + Range hoverRange = doc.toRange(annotation.getStartPosition(), annotation.getLength()); + return hoverRange; + } + } + } catch (BadLocationException e) { + Log.log(e); + } + + return null; + } + + private CompletableFuture provideHover(Annotation annotation, TextDocument doc, + SpringBootApp[] runningApps) { + + try { + List> hoverContent = new ArrayList<>(); + ConditionalParserFromRunningApp parser = new ConditionalParserFromRunningApp(); + Optional> val = parser.parse(annotation, runningApps); + + if (val.isPresent()) { + addHoverContent(val.get(), hoverContent); + } + + Range hoverRange = doc.toRange(annotation.getStartPosition(), annotation.getLength()); + Hover hover = new Hover(); + + hover.setContents(hoverContent); + hover.setRange(hoverRange); + + return CompletableFuture.completedFuture(hover); + } catch (Exception e) { + Log.log(e); + } + + return null; + } + + private void addHoverContent(List conditions, + List> hoverContent) throws Exception { + for (RunningAppConditional condition : conditions) { + hoverContent.add(Either.forLeft("Condition: " + condition.condition)); + hoverContent.add(Either.forLeft("Message: " + condition.message)); + } + } + + /** + * Parsers @ConditionalOn annotations from a spring boot running app's + * autoconfig report. An example of a conditional that is parsed from a running + * app's autoconfig report would be: + * + * {"TraceRepositoryAutoConfiguration#traceRepository":[{"condition":"OnBeanCondition","message":"@ConditionalOnMissingBean + * (types: org.springframework.boot.actuate.trace.TraceRepository; + * SearchStrategy: all) did not find any beans"}] + * + */ + public static class ConditionalParserFromRunningApp { + + public Optional> parse(Annotation annotation, SpringBootApp[] runningApps) { + + try { + for (SpringBootApp app : runningApps) { + String autoConfigRecord = app.getAutoConfigReport(); + JSONObject autoConfigJson = new JSONObject(autoConfigRecord); + return Optional.of(getConditionFromPositiveMatches(annotation, autoConfigJson)); + } + } catch (Exception e) { + Log.log(e); + } + return Optional.empty(); + } + + /** + * + * @param annotation + * @param autoConfigJson + * @return non-null list of conditionals parsed from an autoconfig report. List + * may be empty. + */ + private List getConditionFromPositiveMatches(Annotation annotation, + JSONObject autoConfigJson) { + List conditions = new ArrayList<>(); + + getPositiveMatches(autoConfigJson).ifPresent((positiveMatches) -> { + Iterator pMKeys = positiveMatches.keys(); + while (pMKeys.hasNext()) { + String positiveMatchKey = pMKeys.next(); + if (matchesAnnotation(annotation, positiveMatchKey)) { + JSONArray matchList = (JSONArray) positiveMatches.get(positiveMatchKey); + matchList.forEach((match) -> { + if (match instanceof JSONObject) { + getMatchedCondition((JSONObject) match, annotation) + .ifPresent((condition) -> conditions.add(condition)); + } + }); + } + } + }); + + return conditions; + } + + protected Optional getPositiveMatches(JSONObject autoConfigJson) { + + Iterator keys = autoConfigJson.keys(); + + while (keys.hasNext()) { + String key = keys.next(); + if ("positiveMatches".equals(key)) { + Object obj = autoConfigJson.get(key); + if (obj instanceof JSONObject) { + return Optional.of((JSONObject) obj); + } + } + } + return Optional.empty(); + } + + /** + * + * @param annotation + * @param jsonKey + * @return true if the annotation matches the information in the json key from + * the running app. + */ + protected boolean matchesAnnotation(Annotation annotation, String jsonKey) { + + ASTNode parent = annotation.getParent(); + if (parent instanceof MethodDeclaration) { + MethodDeclaration methodDec = (MethodDeclaration) parent; + IMethodBinding binding = methodDec.resolveBinding(); + String annotationDeclaringClassName = binding.getDeclaringClass().getName(); + String annotationMethodName = binding.getName(); + return jsonKey.contains(annotationDeclaringClassName) && jsonKey.contains(annotationMethodName); + } else if (parent instanceof TypeDeclaration) { + TypeDeclaration typeDec = (TypeDeclaration) parent; + String annotationDeclaringClassName = typeDec.resolveBinding().getName(); + return jsonKey.contains(annotationDeclaringClassName); + } + return false; + } + + protected Optional getMatchedCondition(JSONObject conditionJson, Annotation annotation) { + if (conditionJson != null) { + String condition = (String) conditionJson.get("condition"); + String message = (String) conditionJson.get("message"); + String annotationName = annotation.resolveTypeBinding().getName(); + if (message.contains(annotationName)) { + return Optional.of(new RunningAppConditional(condition, message)); + } + } + return Optional.empty(); + } + } + + public static class RunningAppConditional { + + public final String condition; + public final String message; + + public RunningAppConditional(String condition, String message) { + this.condition = condition; + this.message = message; + } + } +} diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionals/Constants.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionals/Constants.java new file mode 100644 index 000000000..519d7d7b0 --- /dev/null +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionals/Constants.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.conditionals; + +public class Constants { + + public static final String CONDITIONAL_ON_BEAN = "org.springframework.boot.autoconfigure.condition.ConditionalOnBean"; + public static final String CONDITIONAL_ON_MISSING_BEAN = "org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean"; + public static final String CONDITIONAL_ON_PROPERTY = "org.springframework.boot.autoconfigure.condition.ConditionalOnProperty"; + public static final String CONDITIONAL_ON_RESOURCE = "org.springframework.boot.autoconfigure.condition.ConditionalOnResource"; + + public static final String CONDITIONAL_ON_CLASS = "org.springframework.boot.autoconfigure.condition.ConditionalOnClass"; + public static final String CONDITIONAL_ON_CLOUD_PLATFORM = "org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform"; + public static final String CONDITIONAL_ON_WEB_APPLICATION = "org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication"; +}