Skip to content

Commit

Permalink
feat: Support Reactive Routes
Browse files Browse the repository at this point in the history
Fixes #1114

Signed-off-by: azerr <[email protected]>
  • Loading branch information
angelozerr committed Aug 31, 2023
1 parent 3fbeb41 commit 19a1d7f
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import org.eclipse.lsp4j.util.Ranges;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand Down Expand Up @@ -97,6 +99,28 @@ private static PsiAnnotation getFirstAnnotation(PsiAnnotation[] annotations, Str
return null;
}

public static List<PsiAnnotation> getAllAnnotations(PsiElement annotatable, String... annotationNames) {
List<PsiAnnotation> all = new ArrayList<>();
if (annotatable instanceof PsiAnnotationOwner) {
collectAnnotation(((PsiAnnotationOwner) annotatable).getAnnotations(), all, annotationNames);
} else if (annotatable instanceof PsiModifierListOwner) {
collectAnnotation(((PsiModifierListOwner) annotatable).getAnnotations(), all, annotationNames);
}
return all;
}

private static void collectAnnotation(PsiAnnotation[] annotations, List<PsiAnnotation> all, String...annotationNames) {
if (annotations == null || annotations.length == 0 || annotationNames == null || annotationNames.length == 0) {
return;
}
for (PsiAnnotation annotation : annotations) {
for (String annotationName: annotationNames) {
if (isMatchAnnotation(annotation, annotationName)) {
all.add(annotation);
}
}
}
}
/**
* Returns the annotation from the given <code>annotatable</code> element with
* the given name <code>annotationName</code> and null otherwise.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ public class DefaultJaxRsInfoProvider implements IJaxRsInfoProvider {

@Override
public boolean canProvideJaxRsMethodInfoForClass(PsiFile typeRoot, Module javaProject, ProgressIndicator monitor) {
return PsiTypeUtils.findType(javaProject, JAVAX_WS_RS_PATH_ANNOTATION) == null
|| PsiTypeUtils.findType(javaProject, JAKARTA_WS_RS_PATH_ANNOTATION) == null;
return PsiTypeUtils.findType(javaProject, JAVAX_WS_RS_PATH_ANNOTATION) != null
|| PsiTypeUtils.findType(javaProject, JAKARTA_WS_RS_PATH_ANNOTATION) != null;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*******************************************************************************
* Copyright (c) 2023 Red Hat Inc. and others.
* All rights reserved. This program and the accompanying materials
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*******************************************************************************/
package com.redhat.microprofile.psi.internal.quarkus.route.java;

