001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2015 ForgeRock AS. 025 */ 026package org.forgerock.maven; 027 028import static org.forgerock.util.Utils.closeSilently; 029 030import java.io.BufferedReader; 031import java.io.File; 032import java.io.FileReader; 033import java.io.IOException; 034import java.util.ArrayList; 035import java.util.Arrays; 036import java.util.Calendar; 037import java.util.LinkedList; 038import java.util.List; 039 040import org.apache.maven.plugin.AbstractMojo; 041import org.apache.maven.plugin.MojoExecutionException; 042import org.apache.maven.plugin.MojoFailureException; 043import org.apache.maven.plugins.annotations.Parameter; 044import org.apache.maven.project.MavenProject; 045import org.apache.maven.scm.ScmException; 046import org.apache.maven.scm.ScmFile; 047import org.apache.maven.scm.ScmFileSet; 048import org.apache.maven.scm.ScmFileStatus; 049import org.apache.maven.scm.ScmResult; 050import org.apache.maven.scm.ScmVersion; 051import org.apache.maven.scm.command.diff.DiffScmResult; 052import org.apache.maven.scm.command.status.StatusScmResult; 053import org.apache.maven.scm.log.ScmLogDispatcher; 054import org.apache.maven.scm.log.ScmLogger; 055import org.apache.maven.scm.manager.BasicScmManager; 056import org.apache.maven.scm.manager.NoSuchScmProviderException; 057import org.apache.maven.scm.manager.ScmManager; 058import org.apache.maven.scm.provider.ScmProviderRepository; 059import org.apache.maven.scm.provider.git.command.GitCommand; 060import org.apache.maven.scm.provider.git.command.diff.GitDiffConsumer; 061import org.apache.maven.scm.provider.git.gitexe.GitExeScmProvider; 062import org.apache.maven.scm.provider.git.gitexe.command.GitCommandLineUtils; 063import org.apache.maven.scm.provider.git.gitexe.command.diff.GitDiffCommand; 064import org.apache.maven.scm.repository.ScmRepository; 065import org.apache.maven.scm.repository.ScmRepositoryException; 066import org.codehaus.plexus.util.cli.CommandLineUtils; 067import org.codehaus.plexus.util.cli.CommandLineUtils.StringStreamConsumer; 068import org.codehaus.plexus.util.cli.Commandline; 069 070/** 071 * Abstract class which is used for both copyright checks and updates. 072 */ 073public abstract class CopyrightAbstractMojo extends AbstractMojo { 074 075 /** The Maven Project. */ 076 @Parameter(required = true, property = "project", readonly = true) 077 private MavenProject project; 078 079 /** 080 * Copyright owner. 081 * This string token must be present on the same line with 'copyright' keyword and the current year. 082 */ 083 @Parameter(required = true, defaultValue = "ForgeRock AS") 084 private String copyrightOwnerToken; 085 086 /** The path to the root of the scm local workspace to check. */ 087 @Parameter(required = true, defaultValue = "${basedir}") 088 private String baseDir; 089 090 @Parameter(required = true, defaultValue = "${project.scm.connection}") 091 private String scmRepositoryUrl; 092 093 /** 094 * List of file patterns for which copyright check and/or update will be skipped. 095 * Pattern can contain the following wildcards (*, ?, **{@literal /}). 096 */ 097 @Parameter(required = false) 098 private List<String> disabledFiles; 099 100 /** The file extensions to test. */ 101 public static final List<String> CHECKED_EXTENSIONS = new LinkedList<>(Arrays.asList( 102 "bat", "c", "h", "html", "java", "ldif", "Makefile", "mc", "sh", "txt", "xml", "xsd", "xsl")); 103 104 private static final List<String> EXCLUDED_END_COMMENT_BLOCK_TOKEN = new LinkedList<>(Arrays.asList( 105 "*/", "-->")); 106 107 private static final List<String> SUPPORTED_COMMENT_MIDDLE_BLOCK_TOKEN = new LinkedList<>(Arrays.asList( 108 "*", "#", "rem", "!")); 109 110 private static final List<String> SUPPORTED_START_BLOCK_COMMENT_TOKEN = new LinkedList<>(Arrays.asList( 111 "/*", "<!--")); 112 113 private static final class CustomGitExeScmProvider extends GitExeScmProvider { 114 115 @Override 116 protected GitCommand getDiffCommand() { 117 return new CustomGitDiffCommand(); 118 } 119 } 120 121 private static class CustomGitDiffCommand extends GitDiffCommand implements GitCommand { 122 123 @Override 124 protected DiffScmResult executeDiffCommand(ScmProviderRepository repo, ScmFileSet fileSet, 125 ScmVersion unused, ScmVersion unused2) throws ScmException { 126 final GitDiffConsumer consumer = new GitDiffConsumer(getLogger(), fileSet.getBasedir()); 127 final StringStreamConsumer stderr = new CommandLineUtils.StringStreamConsumer(); 128 final Commandline cl = GitCommandLineUtils.getBaseGitCommandLine(fileSet.getBasedir(), "diff"); 129 cl.addArguments(new String[] { "--no-ext-diff", "--relative", "master...HEAD", "." }); 130 131 if (GitCommandLineUtils.execute(cl, consumer, stderr, getLogger()) != 0) { 132 return new DiffScmResult(cl.toString(), "The git-diff command failed.", stderr.getOutput(), false); 133 } 134 return new DiffScmResult( 135 cl.toString(), consumer.getChangedFiles(), consumer.getDifferences(), consumer.getPatch()); 136 } 137 138 } 139 140 private String getLocalScmRootPath(final File basedir) throws ScmException { 141 final Commandline cl = GitCommandLineUtils.getBaseGitCommandLine(basedir, "rev-parse"); 142 cl.addArguments(new String[] { "--show-toplevel" }); 143 144 final StringStreamConsumer stdout = new CommandLineUtils.StringStreamConsumer(); 145 final StringStreamConsumer stderr = new CommandLineUtils.StringStreamConsumer(); 146 final ScmLogger dummyLogger = new ScmLogDispatcher(); 147 148 final int exitCode = GitCommandLineUtils.execute(cl, stdout, stderr, dummyLogger); 149 return exitCode == 0 ? stdout.getOutput().trim().replace(" ", "%20") 150 : basedir.getPath(); 151 } 152 153 154 /** The string representation of the current year. */ 155 Integer currentYear = Calendar.getInstance().get(Calendar.YEAR); 156 157 private final List<String> incorrectCopyrightFilePaths = new LinkedList<>(); 158 159 /** The overall SCM Client Manager. */ 160 private ScmManager scmManager; 161 162 private ScmRepository scmRepository; 163 164 List<String> getIncorrectCopyrightFilePaths() { 165 return incorrectCopyrightFilePaths; 166 } 167 168 private ScmManager getScmManager() throws MojoExecutionException { 169 if (scmManager == null) { 170 scmManager = new BasicScmManager(); 171 String scmProviderID = getScmProviderID(); 172 if (!"git".equals(scmProviderID)) { 173 throw new MojoExecutionException( 174 "Unsupported scm provider: " + scmProviderID + " or " + getIncorrectScmRepositoryUrlMsg()); 175 } 176 scmManager.setScmProvider(scmProviderID, new CustomGitExeScmProvider()); 177 } 178 179 return scmManager; 180 } 181 182 private String getScmProviderID() throws MojoExecutionException { 183 try { 184 return scmRepositoryUrl.split(":")[1]; 185 } catch (Exception e) { 186 throw new MojoExecutionException(getIncorrectScmRepositoryUrlMsg(), e); 187 } 188 } 189 190 String getIncorrectScmRepositoryUrlMsg() { 191 return "the scmRepositoryUrl property with value '" + scmRepositoryUrl + "' is incorrect. " 192 + "The URL has to respect the format: scm:[provider]:[provider_specific_url]"; 193 } 194 195 ScmRepository getScmRepository() throws MojoExecutionException { 196 if (scmRepository == null) { 197 try { 198 scmRepository = getScmManager().makeScmRepository(scmRepositoryUrl); 199 } catch (NoSuchScmProviderException e) { 200 throw new MojoExecutionException("Could not find a provider.", e); 201 } catch (ScmRepositoryException e) { 202 throw new MojoExecutionException("Error while connecting to the repository", e); 203 } 204 } 205 206 return scmRepository; 207 } 208 209 String getBaseDir() { 210 return baseDir; 211 } 212 213 /** 214 * Performs a diff with current working directory state against remote HEAD revision. 215 * Then do a status to check uncommited changes as well. 216 */ 217 List<File> getChangedFiles() throws MojoExecutionException, MojoFailureException { 218 try { 219 final ScmFileSet workspaceFileSet = new ScmFileSet(new File(getBaseDir())); 220 final DiffScmResult diffMasterHeadResult = getScmManager().diff( 221 getScmRepository(), workspaceFileSet, null, null); 222 ensureCommandSuccess(diffMasterHeadResult, "diff master...HEAD ."); 223 224 final StatusScmResult statusResult = getScmManager().status(getScmRepository(), workspaceFileSet); 225 ensureCommandSuccess(statusResult, "status"); 226 227 final List<File> changedFilePaths = new ArrayList<>(); 228 addToChangedFiles(diffMasterHeadResult.getChangedFiles(), getBaseDir(), changedFilePaths); 229 final String localScmRootPath = getLocalScmRootPath(new File(getBaseDir())); 230 addToChangedFiles(statusResult.getChangedFiles(), localScmRootPath, changedFilePaths); 231 232 return changedFilePaths; 233 } catch (ScmException e) { 234 throw new MojoExecutionException("Encountered an error while examining modified files, SCM status: " 235 + e.getMessage() + "No further checks will be performed.", e); 236 } 237 } 238 239 private void ensureCommandSuccess(final ScmResult result, final String cmd) throws MojoFailureException { 240 if (!result.isSuccess()) { 241 final String message = "Impossible to perform scm " + cmd + " command because " + result.getCommandOutput(); 242 getLog().error(message); 243 throw new MojoFailureException(message); 244 } 245 } 246 247 private void addToChangedFiles( 248 final List<ScmFile> scmChangedFiles, final String rootPath, final List<File> changedFiles) { 249 for (final ScmFile scmFile : scmChangedFiles) { 250 final String scmFilePath = scmFile.getPath(); 251 if (scmFile.getStatus() != ScmFileStatus.UNKNOWN 252 && new File(scmFilePath).exists() 253 && !changedFiles.contains(scmFilePath) 254 && !fileIsDisabled(scmFilePath)) { 255 changedFiles.add(new File(rootPath, scmFilePath)); 256 } 257 } 258 } 259 260 private boolean fileIsDisabled(final String scmFilePath) { 261 if (disabledFiles == null) { 262 return false; 263 } 264 for (final String disableFile : disabledFiles) { 265 String regexp = disableFile.replace("**/", "(.+/)+").replace("?", ".?").replace("*", ".*?"); 266 if (scmFilePath.matches(regexp)) { 267 return true; 268 } 269 } 270 return false; 271 } 272 273 /** Examines the provided files list to determine whether each changed file copyright is acceptable. */ 274 void checkCopyrights() throws MojoExecutionException, MojoFailureException { 275 for (final File changedFile : getChangedFiles()) { 276 if (!changedFile.exists() || !changedFile.isFile()) { 277 continue; 278 } 279 280 final String changedFileName = changedFile.getPath(); 281 int lastPeriodPos = changedFileName.lastIndexOf('.'); 282 if (lastPeriodPos > 0) { 283 String extension = changedFileName.substring(lastPeriodPos + 1); 284 if (!CHECKED_EXTENSIONS.contains(extension.toLowerCase())) { 285 continue; 286 } 287 } else if (fileNameEquals("bin", changedFile.getParentFile()) 288 && fileNameEquals("resource", changedFile.getParentFile().getParentFile())) { 289 // ignore resource/bin directory. 290 continue; 291 } 292 293 if (!checkCopyrightForFile(changedFile)) { 294 incorrectCopyrightFilePaths.add(changedFile.getAbsolutePath()); 295 } 296 } 297 } 298 299 private boolean fileNameEquals(String folderName, File file) { 300 return file != null && folderName.equals(file.getName()); 301 } 302 303 /** 304 * Check to see whether the provided file has a comment line containing a 305 * copyright without the current year. 306 */ 307 @SuppressWarnings("resource") 308 private boolean checkCopyrightForFile(File changedFile) throws MojoExecutionException { 309 BufferedReader reader = null; 310 try { 311 reader = new BufferedReader(new FileReader(changedFile)); 312 String line; 313 while ((line = reader.readLine()) != null) { 314 String lowerLine = line.toLowerCase().trim(); 315 if (isCommentLine(lowerLine) 316 && lowerLine.contains("copyright") 317 && line.contains(currentYear.toString()) 318 && line.contains(copyrightOwnerToken)) { 319 reader.close(); 320 return true; 321 } 322 } 323 324 return false; 325 } catch (IOException ioe) { 326 throw new MojoExecutionException("Could not read file " + changedFile.getPath() 327 + " to check copyright date. No further copyright date checking will be performed."); 328 } finally { 329 closeSilently(reader); 330 } 331 } 332 333 private String getCommentToken(String line, boolean includesStartBlock) { 334 List<String> supportedTokens = SUPPORTED_COMMENT_MIDDLE_BLOCK_TOKEN; 335 if (includesStartBlock) { 336 supportedTokens.addAll(SUPPORTED_START_BLOCK_COMMENT_TOKEN); 337 } 338 339 if (trimmedLineStartsWith(line, EXCLUDED_END_COMMENT_BLOCK_TOKEN) != null) { 340 return null; 341 } 342 343 return trimmedLineStartsWith(line, supportedTokens); 344 } 345 346 private String trimmedLineStartsWith(String line, List<String> supportedTokens) { 347 for (String token : supportedTokens) { 348 if (line.trim().startsWith(token)) { 349 return token; 350 } 351 } 352 return null; 353 } 354 355 boolean isNonEmptyCommentedLine(String line) { 356 String commentToken = getCommentTokenInBlock(line); 357 return commentToken == null || !commentToken.equals(line.trim()); 358 } 359 360 String getCommentTokenInBlock(String line) { 361 return getCommentToken(line, false); 362 } 363 364 boolean isCommentLine(String line) { 365 return getCommentToken(line, true) != null; 366 } 367}