diff --git a/commons/pom.xml b/commons/pom.xml index 8342690c..786ca4d1 100644 --- a/commons/pom.xml +++ b/commons/pom.xml @@ -4,7 +4,7 @@ io.polyapi parent-pom - 0.2.6-SNAPSHOT + 0.3.0-SNAPSHOT ../parent-pom diff --git a/commons/src/main/java/io/polyapi/commons/api/error/PolyApiExecutionException.java b/commons/src/main/java/io/polyapi/commons/api/error/PolyApiExecutionException.java new file mode 100644 index 00000000..dc6a349e --- /dev/null +++ b/commons/src/main/java/io/polyapi/commons/api/error/PolyApiExecutionException.java @@ -0,0 +1,38 @@ +package io.polyapi.commons.api.error; + +/** + * Parent exception for all exceptions that want to be handled during the execution of a Poly function. + * Requires a status code that will be returned to the user in case an exception happens. + */ +public abstract class PolyApiExecutionException extends PolyApiException { + + /** + * See {@link PolyApiException#PolyApiException()} + */ + public PolyApiExecutionException() { + super(); + } + + /** + * See {@link PolyApiException#PolyApiException(String)} + */ + public PolyApiExecutionException(String message) { + super(message); + } + + /** + * See {@link PolyApiException#PolyApiException(String, Throwable)} + */ + public PolyApiExecutionException(String message, Throwable cause) { + super(message, cause); + } + + /** + * See {@link PolyApiException#PolyApiException(Throwable)} + */ + public PolyApiExecutionException(Throwable cause) { + super(cause); + } + + public abstract int getStatusCode(); +} diff --git a/commons/src/main/java/io/polyapi/commons/api/error/http/HttpResponseException.java b/commons/src/main/java/io/polyapi/commons/api/error/http/HttpResponseException.java index 6a657927..d555bffb 100644 --- a/commons/src/main/java/io/polyapi/commons/api/error/http/HttpResponseException.java +++ b/commons/src/main/java/io/polyapi/commons/api/error/http/HttpResponseException.java @@ -1,15 +1,17 @@ package io.polyapi.commons.api.error.http; -import io.polyapi.commons.api.error.PolyApiException; +import io.polyapi.commons.api.error.PolyApiExecutionException; import io.polyapi.commons.api.http.Response; import lombok.Getter; +import java.util.Optional; + /** * Parent of exceptions thrown when the response from an HTTP request is different than 2XX. * This class contains an instance of the {@link Response} returned. */ @Getter -public class HttpResponseException extends PolyApiException { +public class HttpResponseException extends PolyApiExecutionException { private final Response response; @@ -23,4 +25,9 @@ public HttpResponseException(String message, Response response) { super(message); this.response = response; } + + @Override + public int getStatusCode() { + return Optional.ofNullable(response).map(Response::statusCode).orElse(500); + } } diff --git a/commons/src/main/java/io/polyapi/commons/api/error/http/UnexpectedInformationalResponseException.java b/commons/src/main/java/io/polyapi/commons/api/error/http/UnexpectedInformationalResponseException.java index 090acdc0..1123864c 100644 --- a/commons/src/main/java/io/polyapi/commons/api/error/http/UnexpectedInformationalResponseException.java +++ b/commons/src/main/java/io/polyapi/commons/api/error/http/UnexpectedInformationalResponseException.java @@ -9,7 +9,7 @@ */ public class UnexpectedInformationalResponseException extends HttpResponseException { - public UnexpectedInformationalResponseException(Response response) { - super(format("An unexpected informational response was received. Status code: %s.", response.statusCode()), response); - } + public UnexpectedInformationalResponseException(Response response) { + super(format("An unexpected informational response was received. Status code: %s.", response.statusCode()), response); + } } diff --git a/commons/src/main/java/io/polyapi/commons/api/model/FunctionType.java b/commons/src/main/java/io/polyapi/commons/api/model/FunctionType.java new file mode 100644 index 00000000..37b0f65c --- /dev/null +++ b/commons/src/main/java/io/polyapi/commons/api/model/FunctionType.java @@ -0,0 +1,8 @@ +package io.polyapi.commons.api.model; + +/** + * Type of a Poly function. + */ +public enum FunctionType { + SERVER, CLIENT; +} diff --git a/commons/src/main/java/io/polyapi/commons/api/model/PolyFunction.java b/commons/src/main/java/io/polyapi/commons/api/model/PolyFunction.java new file mode 100644 index 00000000..c766f188 --- /dev/null +++ b/commons/src/main/java/io/polyapi/commons/api/model/PolyFunction.java @@ -0,0 +1,24 @@ +package io.polyapi.commons.api.model; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.Boolean.FALSE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotation that marks a specific method as a Poly function. + * When uploading functions, + */ +@Target(METHOD) +@Retention(RUNTIME) +public @interface PolyFunction { + FunctionType type() default FunctionType.SERVER; + + String context() default ""; + + String name() default ""; + + boolean isDeployable() default true; +} diff --git a/commons/src/main/java/io/polyapi/commons/api/model/PolyGeneratedClass.java b/commons/src/main/java/io/polyapi/commons/api/model/PolyGeneratedClass.java new file mode 100644 index 00000000..2b5e5fe6 --- /dev/null +++ b/commons/src/main/java/io/polyapi/commons/api/model/PolyGeneratedClass.java @@ -0,0 +1,16 @@ +package io.polyapi.commons.api.model; + + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotation that marks a class as generated by Poly. + */ +@Target(TYPE) +@Retention(RUNTIME) +public @interface PolyGeneratedClass { +} diff --git a/commons/src/main/java/io/polyapi/commons/api/model/RequiredDependencies.java b/commons/src/main/java/io/polyapi/commons/api/model/RequiredDependencies.java new file mode 100644 index 00000000..443ea36f --- /dev/null +++ b/commons/src/main/java/io/polyapi/commons/api/model/RequiredDependencies.java @@ -0,0 +1,13 @@ +package io.polyapi.commons.api.model; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target(METHOD) +@Retention(RUNTIME) +public @interface RequiredDependencies { + RequiredDependency[] value(); +} diff --git a/commons/src/main/java/io/polyapi/commons/api/model/RequiredDependency.java b/commons/src/main/java/io/polyapi/commons/api/model/RequiredDependency.java new file mode 100644 index 00000000..b5a7f1fa --- /dev/null +++ b/commons/src/main/java/io/polyapi/commons/api/model/RequiredDependency.java @@ -0,0 +1,21 @@ +package io.polyapi.commons.api.model; + + +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target(METHOD) +@Retention(RUNTIME) +@Repeatable(RequiredDependencies.class) +public @interface RequiredDependency { + + String groupId() default ""; + + String artifactId() default ".*"; + + String version() default ".*"; +} diff --git a/commons/src/main/java/io/polyapi/commons/internal/http/HardcodedTokenProvider.java b/commons/src/main/java/io/polyapi/commons/internal/http/HardcodedTokenProvider.java index b89de9d0..ce49f1fc 100644 --- a/commons/src/main/java/io/polyapi/commons/internal/http/HardcodedTokenProvider.java +++ b/commons/src/main/java/io/polyapi/commons/internal/http/HardcodedTokenProvider.java @@ -4,8 +4,6 @@ /** * {@link TokenProvider} that always return the same set token. - * - * @deprecated This class should be replaced for one that obtains the token dynamically and refreshes it. */ public class HardcodedTokenProvider implements TokenProvider { diff --git a/commons/src/main/java/io/polyapi/commons/internal/invocation/PolyFunction.java b/commons/src/main/java/io/polyapi/commons/internal/invocation/PolyFunction.java deleted file mode 100644 index 1984ec4f..00000000 --- a/commons/src/main/java/io/polyapi/commons/internal/invocation/PolyFunction.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.polyapi.commons.internal.invocation; - -public @interface PolyFunction { - - String value() default "execute"; -} diff --git a/library/pom.xml b/library/pom.xml index 4fcb9e66..94aae300 100644 --- a/library/pom.xml +++ b/library/pom.xml @@ -4,7 +4,7 @@ io.polyapi parent-pom - 0.2.6-SNAPSHOT + 0.3.0-SNAPSHOT ../parent-pom library diff --git a/parent-pom/pom.xml b/parent-pom/pom.xml index 61290fb0..07da6bd9 100644 --- a/parent-pom/pom.xml +++ b/parent-pom/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.polyapi parent-pom - 0.2.6-SNAPSHOT + 0.3.0-SNAPSHOT pom Poly API Java parent POM https://polyapi.io diff --git a/polyapi-maven-plugin/pom.xml b/polyapi-maven-plugin/pom.xml index 25829243..58217532 100644 --- a/polyapi-maven-plugin/pom.xml +++ b/polyapi-maven-plugin/pom.xml @@ -4,7 +4,7 @@ io.polyapi parent-pom - 0.2.6-SNAPSHOT + 0.3.0-SNAPSHOT ../parent-pom polyapi-maven-plugin @@ -29,6 +29,11 @@ + + org.reflections + reflections + 0.10.2 + org.apache.maven maven-plugin-api diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/error/deploy/DeploymentWrapperException.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/error/deploy/DeploymentWrapperException.java new file mode 100644 index 00000000..08903b98 --- /dev/null +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/error/deploy/DeploymentWrapperException.java @@ -0,0 +1,17 @@ +package io.polyapi.plugin.error.deploy; + +import io.polyapi.commons.api.error.PolyApiException; +import io.polyapi.plugin.error.PolyApiMavenPluginException; + +import java.util.Collection; + +/** + * Exception that wraps all the other exceptions thrown when deploying functions. + */ +public class DeploymentWrapperException extends PolyApiMavenPluginException { + + public DeploymentWrapperException(Collection suppressedExceptions) { + super("Exceptions occurred while deploying functions."); + suppressedExceptions.forEach(this::addSuppressed); + } +} diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/error/validation/InvalidPropertyException.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/error/validation/InvalidPropertyException.java new file mode 100644 index 00000000..28dcc606 --- /dev/null +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/error/validation/InvalidPropertyException.java @@ -0,0 +1,11 @@ +package io.polyapi.plugin.error.validation; + +import java.lang.reflect.Method; + +import static java.lang.String.format; + +public class InvalidPropertyException extends ValidationException { + public InvalidPropertyException(String propertyName, String propertyValue, Method method, String pattern) { + super(propertyName, format("Property '%s' with value '%s' of method '%s' doesn't match pattern '%s'.", "%s", propertyValue, method, pattern)); + } +} diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/error/validation/KeywordUseException.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/error/validation/KeywordUseException.java new file mode 100644 index 00000000..d1acce1d --- /dev/null +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/error/validation/KeywordUseException.java @@ -0,0 +1,13 @@ +package io.polyapi.plugin.error.validation; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.stream.Collectors; + +import static java.lang.String.format; + +public class KeywordUseException extends ValidationException { + public KeywordUseException(String propertyName, String propertyValue, Method method, String... keywords) { + super(propertyName, format("Property '%s' with value '%s' of method '%s' uses Java keywords '%s'. Please rename the context accordingly.", "%s", propertyValue, method, Arrays.stream(keywords).collect(Collectors.joining("', '")))); + } +} diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/error/validation/ValidationException.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/error/validation/ValidationException.java index 5d62adab..eaf19053 100644 --- a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/error/validation/ValidationException.java +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/error/validation/ValidationException.java @@ -1,12 +1,14 @@ package io.polyapi.plugin.error.validation; import io.polyapi.plugin.error.PolyApiMavenPluginException; +import lombok.Getter; import static java.lang.String.format; /** * Parent class for exceptions thrown for validation purposes. Stores the name of the property being validated and injects it into the message. */ +@Getter public class ValidationException extends PolyApiMavenPluginException { private final String propertyName; @@ -33,8 +35,4 @@ public ValidationException(String propertyName, String messageTemplate, Throwabl super(format(messageTemplate, propertyName), cause); this.propertyName = propertyName; } - - public String getPropertyName() { - return propertyName; - } } diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/CodeObject.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/CodeObject.java new file mode 100644 index 00000000..9fc7aab0 --- /dev/null +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/CodeObject.java @@ -0,0 +1,24 @@ +package io.polyapi.plugin.model.function; + + +import io.polyapi.commons.api.model.Visibility; +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static java.lang.String.format; +import static java.util.stream.Collectors.joining; + +@Getter +@Setter +public class CodeObject { + private String packageName; + private String className; + private String methodName; + private String params; + private String code; +} diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/PolyFunction.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/PolyFunction.java index b4be1d7f..6d44cd58 100644 --- a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/PolyFunction.java +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/PolyFunction.java @@ -25,6 +25,7 @@ public class PolyFunction { private String returnType; private Visibility visibility; private Boolean logsEnabled; + private List requirements; private Map returnTypeSchema; private List arguments; diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/PolyFunctionMetadata.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/PolyFunctionMetadata.java new file mode 100644 index 00000000..dbb364ab --- /dev/null +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/PolyFunctionMetadata.java @@ -0,0 +1,23 @@ +package io.polyapi.plugin.model.function; + +import io.polyapi.commons.api.model.FunctionType; + +import java.io.InputStream; +import java.util.List; + +public record PolyFunctionMetadata(String name, String signature, FunctionType type, InputStream sourceCode, String context, List dependencies, Boolean isDeployable) { + + @Override + public String toString() { + return "PolyFunctionMetadata{" + + "name='" + name + '\'' + + ", type=" + type + + ", context='" + context + '\'' + + ", dependencies=" + dependencies + + '}'; + } + + public String getTypedType() { + return type.name().toLowerCase(); + } +} diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/mojo/AddFunctionMojo.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/mojo/AddFunctionMojo.java index c1b73355..815498ab 100644 --- a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/mojo/AddFunctionMojo.java +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/mojo/AddFunctionMojo.java @@ -1,13 +1,10 @@ package io.polyapi.plugin.mojo; -import io.polyapi.commons.api.http.HttpClient; -import io.polyapi.commons.api.http.TokenProvider; -import io.polyapi.commons.api.json.JsonParser; +import io.polyapi.plugin.error.classloader.QualifiedNameNotFoundException; import io.polyapi.plugin.model.function.PolyFunction; import io.polyapi.plugin.model.function.PolyFunctionArgument; import io.polyapi.plugin.service.JavaParserService; import io.polyapi.plugin.service.JavaParserServiceImpl; -import io.polyapi.plugin.service.MavenService; import io.polyapi.plugin.service.PolyFunctionService; import io.polyapi.plugin.service.PolyFunctionServiceImpl; import lombok.Setter; @@ -16,17 +13,22 @@ import org.slf4j.LoggerFactory; import java.io.File; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiConsumer; +import static java.lang.String.format; import static java.util.stream.Collectors.joining; @Setter public abstract class AddFunctionMojo extends PolyApiMojo { private static final Logger logger = LoggerFactory.getLogger(AddFunctionMojo.class); - @Parameter(property = "functionName", required = true) + @Parameter(property = "functionName") private String functionName; - @Parameter(property = "file", required = true) + @Parameter(property = "file") private File file; @Parameter(property = "context") @@ -35,20 +37,48 @@ public abstract class AddFunctionMojo extends PolyApiMojo { @Parameter(property = "description") private String description; - public void execute(String host, Integer port, TokenProvider tokenProvider, HttpClient httpClient, JsonParser jsonParser, MavenService mavenService) { - var classLoader = mavenService.getProjectClassLoader(); - logger.debug("Setting up the Java parser service."); - logger.debug("Setting up class loader for all relevant places in the project."); - JavaParserService javaParserService = new JavaParserServiceImpl(classLoader, jsonParser); - logger.debug("Setting up HTTP service to access the Function API in Poly."); - var functionApiService = new PolyFunctionServiceImpl(host, port, httpClient, jsonParser); - logger.info("Parsing function {} in file {}.", functionName, file.getAbsolutePath()); - var polyFunction = javaParserService.parseFunction(mavenService.getSourceFolders(), mavenService.getJarSources(), file, functionName, description, context); - logger.info("Poly function {}({}) parsed.", polyFunction.getName(), polyFunction.getArguments().stream().map(PolyFunctionArgument::getType).collect(joining(", "))); - logger.info("Deploying function."); - logger.debug("Target URL is {}.", host); - deployFunction(polyFunction, functionApiService); - logger.info("Function deployed successfully."); + @Override + public void execute(String host, Integer port) { + var classLoader = getMavenService().getProjectClassLoader(); + BiConsumer addFunction = (Method method, File file) -> { + logger.debug("Setting up the Java parser service."); + logger.debug("Setting up class loader for all relevant places in the project."); + JavaParserService javaParserService = new JavaParserServiceImpl(classLoader, getJsonParser()); + logger.debug("Setting up HTTP service to access the Function API in Poly."); + logger.info("Parsing function {} in file {}.", method.getName(), file.getAbsolutePath()); + var polyFunction = javaParserService.parseFunction(getMavenService().getSourceFolders(), getMavenService().getJarSources(), file, method, description, context); + logger.info("Poly function {}({}) parsed.", polyFunction.getName(), polyFunction.getArguments().stream().map(PolyFunctionArgument::getType).collect(joining(", "))); + logger.info("Deploying function."); + logger.debug("Target URL is {}.", host); + deployFunction(polyFunction, new PolyFunctionServiceImpl(host, port, getHttpClient(), getJsonParser())); + logger.info("Function deployed successfully."); + }; + if (functionName == null && file == null) { + getMavenService().getPolyFunctionMethods().forEach(method -> { + Class declaringClass = method.getDeclaringClass(); + addFunction.accept(method, new File(format("src/main/java/%s/%s.java", declaringClass.getPackageName().replace(".", "/"), declaringClass.getSimpleName()))); + }); + } else { + if (functionName == null || file == null) { + throw new RuntimeException();// FIXME: Throw the appropriate exception + } + String path = file.getAbsolutePath(); + String className = path.substring(path.lastIndexOf("src/main/java/") + 14, path.length() - 5).replace("/", "."); + try { + Class clazz = classLoader.loadClass(className); + logger.info(clazz.getName()); + List methods = Arrays.stream(clazz.getDeclaredMethods()).filter(method -> method.getName().equalsIgnoreCase(functionName)).toList(); + if (methods.size() > 1) { + throw new RuntimeException(); // FIXME: Throw the appropriate exception + } + if (methods.isEmpty()) { + throw new RuntimeException(); // FIXME: Throw the appropriate exception. + } + methods.forEach(method -> addFunction.accept(method, file)); + } catch (ClassNotFoundException e) { + throw new QualifiedNameNotFoundException(className, e); + } + } } protected abstract void deployFunction(PolyFunction function, PolyFunctionService polyFunctionService); diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/mojo/GenerateSourcesMojo.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/mojo/GenerateSourcesMojo.java index b7766d57..0f33ebeb 100644 --- a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/mojo/GenerateSourcesMojo.java +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/mojo/GenerateSourcesMojo.java @@ -1,9 +1,6 @@ package io.polyapi.plugin.mojo; -import io.polyapi.commons.api.http.HttpClient; -import io.polyapi.commons.api.http.TokenProvider; -import io.polyapi.commons.api.json.JsonParser; import io.polyapi.commons.api.service.file.FileService; import io.polyapi.commons.internal.file.FileServiceImpl; import io.polyapi.plugin.model.specification.Context; @@ -11,9 +8,8 @@ import io.polyapi.plugin.model.specification.function.CustomFunctionSpecification; import io.polyapi.plugin.model.specification.function.FunctionSpecification; import io.polyapi.plugin.model.specification.variable.ServerVariableSpecification; -import io.polyapi.plugin.service.schema.JsonSchemaParser; -import io.polyapi.plugin.service.MavenService; import io.polyapi.plugin.service.SpecificationServiceImpl; +import io.polyapi.plugin.service.schema.JsonSchemaParser; import io.polyapi.plugin.service.template.PolyHandlebars; import io.polyapi.plugin.service.visitor.CodeGenerationVisitor; import lombok.Setter; @@ -47,23 +43,23 @@ public class GenerateSourcesMojo extends PolyApiMojo { @Override - public void execute(String host, Integer port, TokenProvider tokenProvider, HttpClient httpClient, JsonParser jsonParser, MavenService mavenService) { + public void execute(String host, Integer port) { this.fileService = new FileServiceImpl(new PolyHandlebars(), overwrite); this.jsonSchemaParser = new JsonSchemaParser(); - var specifications = new SpecificationServiceImpl(host, port, httpClient, jsonParser).getJsonSpecs(); + var specifications = new SpecificationServiceImpl(host, port, getHttpClient(), getJsonParser()).getJsonSpecs(); var context = new HashMap(); // @FIXME: Are we sure we want to set this ID at this level? context.put("clientId", UUID.randomUUID().toString()); context.put("host", host); context.put("port", port); - context.put("apiKey", tokenProvider.getToken()); + context.put("apiKey", getTokenProvider().getToken()); // FIXME: We should remove the ClientInfo class. context.put("apiBaseUrl", format("%s:%s", host, port)); context.put("packageName", "io.polyapi"); fileService.createClassFileWithDefaultPackage("ClientInfo", "clientInfo", context); fileService.createFileFromTemplate(new File("target/generated-resources/poly.properties"), "poly.properties", context); - fileService.createFileWithContent(new File(new File("target/.poly"), "specs.json"), jsonParser.toJsonString(specifications)); + fileService.createFileWithContent(new File(new File("target/.poly"), "specs.json"), getJsonParser().toJsonString(specifications)); writeContext("Poly", specifications, FunctionSpecification.class); writeContext("Vari", specifications, ServerVariableSpecification.class); logger.info("Sources generated correctly."); diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/mojo/PolyApiMojo.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/mojo/PolyApiMojo.java index 42d0cfe2..f66cb7b6 100644 --- a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/mojo/PolyApiMojo.java +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/mojo/PolyApiMojo.java @@ -8,7 +8,6 @@ import io.polyapi.commons.internal.http.HardcodedTokenProvider; import io.polyapi.commons.internal.json.JacksonJsonParser; import io.polyapi.plugin.error.PolyApiMavenPluginException; -import io.polyapi.plugin.mojo.validation.Validator; import io.polyapi.plugin.service.MavenService; import lombok.Setter; import okhttp3.OkHttpClient; @@ -22,10 +21,9 @@ import org.slf4j.LoggerFactory; import java.io.IOException; -import java.util.Optional; -import java.util.function.Consumer; -import static java.lang.String.format; +import static io.polyapi.plugin.mojo.validation.Validator.validateNotEmpty; +import static io.polyapi.plugin.mojo.validation.Validator.validatePortFormat; import static java.nio.charset.Charset.defaultCharset; import static java.util.concurrent.TimeUnit.MINUTES; @@ -44,43 +42,42 @@ public abstract class PolyApiMojo extends AbstractMojo { @Parameter(property = "apiKey") private String apiKey; + private MavenService mavenService; + private TokenProvider tokenProvider; + private HttpClient httpClient; + private JsonParser jsonParser; @Override public void execute() throws MojoExecutionException, MojoFailureException { try { logger.debug("Setting up Maven service."); - MavenService mavenService = Optional.ofNullable(project).map(MavenService::new).orElse(null); - logger.info(mavenService == null ? "No Maven project." : format("Maven service set. Targeting artifact %s.%s:%s.", project.getGroupId(), project.getArtifactId(), project.getVersion())); - - Optional.ofNullable(mavenService).ifPresent(getPropertyFromPlugin("host", host, this::setHost)); - Validator.validateNotEmpty("host", host); - - Optional.ofNullable(mavenService).ifPresent(getPropertyFromPlugin("port", port, this::setPort)); - Validator.validatePortFormat("port", port); - - Optional.ofNullable(mavenService).ifPresent(getPropertyFromPlugin("apiKey", apiKey, this::setApiKey)); - Validator.validateNotEmpty("apiKey", apiKey); - TokenProvider tokenProvider = new HardcodedTokenProvider(apiKey); - execute(host, Integer.valueOf(port), tokenProvider, new DefaultHttpClient(new OkHttpClient.Builder() - .connectTimeout(10, MINUTES) - .readTimeout(10, MINUTES) - .writeTimeout(10, MINUTES) - .build(), - tokenProvider), - new JacksonJsonParser(), - mavenService); + mavenService = new MavenService(project); + mavenService.getPropertyFromPlugin("host", host, this::setHost); + mavenService.getPropertyFromPlugin("port", port, this::setPort); + validatePortFormat("port", port); + mavenService.getPropertyFromPlugin("apiKey", apiKey, this::setApiKey); + validateNotEmpty("apiKey", apiKey); + tokenProvider = new HardcodedTokenProvider(apiKey); + httpClient = new DefaultHttpClient(new OkHttpClient.Builder() + .connectTimeout(10, MINUTES) + .readTimeout(10, MINUTES) + .writeTimeout(10, MINUTES) + .build(), + tokenProvider); + jsonParser = new JacksonJsonParser(); + execute(host, Integer.valueOf(port)); } catch (PolyApiMavenPluginException e) { logger.error("An exception occurred during the plugin execution.", e); throw new MojoFailureException(e); } catch (UnexpectedHttpResponseException e) { logger.error("An unexpected HTTP response code {} was returned from the server.", e.getResponse().statusCode()); - if (logger.isTraceEnabled()) { + //if (logger.isTraceEnabled()) { try { - logger.trace("Response from server is: {}", IOUtils.toString(e.getResponse().body(), defaultCharset())); + logger.info("Response from server is: {}", IOUtils.toString(e.getResponse().body(), defaultCharset())); } catch (IOException ex) { throw new MojoExecutionException(ex); } - } + //} throw new MojoFailureException(e); } catch (RuntimeException e) { logger.error("An unexpected exception occurred during the plugin execution.", e); @@ -88,10 +85,21 @@ public void execute() throws MojoExecutionException, MojoFailureException { } } - private Consumer getPropertyFromPlugin(String propertyName, String property, Consumer callback) { - logger.debug("Retrieving property 'port'."); - return mavenService -> mavenService.getPropertyFromPlugin(propertyName, property, callback); + protected MavenService getMavenService() { + return this.mavenService; + } + + protected TokenProvider getTokenProvider() { + return tokenProvider; + } + + protected HttpClient getHttpClient() { + return httpClient; + } + + protected JsonParser getJsonParser() { + return jsonParser; } - protected abstract void execute(String host, Integer port, TokenProvider tokenProvider, HttpClient httpClient, JsonParser jsonParser, MavenService mavenService); + protected abstract void execute(String host, Integer port); } diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/mojo/function/DeployFunctionsMojo.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/mojo/function/DeployFunctionsMojo.java new file mode 100644 index 00000000..fbc27258 --- /dev/null +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/mojo/function/DeployFunctionsMojo.java @@ -0,0 +1,63 @@ +package io.polyapi.plugin.mojo.function; + +import io.polyapi.commons.api.error.http.HttpResponseException; +import io.polyapi.plugin.error.deploy.DeploymentWrapperException; +import io.polyapi.plugin.model.function.PolyFunctionMetadata; +import io.polyapi.plugin.mojo.PolyApiMojo; +import io.polyapi.plugin.service.PolyFunctionService; +import io.polyapi.plugin.service.PolyFunctionServiceImpl; +import org.apache.commons.io.IOUtils; +import org.apache.maven.plugins.annotations.Mojo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static org.apache.maven.plugins.annotations.ResolutionScope.COMPILE_PLUS_RUNTIME; + +@Mojo(name = "deploy-functions", requiresDependencyResolution = COMPILE_PLUS_RUNTIME) +public class DeployFunctionsMojo extends PolyApiMojo { + private static final Logger logger = LoggerFactory.getLogger(DeployFunctionsMojo.class); + + @Override + protected void execute(String host, Integer port) { + PolyFunctionService polyFunctionService = new PolyFunctionServiceImpl(host, port, getHttpClient(), getJsonParser(), getMavenService().getProjectClassLoader()); + logger.info("Initiating the deployment of functions."); + Set polyFunctions = getMavenService().scanPolyFunctions(); + Map exceptions = new HashMap<>(); + polyFunctions.forEach(polyFunctionMetadata -> { + logger.debug("Discovered {} function '{}' on context '{}'.", polyFunctionMetadata.getTypedType(), polyFunctionMetadata.name(), polyFunctionMetadata.context()); + if (polyFunctionMetadata.isDeployable()) { + try { + String id = polyFunctionService.deploy(polyFunctionMetadata); + logger.info("Deployed {} function '{}' on context '{}' with id '{}'", polyFunctionMetadata.getTypedType(), polyFunctionMetadata.name(), polyFunctionMetadata.context(), id); + logger.debug("Function can be accessed at {}:{}/functions/{}/{}", host, port, polyFunctionMetadata.getTypedType(), id); + } catch (HttpResponseException e) { + exceptions.put(polyFunctionMetadata, e); + } + } else { + logger.warn("Deployment of {} function '{}' on context '{}' skipped.", polyFunctionMetadata.getTypedType(), polyFunctionMetadata.name(), polyFunctionMetadata.context()); + } + }); + if (exceptions.isEmpty()) { + logger.info("Deployment of {} functions complete.", polyFunctions.size()); + } else { + logger.error("{} Errors occurred while deploying a total of {} functions.", exceptions.size(), polyFunctions.stream().filter(PolyFunctionMetadata::isDeployable).count()); + exceptions.forEach((polyFunctionMetadata, exception) -> { + logger.debug("{} occurred while deploying {} function '{}' on context '{}'. Exception message is '{}'.", exception.getClass(), polyFunctionMetadata.getTypedType(), polyFunctionMetadata.name(), polyFunctionMetadata.context(), Optional.ofNullable(exception.getMessage()).orElse("No message")); + if (exception instanceof HttpResponseException) { + try { + logger.info(IOUtils.toString(HttpResponseException.class.cast(exception).getResponse().body())); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }); + throw new DeploymentWrapperException(exceptions.values()); + } + } +} diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/JavaParserService.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/JavaParserService.java index 8980a089..8a1f9107 100644 --- a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/JavaParserService.java +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/JavaParserService.java @@ -1,12 +1,16 @@ package io.polyapi.plugin.service; import io.polyapi.plugin.model.function.PolyFunction; +import io.polyapi.plugin.model.function.PolyFunctionMetadata; import java.io.File; +import java.lang.reflect.Method; import java.util.List; public interface JavaParserService { + PolyFunction parseFunction(PolyFunctionMetadata polyFunctionMetadata); - PolyFunction parseFunction(List sourceRoots, List jarPaths, File file, String functionName, String description, String context); + @Deprecated + PolyFunction parseFunction(List sourceRoots, List jarPaths, File file, Method method, String description, String context); } diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/JavaParserServiceImpl.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/JavaParserServiceImpl.java index 60d24c42..4bb252bd 100644 --- a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/JavaParserServiceImpl.java +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/JavaParserServiceImpl.java @@ -3,51 +3,58 @@ import com.fasterxml.jackson.databind.JavaType; import com.github.javaparser.JavaParser; import com.github.javaparser.ParserConfiguration; -import com.github.javaparser.ast.Node; import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.PackageDeclaration; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.body.TypeDeclaration; +import com.github.javaparser.ast.comments.JavadocComment; import com.github.javaparser.ast.expr.Expression; import com.github.javaparser.ast.expr.MethodCallExpr; +import com.github.javaparser.ast.expr.Name; import com.github.javaparser.ast.expr.NameExpr; import com.github.javaparser.ast.stmt.BlockStmt; import com.github.javaparser.ast.stmt.ExpressionStmt; import com.github.javaparser.ast.stmt.ReturnStmt; +import com.github.javaparser.ast.type.Type; import com.github.javaparser.resolution.declarations.ResolvedParameterDeclaration; import com.github.javaparser.resolution.types.ResolvedType; -import com.github.javaparser.resolution.types.ResolvedVoidType; import com.github.javaparser.symbolsolver.JavaSymbolSolver; -import com.github.javaparser.symbolsolver.resolution.typesolvers.ClassLoaderTypeSolver; -import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver; -import com.github.javaparser.symbolsolver.resolution.typesolvers.JarTypeSolver; -import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver; -import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.*; import io.polyapi.commons.api.json.JsonParser; +import io.polyapi.commons.api.model.PolyGeneratedClass; import io.polyapi.plugin.error.PolyApiMavenPluginException; import io.polyapi.plugin.error.classloader.QualifiedNameNotFoundException; import io.polyapi.plugin.model.TypeData; +import io.polyapi.plugin.model.function.CodeObject; import io.polyapi.plugin.model.function.PolyFunction; import io.polyapi.plugin.model.function.PolyFunctionArgument; +import io.polyapi.plugin.model.function.PolyFunctionMetadata; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.lang.reflect.Method; import java.util.*; import static com.fasterxml.jackson.databind.type.TypeFactory.defaultInstance; import static com.github.javaparser.ast.Modifier.Keyword.PUBLIC; import static java.lang.Character.isUpperCase; +import static java.lang.String.format; +import static java.util.stream.Collectors.joining; import static java.util.stream.IntStream.range; public class JavaParserServiceImpl implements JavaParserService { private static final Logger logger = LoggerFactory.getLogger(JavaParserServiceImpl.class); private final JsonParser jsonParser; private final ClassLoader classLoader; + private final JavaParser javaParser; public JavaParserServiceImpl(ClassLoader classLoader, JsonParser jsonParser) { this.classLoader = classLoader; + this.javaParser = new JavaParser(new ParserConfiguration().setSymbolResolver(new JavaSymbolSolver(new ClassLoaderTypeSolver(classLoader)))); this.jsonParser = jsonParser; } @@ -84,7 +91,73 @@ private java.lang.reflect.Type toMap(ResolvedType type) { } @Override - public PolyFunction parseFunction(List sourceRoots, List jarPaths, File file, String functionName, String description, String context) { + public PolyFunction parseFunction(PolyFunctionMetadata polyFunctionMetadata) { + var compilationUnit = javaParser.parse(polyFunctionMetadata.sourceCode()).getResult().orElseThrow(); + return compilationUnit.getTypes().stream() + .map(TypeDeclaration::getMethods) + .flatMap(List::stream) + .filter(methodDeclaration -> format("%s(%s)", methodDeclaration.getName(), methodDeclaration.getSignature().getParameterTypes().stream().map(Type::asString).collect(joining(", "))).equals(polyFunctionMetadata.signature())) + .peek(methodDeclaration -> logger.debug("Found matching method declaration: {}", methodDeclaration.getSignature())) + .map(methodDeclaration -> { + CodeObject codeObject = new CodeObject(); + codeObject.setPackageName(compilationUnit.getPackageDeclaration().map(PackageDeclaration::getName).map(Name::asString).orElse("")); + codeObject.setClassName(compilationUnit.getType(0).getNameAsString()); + codeObject.setMethodName(methodDeclaration.getNameAsString()); + + var resolvedMethodDeclaration = methodDeclaration.resolve(); + ClassOrInterfaceDeclaration classOrInterfaceDeclaration = compilationUnit.getType(0).asClassOrInterfaceDeclaration() + .addAnnotation(PolyGeneratedClass.class); + logger.debug("Creating PolyFunction from method declaration: {}", resolvedMethodDeclaration.getName()); + var function = new PolyFunction(); + function.setContext(polyFunctionMetadata.context()); + methodDeclaration.getJavadocComment().map(JavadocComment::asString).ifPresent(function::setDescription); + function.setArguments(new ArrayList<>()); + logger.trace("Parsing return type for {}.", resolvedMethodDeclaration.getName()); + var typeData = parse(resolvedMethodDeclaration.getReturnType()); + function.setName(resolvedMethodDeclaration.getName()); + function.setReturnType(typeData.name()); + logger.trace("Adding JSon schema to return type."); + if (!resolvedMethodDeclaration.getReturnType().isVoid()) { + function.setReturnTypeSchema(jsonParser.parseString(typeData.jsonSchema(), defaultInstance().constructMapType(HashMap.class, String.class, Object.class))); + } + logger.trace("Parsing parameters."); + range(0, resolvedMethodDeclaration.getNumberOfParams()).boxed().map(resolvedMethodDeclaration::getParam) + .peek(param -> logger.trace(" Parsing parameter {}.", param.getName())) + .map(param -> { + logger.debug("Adding parameter '{}' to execute method.", param.getName()); + logger.trace("Converting to PolyFunctionArgument."); + var argument = new PolyFunctionArgument(); + argument.setKey(param.getName()); + argument.setName(param.getName()); + var argumentTypeData = parse(param.getType()); + switch (param.getType().asReferenceType().getQualifiedName()) { + case "java.lang.Integer", "java.lang.Long", "java.lang.Number", "java.lang.Double", "java.lang.Float", "java.lang.Short", "java.lang.Byte" -> + argument.setType("number"); + case "java.lang.Boolean" -> argument.setType("boolean"); + case "java.lang.String", "java.lang.Character" -> argument.setType("string"); + default -> { + argument.setType("object"); + argument.setTypeSchema(argumentTypeData.jsonSchema()); + } + } + return argument; + }) + .forEach(function.getArguments()::add); + codeObject.setParams(function.getArguments().stream().map(PolyFunctionArgument::getType).collect(joining(","))); + logger.trace("Parsed {} parameters.", function.getArguments().size()); + codeObject.setCode(compilationUnit.toString()); + function.setCode(jsonParser.toJsonString(codeObject)); + logger.info(function.getCode()); + function.setRequirements(polyFunctionMetadata.dependencies()); + return function; + }) + .findFirst() + .orElse(null); + } + + @Deprecated + @Override + public PolyFunction parseFunction(List sourceRoots, List jarPaths, File file, Method method, String description, String context) { try { logger.debug("Setting up a combined type solvers."); var combinedTypeSolver = new CombinedTypeSolver(); @@ -118,7 +191,7 @@ public PolyFunction parseFunction(List sourceRoots, List jarPaths, .map(TypeDeclaration::resolve) .forEach(resolvedCompilationUnit -> { resolvedCompilationUnit.getDeclaredMethods().stream() - .filter(methodDeclaration -> methodDeclaration.getName().equals(functionName)) + .filter(methodDeclaration -> methodDeclaration.getName().equals(method.getName())) .peek(methodDeclaration -> logger.debug("Found matching method declaration: {}", methodDeclaration.getSignature())) .forEach(methodDeclaration -> { logger.debug("Creating PolyFunction from method declaration: {}", methodDeclaration.getName()); @@ -180,13 +253,12 @@ public PolyFunction parseFunction(List sourceRoots, List jarPaths, }); if (functions.isEmpty()) { - throw new PolyApiMavenPluginException("No function with name " + functionName + " found in file: " + file.getAbsolutePath()); + throw new PolyApiMavenPluginException("No function with name " + method.getName() + " found in file: " + file.getAbsolutePath()); } else if (functions.size() > 1) { - throw new PolyApiMavenPluginException("More than one function with name " + functionName + " found in file: " + file.getAbsolutePath()); + throw new PolyApiMavenPluginException("More than one function with name " + method.getName() + " found in file: " + file.getAbsolutePath()); } return functions.get(0); - } catch ( - FileNotFoundException e) { + } catch (FileNotFoundException e) { throw new PolyApiMavenPluginException("Error parsing file", e); } diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/MavenService.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/MavenService.java index dfcde8c5..6e4ee833 100644 --- a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/MavenService.java +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/MavenService.java @@ -1,28 +1,52 @@ package io.polyapi.plugin.service; +import io.polyapi.commons.api.model.PolyFunction; +import io.polyapi.commons.api.model.PolyGeneratedClass; +import io.polyapi.commons.api.model.RequiredDependencies; +import io.polyapi.commons.api.model.RequiredDependency; import io.polyapi.plugin.error.PolyApiMavenPluginException; +import io.polyapi.plugin.error.validation.InvalidPropertyException; +import io.polyapi.plugin.error.validation.KeywordUseException; import io.polyapi.plugin.error.validation.PropertyNotFoundException; +import io.polyapi.plugin.model.function.PolyFunctionMetadata; import org.apache.maven.artifact.DependencyResolutionRequiredException; import org.apache.maven.model.Plugin; import org.apache.maven.model.PluginExecution; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.xml.Xpp3Dom; +import org.reflections.Reflections; +import org.reflections.util.ConfigurationBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.lang.model.SourceVersion; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.Type; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; -import java.util.List; -import java.util.Objects; +import java.util.*; import java.util.function.Consumer; +import java.util.regex.Pattern; import java.util.stream.Stream; +import static java.lang.String.format; +import static java.util.function.Predicate.not; +import static java.util.regex.Pattern.compile; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; import static java.util.stream.Stream.concat; +import static javax.lang.model.SourceVersion.isKeyword; +import static org.reflections.scanners.Scanners.MethodsAnnotated; public class MavenService { private static final Logger logger = LoggerFactory.getLogger(MavenService.class); + private static final String FUNCTION_NAME_PATTERN = "^[a-z][\\w$_]+$"; + private static final String CONTEXT_PATTERN = "^[a-z][\\w$_.]*[\\w$_]$"; private final MavenProject project; public MavenService(MavenProject project) { @@ -65,7 +89,7 @@ public String getPropertyFromPlugin(String pluginGroupId, String pluginArtifactI .orElseThrow(() -> new PropertyNotFoundException(propertyName)); } - public ClassLoader getProjectClassLoader() { + public URLClassLoader getProjectClassLoader() { try { return new URLClassLoader(concat(concat(project.getCompileClasspathElements().stream(), project.getRuntimeClasspathElements().stream()), @@ -82,14 +106,14 @@ public ClassLoader getProjectClassLoader() { } }) .toArray(URL[]::new), - ClassLoader.getSystemClassLoader()); + MavenService.class.getClassLoader()); } catch (DependencyResolutionRequiredException e) { throw new RuntimeException(e); } } public List getSourceFolders() { - return Stream.concat(project.getCompileSourceRoots().stream(), Stream.of(project.getBasedir() + "/target/generated-sources")) + return concat(project.getCompileSourceRoots().stream(), Stream.of(project.getBasedir() + "/target/generated-sources")) .peek(sourceRoot -> logger.debug(" Retrieving source root '{}'", sourceRoot)) .map(File::new) .filter(File::exists) @@ -107,4 +131,85 @@ public List getJarSources() { throw new PolyApiMavenPluginException(e); } } + + public Set scanPolyFunctions() { + logger.info("Scanning the project for functions annotated with {}}.", PolyFunction.class.getName()); + URLClassLoader projectClassLoader = getProjectClassLoader(); + Reflections reflections = new Reflections(new ConfigurationBuilder() + .addClassLoaders(projectClassLoader) + .addScanners(MethodsAnnotated) + .addUrls(projectClassLoader.getURLs())); + logger.debug("Reflections URLS: {}", reflections.getConfiguration().getUrls().size()); + Set methods = reflections.getMethodsAnnotatedWith(PolyFunction.class); + logger.info("Found {} methods to convert.", methods.size()); + + List.of(RequiredDependency.class, RequiredDependencies.class).forEach(annotation -> + reflections.getMethodsAnnotatedWith(annotation).stream() + .filter(not(methods::contains)) + .forEach(misusedMethod -> { + logger.warn("Method {} is annotated with {} but is ignored as it needs to be annotated with {} to be scanned.", misusedMethod, misusedMethod.getAnnotation(annotation).getClass().getSimpleName(), PolyFunction.class.getSimpleName()); + })); + return methods.stream() + .filter(not(method -> method.getDeclaringClass().isAnnotationPresent(PolyGeneratedClass.class))) + .map(method -> { + try { + logger.info("Processing method '{}'.", method); + PolyFunction polyFunction = method.getAnnotation(PolyFunction.class); + logger.debug("Retrieving function name."); + String functionName = Optional.ofNullable(polyFunction.name()).filter(not(String::isBlank)).orElseGet(method::getName); + if (!functionName.matches(FUNCTION_NAME_PATTERN)) { + throw new InvalidPropertyException("functionName", functionName, method, FUNCTION_NAME_PATTERN); + } + if (isKeyword(functionName.trim())) { + throw new KeywordUseException("functionName", functionName, method, functionName); + } + Class declaringClass = method.getDeclaringClass(); + logger.debug("Retrieving context."); + String context = Optional.ofNullable(polyFunction.context()).filter(not(String::isEmpty)).orElseGet(declaringClass::getPackageName); + if (!context.matches(CONTEXT_PATTERN)) { + throw new InvalidPropertyException("context", context, method, CONTEXT_PATTERN); + } + Optional.of(Arrays.stream(context.split("\\.")).filter(SourceVersion::isKeyword).toArray(String[]::new)).filter(array -> array.length > 0).ifPresent(keywords -> { + throw new KeywordUseException("context", context, method, keywords); + }); + logger.debug("Retrieving required dependencies."); + Pattern pattern = compile(Optional.of(concat(Optional.ofNullable(method.getAnnotation(RequiredDependencies.class)).map(RequiredDependencies::value).stream().flatMap(Arrays::stream), Optional.ofNullable(method.getAnnotation(RequiredDependency.class)).stream()) + .map(requiredDependency -> format("%s:%s:%s", requiredDependency.groupId(), requiredDependency.artifactId(), requiredDependency.version())) + .collect(joining("|"))) + .filter(not(String::isEmpty)) + .orElse("(?=a)b")); + logger.debug("Pattern used to match required dependencies is: {}", pattern.pattern()); + List requiredDependencies = project.getDependencies().stream() + .map(dependency -> format("%s:%s:%s", dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion())) + .filter(pattern.asPredicate()) + .toList(); + logger.debug("Required dependencies found: {}", requiredDependencies); + return new PolyFunctionMetadata(functionName, + format("%s(%s)", method.getName(), Arrays.stream(method.getParameters()).map(Parameter::getType).map(Class::getName).collect(joining(", "))), + polyFunction.type(), + new FileInputStream(format("src/main/java/%s/%s.java", + declaringClass.getPackageName().replace(".", "/"), + declaringClass.getSimpleName())), + context, + requiredDependencies, + polyFunction.isDeployable()); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); // FIXME: Throw the appropriate exception. + } + }) + .collect(toSet()); + } + + public Set getPolyFunctionMethods() { + logger.info("Scanning projects for methods annotated with @PolyFunction."); + URLClassLoader projectClassLoader = getProjectClassLoader(); + Reflections reflections = new Reflections(new ConfigurationBuilder() + .addClassLoaders(projectClassLoader) + .addScanners(MethodsAnnotated) + .addUrls(projectClassLoader.getURLs())); + logger.info("Reflections URLS: {}", reflections.getConfiguration().getUrls().size()); + Set methods = reflections.getMethodsAnnotatedWith(PolyFunction.class); + logger.info("Methods: {}", methods.size()); + return methods; + } } diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/PolyFunctionService.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/PolyFunctionService.java index 8870da25..0c2de6b4 100644 --- a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/PolyFunctionService.java +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/PolyFunctionService.java @@ -1,10 +1,13 @@ package io.polyapi.plugin.service; import io.polyapi.plugin.model.function.PolyFunction; +import io.polyapi.plugin.model.function.PolyFunctionMetadata; public interface PolyFunctionService { PolyFunction postServerFunction(PolyFunction polyFunction); PolyFunction postClientFunction(PolyFunction polyFunction); + + String deploy(PolyFunctionMetadata polyFunctionMetadata); } diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/PolyFunctionServiceImpl.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/PolyFunctionServiceImpl.java index 759da4c3..cca07557 100644 --- a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/PolyFunctionServiceImpl.java +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/PolyFunctionServiceImpl.java @@ -4,22 +4,45 @@ import io.polyapi.commons.api.json.JsonParser; import io.polyapi.commons.api.service.PolyApiService; import io.polyapi.plugin.model.function.PolyFunction; +import io.polyapi.plugin.model.function.PolyFunctionMetadata; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.util.HashMap; +import java.io.IOException; + +import static java.lang.String.format; public class PolyFunctionServiceImpl extends PolyApiService implements PolyFunctionService { + private static final Logger logger = LoggerFactory.getLogger(PolyFunctionServiceImpl.class); + private final JavaParserService javaParserService; + @Deprecated public PolyFunctionServiceImpl(String host, Integer port, HttpClient client, JsonParser jsonParser) { + this(host, port, client, jsonParser, null); + } + + public PolyFunctionServiceImpl(String host, Integer port, HttpClient client, JsonParser jsonParser, ClassLoader classLoader) { super(host, port, client, jsonParser); + this.javaParserService = new JavaParserServiceImpl(classLoader, jsonParser); } @Override public PolyFunction postServerFunction(PolyFunction polyFunction) { - return post("functions/server", new HashMap<>(), new HashMap<>(), polyFunction, PolyFunction.class); + return post("functions/server", polyFunction, PolyFunction.class); } @Override public PolyFunction postClientFunction(PolyFunction polyFunction) { - return post("functions/client", new HashMap<>(), new HashMap<>(), polyFunction, PolyFunction.class); + return post("functions/client", polyFunction, PolyFunction.class); + } + + @Override + public String deploy(PolyFunctionMetadata polyFunctionMetadata) { + logger.info("Deploying {} function '{}' on context '{}'.", polyFunctionMetadata.getTypedType(), polyFunctionMetadata.name(), polyFunctionMetadata.context()); + PolyFunction function = post(format("functions/%s", polyFunctionMetadata.getTypedType()), javaParserService.parseFunction(polyFunctionMetadata), PolyFunction.class); + logger.info("{} -> {}", polyFunctionMetadata, function.getCode()); + logger.info("Deployment of {} function '{}' on context'{}' successful.", polyFunctionMetadata.getTypedType(), polyFunctionMetadata.name(), polyFunctionMetadata.context()); + return function.getId(); } } diff --git a/pom.xml b/pom.xml index e72b9409..1be01209 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.polyapi polyapi-java - 0.2.6-SNAPSHOT + 0.3.0-SNAPSHOT pom parent-pom