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

org.openrewrite.maven.AbstractRewriteBaseRunMojo Maven / Gradle / Ivy

/*
 * Copyright 2020 the original author or authors.
 * 

* Licensed under the Moderne Source Available License (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* https://docs.moderne.io/licensing/moderne-source-available-license *

* 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 org.openrewrite.maven; import io.micrometer.core.instrument.Metrics; import org.apache.maven.artifact.DependencyResolutionRequiredException; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Parameter; import org.codehaus.plexus.classworlds.realm.ClassRealm; import org.jspecify.annotations.Nullable; import org.openrewrite.*; import org.openrewrite.config.CompositeRecipe; import org.openrewrite.config.DeclarativeRecipe; import org.openrewrite.config.Environment; import org.openrewrite.config.RecipeDescriptor; import org.openrewrite.internal.InMemoryLargeSourceSet; import org.openrewrite.internal.ListUtils; import org.openrewrite.java.tree.J; import org.openrewrite.kotlin.tree.K; import org.openrewrite.marker.*; import org.openrewrite.style.NamedStyles; import org.openrewrite.xml.tree.Xml; import java.io.IOException; import java.io.UncheckedIOException; import java.lang.reflect.Field; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.function.UnaryOperator; import java.util.stream.Stream; import static java.util.Collections.emptyList; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; public abstract class AbstractRewriteBaseRunMojo extends AbstractRewriteMojo { @Parameter(property = "rewrite.exportDatatables", defaultValue = "false") protected boolean exportDatatables; @Parameter(property = "rewrite.options") @Nullable protected LinkedHashSet options; /** * The level used to log changes performed by recipes. */ @Parameter(property = "rewrite.recipeChangeLogLevel", defaultValue = "WARN") protected LogLevel recipeChangeLogLevel; protected void log(LogLevel logLevel, CharSequence content) { switch (logLevel) { case DEBUG: getLog().debug(content); break; case INFO: getLog().info(content); break; case WARN: getLog().warn(content); break; case ERROR: getLog().error(content); break; } } /** * Attempt to determine the root of the git repository for the given project. * Many Gradle builds co-locate the build root with the git repository root, but that is not required. * If no git repository can be located in any folder containing the build, the build root will be returned. */ protected Path repositoryRoot() { Path buildRoot = getBuildRoot(); Path maybeBaseDir = buildRoot; while (maybeBaseDir != null && !Files.exists(maybeBaseDir.resolve(".git"))) { maybeBaseDir = maybeBaseDir.getParent(); } if (maybeBaseDir == null) { return buildRoot; } return maybeBaseDir; } protected ResultsContainer listResults(ExecutionContext ctx) throws MojoExecutionException, MojoFailureException { try (MeterRegistryProvider meterRegistryProvider = new MeterRegistryProvider(getLog(), metricsUri, metricsUsername, metricsPassword)) { Metrics.addRegistry(meterRegistryProvider.registry()); Path repositoryRoot = repositoryRoot(); getLog().info(String.format("Using active recipe(s) %s", getActiveRecipes())); getLog().info(String.format("Using active styles(s) %s", getActiveStyles())); if (getActiveRecipes().isEmpty()) { return new ResultsContainer(repositoryRoot, emptyList()); } URLClassLoader recipeArtifactCoordinatesClassloader = getRecipeArtifactCoordinatesClassloader(); if (recipeArtifactCoordinatesClassloader != null) { merge(getClass().getClassLoader(), recipeArtifactCoordinatesClassloader); } Environment env = environment(recipeArtifactCoordinatesClassloader); Recipe recipe = env.activateRecipes(getActiveRecipes()); if (recipe.getName().equals("org.openrewrite.Recipe$Noop")) { getLog().warn("No recipes were activated." + " Activate a recipe with com.fully.qualified.RecipeClassName in this plugin's in your pom.xml," + " or on the command line with -Drewrite.activeRecipes=com.fully.qualified.RecipeClassName"); return new ResultsContainer(repositoryRoot, emptyList()); } if (options != null && !options.isEmpty()) { configureRecipeOptions(recipe, options); } getLog().info("Validating active recipes..."); List> validations = new ArrayList<>(); recipe.validateAll(ctx, validations); List> failedValidations = validations.stream().map(Validated::failures) .flatMap(Collection::stream).collect(toList()); if (!failedValidations.isEmpty()) { failedValidations.forEach(failedValidation -> getLog().error( "Recipe validation error in " + failedValidation.getProperty() + ": " + failedValidation.getMessage(), failedValidation.getException())); if (failOnInvalidActiveRecipes) { throw new MojoExecutionException("Recipe validation errors detected as part of one or more activeRecipe(s). Please check error logs."); } else { getLog().error("Recipe validation errors detected as part of one or more activeRecipe(s). Execution will continue regardless."); } } LargeSourceSet sourceSet = loadSourceSet(repositoryRoot, env, ctx); List results = runRecipe(recipe, sourceSet, ctx); Metrics.removeRegistry(meterRegistryProvider.registry()); return new ResultsContainer(repositoryRoot, results); } catch (DependencyResolutionRequiredException e) { throw new MojoExecutionException("Dependency resolution required", e); } } private static void configureRecipeOptions(Recipe recipe, Set options) throws MojoExecutionException { if (recipe instanceof CompositeRecipe || recipe instanceof DeclarativeRecipe || recipe instanceof Recipe.DelegatingRecipe || !recipe.getRecipeList().isEmpty()) { // We don't (yet) support configuring potentially nested recipes, as recipes might occur more than once, // and setting the same value twice might lead to unexpected behavior. throw new MojoExecutionException( "Recipes containing other recipes can not be configured from the command line: " + recipe); } Map optionValues = new HashMap<>(); for (String option : options) { String[] parts = option.split("=", 2); if (parts.length == 2) { optionValues.put(parts[0], parts[1]); } } for (Field field : recipe.getClass().getDeclaredFields()) { String removed = optionValues.remove(field.getName()); updateOption(recipe, field, removed); } if (!optionValues.isEmpty()) { throw new MojoExecutionException( String.format("Unknown recipe options: %s", String.join(", ", optionValues.keySet()))); } } private static void updateOption(Recipe recipe, Field field, @Nullable String optionValue) throws MojoExecutionException { Object convertedOptionValue = convertOptionValue(field.getName(), optionValue, field.getType()); if (convertedOptionValue == null) { return; } try { field.setAccessible(true); field.set(recipe, convertedOptionValue); field.setAccessible(false); } catch (IllegalArgumentException | IllegalAccessException e) { throw new MojoExecutionException( String.format("Unable to configure recipe '%s' option '%s' with value '%s'", recipe.getClass().getSimpleName(), field.getName(), optionValue)); } } private static @Nullable Object convertOptionValue(String name, @Nullable String optionValue, Class type) throws MojoExecutionException { if (optionValue == null) { return null; } if (type.isAssignableFrom(String.class)) { return optionValue; } if (type.isAssignableFrom(boolean.class) || type.isAssignableFrom(Boolean.class)) { return Boolean.parseBoolean(optionValue); } if (type.isAssignableFrom(int.class) || type.isAssignableFrom(Integer.class)) { return Integer.parseInt(optionValue); } if (type.isAssignableFrom(long.class) || type.isAssignableFrom(Long.class)) { return Long.parseLong(optionValue); } throw new MojoExecutionException( String.format("Unable to convert option: %s value: %s to type: %s", name, optionValue, type)); } protected LargeSourceSet loadSourceSet(Path repositoryRoot, Environment env, ExecutionContext ctx) throws DependencyResolutionRequiredException, MojoExecutionException, MojoFailureException { List styles = loadStyles(project, env); //Parse and collect source files from each project in the maven session. MavenMojoProjectParser projectParser = new MavenMojoProjectParser(getLog(), repositoryRoot, pomCacheEnabled, pomCacheDirectory, runtime, skipMavenParsing, getExclusions(), getPlainTextMasks(), sizeThresholdMb, mavenSession, settingsDecrypter, runPerSubmodule, true); Stream sourceFiles = projectParser.listSourceFiles(project, styles, ctx); List sourceFileList = sourcesWithAutoDetectedStyles(sourceFiles); return new InMemoryLargeSourceSet(sourceFileList); } protected List runRecipe(Recipe recipe, LargeSourceSet sourceSet, ExecutionContext ctx) { getLog().info("Running recipe(s)..."); RecipeRun recipeRun = recipe.run(sourceSet, ctx); if (exportDatatables) { String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss-SSS")); Path datatableDirectoryPath = Paths.get("target", "rewrite", "datatables", timestamp); getLog().info(String.format("Printing available datatables to: %s", datatableDirectoryPath)); recipeRun.exportDatatablesToCsv(datatableDirectoryPath, ctx); } return recipeRun.getChangeset().getAllResults().stream().filter(source -> { // Remove ASTs originating from generated files if (source.getBefore() != null) { return !source.getBefore().getMarkers().findFirst(Generated.class).isPresent(); } return true; }).collect(toList()); } private List sourcesWithAutoDetectedStyles(Stream sourceFiles) { org.openrewrite.java.style.Autodetect.Detector javaDetector = org.openrewrite.java.style.Autodetect.detector(); org.openrewrite.kotlin.style.Autodetect.Detector kotlinDetector = org.openrewrite.kotlin.style.Autodetect.detector(); org.openrewrite.xml.style.Autodetect.Detector xmlDetector = org.openrewrite.xml.style.Autodetect.detector(); List sourceFileList = sourceFiles .peek(s -> { if (s instanceof K.CompilationUnit) { kotlinDetector.sample(s); } else if (s instanceof J.CompilationUnit) { javaDetector.sample(s); } }) .peek(xmlDetector::sample) .collect(toList()); Map, NamedStyles> stylesByType = new HashMap<>(); stylesByType.put(J.CompilationUnit.class, javaDetector.build()); stylesByType.put(K.CompilationUnit.class, kotlinDetector.build()); stylesByType.put(Xml.Document.class, xmlDetector.build()); return ListUtils.map(sourceFileList, applyAutodetectedStyle(stylesByType)); } private UnaryOperator applyAutodetectedStyle(Map, NamedStyles> stylesByType) { return before -> { for (Map.Entry, NamedStyles> styleTypeEntry : stylesByType.entrySet()) { if (styleTypeEntry.getKey().isAssignableFrom(before.getClass())) { before = before.withMarkers(before.getMarkers().add(styleTypeEntry.getValue())); } } return before; }; } private void merge(ClassLoader targetClassLoader, URLClassLoader sourceClassLoader) { ClassRealm targetClassRealm; try { targetClassRealm = (ClassRealm) targetClassLoader; } catch (ClassCastException e) { getLog().warn("Could not merge ClassLoaders due to unexpected targetClassLoader type", e); return; } Set existingVersionlessJars = new HashSet<>(); for (URL existingUrl : targetClassRealm.getURLs()) { existingVersionlessJars.add(stripVersion(existingUrl)); } for (URL newUrl : sourceClassLoader.getURLs()) { if (!existingVersionlessJars.contains(stripVersion(newUrl))) { targetClassRealm.addURL(newUrl); } } } private String stripVersion(URL jarUrl) { return jarUrl.toString().replaceAll("/[^/]+/[^/]+\\.jar", ""); } public static class ResultsContainer { final Path projectRoot; final List generated = new ArrayList<>(); final List deleted = new ArrayList<>(); final List moved = new ArrayList<>(); final List refactoredInPlace = new ArrayList<>(); public ResultsContainer(Path projectRoot, Collection results) { this.projectRoot = projectRoot; for (Result result : results) { if (result.getBefore() == null && result.getAfter() == null) { // This situation shouldn't happen / makes no sense, log and skip continue; } if (result.getBefore() == null && result.getAfter() != null) { generated.add(result); } else if (result.getBefore() != null && result.getAfter() == null) { deleted.add(result); } else if (result.getBefore() != null && result.getAfter() != null && !result.getBefore().getSourcePath().equals(result.getAfter().getSourcePath())) { moved.add(result); } else { if (!result.diff(Paths.get(""), new ResultsContainer.FencedMarkerPrinter(), true).isEmpty()) { refactoredInPlace.add(result); } } } } public @Nullable RuntimeException getFirstException() { for (Result result : generated) { for (RuntimeException error : getRecipeErrors(result)) { return error; } } for (Result result : deleted) { for (RuntimeException error : getRecipeErrors(result)) { return error; } } for (Result result : moved) { for (RuntimeException error : getRecipeErrors(result)) { return error; } } for (Result result : refactoredInPlace) { for (RuntimeException error : getRecipeErrors(result)) { return error; } } return null; } private List getRecipeErrors(Result result) { List exceptions = new ArrayList<>(); new TreeVisitor() { @Override public Tree preVisit(Tree tree, Integer integer) { Markers markers = tree.getMarkers(); markers.findFirst(Markup.Error.class).ifPresent(e -> { Optional sourceFile = Optional.ofNullable(getCursor().firstEnclosing(SourceFile.class)); String sourcePath = sourceFile.map(SourceFile::getSourcePath).map(Path::toString).orElse(""); exceptions.add(new RuntimeException("Error while visiting " + sourcePath + ": " + e.getDetail())); }); return tree; } }.visit(result.getAfter(), 0); return exceptions; } public Path getProjectRoot() { return projectRoot; } public boolean isNotEmpty() { return !generated.isEmpty() || !deleted.isEmpty() || !moved.isEmpty() || !refactoredInPlace.isEmpty(); } /** * List directories that are empty as a result of applying recipe changes */ public List newlyEmptyDirectories() { Set maybeEmptyDirectories = new LinkedHashSet<>(); for (Result result : moved) { assert result.getBefore() != null; maybeEmptyDirectories.add(projectRoot.resolve(result.getBefore().getSourcePath()).getParent()); } for (Result result : deleted) { assert result.getBefore() != null; maybeEmptyDirectories.add(projectRoot.resolve(result.getBefore().getSourcePath()).getParent()); } if (maybeEmptyDirectories.isEmpty()) { return Collections.emptyList(); } List emptyDirectories = new ArrayList<>(maybeEmptyDirectories.size()); for (Path maybeEmptyDirectory : maybeEmptyDirectories) { try (Stream contents = Files.list(maybeEmptyDirectory)) { if (contents.findAny().isPresent()) { continue; } Files.delete(maybeEmptyDirectory); } catch (IOException e) { throw new UncheckedIOException(e); } } return emptyDirectories; } /** * Only retains output for markers of type {@code SearchResult} and {@code Markup}. */ private static class FencedMarkerPrinter implements PrintOutputCapture.MarkerPrinter { @Override public String beforeSyntax(Marker marker, Cursor cursor, UnaryOperator commentWrapper) { return marker instanceof SearchResult || marker instanceof Markup ? "{{" + marker.getId() + "}}" : ""; } @Override public String afterSyntax(Marker marker, Cursor cursor, UnaryOperator commentWrapper) { return marker instanceof SearchResult || marker instanceof Markup ? "{{" + marker.getId() + "}}" : ""; } } } protected void logRecipesThatMadeChanges(Result result) { String indent = " "; String prefix = " "; for (RecipeDescriptor recipeDescriptor : result.getRecipeDescriptorsThatMadeChanges()) { logRecipe(recipeDescriptor, prefix); prefix = prefix + indent; } } private void logRecipe(RecipeDescriptor rd, String prefix) { StringBuilder recipeString = new StringBuilder(prefix + rd.getName()); if (!rd.getOptions().isEmpty()) { String opts = rd.getOptions().stream().map(option -> { if (option.getValue() != null) { return option.getName() + "=" + option.getValue(); } return null; } ).filter(Objects::nonNull).collect(joining(", ")); if (!opts.isEmpty()) { recipeString.append(": {").append(opts).append("}"); } } log(recipeChangeLogLevel, recipeString.toString()); for (RecipeDescriptor rchild : rd.getRecipeList()) { logRecipe(rchild, prefix + " "); } } protected Duration estimateTimeSavedSum(Result result, Duration timeSaving) { if (null != result.getTimeSavings()) { return timeSaving.plus(result.getTimeSavings()); } return timeSaving; } protected String formatDuration(Duration duration) { return duration.toString() .substring(2) .replaceAll("(\\d[HMS])(?!$)", "$1 ") .toLowerCase() .trim(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy