Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!(generator): load endpoints from Spring components and stop using Maven/Gradle at each generation #2616

Draft
wants to merge 51 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
b299278
First version
cromoteca Jul 16, 2024
bbca209
Check for inactive application contexts
cromoteca Jul 16, 2024
d8f54a2
Remove `configure`mojo
cromoteca Jul 17, 2024
52ac8b3
Support production mode (needs process-aot in pom.xml)
cromoteca Jul 17, 2024
9bbcdbf
Avoid adding new goal to pom.xml in projects
cromoteca Jul 24, 2024
5edbd61
Simplify code a bit
cromoteca Jul 24, 2024
7a99883
Some code improvements
cromoteca Jul 25, 2024
7093a3a
Make sure we use the available classpath
cromoteca Jul 25, 2024
4000a32
Move classpath
cromoteca Jul 26, 2024
20e4cf5
Get more config from Maven
cromoteca Jul 29, 2024
88d9d01
Refactor annotations configuration
cromoteca Jul 30, 2024
6c702af
Revert import change
cromoteca Jul 30, 2024
0b40ef8
Remove direct references to the endpoint annotations
cromoteca Jul 30, 2024
23814a7
support build dir
cromoteca Jul 31, 2024
d5fb531
Restore check for acl annotations
cromoteca Jul 31, 2024
ca9c6a0
Refactor annotations
cromoteca Aug 1, 2024
474bd56
Extract Aot code and use it for hotswap
cromoteca Aug 1, 2024
d64a6cf
Start fixing Gradle plugin
cromoteca Aug 2, 2024
8403ed1
Refactor configuration to not be static
cromoteca Aug 3, 2024
fc64cb9
Refactorings while fixing tests
cromoteca Aug 6, 2024
4023bde
Keep fixing tests
cromoteca Aug 6, 2024
a704d81
Merge branch 'main' into feat/avoid-running-maven
cromoteca Aug 8, 2024
cb5971c
Remove moduleinfo
cromoteca Aug 8, 2024
91d6a30
Add some necessary comments
cromoteca Aug 8, 2024
122ad38
Merge branch 'main' into feat/avoid-running-maven
cromoteca Sep 2, 2024
3927ebc
Merge branch 'main' into feat/avoid-running-maven
cromoteca Sep 12, 2024
dc3cdcc
Merge branch 'main' into feat/avoid-running-maven
cromoteca Oct 15, 2024
ee8698b
Merge branch 'main' into feat/avoid-running-maven
cromoteca Oct 17, 2024
8ed48c4
Merge remote-tracking branch 'origin/main' into feat/avoid-running-maven
cromoteca Oct 22, 2024
be6888b
Merge branch 'main' into feat/avoid-running-maven
cromoteca Oct 30, 2024
c2221e7
chore(annotations): bump version
platosha Nov 13, 2024
a6081ee
text(parser-jvm-plugin-backbone): update test fixture
platosha Nov 13, 2024
2d851aa
fix(gradle-plugin): make it build
platosha Nov 13, 2024
81b2a27
Merge branch 'main' into feat/avoid-running-maven
cromoteca Nov 18, 2024
7db7ff9
Add missing endpoint in test
cromoteca Nov 18, 2024
801e122
Merge branch 'main' into feat/avoid-running-maven
cromoteca Nov 18, 2024
d31aadc
fix(annotations): add module declaration
platosha Nov 19, 2024
dac7549
test(parser-jvm-plugin-backbone): fix backbone tests
platosha Nov 19, 2024
68c8e90
test(parser-jvm-plugin-transfertypes): add annotations module
platosha Nov 19, 2024
26c0427
test(engine-runtime): use SpringBootTest for tasks
platosha Nov 19, 2024
5b19b4a
Revert "test(parser-jvm-plugin-transfertypes): add annotations module"
cromoteca Nov 19, 2024
39873fd
Revert "test(parser-jvm-plugin-backbone): fix backbone tests"
cromoteca Nov 19, 2024
b371072
Revert "fix(annotations): add module declaration"
cromoteca Nov 19, 2024
3469569
Fix some tests
cromoteca Nov 19, 2024
2ede348
Remove class loader from parser
cromoteca Nov 20, 2024
7264098
Merge branch 'main' into feat/avoid-running-maven
platosha Nov 20, 2024
1e1cd08
fix(gradle-plugin): remove classLoader
platosha Nov 20, 2024
8cb42b8
fix(maven-plugin): dependencyConvergence issue
platosha Nov 20, 2024
c8cf885
Fix number of expected params
cromoteca Nov 20, 2024
6413272
Fix some tests
cromoteca Nov 21, 2024
df20ab7
refactor!: make EngineConfiguration read-only, simplify parser/genera…
platosha Nov 22, 2024
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
41 changes: 41 additions & 0 deletions packages/java/annotations/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.vaadin</groupId>
<artifactId>hilla-project</artifactId>
<version>24.6-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>

<artifactId>hilla-annotations</artifactId>
<packaging>jar</packaging>
<name>Hilla Annotations</name>

<properties>
<formatter.basedir>${project.parent.basedir}</formatter.basedir>
</properties>

<repositories>
<repository>
<id>vaadin-prereleases</id>
<url>
https://maven.vaadin.com/vaadin-prereleases/
</url>
</repository>
</repositories>

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
* Makes the methods of the annotated class available to the browser.
* <p>
* For each class, a corresponding TypeScript class is generated in
* {@code frontend/generated} with TypeScript methods for invoking the methods
* in this class.
* {@code src/main/frontend/generated} with TypeScript methods for invoking the
* methods in this class.
* <p>
* This is an alias for {@link Endpoint}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,16 @@

/**
* Annotation to mark the endpoints to be processed by
* {@link EndpointController} class. Each class annotated automatically becomes
* {@code EndpointController} class. Each class annotated automatically becomes
* a Spring {@link Component} bean.
*
* After the class is annotated and processed, it becomes available as a Vaadin
* endpoint. This means that the class name and all its public methods can be
* executed via the post call with the correct parameters sent in a request JSON
* body. The methods' return values will be returned back as a response to the
* calls. Refer to {@link EndpointController} for more details.
* calls. Refer to {@code EndpointController} for more details.
* <p>
* This is an alias for {@link BrowserCallable}.
*
* @see EndpointController
* @see Component
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
Expand All @@ -48,7 +45,7 @@
* <p>
* Note: custom names are not allowed to be blank, be equal to any of the
* ECMAScript reserved words or have whitespaces in them. See
* {@link EndpointNameChecker} for validation implementation details.
* {@code EndpointNameChecker} for validation implementation details.
*
* @return the name of the endpoint to use in post requests
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,45 @@
package com.vaadin.hilla;

import jakarta.annotation.Nonnull;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

@Component
public class ApplicationContextProvider implements ApplicationContextAware {
private static ApplicationContext applicationContext;
private static final List<Consumer<ApplicationContext>> pendingActions = new ArrayList<>();

@Override
public void setApplicationContext(ApplicationContext applicationContext)
public void setApplicationContext(
@Nonnull ApplicationContext applicationContext)
throws BeansException {
ApplicationContextProvider.applicationContext = applicationContext;
pendingActions.forEach(action -> action.accept(applicationContext));
pendingActions.clear();
}

public static ApplicationContext getApplicationContext() {
return applicationContext;
}

// Allows to schedule actions that should be run when the application
// context is available, or to run them immediately if the context is
// already available and still active.
public static void runOnContext(Consumer<ApplicationContext> action) {
if (applicationContext == null
|| (applicationContext instanceof ConfigurableApplicationContext
&& !((ConfigurableApplicationContext) applicationContext)
.isActive())) {
pendingActions.add(action);
} else {
action.accept(applicationContext);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import com.vaadin.flow.server.frontend.FrontendUtils;
import com.vaadin.hilla.engine.EngineConfiguration;
import com.vaadin.hilla.engine.GeneratorProcessor;
import com.vaadin.hilla.engine.ParserProcessor;
Expand All @@ -46,8 +50,8 @@ public class EndpointCodeGenerator {
private Path buildDirectory;

private ApplicationConfiguration configuration;
private String nodeExecutable;
private Set<String> classesUsedInOpenApi = null;
private EngineConfiguration engineConfiguration;

/**
* Creates the singleton.
Expand All @@ -74,37 +78,37 @@ public static EndpointCodeGenerator getInstance() {
/**
* Re-generates the endpoint TypeScript and re-registers the endpoints in
* Java.
*
* @throws IOException
* if something went wrong
*/
public void update() throws IOException {
public void update() {
initIfNeeded();
if (configuration.isProductionMode()) {
throw new IllegalStateException(
"This method is not available in production mode");
}

EngineConfiguration engineConfiguration = EngineConfiguration
.loadDirectory(buildDirectory);
ParserProcessor parser = new ParserProcessor(engineConfiguration,
getClass().getClassLoader(), false);
parser.process();
GeneratorProcessor generator = new GeneratorProcessor(
engineConfiguration, nodeExecutable, false);
generator.process();

OpenAPIUtil.getCurrentOpenAPIPath(buildDirectory, false)
.ifPresent(openApiPath -> {
try {
this.endpointController
.registerEndpoints(openApiPath.toUri().toURL());
} catch (IOException e) {
LOGGER.error(
"Endpoints could not be registered due to an exception: ",
e);
}
});
ApplicationContextProvider.runOnContext(applicationContext -> {
// TODO: extract this logic as it is also used in
// TaskGenerateOpenAPIImpl
List<Class<?>> endpoints = engineConfiguration.getParser()
.getEndpointAnnotations().stream()
.map(applicationContext::getBeansWithAnnotation)
.map(Map::values).flatMap(Collection::stream)
.<Class<?>> map(Object::getClass).distinct().toList();
ParserProcessor parser = new ParserProcessor(engineConfiguration);
parser.process(endpoints);

GeneratorProcessor generator = new GeneratorProcessor(
engineConfiguration);
generator.process();

try {
var openApiPath = engineConfiguration.getOpenAPIFile();
this.endpointController
.registerEndpoints(openApiPath.toUri().toURL());
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}

private void initIfNeeded() {
Expand All @@ -115,30 +119,36 @@ private void initIfNeeded() {
buildDirectory = projectFolder
.resolve(configuration.getBuildFolder());

FrontendTools tools = new FrontendTools(configuration,
var frontendTools = new FrontendTools(configuration,
configuration.getProjectFolder());
nodeExecutable = tools.getNodeBinary();
engineConfiguration = new EngineConfiguration.Builder()
.baseDir(configuration.getProjectFolder().toPath())
.buildDir(configuration.getBuildFolder())
.outputDir(
FrontendUtils
.getFrontendGeneratedFolder(
configuration.getFrontendFolder())
.toPath())
.productionMode(false)
.nodeCommand(frontendTools.getNodeBinary()).create();
}
}

public Optional<Set<String>> getClassesUsedInOpenApi() throws IOException {
if (classesUsedInOpenApi == null) {
initIfNeeded();
OpenAPIUtil.getCurrentOpenAPIPath(buildDirectory, false)
.ifPresent(openApiPath -> {
if (openApiPath.toFile().exists()) {
try {
classesUsedInOpenApi = OpenAPIUtil
.findOpenApiClasses(
Files.readString(openApiPath));
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
LOGGER.debug(
"No OpenAPI file is available yet ...");
}
});
var conf = EngineConfiguration.getDefault();
var openApiPath = conf.getOpenAPIFile();
if (openApiPath != null && openApiPath.toFile().exists()) {
try {
classesUsedInOpenApi = OpenAPIUtil
.findOpenApiClasses(Files.readString(openApiPath));
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
LOGGER.debug("No OpenAPI file is available yet ...");
}
}
return Optional.ofNullable(classesUsedInOpenApi);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,10 @@

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URL;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -130,10 +124,6 @@ public void registerEndpoints(URL openApiResource) {
endpointBeans
.putAll(context.getBeansWithAnnotation(BrowserCallable.class));

// By default, only register those endpoints included in the Hilla
// OpenAPI definition file
registerEndpointsFromApiDefinition(endpointBeans, openApiResource);

if (endpointRegistry.isEmpty() && !endpointBeans.isEmpty()) {
LOGGER.debug("No endpoints found in openapi.json:"
+ " registering all endpoints found using the Spring context");
Expand Down Expand Up @@ -310,46 +300,6 @@ private ResponseEntity<String> buildEnforcementResponseEntity(
}
}

/**
* Parses the <code>openapi.json</code> file to discover defined endpoints.
*
* @param knownEndpointBeans
* the endpoint beans found in the Spring context
*/
private void registerEndpointsFromApiDefinition(
Map<String, Object> knownEndpointBeans, URL openApiResource) {

if (openApiResource == null) {
LOGGER.debug(
"Resource 'hilla-openapi.json' is not available: endpoints cannot be registered yet");
} else {
try (var stream = openApiResource.openStream()) {
// Read the openapi.json file and extract the tags, which in
// turn define the endpoints and their implementation classes
var rootNode = new ObjectMapper().readTree(stream);
var tagsNode = (ArrayNode) rootNode.get("tags");

if (tagsNode != null) {
// Declared endpoints are first searched as Spring Beans. If
// not found, they are, if possible, instantiated as regular
// classes using their default constructor
tagsNode.forEach(tag -> {
Optional.ofNullable(tag.get("name"))
.map(JsonNode::asText)
.map(knownEndpointBeans::get)
.or(() -> Optional
.ofNullable(tag.get("x-class-name"))
.map(JsonNode::asText)
.map(this::instantiateEndpointByClassName))
.ifPresent(endpointRegistry::registerEndpoint);
});
}
} catch (IOException e) {
LOGGER.warn("Failed to read openapi.json", e);
}
}
}

/**
* Instantiates an endpoint by its class name. Nothing special here, the
* main purpose is to allow to instantiate in a lambda expression and log
Expand Down
Loading
Loading