diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/DatasourceProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/DatasourceProcessor.java new file mode 100644 index 000000000..0f3aaf767 --- /dev/null +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/DatasourceProcessor.java @@ -0,0 +1,57 @@ +package xyz.block.ftl.deployment; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import io.quarkus.agroal.spi.JdbcDataSourceBuildItem; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.GeneratedResourceBuildItem; +import io.quarkus.deployment.builditem.SystemPropertyBuildItem; +import xyz.block.ftl.runtime.FTLDatasourceCredentials; +import xyz.block.ftl.runtime.config.FTLConfigSource; +import xyz.block.ftl.v1.schema.Database; +import xyz.block.ftl.v1.schema.Decl; + +public class DatasourceProcessor { + + @BuildStep + public SchemaContributorBuildItem registerDatasources( + List datasources, + BuildProducer systemPropProducer, + BuildProducer generatedResourceBuildItemBuildProducer) { + + List decls = new ArrayList<>(); + List namedDatasources = new ArrayList<>(); + for (var ds : datasources) { + if (!ds.getDbKind().equals("postgresql")) { + throw new RuntimeException("only postgresql is supported not " + ds.getDbKind()); + } + //default name is which is not a valid name + String sanitisedName = ds.getName().replace("<", "").replace(">", ""); + //we use a dynamic credentials provider + if (ds.isDefault()) { + systemPropProducer + .produce(new SystemPropertyBuildItem("quarkus.datasource.credentials-provider", sanitisedName)); + systemPropProducer + .produce(new SystemPropertyBuildItem("quarkus.datasource.credentials-provider-name", + FTLDatasourceCredentials.NAME)); + } else { + namedDatasources.add(ds.getName()); + systemPropProducer.produce(new SystemPropertyBuildItem( + "quarkus.datasource." + ds.getName() + ".credentials-provider", sanitisedName)); + systemPropProducer.produce(new SystemPropertyBuildItem( + "quarkus.datasource." + ds.getName() + ".credentials-provider-name", FTLDatasourceCredentials.NAME)); + } + decls.add( + Decl.newBuilder().setDatabase( + Database.newBuilder().setType("postgres").setName(sanitisedName)) + .build()); + } + generatedResourceBuildItemBuildProducer.produce(new GeneratedResourceBuildItem(FTLConfigSource.DATASOURCE_NAMES, + String.join("\n", namedDatasources).getBytes(StandardCharsets.UTF_8))); + return new SchemaContributorBuildItem(decls); + + } +} diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java new file mode 100644 index 000000000..d469abb59 --- /dev/null +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FTLDotNames.java @@ -0,0 +1,26 @@ +package xyz.block.ftl.deployment; + +import org.jboss.jandex.DotName; + +import xyz.block.ftl.Config; +import xyz.block.ftl.Cron; +import xyz.block.ftl.Export; +import xyz.block.ftl.LeaseClient; +import xyz.block.ftl.Secret; +import xyz.block.ftl.Subscription; +import xyz.block.ftl.Verb; + +public class FTLDotNames { + + private FTLDotNames() { + + } + + public static final DotName SECRET = DotName.createSimple(Secret.class); + public static final DotName CONFIG = DotName.createSimple(Config.class); + public static final DotName EXPORT = DotName.createSimple(Export.class); + public static final DotName VERB = DotName.createSimple(Verb.class); + public static final DotName CRON = DotName.createSimple(Cron.class); + public static final DotName SUBSCRIPTION = DotName.createSimple(Subscription.class); + public static final DotName LEASE_CLIENT = DotName.createSimple(LeaseClient.class); +} diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FtlProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FtlProcessor.java deleted file mode 100644 index 23524b7fc..000000000 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/FtlProcessor.java +++ /dev/null @@ -1,646 +0,0 @@ -package xyz.block.ftl.deployment; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.attribute.PosixFilePermission; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -import org.jboss.jandex.AnnotationInstance; -import org.jboss.jandex.AnnotationTarget; -import org.jboss.jandex.ArrayType; -import org.jboss.jandex.DotName; -import org.jboss.jandex.MethodInfo; -import org.jboss.jandex.PrimitiveType; -import org.jboss.jandex.VoidType; -import org.jboss.logging.Logger; -import org.jboss.resteasy.reactive.common.model.MethodParameter; -import org.jboss.resteasy.reactive.common.model.ParameterType; -import org.jboss.resteasy.reactive.server.core.parameters.ParameterExtractor; -import org.jboss.resteasy.reactive.server.mapping.URITemplate; -import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner; -import org.jetbrains.annotations.NotNull; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import io.quarkus.agroal.spi.JdbcDataSourceBuildItem; -import io.quarkus.arc.deployment.AdditionalBeanBuildItem; -import io.quarkus.deployment.Capabilities; -import io.quarkus.deployment.Capability; -import io.quarkus.deployment.annotations.BuildProducer; -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.annotations.ExecutionTime; -import io.quarkus.deployment.annotations.Record; -import io.quarkus.deployment.builditem.ApplicationInfoBuildItem; -import io.quarkus.deployment.builditem.ApplicationStartBuildItem; -import io.quarkus.deployment.builditem.CombinedIndexBuildItem; -import io.quarkus.deployment.builditem.GeneratedResourceBuildItem; -import io.quarkus.deployment.builditem.LaunchModeBuildItem; -import io.quarkus.deployment.builditem.ShutdownContextBuildItem; -import io.quarkus.deployment.builditem.SystemPropertyBuildItem; -import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; -import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; -import io.quarkus.grpc.deployment.BindableServiceBuildItem; -import io.quarkus.netty.runtime.virtual.VirtualServerChannel; -import io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveResourceMethodEntriesBuildItem; -import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem; -import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; -import io.quarkus.vertx.core.deployment.EventLoopCountBuildItem; -import io.quarkus.vertx.http.deployment.RequireVirtualHttpBuildItem; -import io.quarkus.vertx.http.deployment.WebsocketSubProtocolsBuildItem; -import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; -import io.quarkus.vertx.http.runtime.VertxHttpRecorder; -import xyz.block.ftl.Config; -import xyz.block.ftl.Cron; -import xyz.block.ftl.Export; -import xyz.block.ftl.LeaseClient; -import xyz.block.ftl.Retry; -import xyz.block.ftl.Secret; -import xyz.block.ftl.Subscription; -import xyz.block.ftl.Verb; -import xyz.block.ftl.VerbName; -import xyz.block.ftl.runtime.FTLDatasourceCredentials; -import xyz.block.ftl.runtime.FTLRecorder; -import xyz.block.ftl.runtime.JsonSerializationConfig; -import xyz.block.ftl.runtime.TopicHelper; -import xyz.block.ftl.runtime.VerbClientHelper; -import xyz.block.ftl.runtime.VerbHandler; -import xyz.block.ftl.runtime.VerbRegistry; -import xyz.block.ftl.runtime.builtin.HttpRequest; -import xyz.block.ftl.runtime.builtin.HttpResponse; -import xyz.block.ftl.runtime.config.FTLConfigSource; -import xyz.block.ftl.runtime.http.FTLHttpHandler; -import xyz.block.ftl.v1.CallRequest; -import xyz.block.ftl.v1.schema.Array; -import xyz.block.ftl.v1.schema.Database; -import xyz.block.ftl.v1.schema.Decl; -import xyz.block.ftl.v1.schema.Float; -import xyz.block.ftl.v1.schema.IngressPathComponent; -import xyz.block.ftl.v1.schema.IngressPathLiteral; -import xyz.block.ftl.v1.schema.IngressPathParameter; -import xyz.block.ftl.v1.schema.Metadata; -import xyz.block.ftl.v1.schema.MetadataCalls; -import xyz.block.ftl.v1.schema.MetadataCronJob; -import xyz.block.ftl.v1.schema.MetadataIngress; -import xyz.block.ftl.v1.schema.MetadataRetry; -import xyz.block.ftl.v1.schema.MetadataSubscriber; -import xyz.block.ftl.v1.schema.Optional; -import xyz.block.ftl.v1.schema.Ref; -import xyz.block.ftl.v1.schema.Type; -import xyz.block.ftl.v1.schema.Unit; - -class FtlProcessor { - - private static final Logger log = Logger.getLogger(FtlProcessor.class); - - private static final String SCHEMA_OUT = "schema.pb"; - - public static final String BUILTIN = "builtin"; - public static final DotName EXPORT = DotName.createSimple(Export.class); - public static final DotName VERB = DotName.createSimple(Verb.class); - public static final DotName CRON = DotName.createSimple(Cron.class); - public static final DotName SUBSCRIPTION = DotName.createSimple(Subscription.class); - public static final DotName SECRET = DotName.createSimple(Secret.class); - public static final DotName CONFIG = DotName.createSimple(Config.class); - public static final DotName LEASE_CLIENT = DotName.createSimple(LeaseClient.class); - - @BuildStep - BindableServiceBuildItem verbService() { - var ret = new BindableServiceBuildItem(DotName.createSimple(VerbHandler.class)); - ret.registerBlockingMethod("call"); - ret.registerBlockingMethod("publishEvent"); - ret.registerBlockingMethod("sendFSMEvent"); - ret.registerBlockingMethod("acquireLease"); - ret.registerBlockingMethod("getModuleContext"); - ret.registerBlockingMethod("ping"); - return ret; - } - - @BuildStep - AdditionalBeanBuildItem beans() { - return AdditionalBeanBuildItem.builder() - .addBeanClasses(VerbHandler.class, - VerbRegistry.class, FTLHttpHandler.class, - TopicHelper.class, VerbClientHelper.class, JsonSerializationConfig.class, - FTLDatasourceCredentials.class) - .setUnremovable().build(); - } - - @BuildStep - AdditionalBeanBuildItem verbBeans(CombinedIndexBuildItem index) { - - var beans = AdditionalBeanBuildItem.builder().setUnremovable(); - for (var verb : index.getIndex().getAnnotations(VERB)) { - beans.addBeanClasses(verb.target().asMethod().declaringClass().name().toString()); - } - return beans.build(); - } - - @BuildStep - public SystemPropertyBuildItem moduleNameConfig(ApplicationInfoBuildItem applicationInfoBuildItem) { - return new SystemPropertyBuildItem("ftl.module.name", applicationInfoBuildItem.getName()); - } - - @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) - public MethodScannerBuildItem methodScanners(TopicsBuildItem topics, - VerbClientBuildItem verbClients, FTLRecorder recorder) { - return new MethodScannerBuildItem(new MethodScanner() { - @Override - public ParameterExtractor handleCustomParameter(org.jboss.jandex.Type type, - Map annotations, boolean field, Map methodContext) { - try { - - if (annotations.containsKey(SECRET)) { - Class paramType = loadClass(type); - String name = annotations.get(SECRET).value().asString(); - return new VerbRegistry.SecretSupplier(name, paramType); - } else if (annotations.containsKey(CONFIG)) { - Class paramType = loadClass(type); - String name = annotations.get(CONFIG).value().asString(); - return new VerbRegistry.ConfigSupplier(name, paramType); - } else if (topics.getTopics().containsKey(type.name())) { - var topic = topics.getTopics().get(type.name()); - return recorder.topicParamExtractor(topic.generatedProducer()); - } else if (verbClients.getVerbClients().containsKey(type.name())) { - var client = verbClients.getVerbClients().get(type.name()); - return recorder.verbParamExtractor(client.generatedClient()); - } else if (LEASE_CLIENT.equals(type.name())) { - return recorder.leaseClientExtractor(); - } - return null; - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } - } - }); - } - - @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) - public void registerVerbs(CombinedIndexBuildItem index, - FTLRecorder recorder, - OutputTargetBuildItem outputTargetBuildItem, - ResteasyReactiveResourceMethodEntriesBuildItem restEndpoints, - TopicsBuildItem topics, - VerbClientBuildItem verbClients, - ModuleNameBuildItem moduleNameBuildItem, - SubscriptionMetaAnnotationsBuildItem subscriptionMetaAnnotationsBuildItem, - List datasources, - BuildProducer systemPropProducer, - BuildProducer generatedResourceBuildItemBuildProducer) throws Exception { - String moduleName = moduleNameBuildItem.getModuleName(); - - SchemaBuilder schemaBuilder = new SchemaBuilder(index.getComputingIndex(), moduleName); - - ExtractionContext extractionContext = new ExtractionContext(moduleName, index, recorder, schemaBuilder, - new HashSet<>(), new HashSet<>(), topics.getTopics(), verbClients.getVerbClients()); - var beans = AdditionalBeanBuildItem.builder().setUnremovable(); - - List namedDatasources = new ArrayList<>(); - for (var ds : datasources) { - if (!ds.getDbKind().equals("postgresql")) { - throw new RuntimeException("only postgresql is supported not " + ds.getDbKind()); - } - //default name is which is not a valid name - String sanitisedName = ds.getName().replace("<", "").replace(">", ""); - //we use a dynamic credentials provider - if (ds.isDefault()) { - systemPropProducer - .produce(new SystemPropertyBuildItem("quarkus.datasource.credentials-provider", sanitisedName)); - systemPropProducer - .produce(new SystemPropertyBuildItem("quarkus.datasource.credentials-provider-name", - FTLDatasourceCredentials.NAME)); - } else { - namedDatasources.add(ds.getName()); - systemPropProducer.produce(new SystemPropertyBuildItem( - "quarkus.datasource." + ds.getName() + ".credentials-provider", sanitisedName)); - systemPropProducer.produce(new SystemPropertyBuildItem( - "quarkus.datasource." + ds.getName() + ".credentials-provider-name", FTLDatasourceCredentials.NAME)); - } - schemaBuilder.addDecls( - Decl.newBuilder().setDatabase( - Database.newBuilder().setType("postgres").setName(sanitisedName)) - .build()); - } - generatedResourceBuildItemBuildProducer.produce(new GeneratedResourceBuildItem(FTLConfigSource.DATASOURCE_NAMES, - String.join("\n", namedDatasources).getBytes(StandardCharsets.UTF_8))); - - //register all the topics we are defining in the module definition - for (var topic : topics.getTopics().values()) { - extractionContext.schemaBuilder.addDecls(Decl.newBuilder().setTopic(xyz.block.ftl.v1.schema.Topic.newBuilder() - .setExport(topic.exported()) - .setName(topic.topicName()) - .setEvent(schemaBuilder.buildType(topic.eventType(), topic.exported())).build()).build()); - } - - handleVerbAnnotations(index, beans, extractionContext); - handleCronAnnotations(index, beans, extractionContext); - handleSubscriptionAnnotations(index, subscriptionMetaAnnotationsBuildItem, moduleName, extractionContext, - beans); - - //TODO: make this composable so it is not just one big method, build items should contribute to the schema - for (var endpoint : restEndpoints.getEntries()) { - //TODO: naming - var verbName = methodToName(endpoint.getMethodInfo()); - boolean base64 = false; - - //TODO: handle type parameters properly - org.jboss.jandex.Type bodyParamType = VoidType.VOID; - MethodParameter[] parameters = endpoint.getResourceMethod().getParameters(); - for (int i = 0, parametersLength = parameters.length; i < parametersLength; i++) { - var param = parameters[i]; - if (param.parameterType.equals(ParameterType.BODY)) { - bodyParamType = endpoint.getMethodInfo().parameterType(i); - break; - } - } - - if (bodyParamType instanceof ArrayType) { - org.jboss.jandex.Type component = ((ArrayType) bodyParamType).component(); - if (component instanceof PrimitiveType) { - base64 = component.asPrimitiveType().equals(PrimitiveType.BYTE); - } - } - - recorder.registerHttpIngress(moduleName, verbName, base64); - - StringBuilder pathBuilder = new StringBuilder(); - if (endpoint.getBasicResourceClassInfo().getPath() != null) { - pathBuilder.append(endpoint.getBasicResourceClassInfo().getPath()); - } - if (endpoint.getResourceMethod().getPath() != null && !endpoint.getResourceMethod().getPath().isEmpty()) { - boolean builderEndsSlash = pathBuilder.charAt(pathBuilder.length() - 1) == '/'; - boolean pathStartsSlash = endpoint.getResourceMethod().getPath().startsWith("/"); - if (builderEndsSlash && pathStartsSlash) { - pathBuilder.setLength(pathBuilder.length() - 1); - } else if (!builderEndsSlash && !pathStartsSlash) { - pathBuilder.append('/'); - } - pathBuilder.append(endpoint.getResourceMethod().getPath()); - } - String path = pathBuilder.toString(); - URITemplate template = new URITemplate(path, false); - List pathComponents = new ArrayList<>(); - for (var i : template.components) { - if (i.type == URITemplate.Type.CUSTOM_REGEX) { - throw new RuntimeException( - "Invalid path " + path + " on HTTP endpoint: " + endpoint.getActualClassInfo().name() + "." - + methodToName(endpoint.getMethodInfo()) - + " FTL does not support custom regular expressions"); - } else if (i.type == URITemplate.Type.LITERAL) { - for (var part : i.literalText.split("/")) { - if (part.isEmpty()) { - continue; - } - pathComponents.add(IngressPathComponent.newBuilder() - .setIngressPathLiteral(IngressPathLiteral.newBuilder().setText(part)) - .build()); - } - } else { - pathComponents.add(IngressPathComponent.newBuilder() - .setIngressPathParameter(IngressPathParameter.newBuilder().setName(i.name)) - .build()); - } - } - - //TODO: process path properly - MetadataIngress.Builder ingressBuilder = MetadataIngress.newBuilder() - .setType("http") - .setMethod(endpoint.getResourceMethod().getHttpMethod()); - for (var i : pathComponents) { - ingressBuilder.addPath(i); - } - Metadata ingressMetadata = Metadata.newBuilder() - .setIngress(ingressBuilder - .build()) - .build(); - Type requestTypeParam = schemaBuilder.buildType(bodyParamType, true); - Type responseTypeParam = schemaBuilder.buildType(endpoint.getMethodInfo().returnType(), true); - Type stringType = Type.newBuilder().setString(xyz.block.ftl.v1.schema.String.newBuilder().build()).build(); - Type pathParamType = Type.newBuilder() - .setMap(xyz.block.ftl.v1.schema.Map.newBuilder().setKey(stringType) - .setValue(stringType)) - .build(); - schemaBuilder - .addDecls(Decl.newBuilder().setVerb(xyz.block.ftl.v1.schema.Verb.newBuilder() - .addMetadata(ingressMetadata) - .setName(verbName) - .setExport(true) - .setRequest(Type.newBuilder() - .setRef(Ref.newBuilder().setModule(BUILTIN).setName(HttpRequest.class.getSimpleName()) - .addTypeParameters(requestTypeParam) - .addTypeParameters(pathParamType) - .addTypeParameters(Type.newBuilder() - .setMap(xyz.block.ftl.v1.schema.Map.newBuilder().setKey(stringType) - .setValue(Type.newBuilder() - .setArray(Array.newBuilder().setElement(stringType))) - .build()))) - .build()) - .setResponse(Type.newBuilder() - .setRef(Ref.newBuilder().setModule(BUILTIN).setName(HttpResponse.class.getSimpleName()) - .addTypeParameters(responseTypeParam) - .addTypeParameters(Type.newBuilder().setUnit(Unit.newBuilder()))) - .build())) - .build()); - } - - Path output = outputTargetBuildItem.getOutputDirectory().resolve(SCHEMA_OUT); - try (var out = Files.newOutputStream(output)) { - schemaBuilder.writeTo(out); - } - - output = outputTargetBuildItem.getOutputDirectory().resolve("main"); - try (var out = Files.newOutputStream(output)) { - out.write( - """ - #!/bin/bash - exec java $FTL_JVM_OPTS -jar quarkus-app/quarkus-run.jar""" - .getBytes(StandardCharsets.UTF_8)); - } - var perms = Files.getPosixFilePermissions(output); - EnumSet newPerms = EnumSet.copyOf(perms); - newPerms.add(PosixFilePermission.GROUP_EXECUTE); - newPerms.add(PosixFilePermission.OWNER_EXECUTE); - Files.setPosixFilePermissions(output, newPerms); - } - - private void handleVerbAnnotations(CombinedIndexBuildItem index, AdditionalBeanBuildItem.Builder beans, - ExtractionContext extractionContext) { - for (var verb : index.getIndex().getAnnotations(VERB)) { - boolean exported = verb.target().hasAnnotation(EXPORT); - var method = verb.target().asMethod(); - String className = method.declaringClass().name().toString(); - beans.addBeanClass(className); - - handleVerbMethod(extractionContext, method, className, exported, BodyType.ALLOWED, null); - } - } - - private void handleSubscriptionAnnotations(CombinedIndexBuildItem index, - SubscriptionMetaAnnotationsBuildItem subscriptionMetaAnnotationsBuildItem, String moduleName, - ExtractionContext extractionContext, AdditionalBeanBuildItem.Builder beans) { - for (var subscription : index.getIndex().getAnnotations(SUBSCRIPTION)) { - var info = SubscriptionMetaAnnotationsBuildItem.fromJandex(subscription, moduleName); - if (subscription.target().kind() != AnnotationTarget.Kind.METHOD) { - continue; - } - var method = subscription.target().asMethod(); - String className = method.declaringClass().name().toString(); - generateSubscription(extractionContext, beans, method, className, info); - } - for (var metaSub : subscriptionMetaAnnotationsBuildItem.getAnnotations().entrySet()) { - for (var subscription : index.getIndex().getAnnotations(metaSub.getKey())) { - if (subscription.target().kind() != AnnotationTarget.Kind.METHOD) { - log.warnf("Subscription annotation on non-method target: %s", subscription.target()); - continue; - } - var method = subscription.target().asMethod(); - generateSubscription(extractionContext, beans, method, - method.declaringClass().name().toString(), - metaSub.getValue()); - } - - } - } - - private void handleCronAnnotations(CombinedIndexBuildItem index, AdditionalBeanBuildItem.Builder beans, - ExtractionContext extractionContext) { - for (var cron : index.getIndex().getAnnotations(CRON)) { - var method = cron.target().asMethod(); - String className = method.declaringClass().name().toString(); - beans.addBeanClass(className); - handleVerbMethod(extractionContext, method, className, false, BodyType.DISALLOWED, (builder -> { - builder.addMetadata(Metadata.newBuilder() - .setCronJob(MetadataCronJob.newBuilder().setCron(cron.value().asString())).build()); - })); - } - } - - private void generateSubscription(ExtractionContext extractionContext, - AdditionalBeanBuildItem.Builder beans, MethodInfo method, String className, - SubscriptionMetaAnnotationsBuildItem.SubscriptionAnnotation info) { - beans.addBeanClass(className); - extractionContext.schemaBuilder.addDecls(Decl.newBuilder() - .setSubscription(xyz.block.ftl.v1.schema.Subscription.newBuilder() - .setName(info.name()).setTopic(Ref.newBuilder().setName(info.topic()).setModule(info.module()).build())) - .build()); - handleVerbMethod(extractionContext, method, className, false, BodyType.REQUIRED, (builder -> { - builder.addMetadata(Metadata.newBuilder().setSubscriber(MetadataSubscriber.newBuilder().setName(info.name()))); - if (method.hasAnnotation(Retry.class)) { - RetryRecord retry = RetryRecord.fromJandex(method.annotation(Retry.class), extractionContext.moduleName); - - MetadataRetry.Builder retryBuilder = MetadataRetry.newBuilder(); - if (!retry.catchVerb().isEmpty()) { - retryBuilder.setCatch(Ref.newBuilder().setModule(retry.catchModule()) - .setName(retry.catchVerb()).build()); - } - retryBuilder.setCount(retry.count()) - .setMaxBackoff(retry.maxBackoff()) - .setMinBackoff(retry.minBackoff()); - builder.addMetadata(Metadata.newBuilder().setRetry(retryBuilder).build()); - } - })); - } - - private void handleVerbMethod(ExtractionContext context, MethodInfo method, String className, - boolean exported, BodyType bodyType, Consumer metadataCallback) { - try { - List> parameterTypes = new ArrayList<>(); - List> paramMappers = new ArrayList<>(); - org.jboss.jandex.Type bodyParamType = null; - xyz.block.ftl.v1.schema.Verb.Builder verbBuilder = xyz.block.ftl.v1.schema.Verb.newBuilder(); - String verbName = methodToName(method); - MetadataCalls.Builder callsMetadata = MetadataCalls.newBuilder(); - for (var param : method.parameters()) { - if (param.hasAnnotation(Secret.class)) { - Class paramType = loadClass(param.type()); - parameterTypes.add(paramType); - String name = param.annotation(Secret.class).value().asString(); - paramMappers.add(new VerbRegistry.SecretSupplier(name, paramType)); - if (!context.knownSecrets.contains(name)) { - context.schemaBuilder.addDecls(Decl.newBuilder().setSecret(xyz.block.ftl.v1.schema.Secret.newBuilder() - .setType(context.schemaBuilder.buildType(param.type(), false)).setName(name)).build()); - context.knownSecrets.add(name); - } - } else if (param.hasAnnotation(Config.class)) { - Class paramType = loadClass(param.type()); - parameterTypes.add(paramType); - String name = param.annotation(Config.class).value().asString(); - paramMappers.add(new VerbRegistry.ConfigSupplier(name, paramType)); - if (!context.knownConfig.contains(name)) { - context.schemaBuilder.addDecls(Decl.newBuilder().setConfig(xyz.block.ftl.v1.schema.Config.newBuilder() - .setType(context.schemaBuilder.buildType(param.type(), false)).setName(name)).build()); - context.knownConfig.add(name); - } - } else if (context.knownTopics.containsKey(param.type().name())) { - var topic = context.knownTopics.get(param.type().name()); - Class paramType = loadClass(param.type()); - parameterTypes.add(paramType); - paramMappers.add(context.recorder().topicSupplier(topic.generatedProducer(), verbName)); - } else if (context.verbClients.containsKey(param.type().name())) { - var client = context.verbClients.get(param.type().name()); - Class paramType = loadClass(param.type()); - parameterTypes.add(paramType); - paramMappers.add(context.recorder().verbClientSupplier(client.generatedClient())); - callsMetadata.addCalls(Ref.newBuilder().setName(client.name()).setModule(client.module()).build()); - } else if (LEASE_CLIENT.equals(param.type().name())) { - parameterTypes.add(LeaseClient.class); - paramMappers.add(context.recorder().leaseClientSupplier()); - } else if (bodyType != BodyType.DISALLOWED && bodyParamType == null) { - bodyParamType = param.type(); - Class paramType = loadClass(param.type()); - parameterTypes.add(paramType); - //TODO: map and list types - paramMappers.add(new VerbRegistry.BodySupplier(paramType)); - } else { - throw new RuntimeException("Unknown parameter type " + param.type() + " on FTL method: " - + method.declaringClass().name() + "." + method.name()); - } - } - if (bodyParamType == null) { - if (bodyType == BodyType.REQUIRED) { - throw new RuntimeException("Missing required payload parameter"); - } - bodyParamType = VoidType.VOID; - } - if (callsMetadata.getCallsCount() > 0) { - verbBuilder.addMetadata(Metadata.newBuilder().setCalls(callsMetadata)); - } - - //TODO: we need better handling around Optional - context.recorder.registerVerb(context.moduleName(), verbName, method.name(), parameterTypes, - Class.forName(className, false, Thread.currentThread().getContextClassLoader()), paramMappers, - method.returnType() == VoidType.VOID); - verbBuilder - .setName(verbName) - .setExport(exported) - .setRequest(context.schemaBuilder.buildType(bodyParamType, exported)) - .setResponse(context.schemaBuilder.buildType(method.returnType(), exported)); - - if (metadataCallback != null) { - metadataCallback.accept(verbBuilder); - } - context.schemaBuilder - .addDecls(Decl.newBuilder().setVerb(verbBuilder) - .build()); - - } catch (Exception e) { - throw new RuntimeException("Failed to process FTL method " + method.declaringClass().name() + "." + method.name(), - e); - } - } - - private static @NotNull String methodToName(MethodInfo method) { - if (method.hasAnnotation(VerbName.class)) { - return method.annotation(VerbName.class).value().asString(); - } - return method.name(); - } - - private static Class loadClass(org.jboss.jandex.Type param) throws ClassNotFoundException { - if (param.kind() == org.jboss.jandex.Type.Kind.PARAMETERIZED_TYPE) { - return Class.forName(param.asParameterizedType().name().toString(), false, - Thread.currentThread().getContextClassLoader()); - } else if (param.kind() == org.jboss.jandex.Type.Kind.CLASS) { - return Class.forName(param.name().toString(), false, Thread.currentThread().getContextClassLoader()); - } else if (param.kind() == org.jboss.jandex.Type.Kind.PRIMITIVE) { - switch (param.asPrimitiveType().primitive()) { - case BOOLEAN: - return Boolean.TYPE; - case BYTE: - return Byte.TYPE; - case SHORT: - return Short.TYPE; - case INT: - return Integer.TYPE; - case LONG: - return Long.TYPE; - case FLOAT: - return java.lang.Float.TYPE; - case DOUBLE: - return java.lang.Double.TYPE; - case CHAR: - return Character.TYPE; - default: - throw new RuntimeException("Unknown primitive type " + param.asPrimitiveType().primitive()); - } - } else if (param.kind() == org.jboss.jandex.Type.Kind.ARRAY) { - ArrayType array = param.asArrayType(); - if (array.componentType().kind() == org.jboss.jandex.Type.Kind.PRIMITIVE) { - switch (array.componentType().asPrimitiveType().primitive()) { - case BOOLEAN: - return boolean[].class; - case BYTE: - return byte[].class; - case SHORT: - return short[].class; - case INT: - return int[].class; - case LONG: - return long[].class; - case FLOAT: - return float[].class; - case DOUBLE: - return double[].class; - case CHAR: - return char[].class; - default: - throw new RuntimeException("Unknown primitive type " + param.asPrimitiveType().primitive()); - } - } - } - throw new RuntimeException("Unknown type " + param.kind()); - - } - - /** - * This is a huge hack that is needed until Quarkus supports both virtual and socket based HTTP - */ - @Record(ExecutionTime.RUNTIME_INIT) - @BuildStep - void openSocket(ApplicationStartBuildItem start, - LaunchModeBuildItem launchMode, - CoreVertxBuildItem vertx, - ShutdownContextBuildItem shutdown, - BuildProducer reflectiveClass, - HttpBuildTimeConfig httpBuildTimeConfig, - java.util.Optional requireVirtual, - EventLoopCountBuildItem eventLoopCount, - List websocketSubProtocols, - Capabilities capabilities, - VertxHttpRecorder recorder) throws IOException { - reflectiveClass - .produce(ReflectiveClassBuildItem.builder(VirtualServerChannel.class) - .build()); - recorder.startServer(vertx.getVertx(), shutdown, - launchMode.getLaunchMode(), true, false, - eventLoopCount.getEventLoopCount(), - websocketSubProtocols.stream().map(bi -> bi.getWebsocketSubProtocols()) - .collect(Collectors.toList()), - launchMode.isAuxiliaryApplication(), !capabilities.isPresent(Capability.VERTX_WEBSOCKETS)); - } - - record ExtractionContext(String moduleName, CombinedIndexBuildItem index, FTLRecorder recorder, - SchemaBuilder schemaBuilder, - Set knownSecrets, Set knownConfig, - Map knownTopics, - Map verbClients) { - } - - enum BodyType { - DISALLOWED, - ALLOWED, - REQUIRED - } - -} diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/HTTPProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/HTTPProcessor.java new file mode 100644 index 000000000..c53448470 --- /dev/null +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/HTTPProcessor.java @@ -0,0 +1,196 @@ +package xyz.block.ftl.deployment; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ArrayType; +import org.jboss.jandex.DotName; +import org.jboss.jandex.PrimitiveType; +import org.jboss.jandex.VoidType; +import org.jboss.resteasy.reactive.common.model.MethodParameter; +import org.jboss.resteasy.reactive.common.model.ParameterType; +import org.jboss.resteasy.reactive.server.core.parameters.ParameterExtractor; +import org.jboss.resteasy.reactive.server.mapping.URITemplate; +import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveResourceMethodEntriesBuildItem; +import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem; +import xyz.block.ftl.runtime.FTLRecorder; +import xyz.block.ftl.runtime.VerbRegistry; +import xyz.block.ftl.runtime.builtin.HttpRequest; +import xyz.block.ftl.runtime.builtin.HttpResponse; +import xyz.block.ftl.v1.schema.Array; +import xyz.block.ftl.v1.schema.Decl; +import xyz.block.ftl.v1.schema.IngressPathComponent; +import xyz.block.ftl.v1.schema.IngressPathLiteral; +import xyz.block.ftl.v1.schema.IngressPathParameter; +import xyz.block.ftl.v1.schema.Metadata; +import xyz.block.ftl.v1.schema.MetadataIngress; +import xyz.block.ftl.v1.schema.Ref; +import xyz.block.ftl.v1.schema.Type; +import xyz.block.ftl.v1.schema.Unit; + +public class HTTPProcessor { + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public MethodScannerBuildItem methodScanners(TopicsBuildItem topics, + VerbClientBuildItem verbClients, FTLRecorder recorder) { + return new MethodScannerBuildItem(new MethodScanner() { + @Override + public ParameterExtractor handleCustomParameter(org.jboss.jandex.Type type, + Map annotations, boolean field, Map methodContext) { + try { + + if (annotations.containsKey(FTLDotNames.SECRET)) { + Class paramType = ModuleBuilder.loadClass(type); + String name = annotations.get(FTLDotNames.SECRET).value().asString(); + return new VerbRegistry.SecretSupplier(name, paramType); + } else if (annotations.containsKey(FTLDotNames.CONFIG)) { + Class paramType = ModuleBuilder.loadClass(type); + String name = annotations.get(FTLDotNames.CONFIG).value().asString(); + return new VerbRegistry.ConfigSupplier(name, paramType); + } else if (topics.getTopics().containsKey(type.name())) { + var topic = topics.getTopics().get(type.name()); + return recorder.topicParamExtractor(topic.generatedProducer()); + } else if (verbClients.getVerbClients().containsKey(type.name())) { + var client = verbClients.getVerbClients().get(type.name()); + return recorder.verbParamExtractor(client.generatedClient()); + } else if (FTLDotNames.LEASE_CLIENT.equals(type.name())) { + return recorder.leaseClientExtractor(); + } + return null; + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + }); + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public SchemaContributorBuildItem registerHttpHandlers( + FTLRecorder recorder, + ResteasyReactiveResourceMethodEntriesBuildItem restEndpoints) { + return new SchemaContributorBuildItem(new Consumer() { + @Override + public void accept(ModuleBuilder moduleBuilder) { + //TODO: make this composable so it is not just one big method, build items should contribute to the schema + for (var endpoint : restEndpoints.getEntries()) { + //TODO: naming + var verbName = ModuleBuilder.methodToName(endpoint.getMethodInfo()); + boolean base64 = false; + + //TODO: handle type parameters properly + org.jboss.jandex.Type bodyParamType = VoidType.VOID; + MethodParameter[] parameters = endpoint.getResourceMethod().getParameters(); + for (int i = 0, parametersLength = parameters.length; i < parametersLength; i++) { + var param = parameters[i]; + if (param.parameterType.equals(ParameterType.BODY)) { + bodyParamType = endpoint.getMethodInfo().parameterType(i); + break; + } + } + + if (bodyParamType instanceof ArrayType) { + org.jboss.jandex.Type component = ((ArrayType) bodyParamType).component(); + if (component instanceof PrimitiveType) { + base64 = component.asPrimitiveType().equals(PrimitiveType.BYTE); + } + } + + recorder.registerHttpIngress(moduleBuilder.getModuleName(), verbName, base64); + + StringBuilder pathBuilder = new StringBuilder(); + if (endpoint.getBasicResourceClassInfo().getPath() != null) { + pathBuilder.append(endpoint.getBasicResourceClassInfo().getPath()); + } + if (endpoint.getResourceMethod().getPath() != null && !endpoint.getResourceMethod().getPath().isEmpty()) { + boolean builderEndsSlash = pathBuilder.charAt(pathBuilder.length() - 1) == '/'; + boolean pathStartsSlash = endpoint.getResourceMethod().getPath().startsWith("/"); + if (builderEndsSlash && pathStartsSlash) { + pathBuilder.setLength(pathBuilder.length() - 1); + } else if (!builderEndsSlash && !pathStartsSlash) { + pathBuilder.append('/'); + } + pathBuilder.append(endpoint.getResourceMethod().getPath()); + } + String path = pathBuilder.toString(); + URITemplate template = new URITemplate(path, false); + List pathComponents = new ArrayList<>(); + for (var i : template.components) { + if (i.type == URITemplate.Type.CUSTOM_REGEX) { + throw new RuntimeException( + "Invalid path " + path + " on HTTP endpoint: " + endpoint.getActualClassInfo().name() + "." + + ModuleBuilder.methodToName(endpoint.getMethodInfo()) + + " FTL does not support custom regular expressions"); + } else if (i.type == URITemplate.Type.LITERAL) { + for (var part : i.literalText.split("/")) { + if (part.isEmpty()) { + continue; + } + pathComponents.add(IngressPathComponent.newBuilder() + .setIngressPathLiteral(IngressPathLiteral.newBuilder().setText(part)) + .build()); + } + } else { + pathComponents.add(IngressPathComponent.newBuilder() + .setIngressPathParameter(IngressPathParameter.newBuilder().setName(i.name)) + .build()); + } + } + + //TODO: process path properly + MetadataIngress.Builder ingressBuilder = MetadataIngress.newBuilder() + .setType("http") + .setMethod(endpoint.getResourceMethod().getHttpMethod()); + for (var i : pathComponents) { + ingressBuilder.addPath(i); + } + Metadata ingressMetadata = Metadata.newBuilder() + .setIngress(ingressBuilder + .build()) + .build(); + Type requestTypeParam = moduleBuilder.buildType(bodyParamType, true); + Type responseTypeParam = moduleBuilder.buildType(endpoint.getMethodInfo().returnType(), true); + Type stringType = Type.newBuilder().setString(xyz.block.ftl.v1.schema.String.newBuilder().build()).build(); + Type pathParamType = Type.newBuilder() + .setMap(xyz.block.ftl.v1.schema.Map.newBuilder().setKey(stringType) + .setValue(stringType)) + .build(); + moduleBuilder + .addDecls(Decl.newBuilder().setVerb(xyz.block.ftl.v1.schema.Verb.newBuilder() + .addMetadata(ingressMetadata) + .setName(verbName) + .setExport(true) + .setRequest(Type.newBuilder() + .setRef(Ref.newBuilder().setModule(ModuleBuilder.BUILTIN) + .setName(HttpRequest.class.getSimpleName()) + .addTypeParameters(requestTypeParam) + .addTypeParameters(pathParamType) + .addTypeParameters(Type.newBuilder() + .setMap(xyz.block.ftl.v1.schema.Map.newBuilder().setKey(stringType) + .setValue(Type.newBuilder() + .setArray( + Array.newBuilder().setElement(stringType))) + .build()))) + .build()) + .setResponse(Type.newBuilder() + .setRef(Ref.newBuilder().setModule(ModuleBuilder.BUILTIN) + .setName(HttpResponse.class.getSimpleName()) + .addTypeParameters(responseTypeParam) + .addTypeParameters(Type.newBuilder().setUnit(Unit.newBuilder()))) + .build())) + .build()); + } + } + }); + + } +} diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SchemaBuilder.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java similarity index 55% rename from jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SchemaBuilder.java rename to jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java index 9c36b4b7c..0af0647e3 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SchemaBuilder.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleBuilder.java @@ -6,26 +6,41 @@ import java.time.Instant; import java.time.OffsetDateTime; import java.time.ZonedDateTime; +import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Consumer; import org.jboss.jandex.ArrayType; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; import org.jboss.jandex.PrimitiveType; +import org.jboss.jandex.VoidType; import org.jetbrains.annotations.NotNull; import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import io.quarkus.arc.processor.DotNames; +import xyz.block.ftl.Config; import xyz.block.ftl.Export; import xyz.block.ftl.GeneratedRef; +import xyz.block.ftl.LeaseClient; +import xyz.block.ftl.Secret; +import xyz.block.ftl.VerbName; +import xyz.block.ftl.runtime.FTLRecorder; +import xyz.block.ftl.runtime.VerbRegistry; import xyz.block.ftl.runtime.builtin.HttpRequest; import xyz.block.ftl.runtime.builtin.HttpResponse; +import xyz.block.ftl.v1.CallRequest; import xyz.block.ftl.v1.schema.Any; import xyz.block.ftl.v1.schema.Array; import xyz.block.ftl.v1.schema.Bool; @@ -37,14 +52,16 @@ import xyz.block.ftl.v1.schema.Int; import xyz.block.ftl.v1.schema.Metadata; import xyz.block.ftl.v1.schema.MetadataAlias; +import xyz.block.ftl.v1.schema.MetadataCalls; import xyz.block.ftl.v1.schema.Module; import xyz.block.ftl.v1.schema.Optional; import xyz.block.ftl.v1.schema.Ref; import xyz.block.ftl.v1.schema.Time; import xyz.block.ftl.v1.schema.Type; import xyz.block.ftl.v1.schema.Unit; +import xyz.block.ftl.v1.schema.Verb; -public class SchemaBuilder { +public class ModuleBuilder { public static final String BUILTIN = "builtin"; @@ -56,17 +73,180 @@ public class SchemaBuilder { public static final DotName GENERATED_REF = DotName.createSimple(GeneratedRef.class); public static final DotName EXPORT = DotName.createSimple(Export.class); - final IndexView index; - final Module.Builder moduleBuilder; - final Map dataElements = new HashMap<>(); - final String moduleName; + private final IndexView index; + private final Module.Builder moduleBuilder; + private final Map dataElements = new HashMap<>(); + private final String moduleName; + private final Set knownSecrets = new HashSet<>(); + private final Set knownConfig = new HashSet<>(); + private final Map knownTopics; + private final Map verbClients; + private final FTLRecorder recorder; - public SchemaBuilder(IndexView index, String moduleName) { + public ModuleBuilder(IndexView index, String moduleName, Map knownTopics, + Map verbClients, FTLRecorder recorder) { this.index = index; this.moduleName = moduleName; this.moduleBuilder = Module.newBuilder() .setName(moduleName) .setBuiltin(false); + this.knownTopics = knownTopics; + this.verbClients = verbClients; + this.recorder = recorder; + } + + public static @NotNull String methodToName(MethodInfo method) { + if (method.hasAnnotation(VerbName.class)) { + return method.annotation(VerbName.class).value().asString(); + } + return method.name(); + } + + public String getModuleName() { + return moduleName; + } + + public static Class loadClass(org.jboss.jandex.Type param) throws ClassNotFoundException { + if (param.kind() == org.jboss.jandex.Type.Kind.PARAMETERIZED_TYPE) { + return Class.forName(param.asParameterizedType().name().toString(), false, + Thread.currentThread().getContextClassLoader()); + } else if (param.kind() == org.jboss.jandex.Type.Kind.CLASS) { + return Class.forName(param.name().toString(), false, Thread.currentThread().getContextClassLoader()); + } else if (param.kind() == org.jboss.jandex.Type.Kind.PRIMITIVE) { + switch (param.asPrimitiveType().primitive()) { + case BOOLEAN: + return Boolean.TYPE; + case BYTE: + return Byte.TYPE; + case SHORT: + return Short.TYPE; + case INT: + return Integer.TYPE; + case LONG: + return Long.TYPE; + case FLOAT: + return java.lang.Float.TYPE; + case DOUBLE: + return Double.TYPE; + case CHAR: + return Character.TYPE; + default: + throw new RuntimeException("Unknown primitive type " + param.asPrimitiveType().primitive()); + } + } else if (param.kind() == org.jboss.jandex.Type.Kind.ARRAY) { + ArrayType array = param.asArrayType(); + if (array.componentType().kind() == org.jboss.jandex.Type.Kind.PRIMITIVE) { + switch (array.componentType().asPrimitiveType().primitive()) { + case BOOLEAN: + return boolean[].class; + case BYTE: + return byte[].class; + case SHORT: + return short[].class; + case INT: + return int[].class; + case LONG: + return long[].class; + case FLOAT: + return float[].class; + case DOUBLE: + return double[].class; + case CHAR: + return char[].class; + default: + throw new RuntimeException("Unknown primitive type " + param.asPrimitiveType().primitive()); + } + } + } + throw new RuntimeException("Unknown type " + param.kind()); + + } + + public void registerVerbMethod(MethodInfo method, String className, + boolean exported, BodyType bodyType, Consumer metadataCallback) { + try { + List> parameterTypes = new ArrayList<>(); + List> paramMappers = new ArrayList<>(); + org.jboss.jandex.Type bodyParamType = null; + xyz.block.ftl.v1.schema.Verb.Builder verbBuilder = xyz.block.ftl.v1.schema.Verb.newBuilder(); + String verbName = ModuleBuilder.methodToName(method); + MetadataCalls.Builder callsMetadata = MetadataCalls.newBuilder(); + for (var param : method.parameters()) { + if (param.hasAnnotation(Secret.class)) { + Class paramType = ModuleBuilder.loadClass(param.type()); + parameterTypes.add(paramType); + String name = param.annotation(Secret.class).value().asString(); + paramMappers.add(new VerbRegistry.SecretSupplier(name, paramType)); + if (!knownSecrets.contains(name)) { + addDecls(Decl.newBuilder().setSecret(xyz.block.ftl.v1.schema.Secret.newBuilder() + .setType(buildType(param.type(), false)).setName(name)).build()); + knownSecrets.add(name); + } + } else if (param.hasAnnotation(Config.class)) { + Class paramType = ModuleBuilder.loadClass(param.type()); + parameterTypes.add(paramType); + String name = param.annotation(Config.class).value().asString(); + paramMappers.add(new VerbRegistry.ConfigSupplier(name, paramType)); + if (!knownConfig.contains(name)) { + addDecls(Decl.newBuilder().setConfig(xyz.block.ftl.v1.schema.Config.newBuilder() + .setType(buildType(param.type(), false)).setName(name)).build()); + knownConfig.add(name); + } + } else if (knownTopics.containsKey(param.type().name())) { + var topic = knownTopics.get(param.type().name()); + Class paramType = ModuleBuilder.loadClass(param.type()); + parameterTypes.add(paramType); + paramMappers.add(recorder.topicSupplier(topic.generatedProducer(), verbName)); + } else if (verbClients.containsKey(param.type().name())) { + var client = verbClients.get(param.type().name()); + Class paramType = ModuleBuilder.loadClass(param.type()); + parameterTypes.add(paramType); + paramMappers.add(recorder.verbClientSupplier(client.generatedClient())); + callsMetadata.addCalls(Ref.newBuilder().setName(client.name()).setModule(client.module()).build()); + } else if (FTLDotNames.LEASE_CLIENT.equals(param.type().name())) { + parameterTypes.add(LeaseClient.class); + paramMappers.add(recorder.leaseClientSupplier()); + } else if (bodyType != BodyType.DISALLOWED && bodyParamType == null) { + bodyParamType = param.type(); + Class paramType = ModuleBuilder.loadClass(param.type()); + parameterTypes.add(paramType); + //TODO: map and list types + paramMappers.add(new VerbRegistry.BodySupplier(paramType)); + } else { + throw new RuntimeException("Unknown parameter type " + param.type() + " on FTL method: " + + method.declaringClass().name() + "." + method.name()); + } + } + if (bodyParamType == null) { + if (bodyType == BodyType.REQUIRED) { + throw new RuntimeException("Missing required payload parameter"); + } + bodyParamType = VoidType.VOID; + } + if (callsMetadata.getCallsCount() > 0) { + verbBuilder.addMetadata(Metadata.newBuilder().setCalls(callsMetadata)); + } + + //TODO: we need better handling around Optional + recorder.registerVerb(moduleName, verbName, method.name(), parameterTypes, + Class.forName(className, false, Thread.currentThread().getContextClassLoader()), paramMappers, + method.returnType() == VoidType.VOID); + verbBuilder + .setName(verbName) + .setExport(exported) + .setRequest(buildType(bodyParamType, exported)) + .setResponse(buildType(method.returnType(), exported)); + + if (metadataCallback != null) { + metadataCallback.accept(verbBuilder); + } + addDecls(Decl.newBuilder().setVerb(verbBuilder) + .build()); + + } catch (Exception e) { + throw new RuntimeException("Failed to process FTL method " + method.declaringClass().name() + "." + method.name(), + e); + } } public Type buildType(org.jboss.jandex.Type type, boolean export) { @@ -240,7 +420,7 @@ private void buildDataElement(Data.Builder data, DotName className) { buildDataElement(data, clazz.superName()); } - public SchemaBuilder addDecls(Decl decl) { + public ModuleBuilder addDecls(Decl decl) { moduleBuilder.addDecls(decl); return this; } @@ -257,4 +437,9 @@ private record TypeKey(String name, List typeParams) { } + public enum BodyType { + DISALLOWED, + ALLOWED, + REQUIRED + } } diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleProcessor.java index 21197e4e4..f0fa2b59d 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/ModuleProcessor.java @@ -1,21 +1,44 @@ package xyz.block.ftl.deployment; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.util.EnumSet; +import java.util.List; import java.util.Objects; import java.util.stream.Collectors; +import org.jboss.jandex.DotName; import org.jboss.logging.Logger; import org.tomlj.Toml; import org.tomlj.TomlParseResult; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem; import io.quarkus.deployment.builditem.ApplicationInfoBuildItem; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.RunTimeConfigBuilderBuildItem; +import io.quarkus.deployment.builditem.SystemPropertyBuildItem; +import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; +import io.quarkus.grpc.deployment.BindableServiceBuildItem; +import io.quarkus.vertx.http.deployment.RequireSocketHttpBuildItem; +import io.quarkus.vertx.http.deployment.RequireVirtualHttpBuildItem; +import xyz.block.ftl.runtime.FTLDatasourceCredentials; +import xyz.block.ftl.runtime.FTLRecorder; +import xyz.block.ftl.runtime.JsonSerializationConfig; +import xyz.block.ftl.runtime.TopicHelper; +import xyz.block.ftl.runtime.VerbClientHelper; +import xyz.block.ftl.runtime.VerbHandler; +import xyz.block.ftl.runtime.VerbRegistry; import xyz.block.ftl.runtime.config.FTLConfigSourceFactoryBuilder; +import xyz.block.ftl.runtime.http.FTLHttpHandler; public class ModuleProcessor { @@ -23,6 +46,25 @@ public class ModuleProcessor { private static final String FEATURE = "ftl-java-runtime"; + private static final String SCHEMA_OUT = "schema.pb"; + + @BuildStep + BindableServiceBuildItem verbService() { + var ret = new BindableServiceBuildItem(DotName.createSimple(VerbHandler.class)); + ret.registerBlockingMethod("call"); + ret.registerBlockingMethod("publishEvent"); + ret.registerBlockingMethod("sendFSMEvent"); + ret.registerBlockingMethod("acquireLease"); + ret.registerBlockingMethod("getModuleContext"); + ret.registerBlockingMethod("ping"); + return ret; + } + + @BuildStep + public SystemPropertyBuildItem moduleNameConfig(ApplicationInfoBuildItem applicationInfoBuildItem) { + return new SystemPropertyBuildItem("ftl.module.name", applicationInfoBuildItem.getName()); + } + @BuildStep ModuleNameBuildItem moduleName(ApplicationInfoBuildItem applicationInfoBuildItem, ApplicationArchivesBuildItem archivesBuildItem) throws IOException { @@ -55,6 +97,44 @@ ModuleNameBuildItem moduleName(ApplicationInfoBuildItem applicationInfoBuildItem return new ModuleNameBuildItem(applicationInfoBuildItem.getName()); } + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public void generateSchema(CombinedIndexBuildItem index, + FTLRecorder recorder, + OutputTargetBuildItem outputTargetBuildItem, + ModuleNameBuildItem moduleNameBuildItem, + TopicsBuildItem topicsBuildItem, + VerbClientBuildItem verbClientBuildItem, + List schemaContributorBuildItems) throws Exception { + String moduleName = moduleNameBuildItem.getModuleName(); + + ModuleBuilder moduleBuilder = new ModuleBuilder(index.getComputingIndex(), moduleName, topicsBuildItem.getTopics(), + verbClientBuildItem.getVerbClients(), recorder); + + for (var i : schemaContributorBuildItems) { + i.getSchemaContributor().accept(moduleBuilder); + } + + Path output = outputTargetBuildItem.getOutputDirectory().resolve(SCHEMA_OUT); + try (var out = Files.newOutputStream(output)) { + moduleBuilder.writeTo(out); + } + + output = outputTargetBuildItem.getOutputDirectory().resolve("main"); + try (var out = Files.newOutputStream(output)) { + out.write( + """ + #!/bin/bash + exec java $FTL_JVM_OPTS -jar quarkus-app/quarkus-run.jar""" + .getBytes(StandardCharsets.UTF_8)); + } + var perms = Files.getPosixFilePermissions(output); + EnumSet newPerms = EnumSet.copyOf(perms); + newPerms.add(PosixFilePermission.GROUP_EXECUTE); + newPerms.add(PosixFilePermission.OWNER_EXECUTE); + Files.setPosixFilePermissions(output, newPerms); + } + @BuildStep RunTimeConfigBuilderBuildItem runTimeConfigBuilderBuildItem() { return new RunTimeConfigBuilderBuildItem(FTLConfigSourceFactoryBuilder.class.getName()); @@ -64,4 +144,21 @@ RunTimeConfigBuilderBuildItem runTimeConfigBuilderBuildItem() { FeatureBuildItem feature() { return new FeatureBuildItem(FEATURE); } + + @BuildStep + AdditionalBeanBuildItem beans() { + return AdditionalBeanBuildItem.builder() + .addBeanClasses(VerbHandler.class, + VerbRegistry.class, FTLHttpHandler.class, + TopicHelper.class, VerbClientHelper.class, JsonSerializationConfig.class, + FTLDatasourceCredentials.class) + .setUnremovable().build(); + } + + @BuildStep + void openSocket(BuildProducer virtual, + BuildProducer socket) throws IOException { + socket.produce(RequireSocketHttpBuildItem.MARKER); + virtual.produce(RequireVirtualHttpBuildItem.MARKER); + } } diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SchemaContributorBuildItem.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SchemaContributorBuildItem.java new file mode 100644 index 000000000..60564b4e9 --- /dev/null +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SchemaContributorBuildItem.java @@ -0,0 +1,36 @@ +package xyz.block.ftl.deployment; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import io.quarkus.builder.item.MultiBuildItem; +import xyz.block.ftl.v1.schema.Decl; + +/** + * A build item that contributes information to the final Schema + */ +public final class SchemaContributorBuildItem extends MultiBuildItem { + + final Consumer schemaContributor; + + public SchemaContributorBuildItem(Consumer schemaContributor) { + this.schemaContributor = schemaContributor; + } + + public SchemaContributorBuildItem(List decls) { + var data = new ArrayList<>(decls); + this.schemaContributor = new Consumer() { + @Override + public void accept(ModuleBuilder moduleBuilder) { + for (var i : data) { + moduleBuilder.addDecls(i); + } + } + }; + } + + public Consumer getSchemaContributor() { + return schemaContributor; + } +} diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java new file mode 100644 index 000000000..18a61b47b --- /dev/null +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/SubscriptionProcessor.java @@ -0,0 +1,105 @@ +package xyz.block.ftl.deployment; + +import java.util.HashMap; +import java.util.Map; + +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.DotName; +import org.jboss.jandex.MethodInfo; +import org.jboss.logging.Logger; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import xyz.block.ftl.Retry; +import xyz.block.ftl.Subscription; +import xyz.block.ftl.v1.schema.Decl; +import xyz.block.ftl.v1.schema.Metadata; +import xyz.block.ftl.v1.schema.MetadataRetry; +import xyz.block.ftl.v1.schema.MetadataSubscriber; +import xyz.block.ftl.v1.schema.Ref; + +public class SubscriptionProcessor { + + private static final Logger log = Logger.getLogger(SubscriptionProcessor.class); + + @BuildStep + SubscriptionMetaAnnotationsBuildItem subscriptionAnnotations(CombinedIndexBuildItem combinedIndexBuildItem, + ModuleNameBuildItem moduleNameBuildItem) { + Map annotations = new HashMap<>(); + for (var subscriptions : combinedIndexBuildItem.getComputingIndex().getAnnotations(Subscription.class)) { + if (subscriptions.target().kind() != AnnotationTarget.Kind.CLASS) { + continue; + } + annotations.put(subscriptions.target().asClass().name(), + SubscriptionMetaAnnotationsBuildItem.fromJandex(subscriptions, moduleNameBuildItem.getModuleName())); + } + return new SubscriptionMetaAnnotationsBuildItem(annotations); + } + + @BuildStep + public void registerSubscriptions(CombinedIndexBuildItem index, + ModuleNameBuildItem moduleNameBuildItem, + BuildProducer additionalBeanBuildItemBuildProducer, + SubscriptionMetaAnnotationsBuildItem subscriptionMetaAnnotationsBuildItem, + BuildProducer schemaContributorBuildItems) throws Exception { + AdditionalBeanBuildItem.Builder beans = AdditionalBeanBuildItem.builder().setUnremovable(); + var moduleName = moduleNameBuildItem.getModuleName(); + for (var subscription : index.getIndex().getAnnotations(FTLDotNames.SUBSCRIPTION)) { + var info = SubscriptionMetaAnnotationsBuildItem.fromJandex(subscription, moduleName); + if (subscription.target().kind() != AnnotationTarget.Kind.METHOD) { + continue; + } + var method = subscription.target().asMethod(); + String className = method.declaringClass().name().toString(); + beans.addBeanClass(className); + schemaContributorBuildItems.produce(generateSubscription(method, className, info)); + } + for (var metaSub : subscriptionMetaAnnotationsBuildItem.getAnnotations().entrySet()) { + for (var subscription : index.getIndex().getAnnotations(metaSub.getKey())) { + if (subscription.target().kind() != AnnotationTarget.Kind.METHOD) { + log.warnf("Subscription annotation on non-method target: %s", subscription.target()); + continue; + } + var method = subscription.target().asMethod(); + + String className = method.declaringClass().name().toString(); + beans.addBeanClass(className); + schemaContributorBuildItems.produce(generateSubscription(method, + className, + metaSub.getValue())); + } + + } + additionalBeanBuildItemBuildProducer.produce(beans.build()); + } + + private SchemaContributorBuildItem generateSubscription(MethodInfo method, String className, + SubscriptionMetaAnnotationsBuildItem.SubscriptionAnnotation info) { + return new SchemaContributorBuildItem(moduleBuilder -> { + + moduleBuilder.addDecls(Decl.newBuilder() + .setSubscription(xyz.block.ftl.v1.schema.Subscription.newBuilder() + .setName(info.name()) + .setTopic(Ref.newBuilder().setName(info.topic()).setModule(info.module()).build())) + .build()); + moduleBuilder.registerVerbMethod(method, className, false, ModuleBuilder.BodyType.REQUIRED, (builder -> { + builder.addMetadata(Metadata.newBuilder().setSubscriber(MetadataSubscriber.newBuilder().setName(info.name()))); + if (method.hasAnnotation(Retry.class)) { + RetryRecord retry = RetryRecord.fromJandex(method.annotation(Retry.class), moduleBuilder.getModuleName()); + + MetadataRetry.Builder retryBuilder = MetadataRetry.newBuilder(); + if (!retry.catchVerb().isEmpty()) { + retryBuilder.setCatch(Ref.newBuilder().setModule(retry.catchModule()) + .setName(retry.catchVerb()).build()); + } + retryBuilder.setCount(retry.count()) + .setMaxBackoff(retry.maxBackoff()) + .setMinBackoff(retry.minBackoff()); + builder.addMetadata(Metadata.newBuilder().setRetry(retryBuilder).build()); + } + })); + }); + } +} diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java index 545941145..5f70c350e 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/TopicsProcessor.java @@ -4,8 +4,8 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; -import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.DotName; import org.jboss.jandex.Type; @@ -17,10 +17,10 @@ import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.MethodDescriptor; import xyz.block.ftl.Export; -import xyz.block.ftl.Subscription; import xyz.block.ftl.Topic; import xyz.block.ftl.TopicDefinition; import xyz.block.ftl.runtime.TopicHelper; +import xyz.block.ftl.v1.schema.Decl; public class TopicsProcessor { @@ -81,17 +81,18 @@ TopicsBuildItem handleTopics(CombinedIndexBuildItem index, BuildProducer annotations = new HashMap<>(); - for (var subscriptions : combinedIndexBuildItem.getComputingIndex().getAnnotations(Subscription.class)) { - if (subscriptions.target().kind() != AnnotationTarget.Kind.CLASS) { - continue; + public SchemaContributorBuildItem topicSchema(TopicsBuildItem topics) { + //register all the topics we are defining in the module definition + return new SchemaContributorBuildItem(new Consumer() { + @Override + public void accept(ModuleBuilder moduleBuilder) { + for (var topic : topics.getTopics().values()) { + moduleBuilder.addDecls(Decl.newBuilder().setTopic(xyz.block.ftl.v1.schema.Topic.newBuilder() + .setExport(topic.exported()) + .setName(topic.topicName()) + .setEvent(moduleBuilder.buildType(topic.eventType(), topic.exported())).build()).build()); + } } - annotations.put(subscriptions.target().asClass().name(), - SubscriptionMetaAnnotationsBuildItem.fromJandex(subscriptions, moduleNameBuildItem.getModuleName())); - } - return new SubscriptionMetaAnnotationsBuildItem(annotations); + }); } } diff --git a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbClientsProcessor.java b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbProcessor.java similarity index 87% rename from jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbClientsProcessor.java rename to jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbProcessor.java index 2d5865354..e746bd0e9 100644 --- a/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbClientsProcessor.java +++ b/jvm-runtime/ftl-runtime/common/deployment/src/main/java/xyz/block/ftl/deployment/VerbProcessor.java @@ -10,6 +10,7 @@ import org.jboss.jandex.DotName; import org.jboss.jandex.Type; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; import io.quarkus.deployment.GeneratedClassGizmoAdaptor; @@ -27,8 +28,10 @@ import xyz.block.ftl.VerbClientSink; import xyz.block.ftl.VerbClientSource; import xyz.block.ftl.runtime.VerbClientHelper; +import xyz.block.ftl.v1.schema.Metadata; +import xyz.block.ftl.v1.schema.MetadataCronJob; -public class VerbClientsProcessor { +public class VerbProcessor { public static final DotName VERB_CLIENT = DotName.createSimple(VerbClient.class); public static final DotName VERB_CLIENT_SINK = DotName.createSimple(VerbClientSink.class); @@ -37,7 +40,7 @@ public class VerbClientsProcessor { public static final String TEST_ANNOTATION = "xyz.block.ftl.java.test.FTLManaged"; @BuildStep - VerbClientBuildItem handleTopics(CombinedIndexBuildItem index, BuildProducer generatedClients, + VerbClientBuildItem handleVerbClients(CombinedIndexBuildItem index, BuildProducer generatedClients, BuildProducer generatedBeanBuildItemBuildProducer, ModuleNameBuildItem moduleNameBuildItem, LaunchModeBuildItem launchModeBuildItem) { @@ -218,4 +221,34 @@ VerbClientBuildItem handleTopics(CombinedIndexBuildItem index, BuildProducer additionalBeanBuildItem, + BuildProducer schemaContributorBuildItemBuildProducer) { + var beans = AdditionalBeanBuildItem.builder().setUnremovable(); + for (var verb : index.getIndex().getAnnotations(FTLDotNames.VERB)) { + boolean exported = verb.target().hasAnnotation(FTLDotNames.EXPORT); + var method = verb.target().asMethod(); + String className = method.declaringClass().name().toString(); + beans.addBeanClass(className); + schemaContributorBuildItemBuildProducer.produce(new SchemaContributorBuildItem(moduleBuilder -> moduleBuilder + .registerVerbMethod(method, className, exported, ModuleBuilder.BodyType.ALLOWED, null))); + } + for (var cron : index.getIndex().getAnnotations(FTLDotNames.CRON)) { + var method = cron.target().asMethod(); + String className = method.declaringClass().name().toString(); + beans.addBeanClass(className); + + schemaContributorBuildItemBuildProducer + .produce( + new SchemaContributorBuildItem(moduleBuilder -> moduleBuilder.registerVerbMethod(method, className, + false, ModuleBuilder.BodyType.ALLOWED, (builder -> builder.addMetadata(Metadata.newBuilder() + .setCronJob(MetadataCronJob.newBuilder().setCron(cron.value().asString())) + .build()))))); + } + additionalBeanBuildItem.produce(beans.build()); + + } + }