/* * 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 net.revelc.code.impsort.maven.plugin; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.time.Duration; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.descriptor.PluginDescriptor; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.DirectoryScanner; import net.revelc.code.impsort.Grouper; import net.revelc.code.impsort.ImpSort; import net.revelc.code.impsort.LineEnding; import net.revelc.code.impsort.Result; abstract class AbstractImpSortMojo extends AbstractMojo { private static final String[] DEFAULT_INCLUDES = new String[] {"**/*.java"}; @Parameter(defaultValue = "${project}", readonly = true) protected MavenProject project; @Parameter(defaultValue = "${plugin}", readonly = true) protected PluginDescriptor plugin; @Parameter(defaultValue = "${project.build.sourceEncoding}", readonly = true) protected String sourceEncoding = StandardCharsets.UTF_8.name(); /** * Allows skipping execution of this plugin. * * @since 1.0.0 */ @Parameter(alias = "skip", property = "impsort.skip", defaultValue = "false") private boolean skip; /** * Configures the grouping of static imports. Groups are defined with comma-separated package name * prefixes. The special "*" group refers to imports not matching any other group, and is implied * after all other groups, if not specified. More specific groups are prioritized over less * specific ones. All groups are sorted. * * @since 1.0.0 */ @Parameter(alias = "staticGroups", property = "impsort.staticGroups", defaultValue = "*") protected String staticGroups; /** * Configures the grouping of non-static imports. Groups are defined with comma-separated package * name prefixes. The special "*" group refers to imports not matching any other group, and is * implied after all other groups, if not specified. More specific groups are prioritized over * less specific ones. All groups are sorted. * * @since 1.0.0 */ @Parameter(alias = "groups", property = "impsort.groups", defaultValue = "*") protected String groups; /** * Configures whether static groups will appear after non-static groups. * * @since 1.0.0 */ @Parameter(alias = "staticAfter", property = "impsort.staticAfter", defaultValue = "false") protected boolean staticAfter; /** * Allows omitting the blank line between the static and non-static sections. * * @since 1.0.0 */ @Parameter(alias = "joinStaticWithNonStatic", property = "impsort.joinStaticWithNonStatic", defaultValue = "false") protected boolean joinStaticWithNonStatic; /** * Project's main source directory as specified in the POM. Used by default if * <code>directories</code> is not set. * * @since 1.0.0 */ @Parameter(alias = "sourceDirectory", defaultValue = "${project.build.sourceDirectory}", readonly = true) private File sourceDirectory; /** * Project's test source directory as specified in the POM. Used by default if * <code>directories</code> is not set. * * @since 1.0.0 */ @Parameter(alias = "testSourceDirectory", defaultValue = "${project.build.testSourceDirectory}", readonly = true) private File testSourceDirectory; /** * Location of the Java source files to process. Defaults to source main and test directories if * not set. * * @since 1.0.0 */ @Parameter(alias = "directories", property = "impsort.directories") private File[] directories; /** * List of fileset patterns for Java source locations to include. Patterns are relative to the * directories selected. When not specified, the default include is <code>**/*.java</code> * * @since 1.0.0 */ @Parameter(alias = "includes", property = "impsort.includes") private String[] includes; /** * List of fileset patterns for Java source locations to exclude. Patterns are relative to the * directories selected. When not specified, there is no default exclude. * * @since 1.0.0 */ @Parameter(alias = "excludes", property = "impsort.excludes") private String[] excludes; /** * Configures whether to remove unused imports. * * @since 1.1.0 */ @Parameter(alias = "removeUnused", property = "impsort.removeUnused", defaultValue = "false") private boolean removeUnused; /** * Configures whether to treat imports in the current package as unused and subject to removal * along with other unused imports. * * @since 1.2.0 */ @Parameter(alias = "treatSamePackageAsUnused", property = "impsort.treatSamePackageAsUnused", defaultValue = "true") private boolean treatSamePackageAsUnused; /** * Configures whether to use a breadth first comparator for sorting static imports. This will * ensure all static imports from one class are grouped together before any static imports from an * inner-class. * * @since 1.3.0 */ @Parameter(alias = "breadthFirstComparator", property = "impsort.breadthFirstComparator", defaultValue = "true") private boolean breadthFirstComparator; /** * Sets the line-ending of files after formatting. Valid values are: * <ul> * <li><b>"AUTO"</b> - Use line endings of current system</li> * <li><b>"KEEP"</b> - Preserve line endings of files, default to AUTO if ambiguous</li> * <li><b>"LF"</b> - Use Unix and Mac style line endings</li> * <li><b>"CRLF"</b> - Use DOS and Windows style line endings</li> * <li><b>"CR"</b> - Use early Mac style line endings</li> * </ul> * * @since 1.4.0 */ @Parameter(alias = "lineEnding", property = "impsort.lineEnding", defaultValue = "AUTO") private LineEnding lineEnding; abstract void processResult(Path path, Result results) throws MojoFailureException; @Override public final void execute() throws MojoExecutionException, MojoFailureException { if (skip) { getLog().info("Skipping execution of impsort-maven-plugin"); return; } // find all matching files Stream<File> files; if (directories != null && directories.length > 0) { // warn if a user-specified directory doesn't exist files = Stream.of(directories).flatMap(d -> searchDir(d, true)).parallel(); } else { // default to src/main/java and src/test/java, without existence warnings files = Stream.of(sourceDirectory, testSourceDirectory).flatMap(d -> searchDir(d, false)) .parallel(); } Stream<Path> paths = files.map(File::toPath); // process all found files, and aggregate any failures Grouper grouper = new Grouper(groups, staticGroups, staticAfter, joinStaticWithNonStatic, breadthFirstComparator); Charset encoding = Charset.forName(sourceEncoding); ImpSort impSort = new ImpSort(encoding, grouper, removeUnused, treatSamePackageAsUnused, lineEnding); AtomicLong numAlreadySorted = new AtomicLong(0); AtomicLong numProcessed = new AtomicLong(0); Function<Path, MojoFailureException> visitor = path -> { try { getLog().debug("Reading file " + path); try { Result result = impSort.parseFile(path); result.getImports().forEach(imp -> getLog().debug("Found import: " + imp)); if (result.isSorted()) { numAlreadySorted.getAndIncrement(); } else { numProcessed.getAndIncrement(); } processResult(path, result); } catch (IOException e) { fail("Error reading file " + path, e); } return null; } catch (MojoFailureException e) { return e; } }; Predicate<MojoFailureException> notNull = e -> e != null; BinaryOperator<MojoFailureException> agg = (e1, e2) -> { e1.addSuppressed(e2); return e1; }; long startTime = System.nanoTime(); MojoFailureException failure = paths.map(visitor).filter(notNull).reduce(agg).orElse(null); Duration totalTime = Duration.ofNanos(System.nanoTime() - startTime); long total = numAlreadySorted.get() + numProcessed.get(); long minutes = totalTime.getSeconds() / 60; long seconds = totalTime.getSeconds() - minutes * 60; long millis = totalTime.getNano() / 1_000_000; String fmt = "%22s: %" + Long.toString(total).length() + "d"; getLog().info(String.format(fmt + " in %02d:%02d.%03d", "Total Files Processed", total, minutes, seconds, millis)); getLog().info(String.format(fmt, "Already Sorted", numAlreadySorted.get())); getLog().info(String.format(fmt, "Needed Sorting", numProcessed.get())); // check for failures during processing if (failure != null) { throw failure; } } private Stream<File> searchDir(File dir, boolean warnOnBadDir) { if (dir == null || !dir.exists() || !dir.isDirectory()) { if (warnOnBadDir && dir != null) { getLog().warn("Directory does not exist or is not a directory: " + dir); } return Stream.empty(); } getLog().debug("Adding directory " + dir); DirectoryScanner ds = new DirectoryScanner(); ds.setBasedir(dir); ds.setIncludes(includes != null && includes.length > 0 ? includes : DEFAULT_INCLUDES); ds.setExcludes(excludes); ds.addDefaultExcludes(); ds.setCaseSensitive(false); ds.setFollowSymlinks(false); ds.scan(); return Stream.of(ds.getIncludedFiles()).map(filename -> new File(dir, filename)).parallel(); } protected void fail(String message) throws MojoFailureException { fail(message, null); } protected void fail(String message, Throwable cause) throws MojoFailureException { throw cause == null ? new MojoFailureException(message) : new MojoFailureException(message, cause); } }