All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.polyapi.plugin.service.MavenService Maven / Gradle / Ivy

There is a newer version: 0.15.3
Show newest version
package io.polyapi.plugin.service;

import io.polyapi.commons.api.model.PolyClientFunction;
import io.polyapi.commons.api.model.PolyFunctionAnnotationRecord;
import io.polyapi.commons.api.model.PolyGeneratedClass;
import io.polyapi.commons.api.model.PolyServerFunction;
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.PropertyNotFoundException;
import lombok.extern.slf4j.Slf4j;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.model.Build;
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 javax.lang.model.SourceVersion;
import java.io.File;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
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;

@Slf4j
public class MavenService {
    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) {
        this.project = project;
    }

    public void getPropertyFromPlugin(String propertyName, String currentValue, Consumer callback) {
        log.debug("Checking value of '{}' as an input parameter.", propertyName);
        if (currentValue == null) {
            log.debug("Parameter '{}' is empty. Attempting to retrieve it from plugin configuration.", propertyName);
            callback.andThen(value -> log.debug("Parameter '{}' value is '{}'.", propertyName, value))
                    .accept(getPropertyFromPlugin("io.polyapi.client", "library", propertyName));
        } else {
            log.debug("Parameter '{}' value is '{}'", propertyName, currentValue);
        }
    }

    public String getPropertyFromPlugin(String pluginGroupId, String pluginArtifactId, String propertyName) {
        log.debug("Scanning plugins.");
        List plugins = Optional.ofNullable(project).map(MavenProject::getBuild).map(Build::getPlugins).orElseGet(ArrayList::new);
        log.debug("Found {} plugins. Filtering by group ID matching '{}' and artifact ID matching '{}'.", plugins.size(), pluginGroupId, pluginArtifactId);
        return plugins.stream()
                .filter(plugin -> pluginGroupId.equals(plugin.getGroupId()))
                .filter(plugin -> pluginArtifactId.equals(plugin.getArtifactId()))
                .flatMap(plugin -> {
                    log.debug("Found match: {}.{}:{}.\nRetrieving executions.", plugin.getGroupId(), plugin.getArtifactId(), plugin.getVersion());
                    List executions = Optional.ofNullable(plugin.getExecutions()).orElseGet(ArrayList::new);
                    log.debug("Found {} executions.", executions.size());
                    return executions.stream();
                })
                .map(PluginExecution::getConfiguration)
                .filter(Objects::nonNull)
                .map(Xpp3Dom.class::cast)
                .flatMap(configuration -> {
                    log.debug("Found configuration within the execution. Retrieving children.");
                    Xpp3Dom[] children = Optional.ofNullable(configuration.getChildren()).orElse(new Xpp3Dom[]{});
                    log.debug("Found {} children properties.", children.length);
                    return Arrays.stream(children);
                })
                .map(Xpp3Dom::getValue)
                .findFirst()
                .orElseThrow(() -> new PropertyNotFoundException(propertyName));
    }

    public URLClassLoader getProjectClassLoader() {
        try {
            return new URLClassLoader(concat(concat(project.getCompileClasspathElements().stream(),
                            project.getRuntimeClasspathElements().stream()),
                    Stream.of(project.getBuild().getOutputDirectory()))
                    .map(File::new)
                    .filter(File::exists)
                    .map(File::toURI)
                    .map(uri -> {
                        try {
                            return uri.toURL();
                        } catch (MalformedURLException e) {
                            // FIXME: Throw appropriate exception.
                            throw new RuntimeException(e);
                        }
                    })
                    .toArray(URL[]::new),
                    MavenService.class.getClassLoader());
        } catch (DependencyResolutionRequiredException e) {
            throw new RuntimeException(e); // FIXME: Throw the appropriate exception.
        }
    }

    public Set scanPolyFunctions(Predicate filter) {
        log.info("Scanning the project for functions annotated with {} or {}.", PolyServerFunction.class.getName(), PolyClientFunction.class.getName());
        URLClassLoader projectClassLoader = getProjectClassLoader();
        Reflections reflections = new Reflections(new ConfigurationBuilder()
                .addClassLoaders(projectClassLoader)
                .addScanners(MethodsAnnotated)
                .addUrls(projectClassLoader.getURLs()));
        log.info("Reflections URLS: {}", reflections.getConfiguration().getUrls().size());
        Set methods = Stream.concat(reflections.getMethodsAnnotatedWith(PolyServerFunction.class).stream(),
                reflections.getMethodsAnnotatedWith(PolyClientFunction.class).stream())
                .filter(filter)
                .filter(not(method -> method.getDeclaringClass().isAnnotationPresent(PolyGeneratedClass.class)))
                .collect(toSet());
        log.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 -> log.warn("Method {} is annotated with {} but is ignored as it needs to be annotated with either {} or {} to be scanned.", misusedMethod, misusedMethod.getAnnotation(annotation).getClass().getSimpleName(), PolyServerFunction.class.getSimpleName(), PolyClientFunction.class.getSimpleName())));
        Set validatedMethods = methods.stream()
                .filter(method -> {
                    boolean result = true;
                    PolyFunctionAnnotationRecord polyFunction = PolyFunctionAnnotationRecord.createFrom(method);
                    log.debug("Validating function name.");
                    String functionName = Optional.ofNullable(polyFunction.name()).filter(not(String::isBlank)).orElseGet(method::getName);
                    if (!functionName.matches(FUNCTION_NAME_PATTERN)) {
                        log.error("Method '{}' skipped. Property 'functionName' with value '{}' doesn't match pattern '{}'.", method, functionName, FUNCTION_NAME_PATTERN);
                        result = false;
                    }
                    if (isKeyword(functionName.trim())) {
                        log.error("Method '{}' skipped. Property 'functionName' with value '{}' is a Java keyword.", method, functionName);
                        result = false;
                    }
                    return result;
                })
                .filter(method -> {
                    boolean result = true;
                    PolyFunctionAnnotationRecord polyFunction = PolyFunctionAnnotationRecord.createFrom(method);
                    log.debug("Validating context.");
                    String context = Optional.ofNullable(polyFunction.context()).filter(not(String::isEmpty)).orElseGet(method.getDeclaringClass()::getPackageName);
                    if (!context.matches(CONTEXT_PATTERN)) {
                        log.error("Method '{}' skipped. Property 'context' with value '{}' doesn't match pattern '{}'.", method, context, CONTEXT_PATTERN);
                        result = false;
                    }
                    String keywords = Arrays.stream(context.split("\\.")).filter(SourceVersion::isKeyword).collect(joining(","));
                    if (!keywords.isEmpty()) {
                        log.error("Method '{}' skipped. Property 'context' with value '{}' uses Java keywords '{}}'. Please rename the context accordingly.", method, context, keywords);
                        result = false;
                    }
                    return result;
                })
                .filter(method -> {
                    PolyFunctionAnnotationRecord polyFunction = PolyFunctionAnnotationRecord.createFrom(method);
                    boolean isDeployable = polyFunction.deployFunction();
                    if (!isDeployable) {
                        log.warn("Method '{}' skipped. Marked as not deployable.", method);
                    }
                    return isDeployable;
                })
                .collect(toSet());
        if (validatedMethods.size() < methods.size()) {
            log.warn("Only {} of {} methods are valid.", validatedMethods.size(), methods.size());
        }
        return validatedMethods;
    }

    public List getMatchingDependencies(List patterns) {
        log.debug("Retrieving required dependencies.");
        Pattern pattern = compile(Optional.of(String.join("|", patterns))
                .filter(not(String::isEmpty))
                .orElse("(?=a)b"));
        log.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();
        log.debug("Required dependencies found: {}", requiredDependencies);
        return requiredDependencies;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy