001/* 002 * Copyright 2017-2022 original authors 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package io.micronaut.maven; 017 018import org.apache.maven.artifact.Artifact; 019import org.apache.maven.plugin.MojoExecutionException; 020import org.apache.maven.plugins.annotations.LifecyclePhase; 021import org.apache.maven.plugins.annotations.Mojo; 022import org.apache.maven.plugins.annotations.Parameter; 023import org.apache.maven.plugins.annotations.ResolutionScope; 024import org.apache.maven.project.MavenProject; 025 026import javax.inject.Inject; 027import java.io.File; 028import java.io.IOException; 029import java.nio.charset.StandardCharsets; 030import java.nio.file.Files; 031import java.nio.file.Path; 032import java.nio.file.StandardOpenOption; 033import java.util.ArrayList; 034import java.util.Collections; 035import java.util.List; 036import java.util.jar.JarEntry; 037import java.util.jar.JarFile; 038import java.util.regex.Pattern; 039 040import static io.micronaut.maven.RunMojo.THIS_PLUGIN; 041 042/** 043 * Import beans from project dependencies by generating factories annotated with 044 * <code>@Import</code> containing the list of packages. 045 * 046 * @author Auke Schrijnen 047 * @since 4.5.0 048 */ 049@Mojo(name = ImportFactoryMojo.MOJO_NAME, defaultPhase = LifecyclePhase.GENERATE_SOURCES, threadSafe = true, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME) 050public class ImportFactoryMojo extends AbstractMicronautMojo { 051 052 /** 053 * Name of the import factory mojo. 054 */ 055 public static final String MOJO_NAME = "generate-import-factory"; 056 057 private static final String MICRONAUT_IMPORTFACTORY_PREFIX = "micronaut.importfactory"; 058 059 /** 060 * Reference to the Maven project on which the plugin is invoked. 061 */ 062 private final MavenProject project; 063 064 /** 065 * Whether to enable or disable the generation of the import factory. 066 * 067 * @since 4.5.0 068 */ 069 @Parameter(property = MICRONAUT_IMPORTFACTORY_PREFIX + ".enabled", defaultValue = "false") 070 private boolean enabled; 071 072 /** 073 * The output directory to which all the sources will be generated. 074 * 075 * @since 4.5.0 076 */ 077 @Parameter(defaultValue = "${project.build.directory}/generated-sources/importfactory", property = MICRONAUT_IMPORTFACTORY_PREFIX + ".outputDirectory", required = true) 078 private File outputDirectory; 079 080 /** 081 * Add the output directory to the project as a source root in order to let the generated java 082 * classes be compiled and included in the project artifact. 083 * 084 * @since 4.5.0 085 */ 086 @Parameter(defaultValue = "true", property = MICRONAUT_IMPORTFACTORY_PREFIX + ".addCompileSourceRoot") 087 private boolean addCompileSourceRoot; 088 089 /** 090 * Regexp pattern which allows including certain dependencies. 091 * 092 * @since 4.5.0 093 */ 094 @Parameter(defaultValue = "^.*:.*$", property = MICRONAUT_IMPORTFACTORY_PREFIX + ".includeDependenciesFilter", required = true) 095 private String includeDependenciesFilter; 096 097 /** 098 * Regexp pattern which allows excluding certain dependencies. 099 * 100 * @since 4.5.0 101 */ 102 @Parameter(defaultValue = "^$", property = MICRONAUT_IMPORTFACTORY_PREFIX + ".excludeDependenciesFilter", required = true) 103 private String excludeDependenciesFilter; 104 105 /** 106 * Regexp pattern which allows including certain packages. 107 * 108 * @since 4.5.0 109 */ 110 @Parameter(defaultValue = "^.*$", property = MICRONAUT_IMPORTFACTORY_PREFIX + ".includePackagesFilter", required = true) 111 private String includePackagesFilter; 112 113 /** 114 * Regexp pattern which allows excluding certain packages. 115 * 116 * @since 4.5.0 117 */ 118 @Parameter(defaultValue = "^$", property = MICRONAUT_IMPORTFACTORY_PREFIX + ".excludePackagesFilter", required = true) 119 private String excludePackagesFilter; 120 121 /** 122 * The package name which is used for the generated import factories. When not specified a factory 123 * is generated for each package within that package in order to access package protected fields. 124 * 125 * @since 4.5.0 126 */ 127 @Parameter(property = MICRONAUT_IMPORTFACTORY_PREFIX + ".targetPackage") 128 private String targetPackage; 129 130 @Inject 131 public ImportFactoryMojo(MavenProject project) { 132 this.project = project; 133 } 134 135 @Override 136 public void execute() throws MojoExecutionException { 137 if (!enabled) { 138 getLog().debug(this.getClass().getSimpleName() + " is disabled"); 139 return; 140 } 141 142 if (addCompileSourceRoot) { 143 project.addCompileSourceRoot(outputDirectory.getPath()); 144 } 145 146 List<Artifact> dependencies = getFilteredDependencies(); 147 148 if (dependencies.isEmpty()) { 149 getLog().warn("No matching dependencies."); 150 return; 151 } 152 153 getLog().info("Found " + dependencies.size() + " matching dependencies:"); 154 dependencies.forEach( 155 dependency -> getLog().info(getIdentifier(dependency) + " at " + dependency.getFile())); 156 157 List<String> packages = getFilteredPackages(dependencies); 158 159 if (packages.isEmpty()) { 160 getLog().warn("No matching packages."); 161 return; 162 } 163 164 getLog().info("Found " + packages.size() + " matching packages:"); 165 packages.forEach(getLog()::info); 166 167 if (targetPackage == null || targetPackage.isEmpty()) { 168 for (String packageName : packages) { 169 try { 170 generateImportFactory(packageName, Collections.singletonList(packageName)); 171 } catch (IOException e) { 172 throw new MojoExecutionException("Error creating factory for " + packageName, e); 173 } 174 } 175 } else { 176 try { 177 generateImportFactory(targetPackage, packages); 178 } catch (IOException e) { 179 throw new MojoExecutionException("Error creating factory for " + targetPackage, e); 180 } 181 } 182 } 183 184 /** 185 * Get the list of matching packages from the given list of dependencies. 186 * 187 * @param dependencies the Maven dependencies 188 * @return a list of matching packages 189 * @throws MojoExecutionException when the artifacts can't be read 190 */ 191 private List<String> getFilteredPackages(List<Artifact> dependencies) throws MojoExecutionException { 192 var packages = new ArrayList<String>(); 193 for (Artifact dependency : dependencies) { 194 packages.addAll(getPackages(dependency)); 195 } 196 197 var includePackages = Pattern.compile(includePackagesFilter); 198 var excludePackages = Pattern.compile(excludePackagesFilter); 199 200 return packages.stream() 201 .filter(includePackages.asMatchPredicate()) 202 .filter(excludePackages.asMatchPredicate().negate()) 203 .sorted() 204 .toList(); 205 } 206 207 /** 208 * Get the list of matching dependencies. 209 * 210 * @return the matching dependencies 211 */ 212 private List<Artifact> getFilteredDependencies() { 213 var includeDependency = Pattern.compile(includeDependenciesFilter); 214 var excludeDependency = Pattern.compile(excludeDependenciesFilter); 215 216 return project.getArtifacts().stream() 217 .filter(dependency -> includeDependency.matcher(getIdentifier(dependency)).matches()) 218 .filter(dependency -> !excludeDependency.matcher(getIdentifier(dependency)).matches()) 219 .toList(); 220 } 221 222 /** 223 * Get the packages from the given artifact. 224 * 225 * @param artifact the Maven artifact 226 * @return the list of packages 227 * @throws MojoExecutionException when the artifacts can't be read 228 */ 229 private List<String> getPackages(Artifact artifact) throws MojoExecutionException { 230 try (var file = new JarFile(artifact.getFile(), false)) { 231 return file.stream() 232 .filter(entry -> entry.getName().endsWith(".class") && entry.getName().contains("/")) 233 .map(this::getPackageName).distinct().sorted().toList(); 234 } catch (IOException e) { 235 throw new MojoExecutionException("Unable to read " + artifact.getFile(), e); 236 } 237 } 238 239 /** 240 * Get the identifier for the given Maven artifact. 241 * 242 * @param artifact the Maven artifact 243 * @return the identifier for the artifact 244 */ 245 private String getIdentifier(Artifact artifact) { 246 return artifact.getGroupId() + ":" + artifact.getArtifactId(); 247 } 248 249 /** 250 * Get the Java package name from the given Jar entry. 251 * 252 * @param entry the Jar entry 253 * @return the Java package name 254 */ 255 private String getPackageName(JarEntry entry) { 256 String entryName = entry.getName(); 257 // remove .class from the end and change format to use periods instead of forward slashes 258 return entryName.substring(0, entryName.lastIndexOf('/')).replace('/', '.'); 259 } 260 261 /** 262 * Generate the import factory with the given packages in the given package. 263 * 264 * @param packageName the package in which the factory is generated 265 * @param packages the list of the packages which are imported 266 * @throws IOException when the writing of the factory class fails 267 */ 268 private void generateImportFactory(String packageName, List<String> packages) throws IOException { 269 Path factoryPath = outputDirectory.toPath().resolve(packageName.replace('.', '/')) 270 .resolve("ImportFactory.java"); 271 272 var code = new ArrayList<String>(); 273 code.add("package " + packageName + ";"); 274 code.add(""); 275 code.add("import io.micronaut.context.annotation.Factory;"); 276 code.add("import io.micronaut.context.annotation.Import;"); 277 code.add("import jakarta.annotation.Generated;"); 278 code.add(""); 279 code.add("/** Factory which allows Micronaut to import beans from the specified packages. */"); 280 code.add("@Generated(\"%s\")".formatted(THIS_PLUGIN)); 281 code.add("@Factory"); 282 code.add("@Import("); 283 code.add(" packages = {"); 284 for (String name : packages) { 285 code.add(" \"" + name + "\","); 286 } 287 code.add(" })"); 288 code.add("public class ImportFactory { }\n"); 289 290 Files.createDirectories(factoryPath.getParent()); 291 Files.write(factoryPath, code, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); 292 } 293 294}