/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2015-2016 ForgeRock AS. */ package org.forgerock.maven; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.LinkedList; import java.util.List; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; import org.apache.maven.scm.ScmException; import org.apache.maven.scm.ScmFile; import org.apache.maven.scm.ScmFileSet; import org.apache.maven.scm.ScmFileStatus; import org.apache.maven.scm.ScmResult; import org.apache.maven.scm.ScmVersion; import org.apache.maven.scm.command.diff.DiffScmResult; import org.apache.maven.scm.command.status.StatusScmResult; import org.apache.maven.scm.log.ScmLogDispatcher; import org.apache.maven.scm.log.ScmLogger; import org.apache.maven.scm.manager.BasicScmManager; import org.apache.maven.scm.manager.NoSuchScmProviderException; import org.apache.maven.scm.manager.ScmManager; import org.apache.maven.scm.provider.ScmProviderRepository; import org.apache.maven.scm.provider.git.command.GitCommand; import org.apache.maven.scm.provider.git.command.diff.GitDiffConsumer; import org.apache.maven.scm.provider.git.gitexe.GitExeScmProvider; import org.apache.maven.scm.provider.git.gitexe.command.GitCommandLineUtils; import org.apache.maven.scm.provider.git.gitexe.command.diff.GitDiffCommand; import org.apache.maven.scm.repository.ScmRepository; import org.apache.maven.scm.repository.ScmRepositoryException; import org.codehaus.plexus.util.cli.CommandLineUtils; import org.codehaus.plexus.util.cli.CommandLineUtils.StringStreamConsumer; import org.codehaus.plexus.util.cli.Commandline; /** * Abstract class which is used for both copyright checks and updates. */ public abstract class CopyrightAbstractMojo extends AbstractMojo { static final String DIFF_REFERENCE_BRANCH_NAME_PROPERTY = "checkCopyrightDiffReferenceBranchName"; /** The Maven Project. */ @Parameter(required = true, property = "project", readonly = true) private MavenProject project; /** * Copyright owner. * This string token must be present on the same line with 'copyright' keyword and the current year. */ @Parameter(required = true, defaultValue = "ForgeRock AS") private String copyrightOwnerToken; /** The path to the root of the scm local workspace to check. */ @Parameter(required = true, defaultValue = "${basedir}") private String baseDir; @Parameter(required = true, defaultValue = "${project.scm.connection}") private String scmRepositoryUrl; /** The Git master branch name of the repository. **/ @Parameter(required = true, property = DIFF_REFERENCE_BRANCH_NAME_PROPERTY, defaultValue = "origin/master") private String gitMasterBranchName; /** * List of file patterns for which copyright check and/or update will be skipped. * Pattern can contain the following wildcards (*, ?, **{@literal /}). */ @Parameter(required = false) private List disabledFiles; /** The file extensions to test. */ public static final List CHECKED_EXTENSIONS = Arrays.asList( "bat", "c", "fml", "h", "html", "java", "java.stub", "ldif", "mc", "md", "properties", "security", "sh", "txt", "xjb", "xml", "xml.vm", "xsd", "xsl"); private final class CustomGitExeScmProvider extends GitExeScmProvider { @Override protected GitCommand getDiffCommand() { return new CustomGitDiffCommand(); } } private class CustomGitDiffCommand extends GitDiffCommand implements GitCommand { @Override protected DiffScmResult executeDiffCommand(ScmProviderRepository repo, ScmFileSet fileSet, ScmVersion unused, ScmVersion unused2) throws ScmException { final GitDiffConsumer consumer = new GitDiffConsumer(getLogger(), fileSet.getBasedir()); final StringStreamConsumer stderr = new CommandLineUtils.StringStreamConsumer(); final Commandline cl = GitCommandLineUtils.getBaseGitCommandLine(fileSet.getBasedir(), "diff"); cl.addArguments(new String[] { "--no-ext-diff", "--relative", gitMasterBranchName + "...HEAD", "." }); if (GitCommandLineUtils.execute(cl, consumer, stderr, getLogger()) != 0) { return new DiffScmResult(cl.toString(), "The git-diff command failed.", stderr.getOutput(), false); } return new DiffScmResult( cl.toString(), consumer.getChangedFiles(), consumer.getDifferences(), consumer.getPatch()); } } private static String resolveExtension(final String filePath) { int firstPeriodPos = filePath.indexOf('.'); if (firstPeriodPos > 0) { return filePath.substring(firstPeriodPos + 1); } return ""; } enum CommentParser { BAT("rem"), DEFAULT("#", "*", "!", "//"), JAVA("/*", "*/", Arrays.asList("*", "//", "")), MC(";"), SH("#"), XML("", Arrays.asList("!", "~", "")); static CommentParser createParserForFile(final String filePath) { switch (resolveExtension(filePath)) { case "java": case "c": case "h": return JAVA; case "fml": case "html": case "md": case "xjb": case "xml": case "xml.vm": case "xsd": case "xsl": return XML; case "bat": return BAT; case "security": case "sh": case "ldif": case "properties": return SH; case "mc": return MC; default: return DEFAULT; } } private final String startBlock; private final String endBlock; private final List middleBlockTokens; private String currentLine = ""; private boolean supportCommentBlocks; private boolean commentBlockOpened; CommentParser(final String... middleBlockTokens) { this(null, null, Arrays.asList(middleBlockTokens)); } CommentParser(final String startBlock, final String endBlock, final List middleBlockTokens) { this.startBlock = startBlock; this.endBlock = endBlock; this.middleBlockTokens = middleBlockTokens; this.supportCommentBlocks = startBlock != null && endBlock != null; } void consumeLine(final String line) { if (line != null) { if (supportCommentBlocks && currentLine.endsWith(endBlock)) { commentBlockOpened = false; } currentLine = line.trim(); if (supportCommentBlocks && currentLine.startsWith(startBlock)) { commentBlockOpened = true; } } } boolean isCommentLine() { return commentBlockOpened || startsWithCommentLineToken(); } private boolean startsWithCommentLineToken() { return getCommentTokenInLine(currentLine) != null; } private String getCommentTokenInLine(final String line) { for (final String token : middleBlockTokens) { if (line.startsWith(token)) { return token; } } return null; } String getNewCommentedLine(final String line) throws Exception { final String commentToken = getCommentTokenInLine(line.trim()); if (commentToken == null) { throw new Exception("Incompatibles comments lines in the file."); } String resultLine = ""; if (commentToken.isEmpty()) { resultLine = " "; } return resultLine + line.substring(0, line.indexOf(commentToken) + commentToken.length()); } boolean isNonEmptyCommentedLine() { final String commentToken = getCommentTokenInLine(currentLine); return commentToken == null || !commentToken.equals(currentLine); } } /** The string representation of the current year. */ Integer currentYear = Calendar.getInstance().get(Calendar.YEAR); private final List incorrectCopyrightFilePaths = new LinkedList<>(); /** The overall SCM Client Manager. */ private ScmManager scmManager; private ScmRepository scmRepository; List getIncorrectCopyrightFilePaths() { return incorrectCopyrightFilePaths; } private ScmManager getScmManager() throws MojoExecutionException { if (scmManager == null) { scmManager = new BasicScmManager(); String scmProviderID = getScmProviderID(); if (!"git".equals(scmProviderID)) { throw new MojoExecutionException( "Unsupported scm provider: " + scmProviderID + " or " + getIncorrectScmRepositoryUrlMsg()); } scmManager.setScmProvider(scmProviderID, new CustomGitExeScmProvider()); } return scmManager; } private String getScmProviderID() throws MojoExecutionException { try { return scmRepositoryUrl.split(":")[1]; } catch (Exception e) { throw new MojoExecutionException(getIncorrectScmRepositoryUrlMsg(), e); } } String getIncorrectScmRepositoryUrlMsg() { return "the scmRepositoryUrl property with value '" + scmRepositoryUrl + "' is incorrect. " + "The URL has to respect the format: scm:[provider]:[provider_specific_url]"; } ScmRepository getScmRepository() throws MojoExecutionException { if (scmRepository == null) { try { scmRepository = getScmManager().makeScmRepository(scmRepositoryUrl); } catch (NoSuchScmProviderException e) { throw new MojoExecutionException("Could not find a provider.", e); } catch (ScmRepositoryException e) { throw new MojoExecutionException("Error while connecting to the repository", e); } } return scmRepository; } String getBaseDir() { return baseDir; } /** * Performs a diff with current working directory state against remote HEAD revision. * Then do a status to check uncommited changes as well. */ List getChangedFiles() throws MojoExecutionException, MojoFailureException { try { final ScmFileSet workspaceFileSet = new ScmFileSet(new File(getBaseDir())); final DiffScmResult diffMasterHeadResult = getScmManager().diff( getScmRepository(), workspaceFileSet, null, null); ensureCommandSuccess(diffMasterHeadResult, "diff " + gitMasterBranchName + "...HEAD ."); final StatusScmResult statusResult = getScmManager().status(getScmRepository(), workspaceFileSet); ensureCommandSuccess(statusResult, "status"); final List changedFilePaths = new ArrayList<>(); addToChangedFiles(diffMasterHeadResult.getChangedFiles(), getBaseDir(), changedFilePaths); final String localScmRootPath = getLocalScmRootPath(new File(getBaseDir())); addToChangedFiles(statusResult.getChangedFiles(), localScmRootPath, changedFilePaths); return changedFilePaths; } catch (ScmException e) { throw new MojoExecutionException("Encountered an error while examining modified files, SCM status: " + e.getMessage() + "No further checks will be performed.", e); } } private String getLocalScmRootPath(final File basedir) throws ScmException { final Commandline cl = GitCommandLineUtils.getBaseGitCommandLine(basedir, "rev-parse"); cl.addArguments(new String[] { "--show-toplevel" }); final StringStreamConsumer stdout = new CommandLineUtils.StringStreamConsumer(); final StringStreamConsumer stderr = new CommandLineUtils.StringStreamConsumer(); final ScmLogger dummyLogger = new ScmLogDispatcher(); final int exitCode = GitCommandLineUtils.execute(cl, stdout, stderr, dummyLogger); return exitCode == 0 ? stdout.getOutput().trim().replace(" ", "%20") : basedir.getPath(); } private void ensureCommandSuccess(final ScmResult result, final String cmd) throws MojoFailureException { if (!result.isSuccess()) { final String message = "Impossible to perform scm " + cmd + " command because " + result.getCommandOutput(); getLog().error(message); throw new MojoFailureException(message); } } private void addToChangedFiles( final List scmChangedFiles, final String rootPath, final List changedFiles) { for (final ScmFile scmFile : scmChangedFiles) { final String scmFilePath = scmFile.getPath(); final File file = new File(rootPath, scmFilePath); if (scmFile.getStatus() != ScmFileStatus.UNKNOWN && file.exists() && !changedFiles.contains(file) && !fileIsDisabled(scmFilePath)) { changedFiles.add(file); } } } private boolean fileIsDisabled(final String scmFilePath) { if (disabledFiles == null) { return false; } for (final String disableFile : disabledFiles) { String regexp = disableFile.replace("**/", "(.+/)+").replace("?", ".?").replace("*", ".*?"); if (scmFilePath.matches(regexp)) { return true; } } return false; } /** Examines the provided files list to determine whether each changed file copyright is acceptable. */ void checkCopyrights() throws MojoExecutionException, MojoFailureException { for (final File changedFile : getChangedFiles()) { if (!changedFile.exists() || !changedFile.isFile() || !CHECKED_EXTENSIONS.contains(resolveExtension(changedFile.getPath()).toLowerCase()) || (fileNameEquals("bin", changedFile.getParentFile()) && fileNameEquals("resource", changedFile.getParentFile().getParentFile()))) { // Verify that the file must be checked (ignore bin/resource directory) continue; } if (!checkCopyrightForFile(changedFile)) { incorrectCopyrightFilePaths.add(changedFile.getAbsolutePath()); } } } private boolean fileNameEquals(String folderName, File file) { return file != null && folderName.equals(file.getName()); } /** * Check to see whether the provided file has a comment line containing a * copyright without the current year. */ private boolean checkCopyrightForFile(File changedFile) throws MojoExecutionException { try (BufferedReader reader = new BufferedReader(new FileReader(changedFile))) { final CommentParser commentParser = CommentParser.createParserForFile(changedFile.getPath()); String line; while ((line = reader.readLine()) != null) { commentParser.consumeLine(line); if (commentParser.isCommentLine() && line.toLowerCase().trim().contains("copyright") && line.contains(currentYear.toString()) && line.contains(copyrightOwnerToken)) { return true; } } return false; } catch (IOException ioe) { throw new MojoExecutionException("Could not read file " + changedFile.getPath() + " to check copyright date. No further copyright date checking will be performed."); } } }