Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ dependency-reduced-pom.xml
*.factorypath
*.vscode
.DS_Store
tmlog*.log
tmlog*.log
/elide-quarkus/deployment/build.dot
11 changes: 8 additions & 3 deletions elide-quarkus/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-common-spi</artifactId>
<artifactId>quarkus-rest-common</artifactId>
</dependency>
<dependency>
<groupId>com.yahoo.elide</groupId>
Expand Down Expand Up @@ -70,8 +70,13 @@
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-deployment</artifactId>
<artifactId>quarkus-rest-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
Expand All @@ -80,7 +85,7 @@
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-server-common-deployment</artifactId>
<artifactId>quarkus-rest-common-deployment</artifactId>
</dependency>
<dependency>
<groupId>com.yahoo.elide</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,26 @@
package com.yahoo.elide.extension.deployment;

import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;
import static io.quarkus.gizmo.Type.classType;
import static io.quarkus.gizmo.Type.parameterizedType;
import static io.quarkus.gizmo.Type.voidType;

import com.yahoo.elide.Elide;
import com.yahoo.elide.annotation.Include;
import com.yahoo.elide.annotation.LifeCycleHookBinding;
import com.yahoo.elide.annotation.SecurityCheck;
import com.yahoo.elide.core.dictionary.RelationshipType;
import com.yahoo.elide.core.request.route.RouteResolver;
import com.yahoo.elide.core.security.checks.OperationCheck;
import com.yahoo.elide.core.security.checks.prefab.Collections;
import com.yahoo.elide.core.utils.ClassScanner;
import com.yahoo.elide.core.utils.coerce.converters.ElideTypeConverter;
import com.yahoo.elide.extension.runtime.ElideBeans;
import com.yahoo.elide.extension.runtime.ElideConfig;
import com.yahoo.elide.extension.runtime.ElideRecorder;
import com.yahoo.elide.extension.runtime.ElideResourceBuilder;
import com.yahoo.elide.graphql.DeferredId;
import com.yahoo.elide.graphql.GraphQLEndpoint;
import com.yahoo.elide.jsonapi.models.JsonApiDocument;
import com.yahoo.elide.jsonapi.models.*;
import com.yahoo.elide.jsonapi.resources.JsonApiEndpoint;
import com.yahoo.elide.jsonapi.serialization.DataDeserializer;
import com.yahoo.elide.jsonapi.serialization.DataSerializer;
Expand All @@ -34,8 +41,8 @@
import org.jboss.jandex.DotName;
import org.jboss.jandex.Type;
import org.jboss.logging.Logger;
import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters;

import graphql.execution.DataFetcherExceptionHandler;
import graphql.schema.GraphQLSchema;
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.BeanContainerListenerBuildItem;
Expand All @@ -44,21 +51,31 @@
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.IndexDependencyBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem;
import io.quarkus.resteasy.server.common.deployment.ResteasyDeploymentCustomizerBuildItem;
import io.quarkus.undertow.deployment.ServletInitParamBuildItem;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.ClassOutput;
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.gizmo.SignatureBuilder;
import io.quarkus.resteasy.reactive.spi.GeneratedJaxRsResourceBuildItem;
import io.quarkus.resteasy.reactive.spi.GeneratedJaxRsResourceGizmoAdaptor;
import jakarta.enterprise.inject.Default;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import jakarta.ws.rs.Path;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.*;

class ElideExtensionProcessor {
/**
* Quarkus extension processor for Elide.
*/
public class ElideExtensionProcessor {
private static final Logger LOG = Logger.getLogger(ElideExtensionProcessor.class.getName());

private static final String FEATURE = "elide";
Expand All @@ -70,7 +87,6 @@ FeatureBuildItem feature() {

@BuildStep
public void indexDependencies(BuildProducer<IndexDependencyBuildItem> dependencies) {
dependencies.produce(new IndexDependencyBuildItem("com.yahoo.elide", "elide-core"));
dependencies.produce(new IndexDependencyBuildItem("io.swagger.core.v3", "swagger-core-jakarta"));
dependencies.produce(new IndexDependencyBuildItem("io.swagger.core.v3", "swagger-models-jakarta"));

Expand All @@ -79,55 +95,69 @@ public void indexDependencies(BuildProducer<IndexDependencyBuildItem> dependenci
dependencies.produce(new IndexDependencyBuildItem("com.graphql-java", "graphql-java"));
}

/**
* When Quarkus warns during build-time about Elide-specific classes that "are not in the Jandex index"
* we add those classes here. Unlike using the IndexDependencyBuildItem, this more specific approach
* prevents the Elide JAX-RS endpoints from being deployed at their default "/" paths.
* @param additionalIndexedClassesBuildItemBuildProducer
*/
@BuildStep
public AdditionalBeanBuildItem configureElideEndpoints(ElideConfig config) {
AdditionalBeanBuildItem.Builder builder = AdditionalBeanBuildItem.builder();
public void indexElideClasses(BuildProducer<AdditionalIndexedClassesBuildItem>
indexedClassesProducer) {
indexedClassesProducer.produce(
new io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem(
OperationCheck.class.getCanonicalName(),
RelationshipType.class.getCanonicalName(),
JsonApiDocument.class.getCanonicalName(),
Data.class.getCanonicalName(),
Meta.class.getCanonicalName(),
Relationship.class.getCanonicalName(),
Resource.class.getCanonicalName(),
ResourceIdentifier.class.getCanonicalName(),
OpenApiDocument.class.getCanonicalName()
));
}

if (config.baseJsonapi != null) {
LOG.info("Enabling JSON-API Endpoint");
builder = builder.addBeanClass(JsonApiEndpoint.class);
}
@BuildStep
public void registerElideBeans(BuildProducer<AdditionalBeanBuildItem> additionalBeans) {
additionalBeans.produce(AdditionalBeanBuildItem.builder()
.addBeanClass(ElideBeans.class)
.build());
additionalBeans.produce(AdditionalBeanBuildItem.builder()
.addBeanClass(ElideConfig.class)
.build());
additionalBeans.produce(AdditionalBeanBuildItem.builder()
.addBeanClass(ElideRecorder.class)
.build());
}

if (config.baseGraphql != null) {
LOG.info("Enabling GraphQL Endpoint");
builder = builder.addBeanClass(GraphQLEndpoint.class);
@BuildStep
public void configureElideEndpoints(ElideConfig config,
BuildProducer<GeneratedJaxRsResourceBuildItem> generatedJaxRsResourceBuildItemBuildProducer) {
if (config.jsonApiPath != null) {
LOG.infof("Enabling JSON-API Endpoint for path: %s", config.jsonApiPath);
generateEndpointClass(generatedJaxRsResourceBuildItemBuildProducer, "JsonApi",
JsonApiEndpoint.class, config.jsonApiPath,
new Param("elide", Elide.class, null), new Param(Optional.class, RouteResolver.class));
}

if (config.baseSwagger != null && config.baseJsonapi != null) {
LOG.info("Enabling Swagger Endpoint");
builder = builder.addBeanClass(ApiDocsEndpoint.class);
if (config.graphqlPath != null) {
LOG.infof("Enabling GraphQL Endpoint for path: %s", config.graphqlPath);
generateEndpointClass(generatedJaxRsResourceBuildItemBuildProducer, "GraphQL",
GraphQLEndpoint.class, config.graphqlPath,
new Param("elide", Elide.class, null),
new Param(Optional.class, DataFetcherExceptionHandler.class),
new Param(Optional.class, RouteResolver.class));
}

return builder.build();
}

@BuildStep
public void configureRestEasy(
BuildProducer<ResteasyDeploymentCustomizerBuildItem> deploymentCustomizerProducer,
BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
BuildProducer<ServletInitParamBuildItem> initParamProducer
) {
initParamProducer.produce(
new ServletInitParamBuildItem(
ResteasyContextParameters.RESTEASY_SCANNED_RESOURCE_CLASSES_WITH_BUILDER,
ElideResourceBuilder.class.getName() + ":"
+ JsonApiEndpoint.class.getCanonicalName() + ","
+ GraphQLEndpoint.class.getCanonicalName() + ","
+ ApiDocsEndpoint.class.getCanonicalName()
));

deploymentCustomizerProducer.produce(new ResteasyDeploymentCustomizerBuildItem((deployment) -> {
deployment.getScannedResourceClassesWithBuilder().put(
ElideResourceBuilder.class.getCanonicalName(),
Arrays.asList(
JsonApiEndpoint.class.getCanonicalName(),
GraphQLEndpoint.class.getCanonicalName(),
ApiDocsEndpoint.class.getCanonicalName()
));
}));

reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false,
ElideResourceBuilder.class.getName()));
if (config.apiDocsPath != null && config.jsonApiPath != null) {
LOG.infof("Enabling Swagger Endpoint for path: %s", config.apiDocsPath);
generateEndpointClass(generatedJaxRsResourceBuildItemBuildProducer, "ApiDocs",
ApiDocsEndpoint.class, config.apiDocsPath,
new Param("apiDocs", List.class, ApiDocsEndpoint.ApiDocsRegistration.class),
new Param("elide", Elide.class, null),
new Param(Optional.class, RouteResolver.class));
}
}

@Record(STATIC_INIT)
Expand Down Expand Up @@ -235,7 +265,100 @@ public void configureElideModels(
reflectionBuildItems.produce(new ReflectiveClassBuildItem(true, true, SimpleLog.class));
}

private Type convertToType(Class<?> cls) {
return Type.create(DotName.createSimple(cls.getCanonicalName()), Type.Kind.CLASS);
private org.jboss.jandex.Type convertToType(Class<?> cls) {
return org.jboss.jandex.Type.create(DotName.createSimple(cls.getCanonicalName()), Type.Kind.CLASS);
}

/**
* We customise @Path.value at build time by generating a subclass of the corresponding Elide endpoint with Gizmo.
* Note that the ability to customise the value of @Path.value depends on us NOT including the standard Elide
* endpoint classes by default. If they were present (for example, if they were added to the indexed classes),
* Quarkus would register them with their default @Path values (in addition to these generated instances).
*
* @param generatedJaxRsResourceBuildItemBuildProducer the build producer
* @param endpointStyle the endpoint style name
* @param endpointClass the endpoint class
* @param customPath the custom path
* @param params the constructor parameters
*/
private void generateEndpointClass(
BuildProducer<GeneratedJaxRsResourceBuildItem> generatedJaxRsResourceBuildItemBuildProducer,
String endpointStyle, Class endpointClass, String customPath, Param... params) {
Comment on lines +284 to +286
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The parameter name generatedJaxRsResourceBuildItemBuildProducer is excessively long and makes the method signature hard to read. Consider renaming to resourceProducer or generatedResourceProducer.

Copilot uses AI. Check for mistakes.
// Deconstruct Param instances
Class[] paramClasses = new Class[params.length];
io.quarkus.gizmo.Type[] paramTypes = new io.quarkus.gizmo.Type[params.length];
String[] paramNames = new String[params.length];
for (int i = 0; i < params.length; i++) {
paramClasses[i] = params[i].clazz;
paramNames[i] = params[i].atNamed;
if (params[i].parameterizedType != null) {
paramTypes[i] = parameterizedType(classType(params[i].clazz),
classType(params[i].parameterizedType));
} else {
paramTypes[i] = classType(params[i].clazz);
}
}
// Use Gizmo to generate an instance of the endpoint class with a custom @Path(value)
String generatedClassName = "com.yahoo.elide.extension.generated.Configurable"
+ endpointStyle + "Endpoint";
ClassOutput classOutput = new GeneratedJaxRsResourceGizmoAdaptor(
generatedJaxRsResourceBuildItemBuildProducer);
try (ClassCreator classCreator = ClassCreator.builder()
.classOutput(classOutput)
.className(generatedClassName)
.superClass(endpointClass)
.build()) {

// Add @Path annotation with custom path value
classCreator.addAnnotation(Path.class).addValue("value", customPath);

// Create a constructor that matches endpoint's current @Injected constructor
try (MethodCreator constructor = classCreator.getMethodCreator("<init>", void.class,
paramClasses)) {
// Using Gizmo's SignatureBuilder allows us to define a parameterised injection point
SignatureBuilder.MethodSignatureBuilder signatureBuilder = SignatureBuilder.forMethod()
.setReturnType(voidType());
Arrays.stream(paramTypes).forEach(signatureBuilder::addParameterType);
constructor.setSignature(signatureBuilder.build());
// Add @Inject annotation to constructor
constructor.addAnnotation(jakarta.inject.Inject.class);
// Add @Named annotations to parameters where applicable
Arrays.stream(paramNames).forEach((paramName) -> {
if (paramName != null) {
int paramIndex = Arrays.asList(paramNames).indexOf(paramName);
constructor.getParameterAnnotations(paramIndex).addAnnotation(Named.class)
.addValue("value", paramName);
}
});
// Call super constructor with the parameters
ResultHandle[] constructorParams = new ResultHandle[constructor.getMethodDescriptor()
.getParameterTypes().length];
for (int i = 0; i < constructorParams.length; i++) {
constructorParams[i] = constructor.getMethodParam(i);
}
constructor.invokeSpecialMethod(
MethodDescriptor.ofConstructor(endpointClass, paramClasses),
constructor.getThis(), constructorParams);
constructor.returnValue(null);
}
}
LOG.infof("Generated configurable %s endpoint with path: %s", endpointStyle, customPath);
}

static class Param {
String atNamed;
Class<?> clazz;
Class<?> parameterizedType;

public Param(String atNamed, Class<?> clazz, Class<?> parameterizedType) {
this.atNamed = atNamed;
this.clazz = clazz;
this.parameterizedType = parameterizedType;
}

public Param(Class<?> clazz, Class<?> parameterizedType) {
this.clazz = clazz;
this.parameterizedType = parameterizedType;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static com.yahoo.elide.test.jsonapi.JsonApiDSL.resource;
import static com.yahoo.elide.test.jsonapi.JsonApiDSL.type;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import com.yahoo.elide.Elide;
import com.yahoo.elide.core.dictionary.EntityDictionary;
Expand Down Expand Up @@ -74,7 +75,7 @@ public void testBookJsonApiEndpoint() {
)
)
)
.post("/book")
.post("/test-jsonapi/book")
.then()
.log().all()
.statusCode(HttpStatus.SC_CREATED);
Expand All @@ -86,6 +87,18 @@ public void testTypeConvertersAreRegistered() {
RestAssured.when().get("/test-jsonapi/book?filter=releaseDate<2025-02-19T19:17:53Z").then().log().all().statusCode(200);
}

/**
* Previously, when we had a IndexDependencyBuildItem for "com.yahoo.elide:elide-core", it would cause each Elide JAX-RS endpoint to be
* deployed at its default, root path, as well as at the configured path. This test is a reminder that the current build-time advice of
* "Consider adding them to the index" for certain Elide classes is not without consequence.
*/
@Test
public void testNothingDeployedAtRoot() {
RestAssured.when().get("/").then().log().all()
.body(containsString("Resource not found"))
.statusCode(404);
}

@Test
public void testBookGraphqlEndpoint() {
String query = document(
Expand Down Expand Up @@ -128,7 +141,7 @@ public void shouldDisallowSupplierCreation() {
)
)
)
.post("/supplier")
.post("/test-jsonapi/supplier")
.then()
.log().all()
.statusCode(HttpStatus.SC_FORBIDDEN);
Expand Down
Loading