Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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 Quaruks warns during build-time about Elide-specific classes that "are not in the Jandex index"
Comment thread
aklish marked this conversation as resolved.
Outdated
* 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>
additionalIndexedClassesBuildItemBuildProducer) {
additionalIndexedClassesBuildItemBuildProducer.produce(
Comment thread
aklish marked this conversation as resolved.
Outdated
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);
}

class Param {
Comment thread
aklish marked this conversation as resolved.
Outdated
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