/**
* Reactive route constants.
*
* @see <a href="https://quarkus.io/guides/reactive-routes#declaring-reactive-routes">https://quarkus.io/guides/reactive-routes#declaring-reactive-routes</a>
*/
public class ReactiveRouteConstants {

public static final String ROUTE_BASE_FQN = "io.quarkus.vertx.web.RouteBase";

public static final String ROUTE_BASE_PATH = "path";

public static final String ROUTE_FQN = "io.quarkus.vertx.web.Route";

public static final String ROUTE_PATH = "path";

public static final String REACTIVE_ROUTE_GET_ANNOTATION = "io.quarkus.vertx.web.Route.HttpMethod.GET";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*******************************************************************************
* Copyright (c) 2023 Red Hat Inc. and others.
* All rights reserved. This program and the accompanying materials
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*******************************************************************************/
package com.redhat.microprofile.psi.internal.quarkus.route.java;


import com.intellij.openapi.module.Module;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.psi.*;
import com.intellij.util.KeyedLazyInstanceEP;
import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils;
import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.jaxrs.*;
import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.utils.IPsiUtils;
import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.utils.PsiTypeUtils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import static com.redhat.devtools.intellij.lsp4mp4ij.psi.core.utils.PsiTypeUtils.overlaps;
import static com.redhat.microprofile.psi.internal.quarkus.route.java.ReactiveRouteConstants.ROUTE_FQN;

/**
* Use custom logic for all JAX-RS features for Reactive @Route.
*
* @see <a href="https://quarkus.io/guides/reactive-routes#declaring-reactive-routes">https://quarkus.io/guides/reactive-routes#declaring-reactive-routes</a>
*/
public class ReactiveRouteJaxRsInfoProvider extends KeyedLazyInstanceEP<IJaxRsInfoProvider> implements IJaxRsInfoProvider {

private static final Logger LOGGER = Logger.getLogger(ReactiveRouteJaxRsInfoProvider.class.getName());

@Override
public boolean canProvideJaxRsMethodInfoForClass(PsiFile typeRoot, Module javaProject, ProgressIndicator monitor) {
return PsiTypeUtils.findType(javaProject, ROUTE_FQN) != null;
}

@Override
public Set<PsiClass> getAllJaxRsClasses(Module javaProject, ProgressIndicator monitor) {
// TODO: implement when LSP4IJ will support workspace symbols
return Collections.emptySet();
}

@Override
public List<JaxRsMethodInfo> getJaxRsMethodInfo(PsiFile typeRoot, JaxRsContext jaxrsContext, IPsiUtils utils,
ProgressIndicator monitor) {
try {
PsiClass type = findFirstClass(typeRoot);
if (type == null) {
return Collections.emptyList();
}
// See https://quarkus.io/guides/reactive-routes#routebase
// Try to get the @RouteBase declared in the Java type

PsiAnnotation routeBaseAnnotation = ReactiveRouteUtils.getRouteBaseAnnotation(type);
String pathSegment = routeBaseAnnotation != null ? ReactiveRouteUtils.getRouteBasePath(routeBaseAnnotation) : null;

List<JaxRsMethodInfo> methodInfos = new ArrayList<>();
for (PsiMethod method : type.getMethods()) {

if (method.isConstructor() || utils.isHiddenGeneratedElement(method)) {
continue;
}
// ignore element if method range overlaps the type range,
// happens for generated
// bytecode, i.e. with lombok
if (overlaps(type.getNameIdentifier().getTextRange(), method.getNameIdentifier().getTextRange())) {
continue;
}

// A route method must be a non-private non-static method of a CDI bean.
// See https://quarkus.io/guides/reactive-routes#reactive-route-methods
if (!method.getModifierList().hasExplicitModifier(PsiModifier.PRIVATE) && !method.getModifierList().hasExplicitModifier(PsiModifier.STATIC)) {

// Method can have several @Route
// @Route(path = "/first")
// @Route(path = "/second")
// public void route(RoutingContext rc) {
// // ...
List<PsiAnnotation> routeAnnotations = ReactiveRouteUtils.getRouteAnnotations(method);
if (routeAnnotations.isEmpty()) {
continue;
}

// Loop for @Route annotation
for (PsiAnnotation routeAnnotation : routeAnnotations) {
// @Route(path = "/first")
String methodSegment = ReactiveRouteUtils.getRoutePath(routeAnnotation);
if (methodSegment == null) {
// @Route(methods = Route.HttpMethod.GET)
// void hello(RoutingContext rc)
// Here the segment is the method name
methodSegment = method.getName();
}
String path;
if (pathSegment == null) {
path = methodSegment;
} else {
path = JaxRsUtils.buildURL(pathSegment, methodSegment);
}
String url = JaxRsUtils.buildURL(jaxrsContext.getLocalBaseURL(), path);

JaxRsMethodInfo methodInfo = createMethodInfo(method, routeAnnotation, url);
if (methodInfo != null) {
methodInfos.add(methodInfo);
}
}
}
}
return methodInfos;
} catch (ProcessCanceledException e) {
throw e;
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Error while collecting JAX-RS methods for Reactive @Route", e);
return Collections.emptyList();
}
}

private PsiClass findFirstClass(PsiFile typeRoot) {
for (PsiElement element : typeRoot.getChildren()) {
if (element instanceof PsiClass) {
return (PsiClass) element;
}
}
return null;
}

private static JaxRsMethodInfo createMethodInfo(PsiMethod method, PsiAnnotation routeAnnotation, String url) {

PsiFile resource = method.getContainingFile();
if (resource == null) {
return null;
}
String documentUri = LSPIJUtils.toUriAsString(resource);

HttpMethod httpMethod = HttpMethod.GET;
// TODO: collect the proper http method from @Route(methods=)
/*for (String methodAnnotationFQN : JaxRsConstants.HTTP_METHOD_ANNOTATIONS) {
if (hasAnnotation(method, methodAnnotationFQN)) {
httpMethod = ReactiveRouteUtils.getHttpMethodForAnnotation(methodAnnotationFQN);
break;
}
}*/

return new JaxRsMethodInfo(url, httpMethod, method, documentUri);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*******************************************************************************
* Copyright (c) 2023 Red Hat Inc. and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
*
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*******************************************************************************/
package com.redhat.microprofile.psi.internal.quarkus.route.java;

import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiElement;
import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.jaxrs.HttpMethod;
import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.jaxrs.JaxRsConstants;

import java.util.List;

import static com.redhat.devtools.intellij.lsp4mp4ij.psi.core.utils.AnnotationUtils.*;
import static com.redhat.microprofile.psi.internal.quarkus.route.java.ReactiveRouteConstants.*;


/**
* Reactive @Route utilities.
*
* @see <a href="https://quarkus.io/guides/reactive-routes#declaring-reactive-routes">https://quarkus.io/guides/reactive-routes#declaring-reactive-routes</a>
*/
public class ReactiveRouteUtils {

private ReactiveRouteUtils() {

}

public static PsiAnnotation getRouteBaseAnnotation(PsiElement annotatable) {
return getFirstAnnotation(annotatable, ROUTE_BASE_FQN);
}

public static String getRouteBasePath(PsiAnnotation routeBaseAnnotation) {
return getAnnotationMemberValue(routeBaseAnnotation, ROUTE_BASE_PATH);
}

public static List<PsiAnnotation> getRouteAnnotations(PsiElement annotatable) {
return getAllAnnotations(annotatable, ROUTE_FQN);
}

public static String getRoutePath(PsiAnnotation routeAnnotation) {
return getAnnotationMemberValue(routeAnnotation, ROUTE_PATH);
}

/**
* Returns an HttpMethod given the FQN of a JAX-RS or Jakarta RESTful
* annotation, nor null if the FQN doesn't match any HttpMethod.
*
* @param annotationFQN the FQN of the annotation to convert into a HttpMethod
* @return an HttpMethod given the FQN of a JAX-RS or Jakarta RESTful
* annotation, nor null if the FQN doesn't match any HttpMethod
*/
public static HttpMethod getHttpMethodForAnnotation(String annotationFQN) {
switch (annotationFQN) {
case ReactiveRouteConstants.REACTIVE_ROUTE_GET_ANNOTATION:
case JaxRsConstants.JAVAX_WS_RS_GET_ANNOTATION:
return HttpMethod.GET;
case JaxRsConstants.JAKARTA_WS_RS_HEAD_ANNOTATION:
case JaxRsConstants.JAVAX_WS_RS_HEAD_ANNOTATION:
return HttpMethod.HEAD;
case JaxRsConstants.JAKARTA_WS_RS_POST_ANNOTATION:
case JaxRsConstants.JAVAX_WS_RS_POST_ANNOTATION:
return HttpMethod.POST;
case JaxRsConstants.JAKARTA_WS_RS_PUT_ANNOTATION:
case JaxRsConstants.JAVAX_WS_RS_PUT_ANNOTATION:
return HttpMethod.PUT;
case JaxRsConstants.JAKARTA_WS_RS_DELETE_ANNOTATION:
case JaxRsConstants.JAVAX_WS_RS_DELETE_ANNOTATION:
return HttpMethod.DELETE;
case JaxRsConstants.JAKARTA_WS_RS_PATCH_ANNOTATION:
case JaxRsConstants.JAVAX_WS_RS_PATCH_ANNOTATION:
return HttpMethod.PATCH;
default:
return null;
}
}
}
2 changes: 2 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,8 @@
<projectLabelProvider implementation="com.redhat.microprofile.psi.internal.quarkus.providers.QuarkusProjectLabelProvider"/>
<jaxRsInfoProvider
implementation="com.redhat.microprofile.psi.internal.quarkus.renarde.java.RenardeJaxRsInfoProvider" />
<jaxRsInfoProvider
implementation="com.redhat.microprofile.psi.internal.quarkus.route.java.ReactiveRouteJaxRsInfoProvider" />

<!-- Quarkus Core support -->
<propertiesProvider implementation="com.redhat.microprofile.psi.internal.quarkus.core.properties.QuarkusCoreProvider"/>
Expand Down

0 comments on commit 19a1d7f

Please sign in to comment.