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

io.helidon.build.maven.snakeyaml.codegen.SnakeYAMLMojo Maven / Gradle / Ivy

There is a newer version: 4.0.15
Show newest version
/*
 * Copyright (c) 2020, 2021 Oracle and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.helidon.build.maven.snakeyaml.codegen;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.DiagnosticListener;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ParameterizedTypeTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreePathScanner;
import com.sun.source.util.Trees;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;

/**
 * Goal which generates code to help with parsing YAML and JSON using SnakeYAML.
 * 

* SnakeYAML does a lot of work converting between JSON or YAML and bean classes, but it needs * some help with information it cannot get from compiled class bytecode. This plug-in * generates that helper code. *

*

* It analyzes Java sources in a specified location and generates a SnakeYAML * {@code TypeDefinition} for each class. It also uses a second location to locate implementations of interfaces from the * first area for use in setting the impl classes in those {@code TypeDefinition} instances. *

*

* Often, builds will use the maven dependency plug-in to extract sources into those * two locations and then invoke the plug-in to analyze the code and generate the helper class. *

* */ @Mojo(name = "generate", requiresProject = true, defaultPhase = LifecyclePhase.GENERATE_SOURCES, requiresDependencyResolution = ResolutionScope.RUNTIME) public class SnakeYAMLMojo extends AbstractMojo { // //

// Here is an example from the maven dependency plug-in showing how to extract the sources so // they are available for this plug-in to consume: //

//
{@code
// 
//     [...]
//     
//         
//         
//             org.apache.maven.plugins
//             maven-dependency-plugin
//              
//                  
//                      unpack-openapi-interfaces
//                      
//                          unpack-dependencies
//                      
//                      generate-sources
//                      
//                          sources
//                          true
//                          ${openapi-interfaces-dir}
//                          org.eclipse.microprofile.openapi
//                          microprofile-openapi-api
//                          org/eclipse/microprofile/openapi/models/**/*.java
//                      
//                  
//                  
//                      unpack-openapi-impls
//                      
//                          unpack-dependencies
//                      
//                      generate-sources
//                      
//                          sources
//                          true
//                          ${openapi-impls-dir}
//                          io.smallrye
//                          smallrye-open-api
//                          io/smallrye/openapi/api/models/**/*.java
//                      
//                  
//              
//          
//     
//   
//   [...]
// 
// }
//
// 
// // private static final String PROPERTY_PREFIX = "snakeyamlgen."; private static final String PARAM_DUMP_FORMAT = "Code generation for SnakeYAML parsing helper:%n" + " %s:%n" + " Read from directory %s%n" + " Matching %s%n" + " Excluding %s%n"; private static final String DEFAULT_OUTPUT_CLASS_NAME = "io.helidon.snakeyaml.SnakeYAMLParserHelper"; private static final String OS_NAME = System.getProperty("os.name").toLowerCase(); private final Map types = new HashMap<>(); private final Map implementations = new HashMap<>(); private final Set imports = preloadedImports(); private final Map> interfaces = new HashMap<>(); @Parameter(defaultValue = "${project}", readonly = true, required = true) private MavenProject mavenProject; /** * Directory to contain the generated class source file. */ @Parameter(property = PROPERTY_PREFIX + "outputDirectory", defaultValue = "${project.build.directory}/generated-sources", required = true) private File outputDirectory; /** * Fully-qualified name for the class to generate. */ @Parameter(property = PROPERTY_PREFIX + "outputClass", defaultValue = DEFAULT_OUTPUT_CLASS_NAME, required = true) private String outputClass; /** * Configuration for compiling the interfaces for which SnakeYAML will need to parse. */ @Parameter(property = PROPERTY_PREFIX + "interfacesConfig", required = true) private CompilerConfig interfacesConfig; /** * Configuration for compiling the implementations SnakeYAML will use to instantiate parsed interfaces. */ @Parameter(property = PROPERTY_PREFIX + "implementationsConfig", required = true) private CompilerConfig implementationsConfig; /** * Controls debug output from the plug-in. */ @Parameter(property = PROPERTY_PREFIX + "debug", defaultValue = "false") private boolean debug; /** * Prefix common to all implementation classes submitted for analysis. Used for grouping import statements in the generated * code. */ @Parameter(property = PROPERTY_PREFIX + "implementationPrefix", required = true) private String implementationPrefix; /** * Prefix common to all interfaces submitted for analysis. Used for grouping import statements in the generated code. */ @Parameter(property = PROPERTY_PREFIX + "interfacePrefix", required = true) private String interfacePrefix; /** * Prescribes how the plug-in finds Java classes to analyze for either the interfaces or the * implementations. */ public static class CompilerConfig { /** * Directory where the source files to be compiled are located. */ private String inputDirectory = null; // defaults to "interfaces" or "implementations" /** * Java file path expressions indicating which files to include in analysis. */ private List includes = defaultIncludes(); /** * Java file path expressions indicating which files to exclude from analysis. */ private List excludes = Collections.emptyList(); private static List defaultIncludes() { final List result = new ArrayList<>(); result.add("**/*.java"); return result; } } /** * Sets the compiler config for analyzing the interfaces. * * @param config CompilerConfig for analyzing interfaces */ public void setInterfacesConfig(CompilerConfig config) { this.interfacesConfig = fillInConfig(config, "interfaces"); } /** * Sets the compiler config for analyzing the implementations. * * @param config CompilerConfig for analyzing implementations */ public void setImplementationsConfig(CompilerConfig config) { this.implementationsConfig = fillInConfig(config, "implementations"); } private CompilerConfig fillInConfig(CompilerConfig config, String defaultInputDirectory) { if (config.inputDirectory == null) { config.inputDirectory = mavenProject.getBuild().getOutputDirectory().concat("/").concat(defaultInputDirectory); } return config; } /** * Runs the goal, analyzing interfaces and implementations and generating the helper class. * * @throws MojoExecutionException in case of errors while executing the goal */ public void execute() throws MojoExecutionException { validateParameters(); dumpParams(); File f = outputDirectory; if (!f.exists()) { if (f.mkdirs()) { debugLog(() -> "Created output directory " + f.getAbsolutePath()); } else { debugLog(() -> "Using existing output directory " + f.getAbsolutePath()); } } try { analyzeInterfaces(types, imports); addImportsForTypes(types, imports); analyzeImplementations(implementations, imports, interfaces); associateImplementationsWithInterfaces(types, interfaces); generateHelperClass(types, imports); addGeneratedCodeToCompilation(); } catch (Throwable e) { throw new MojoExecutionException("Error compiling and analyzing source files", e); } } Set imports() { return imports; } Map> interfaces() { return interfaces; } Map types() { return types; } private void debugLog(Supplier msgSupplier) { if (getLog().isDebugEnabled() || debug) { getLog().debug(msgSupplier.get()); } } private void dumpParams() { if (!getLog().isDebugEnabled() && !debug) { return; } debugLog(() -> String.format("Output directory: %s%nOutput class: %s%n", outputDirectory, outputClass)); debugLog(() -> dumpConfig(interfacesConfig, "interfaces")); debugLog(() -> dumpConfig(implementationsConfig, "implementations")); } private String dumpConfig(CompilerConfig config, String category) { return String.format(PARAM_DUMP_FORMAT, category, config.inputDirectory, config.includes, config.excludes); } /** * An import required in the generated source. */ static class Import implements Comparable { private String name; private boolean isStatic; Import(String name, boolean isStatic) { this.name = name; this.isStatic = isStatic; } Import(Class c) { this(c.getName(), false); } String name() { return name; } boolean isStatic() { return isStatic; } @Override public String toString() { return "Import{" + "name='" + name + '\'' + ", isStatic=" + isStatic + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Import anImport = (Import) o; return isStatic == anImport.isStatic && name.equals(anImport.name); } @Override public int hashCode() { return Objects.hash(name, isStatic); } @Override public int compareTo(Import o) { if (this == o) { return 0; } if (o == null) { return 1; } int nameResult = name.compareTo(o.name); if (nameResult != 0) { return nameResult; } return isStatic ? (o.isStatic ? 0 : 1) : (o.isStatic ? -1 : 0); } } /** * Preloads the set of imports with items needed by the code in the class template so * we do not accidentally repeat imports which are triggered by the classes analyzed. * * @return Set of Import instances used by hard-coded methods in the template */ private static Set preloadedImports() { Set result = new HashSet<>(); result.add(new Import(java.util.List.class)); result.add(new Import(java.util.HashMap.class)); result.add(new Import(java.util.Map.class)); result.add(new Import(java.util.Set.class)); result.add(new Import(java.util.function.Function.class)); result.add(new Import(java.util.function.BiFunction.class)); result.add(new Import(java.util.AbstractMap.class)); return result; } private void validateParameters() throws MojoExecutionException { validateInputDirectory(interfacesConfig.inputDirectory); validateInputDirectory(implementationsConfig.inputDirectory); } private void validateInputDirectory(String dir) throws MojoExecutionException { Path inputDirPath = resolveDirectory(dir); if (!Files.exists(inputDirPath)) { throw new MojoExecutionException("Cannot find specified inputDirectory " + inputDirPath.toString()); } } private void analyzeInterfaces(Map types, Set imports) throws IOException { analyzeClasses(types, imports, null, inputs(interfacesConfig), "interfaces"); } private void analyzeImplementations(Map types, Set imports, Map> interfaces) throws IOException { analyzeClasses(types, imports, interfaces, inputs(implementationsConfig), "implementations"); } private void associateImplementationsWithInterfaces(Map types, Map> interfaces) { types.values() .forEach(t -> { List impls = interfaces.get(t.simpleName()); if (impls != null) { if (impls.size() > 1) { getLog().warn(String.format("Multiple implementations found for %s: %s", t.simpleName(), impls)); } t.implementationType(impls.get(0)); } }); } private void analyzeClasses(Map types, Set imports, Map> interfaces, Collection pathsToCommpile, String note) { /* * There should not be compilation errors, but without our own diagnostic listener any compilation errors will * appear in the build output. We want to suppress those because we want to gather information about the interfaces * and classes, not actually compile them into .class files. */ DiagnosticListener diagListener = diagnostic -> { debugLog(() -> diagnostic.toString()); }; JavaCompiler jc = ToolProvider.getSystemJavaCompiler(); StandardJavaFileManager fm = jc.getStandardFileManager(null, null, null); JavaCompiler.CompilationTask task = jc.getTask(null, fm, diagListener, null, null, javaFilesToCompile(fm, pathsToCommpile)); List procs = new ArrayList<>(); EndpointProcessor ep = new EndpointProcessor(new EndpointScanner(types, imports, interfaces)); procs.add(ep); task.setProcessors(procs); task.call(); getLog().info(String.format("Types prepared for %s: %d", note, types.size())); debugLog(() -> String.format("Types prepared for %s: %s", note, types)); debugLog(() -> String.format("Imports after analyzing %s: %s", note, imports.stream().sorted().map(Import::toString).collect(Collectors.joining(",")))); debugLog(() -> String.format("Interface impls after analyzing %s: %s", note, interfaces)); } private void addImportsForTypes(Map types, Set imports) { types.forEach((name, type) -> imports.add(new Import(type.fullName(), false))); } private void generateHelperClass(Map types, Set imports) throws IOException { List pathElementsToGeneratedClass = new ArrayList<>(); String outputPackage = outputClass.substring(0, outputClass.lastIndexOf('.')); for (String segment : outputPackage.split("\\.")) { pathElementsToGeneratedClass.add(segment); } Path outputDir = Paths.get(outputDirectory.getAbsolutePath(), pathElementsToGeneratedClass.toArray(new String[0])); Files.createDirectories(outputDir); String simpleClassName = outputClass.substring(outputClass.lastIndexOf('.') + 1); Path outputPath = outputDir.resolve(simpleClassName + ".java"); Writer writer = new OutputStreamWriter(new FileOutputStream(outputPath.toFile())); MustacheFactory mf = new DefaultMustacheFactory(); Mustache m = mf.compile("typeClassTemplate.mustache"); CodeGenModel model = new CodeGenModel(outputPackage, simpleClassName, types.values(), imports, interfacePrefix, implementationPrefix); m.execute(writer, model); writer.close(); } private void addGeneratedCodeToCompilation() { mavenProject.addCompileSourceRoot(outputDirectory.getPath()); } private Path resolveDirectory(String directoryWithinProject) { Path baseDirPath = mavenProject.getBasedir().toPath(); return (directoryWithinProject != null) ? baseDirPath.resolve(directoryWithinProject) : baseDirPath; } private Iterable javaFilesToCompile(StandardJavaFileManager fm, Collection paths) { List files = paths.stream() .map(Path::toFile) .collect(Collectors.toList()); debugLog(() -> "Files to be compiled: " + files.toString()); return fm.getJavaFileObjectsFromFiles(files); } /** * Computes paths to be processed as inputs, based on an input directory * and glob-style include and exclude expressions identifying paths within * that input directory. * @param compilerConfig conveys the directory, includes, and excludes to compile * @throws IOException in case of errors matching candidate paths */ Collection inputs(CompilerConfig compilerConfig) throws IOException { Path inputPath = resolveDirectory(compilerConfig.inputDirectory); Collection result = Files.find(inputPath, Integer.MAX_VALUE, (path, attrs) -> matches(path, pathMatchers(inputPath, compilerConfig.includes)) && !matches(path, pathMatchers(inputPath, compilerConfig.excludes))) .collect(Collectors.toSet()); if (result.isEmpty()) { throw new IllegalArgumentException("No input files selected from " + compilerConfig.inputDirectory); } return result; } /** * Creates a stream of PathMatchers, one for each glob. * @param inputDirectory Path within which the globs are applied * @param globs the glob patterns * @return PathMatchers for the globs */ private static Stream pathMatchers(Path inputDirectory, List globs) { return globs.stream() .map(glob -> FileSystems.getDefault().getPathMatcher("glob:" + sanitizeWindowsPath(inputDirectory) + "/" + glob)); } /** * PathMather does not produce correct matching String if directory is not in proper format. * * @param inputDirectory input directory * @return properly escaped formatted path */ private static String sanitizeWindowsPath(Path inputDirectory) { if (OS_NAME.contains("win")) { return inputDirectory.toString().replaceAll("\\\\", "/"); } return inputDirectory.toString(); } private static boolean matches(Path candidate, Stream matchers) { return matchers.anyMatch((m) -> (m.matches(candidate))); } /** * Collects information about types analyzed by the compiler, gathering class/interface, * enum, and method information. */ static class EndpointScanner extends TreePathScanner { private final Map types; private final Set imports; private final Map> interfacesToImpls; EndpointScanner(Map types, Set imports, Map> interfacesToImpls) { this.types = types; this.imports = imports; this.interfacesToImpls = interfacesToImpls; } @Override public Type visitClass(ClassTree node, Type type) { String typeName = fullyQualifiedPath(getCurrentPath()); // null for anon. inner class Tree.Kind kind = node.getKind(); if ((kind == Tree.Kind.CLASS || kind == Tree.Kind.INTERFACE) && typeName != null) { List interfaces = SnakeYAMLMojo.treesToStrings(node.getImplementsClause()); final Type newType = new Type(typeName, node.getSimpleName().toString(), kind == Tree.Kind.INTERFACE, interfaces); if (interfacesToImpls != null && kind == Tree.Kind.CLASS) { // Add this class to our map from each interface to all of its implementations. interfaces.stream() .map(intf -> interfacesToImpls.computeIfAbsent(intf, key -> new ArrayList<>())) .forEach(list -> list.add(newType.simpleName())); } types.put(typeName, newType); imports.add(new Import(newType.fullName(), false)); type = newType; } return super.visitClass(node, type); } @Override public Type visitImport(ImportTree node, Type type) { imports.add(new Import(node.getQualifiedIdentifier().toString(), node.isStatic())); return super.visitImport(node, type); } @Override public Type visitMethod(MethodTree node, Type type) { CharSequence methodName = node.getName(); CharSequence namePrefix = methodName.subSequence(0, Math.min(3, methodName.length())); if (namePrefix.equals("set") && methodName.length() > 3 && node.getParameters().size() == 1) { VariableTree propertyTree = node.getParameters().get(0); addPropertyParametersIfNeeded(propertyTree, type, propertyName(methodName)); } return super.visitMethod(node, type); } static String propertyName(CharSequence setterMethodName) { /* * Normally, we force the property name to start with a lower-case character. * But, if the SECOND character of the property name is upper-case then we should not force the first character to * lower-case. For example, setIPAddress should NOT yield the property name iPAddress but rather it should be * IPAddress. */ boolean forceFirstCharOfPropertyNameToLowerCase = setterMethodName.length() <= 4 // just one extra character so // we cannot look past it || !Character.isUpperCase(setterMethodName.charAt(4)); return ( forceFirstCharOfPropertyNameToLowerCase ? Character.toLowerCase(setterMethodName.charAt(3)) : setterMethodName.charAt(3)) + setterMethodName.subSequence(4, setterMethodName.length()).toString(); } private static void addPropertyParametersIfNeeded(VariableTree node, Type type, String propName) { if (node.getType().getKind() == Tree.Kind.PARAMETERIZED_TYPE) { ParameterizedTypeTree pTree = (ParameterizedTypeTree) node.getType(); List parameterTypeNames = pTree.getTypeArguments().stream() .filter(argTree -> argTree.getKind() == Tree.Kind.IDENTIFIER) .map(argTree -> ((IdentifierTree) argTree).getName().toString()) .collect(Collectors.toList()); type.propertyParameter(propName, parameterTypeNames); } } } @SupportedSourceVersion(SourceVersion.RELEASE_8) @SupportedAnnotationTypes("*") private static class EndpointProcessor extends AbstractProcessor { private final EndpointScanner scanner; private Trees trees; EndpointProcessor(EndpointScanner scanner) { this.scanner = scanner; } @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); trees = Trees.instance(processingEnv); } @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { if (!roundEnv.processingOver()) { for (Element e : roundEnv.getRootElements()) { scanner.scan(trees.getPath(e).getParentPath(), null); } } return true; } } private static List treesToStrings(List trees) { return trees.stream() .map(Tree::toString) .collect(Collectors.toList()); } private static String fullyQualifiedPath(TreePath path) { ExpressionTree packageNameExpr = path.getCompilationUnit().getPackageName(); MemberSelectTree packageID = packageNameExpr.getKind() == Tree.Kind.MEMBER_SELECT ? ((MemberSelectTree) packageNameExpr) : null; StringBuilder result = new StringBuilder(); if (packageID != null) { result.append(packageID.getExpression().toString()) .append(".") .append(packageID.getIdentifier().toString()); } Tree.Kind kind = path.getLeaf().getKind(); String leafName = null; if (kind == Tree.Kind.CLASS || kind == Tree.Kind.INTERFACE) { leafName = ((ClassTree) path.getLeaf()).getSimpleName().toString(); } else if (kind == Tree.Kind.ENUM) { if (path.getParentPath() != null) { Tree parent = path.getParentPath().getLeaf(); if (parent.getKind() == Tree.Kind.CLASS || parent.getKind() == Tree.Kind.INTERFACE) { result.append(((ClassTree) parent).getSimpleName().toString()).append("."); } leafName = ((ClassTree) path.getLeaf()).getSimpleName().toString(); } } // leafName can be empty for anonymous inner classes, for example. boolean isUsefulLeaf = leafName != null && !leafName.isEmpty(); if (isUsefulLeaf) { result.append(".").append(leafName); } return isUsefulLeaf ? result.toString() : null; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy