diff --git a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/WebfluxPathFinder.java b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/WebfluxPathFinder.java new file mode 100644 index 000000000..1a5761cbf --- /dev/null +++ b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/WebfluxPathFinder.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * Copyright (c) 2018 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.requestmapping; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.IMethodBinding; +import org.eclipse.jdt.core.dom.MethodInvocation; + +/** + * @author Martin Lippert + */ +public class WebfluxPathFinder extends ASTVisitor { + + private String path; + private ASTNode root; + + public WebfluxPathFinder(ASTNode root) { + this.root = root; + } + + public String getPath() { + return path; + } + + @Override + public boolean visit(MethodInvocation node) { + boolean visitChildren = true; + + if (node != this.root) { + IMethodBinding methodBinding = node.resolveMethodBinding(); + + if (WebfluxRouterSymbolProvider.REQUEST_PREDICATES_TYPE.equals(methodBinding.getDeclaringClass().getBinaryName())) { + String name = methodBinding.getName(); + if (name != null && WebfluxRouterSymbolProvider.REQUEST_PREDICATE_PATH_METHODS.contains(name)) { + path = WebfluxRouterSymbolProvider.extractPath(node); + } + } + + if (WebfluxRouterSymbolProvider.ROUTER_FUNCTIONS_TYPE.equals(methodBinding.getDeclaringClass().getBinaryName())) { + String name = methodBinding.getName(); + if ("route".equals(name)) { + visitChildren = false; + } + } + else if (WebfluxRouterSymbolProvider.ROUTER_FUNCTION_TYPE.equals(methodBinding.getDeclaringClass().getBinaryName())) { + String name = methodBinding.getName(); + if ("andRoute".equals(name)) { + visitChildren = false; + } + } + } + return visitChildren; + } + +} diff --git a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/WebfluxRouterSymbolProvider.java b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/WebfluxRouterSymbolProvider.java index 60db3b12c..3fd355a3d 100644 --- a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/WebfluxRouterSymbolProvider.java +++ b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/WebfluxRouterSymbolProvider.java @@ -11,9 +11,13 @@ package org.springframework.ide.vscode.boot.java.requestmapping; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.Block; @@ -21,10 +25,14 @@ 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.MethodInvocation; +import org.eclipse.jdt.core.dom.StringLiteral; import org.eclipse.jdt.core.dom.Type; import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.SymbolKind; import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider; +import org.springframework.ide.vscode.commons.util.BadLocationException; import org.springframework.ide.vscode.commons.util.text.TextDocument; /** @@ -32,8 +40,11 @@ import org.springframework.ide.vscode.commons.util.text.TextDocument; */ public class WebfluxRouterSymbolProvider implements SymbolProvider { - private static final String ROUTER_FUNCTION_TYPE = "org.springframework.web.reactive.function.server.RouterFunction"; - private static final String ROUTER_FUNCTIONS_TYPE = "org.springframework.web.reactive.function.server.RouterFunctions"; + public static final String ROUTER_FUNCTION_TYPE = "org.springframework.web.reactive.function.server.RouterFunction"; + public static final String ROUTER_FUNCTIONS_TYPE = "org.springframework.web.reactive.function.server.RouterFunctions"; + public static final String REQUEST_PREDICATES_TYPE = "org.springframework.web.reactive.function.server.RequestPredicates"; + + public static final Set REQUEST_PREDICATE_PATH_METHODS = new HashSet<>(Arrays.asList("path", "GET", "POST", "DELETE", "PUT", "PATCH", "HEAD", "OPTIONS")); @Override public Collection getSymbols(Annotation node, ITypeBinding typeBinding, @@ -71,13 +82,17 @@ public class WebfluxRouterSymbolProvider implements SymbolProvider { public boolean visit(MethodInvocation node) { IMethodBinding methodBinding = node.resolveMethodBinding(); - if (ROUTER_FUNCTIONS_TYPE.equals(methodBinding.getDeclaringClass().getBinaryName()) - && "route".equals(node.getName().toString())) { - extractMappingSymbol(node, doc, result); + if (ROUTER_FUNCTIONS_TYPE.equals(methodBinding.getDeclaringClass().getBinaryName())) { + String name = methodBinding.getName(); + if ("route".equals(name)) { + extractMappingSymbol(node, doc, result); + } } - else if (ROUTER_FUNCTION_TYPE.equals(methodBinding.getDeclaringClass().getBinaryName()) - && "andRoute".equals(node.getName().toString())) { - extractMappingSymbol(node, doc, result); + else if (ROUTER_FUNCTION_TYPE.equals(methodBinding.getDeclaringClass().getBinaryName())) { + String name = methodBinding.getName(); + if ("andRoute".equals(name)) { + extractMappingSymbol(node, doc, result); + } } return super.visit(node); @@ -89,12 +104,83 @@ public class WebfluxRouterSymbolProvider implements SymbolProvider { } protected void extractMappingSymbol(MethodInvocation node, TextDocument doc, List result) { - List arguments = node.arguments(); - if (arguments != null) { - for (Object argument : arguments) { - System.out.println(argument); + String foundPath = extractPathFromRouterFunction(node); + String path = extractPath(node, foundPath); + String httpMethod = extractMethod(node); + + int methodNameStart = node.getName().getStartPosition(); + int invocationStart = node.getStartPosition(); + + if (path != null && path.length() > 0) { + try { + Location location = new Location(doc.getUri(), doc.toRange(methodNameStart, node.getLength() - (methodNameStart - invocationStart))); + String label = "@" + (path.startsWith("/") ? path : ("/" + path)) + (httpMethod == null || httpMethod.isEmpty() ? "" : " -- " + httpMethod); + result.add(new SymbolInformation(label, SymbolKind.Interface, location)); + } catch (BadLocationException e) { + e.printStackTrace(); } } } + private String extractPathFromRouterFunction(MethodInvocation routerInvocation) { + WebfluxPathFinder pathFinder = new WebfluxPathFinder(routerInvocation); + routerInvocation.accept(pathFinder); + + String path = pathFinder.getPath(); + if (path == null) path = ""; + + return path; + } + + private String extractPath(ASTNode node, String path) { + if (node == null || node instanceof TypeDeclaration) { + return path; + } + + if (node instanceof MethodInvocation) { + MethodInvocation methodInvocation = (MethodInvocation) node; + IMethodBinding methodBinding = methodInvocation.resolveMethodBinding(); + + if (ROUTER_FUNCTIONS_TYPE.equals(methodBinding.getDeclaringClass().getBinaryName())) { + String name = methodBinding.getName(); + if ("nest".equals(name)) { + List arguments = methodInvocation.arguments(); + for (Object argument : arguments) { + if (argument instanceof MethodInvocation) { + MethodInvocation nestedMethod = (MethodInvocation) argument; + IMethodBinding nestedMethodBinding = nestedMethod.resolveMethodBinding(); + + String nestedMethodName = nestedMethodBinding.getName(); + if ("path".equals(nestedMethodName)) { + String additionalPath = extractPath(nestedMethod); + if (additionalPath != null && additionalPath.length() > 0) { + path = additionalPath + path; + } + } + } + + } + } + } + } + + return extractPath(node.getParent(), path); + } + + protected static String extractPath(MethodInvocation node) { + List arguments = node.arguments(); + if (arguments != null && arguments.size() > 0) { + Object object = arguments.get(0); + if (object instanceof StringLiteral) { + String path = ((StringLiteral) object).getLiteralValue(); + return path; + } + } + return null; + } + + private String extractMethod(MethodInvocation node) { + return null; + } + } diff --git a/headless-services/commons/commons-boot/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/WebFluxMappingSymbolProviderTest.java b/headless-services/commons/commons-boot/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/WebFluxMappingSymbolProviderTest.java index dfe7c6409..1d1f72ae0 100644 --- a/headless-services/commons/commons-boot/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/WebFluxMappingSymbolProviderTest.java +++ b/headless-services/commons/commons-boot/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/WebFluxMappingSymbolProviderTest.java @@ -55,8 +55,23 @@ public class WebFluxMappingSymbolProviderTest { String docUri = directory.toPath().resolve("src/main/java/org/test/QuoteRouter.java").toUri().toString(); List symbols = getSymbols(docUri); assertEquals(6, symbols.size()); - assertTrue(containsSymbol(symbols, "@/hello", docUri, 22, 23, 22, 81)); - assertTrue(containsSymbol(symbols, "@/echo", docUri, 23, 1, 23, 85)); + assertTrue(containsSymbol(symbols, "@/hello", docUri, 22, 5, 22, 70)); + assertTrue(containsSymbol(symbols, "@/echo", docUri, 23, 5, 23, 101)); + assertTrue(containsSymbol(symbols, "@/quotes", docUri, 24, 5, 24, 86)); + assertTrue(containsSymbol(symbols, "@/quotes", docUri, 25, 5, 25, 94)); + } + + @Test + public void testNestedRoutesMappingSymbols() throws Exception { + harness.intialize(new File(ProjectsHarness.class.getResource("/test-projects/test-webflux-project/").toURI())); + File directory = new File(ProjectsHarness.class.getResource("/test-projects/test-webflux-project/").toURI()); + + String docUri = directory.toPath().resolve("src/main/java/org/test/NestedRouter.java").toUri().toString(); + List symbols = getSymbols(docUri); + assertEquals(5, symbols.size()); + assertTrue(containsSymbol(symbols, "@/person/{id}", docUri, 27, 6, 27, 45)); + assertTrue(containsSymbol(symbols, "@/person/", docUri, 29, 6, 29, 83)); + assertTrue(containsSymbol(symbols, "@/person", docUri, 28, 7, 28, 60)); } private boolean containsSymbol(List symbols, String name, String uri, int startLine, int startCHaracter, int endLine, int endCharacter) { diff --git a/headless-services/commons/commons-boot/src/test/resources/test-projects/test-webflux-project/src/main/java/org/test/NestedRouter.java b/headless-services/commons/commons-boot/src/test/resources/test-projects/test-webflux-project/src/main/java/org/test/NestedRouter.java new file mode 100644 index 000000000..89d063b17 --- /dev/null +++ b/headless-services/commons/commons-boot/src/test/resources/test-projects/test-webflux-project/src/main/java/org/test/NestedRouter.java @@ -0,0 +1,33 @@ +package org.test; + +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RequestPredicates.POST; +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; +import static org.springframework.web.reactive.function.server.RequestPredicates.contentType; +import static org.springframework.web.reactive.function.server.RequestPredicates.method; +import static org.springframework.web.reactive.function.server.RequestPredicates.path; +import static org.springframework.web.reactive.function.server.RouterFunctions.nest; +import static org.springframework.web.reactive.function.server.RouterFunctions.route; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; + +@Configuration +public class NestedRouter { + + @Bean + public RouterFunction routingFunction() { + PersonHandler handler = new PersonHandler(); + + return nest(path("/person"), + nest(accept(APPLICATION_JSON), + route(GET("/{id}"), handler::getPerson) + .andRoute(method(HttpMethod.GET), handler::listPeople) + ).andRoute(POST("/").and(contentType(APPLICATION_JSON)), handler::createPerson)); + } + +} diff --git a/headless-services/commons/commons-boot/src/test/resources/test-projects/test-webflux-project/src/main/java/org/test/PersonHandler.java b/headless-services/commons/commons-boot/src/test/resources/test-projects/test-webflux-project/src/main/java/org/test/PersonHandler.java new file mode 100644 index 000000000..a1bdb96b4 --- /dev/null +++ b/headless-services/commons/commons-boot/src/test/resources/test-projects/test-webflux-project/src/main/java/org/test/PersonHandler.java @@ -0,0 +1,22 @@ +package org.test; + +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; + +import reactor.core.publisher.Mono; + +public class PersonHandler { + + public Mono getPerson(ServerRequest request) { + return ServerResponse.notFound().build(); + } + + public Mono createPerson(ServerRequest request) { + return ServerResponse.notFound().build(); + } + + public Mono listPeople(ServerRequest request) { + return ServerResponse.notFound().build(); + } + +}