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 * Copyright 2015 ForgeRock AS 024 */ 025package org.forgerock.maven; 026 027import static java.util.regex.Pattern.*; 028 029import static org.apache.maven.plugins.annotations.LifecyclePhase.*; 030 031import java.io.BufferedReader; 032import java.io.BufferedWriter; 033import java.io.File; 034import java.io.FileReader; 035import java.io.FileWriter; 036import java.io.IOException; 037import java.util.LinkedList; 038import java.util.List; 039import java.util.regex.Matcher; 040import java.util.regex.Pattern; 041 042import org.apache.maven.plugin.MojoExecutionException; 043import org.apache.maven.plugin.MojoFailureException; 044import org.apache.maven.plugins.annotations.Mojo; 045import org.apache.maven.plugins.annotations.Parameter; 046import org.forgerock.util.Utils; 047 048/** 049 * This goals can be used to automatically updates copyrights of modified files. 050 * 051 * <p> 052 * Copyright sections must respect the following format: 053 * <pre> 054 * (.)* //This line references 0..N lines. 055 * [COMMMENT_CHAR][lineBeforeCopyrightRegExp] 056 * [COMMMENT_CHAR]* //This line references 0..N commented empty lines. 057 * ([COMMMENT_CHAR][oldCopyrightToken])* 058 * ([COMMMENT_CHAR] [YEAR] [copyrightEndToken])? 059 * </pre> 060 * <p> 061 * Formatter details: 062 * <ul> 063 * <li>COMMENT_CHAR: Auto-detected by plugin. 064 * Comment character used in comment blocks ('*' for Java, '!' for xml...)</li> 065 * 066 * <li>lineBeforeCopyrightRegExp: Parameter regExp case insensitive 067 * Used by the plugin to start it's inspection for the copyright line. 068 * Next non blank commented lines after this lines must be 069 * old copyright owner lines or/and old ForgeRock copyright lines.</li> 070 * 071 * <li>oldCopyrightToken: Detected by plugin ('copyright' keyword case insensitive) 072 * If one line contains this token, the plugin will use 073 * the newPortionsCopyrightLabel instead of the newCopyrightLabel 074 * if there is no ForgeRock copyrighted line.</li> 075 * 076 * <li>forgerockCopyrightRegExp: Parameter regExp case insensitive 077 * The regular expression which identifies a copyrighted line as a ForgeRock one.</li> 078 * 079 * <li>YEAR: Computed by plugin 080 * Current year if there is no existing copyright line. 081 * If the copyright section already exists, the year will be updated as follow: 082 * <ul> 083 * <li>OLD_YEAR => OLD_YEAR-CURRENT_YEAR</li> 084 * <li>VERY_OLD_YEAR-OLD_YEAR => VERY_OLD_YEAR-CURRENT_YEAR</li> 085 * </ul></li> 086 * </ul> 087 * </p> 088 * <p> 089 * If no ForgeRock copyrighted line is detected, the plugin will add according to the following format 090 * <ul> 091 * <li> If there is one or more old copyright lines: 092 * <pre> 093 * [COMMMENT_CHAR][lineBeforeCopyrightRegExp] 094 * [COMMMENT_CHAR]* //This line references 0..N commented empty lines. 095 * ([COMMMENT_CHAR][oldCopyrightToken])* 096 * [indent][newPortionsCopyrightLabel] [YEAR] [forgerockCopyrightLabel] 097 * </pre></li><br> 098 * <li> If there is no old copyright lines: 099 * <pre> 100 * [COMMMENT_CHAR][lineBeforeCopyrightRegExp] 101 * [COMMMENT_CHAR]*{nbLinesToSkip} //This line nbLinesToSkip commented empty lines. 102 * [indent][newCopyrightLabel] [YEAR] [forgerockCopyrightLabel] 103 * </pre></li> 104 * </ul> 105 * 106 */ 107@Mojo(name = "update-copyright", defaultPhase = VALIDATE) 108public class UpdateCopyrightMojo extends CopyrightAbstractMojo { 109 110 private final class UpdateCopyrightFile { 111 private final String filePath; 112 private final List<String> bufferedLines = new LinkedList<>(); 113 private boolean copyrightUpdated; 114 private boolean lineBeforeCopyrightReaded; 115 private boolean commentBlockEnded; 116 private boolean portionsCopyrightNeeded; 117 private boolean copyrightSectionPresent; 118 private String curLine; 119 private String curLowerLine; 120 private Integer startYear; 121 private Integer endYear; 122 private final BufferedReader reader; 123 private final BufferedWriter writer; 124 125 private UpdateCopyrightFile(String filePath) throws IOException { 126 this.filePath = filePath; 127 reader = new BufferedReader(new FileReader(filePath)); 128 final File tmpFile = new File(filePath + ".tmp"); 129 if (!tmpFile.exists()) { 130 tmpFile.createNewFile(); 131 } 132 writer = new BufferedWriter(new FileWriter(tmpFile)); 133 } 134 135 private void updateCopyrightForFile() throws MojoExecutionException { 136 try { 137 readLineBeforeCopyrightToken(); 138 portionsCopyrightNeeded = readOldCopyrightLine(); 139 copyrightSectionPresent = readCopyrightLine(); 140 writeCopyrightLine(); 141 writeChanges(); 142 } catch (final Exception e) { 143 throw new MojoExecutionException(e.getMessage(), e); 144 } finally { 145 Utils.closeSilently(reader, writer); 146 } 147 } 148 149 private void writeChanges() throws Exception { 150 while (curLine != null) { 151 nextLine(); 152 } 153 reader.close(); 154 155 for (final String line : bufferedLines) { 156 writer.write(line); 157 writer.newLine(); 158 } 159 writer.close(); 160 161 if (!dryRun) { 162 final File updatedFile = new File(filePath); 163 if (!updatedFile.delete()) { 164 throw new Exception("impossible to perform rename on the file."); 165 } 166 new File(filePath + ".tmp").renameTo(updatedFile); 167 } 168 } 169 170 private void writeCopyrightLine() throws Exception { 171 if (copyrightSectionPresent) { 172 updateExistingCopyrightLine(); 173 copyrightUpdated = true; 174 return; 175 } 176 177 int indexAdd = bufferedLines.size() - 1; 178 final Pattern stopRegExp = portionsCopyrightNeeded ? OLD_COPYRIGHT_REGEXP 179 : lineBeforeCopyrightCompiledRegExp; 180 String previousLine = curLine; 181 while (!lineMatches(previousLine, stopRegExp)) { 182 indexAdd--; 183 previousLine = bufferedLines.get(indexAdd); 184 } 185 indexAdd++; 186 if (!portionsCopyrightNeeded) { 187 for (int i = 0; i < nbLinesToSkip; i++) { 188 bufferedLines.add(indexAdd++, getNewCommentedLine()); 189 } 190 } 191 final String newCopyrightLine = getNewCommentedLine() 192 + indent() + (portionsCopyrightNeeded ? newPortionsCopyrightLabel : newCopyrightLabel) 193 + " " + currentYear + " " + forgeRockCopyrightLabel; 194 bufferedLines.add(indexAdd, newCopyrightLine); 195 copyrightUpdated = true; 196 } 197 198 private void updateExistingCopyrightLine() throws Exception { 199 readYearSection(); 200 final String newCopyrightLine; 201 if (endYear == null) { 202 // OLD_YEAR => OLD_YEAR-CURRENT_YEAR 203 newCopyrightLine = curLine.replace(startYear.toString(), intervalToString(startYear, currentYear)); 204 } else { 205 // VERY_OLD_YEAR-OLD_YEAR => VERY_OLD_YEAR-CURRENT_YEAR 206 newCopyrightLine = curLine.replace(intervalToString(startYear, endYear), 207 intervalToString(startYear, currentYear)); 208 } 209 bufferedLines.remove(bufferedLines.size() - 1); 210 bufferedLines.add(newCopyrightLine); 211 } 212 213 private void readYearSection() throws Exception { 214 final String copyrightLineRegExp = ".*\\s+(\\d{4})(-(\\d{4}))?\\s+" + forgerockCopyrightRegExp + ".*"; 215 final Matcher copyrightMatcher = Pattern.compile(copyrightLineRegExp, CASE_INSENSITIVE).matcher(curLine); 216 if (copyrightMatcher.matches()) { 217 startYear = Integer.parseInt(copyrightMatcher.group(1)); 218 final String endYearString = copyrightMatcher.group(3); 219 if (endYearString != null) { 220 endYear = Integer.parseInt(endYearString); 221 } 222 } else { 223 throw new Exception("Malformed year section in copyright line " + curLine); 224 } 225 } 226 227 private void readLineBeforeCopyrightToken() throws Exception { 228 nextLine(); 229 while (curLine != null) { 230 if (curLineMatches(lineBeforeCopyrightCompiledRegExp)) { 231 if (!isCommentLine(curLowerLine)) { 232 throw new Exception("The line before copyright token must be a commented line"); 233 } 234 lineBeforeCopyrightReaded = true; 235 return; 236 } else if (commentBlockEnded) { 237 throw new Exception("unexpected non commented line found before copyright section"); 238 } 239 nextLine(); 240 } 241 } 242 243 private boolean readOldCopyrightLine() throws Exception { 244 nextLine(); 245 while (curLine != null) { 246 if (isOldCopyrightOwnerLine()) { 247 return true; 248 } else if (isNonEmptyCommentedLine(curLine) 249 || isCopyrightLine() 250 || commentBlockEnded) { 251 return false; 252 } 253 nextLine(); 254 } 255 throw new Exception("unexpected end of file while trying to read copyright"); 256 } 257 258 private boolean readCopyrightLine() throws Exception { 259 while (curLine != null) { 260 if (isCopyrightLine()) { 261 return true; 262 } else if ((isNonEmptyCommentedLine(curLine) && !isOldCopyrightOwnerLine()) 263 || commentBlockEnded) { 264 return false; 265 } 266 nextLine(); 267 } 268 throw new Exception("unexpected end of file while trying to read copyright"); 269 } 270 271 private boolean isOldCopyrightOwnerLine() { 272 return curLineMatches(OLD_COPYRIGHT_REGEXP) && !curLineMatches(copyrightOwnerCompiledRegExp); 273 } 274 275 private boolean isCopyrightLine() { 276 return curLineMatches(copyrightOwnerCompiledRegExp); 277 } 278 279 private boolean curLineMatches(Pattern compiledRegExp) { 280 return lineMatches(curLine, compiledRegExp); 281 } 282 283 private boolean lineMatches(String line, Pattern compiledRegExp) { 284 return compiledRegExp.matcher(line).matches(); 285 } 286 287 private void nextLine() throws Exception { 288 curLine = reader.readLine(); 289 if (curLine == null && !copyrightUpdated) { 290 throw new Exception("unexpected end of file while trying to read copyright"); 291 } else if (curLine != null) { 292 bufferedLines.add(curLine); 293 } 294 295 if (!copyrightUpdated) { 296 curLowerLine = curLine.trim().toLowerCase(); 297 if (lineBeforeCopyrightReaded && !isCommentLine(curLowerLine)) { 298 commentBlockEnded = true; 299 } 300 } 301 } 302 303 private String getNewCommentedLine() throws Exception { 304 int indexCommentToken = 1; 305 String commentToken = null; 306 String linePattern = null; 307 while (bufferedLines.size() > indexCommentToken && commentToken == null) { 308 linePattern = bufferedLines.get(indexCommentToken++); 309 commentToken = getCommentTokenInBlock(linePattern); 310 } 311 if (commentToken != null) { 312 return linePattern.substring(0, linePattern.indexOf(commentToken) + 1); 313 } else { 314 throw new Exception("Uncompatibles comments lines in the file."); 315 } 316 } 317 318 } 319 320 private static final Pattern OLD_COPYRIGHT_REGEXP = Pattern.compile(".*copyright.*", CASE_INSENSITIVE); 321 322 /** 323 * Number of lines to add after the line which contains the lineBeforeCopyrightToken. 324 * Used only if a new copyright line is needed. 325 */ 326 @Parameter(required = true, defaultValue = "2") 327 private Integer nbLinesToSkip; 328 329 /** 330 * Number of spaces to add after the comment line token before adding new 331 * copyright section. Used only if a new copyright or portion copyright is 332 * needed. 333 */ 334 @Parameter(required = true, defaultValue = "6") 335 private Integer numberSpaceIdentation; 336 337 /** The last non empty commented line before the copyright section. */ 338 @Parameter(required = true, defaultValue = "CDDL\\s+HEADER\\s+END") 339 private String lineBeforeCopyrightRegExp; 340 341 /** The regular expression which identifies a copyrighted line. */ 342 @Parameter(required = true, defaultValue = "ForgeRock\\s+AS") 343 private String forgerockCopyrightRegExp; 344 345 /** Line to add if there is no existing copyright. */ 346 @Parameter(required = true, defaultValue = "Copyright") 347 private String newCopyrightLabel; 348 349 /** Portions copyright start line token. */ 350 @Parameter(required = true, defaultValue = "Portions Copyright") 351 private String newPortionsCopyrightLabel; 352 353 /** ForgeRock copyright label to print if a new (portions) copyright line is needed. */ 354 @Parameter(required = true, defaultValue = "ForgeRock AS.") 355 private String forgeRockCopyrightLabel; 356 357 /** A dry run will not change source code. It creates new files with '.tmp' extension. */ 358 @Parameter(required = true, defaultValue = "false") 359 private boolean dryRun; 360 361 /** RegExps corresponding to user token. */ 362 private Pattern lineBeforeCopyrightCompiledRegExp; 363 private Pattern copyrightOwnerCompiledRegExp; 364 365 private boolean buildOK = true; 366 367 368 /** 369 * Updates copyright of modified files. 370 * 371 * @throws MojoFailureException 372 * if any 373 * @throws MojoExecutionException 374 * if any 375 */ 376 @Override 377 public void execute() throws MojoExecutionException, MojoFailureException { 378 compileRegExps(); 379 checkCopyrights(); 380 for (final String filePath : getIncorrectCopyrightFilePaths()) { 381 try { 382 new UpdateCopyrightFile(filePath).updateCopyrightForFile(); 383 getLog().info("Copyright of file " + filePath + " has been successfully updated."); 384 } catch (final Exception e) { 385 getLog().error("Impossible to update copyright of file " + filePath); 386 getLog().error(" Details: " + e.getMessage()); 387 getLog().error(" No modification has been performed on this file"); 388 buildOK = false; 389 } 390 } 391 392 if (!buildOK) { 393 throw new MojoFailureException("Error(s) occured while trying to update some copyrights."); 394 } 395 } 396 397 private void compileRegExps() { 398 lineBeforeCopyrightCompiledRegExp = compileRegExp(lineBeforeCopyrightRegExp); 399 copyrightOwnerCompiledRegExp = compileRegExp(forgerockCopyrightRegExp); 400 } 401 402 private Pattern compileRegExp(String regExp) { 403 return Pattern.compile(".*" + regExp + ".*", CASE_INSENSITIVE); 404 } 405 406 private String intervalToString(Integer startYear, Integer endYear) { 407 return startYear + "-" + endYear; 408 } 409 410 private String indent() { 411 String indentation = ""; 412 for (int i = 0; i < numberSpaceIdentation; i++) { 413 indentation += " "; 414 } 415 return indentation; 416 } 417 418 // Setters to allow tests 419 420 void setLineBeforeCopyrightToken(String lineBeforeCopyrightToken) { 421 this.lineBeforeCopyrightRegExp = lineBeforeCopyrightToken; 422 } 423 424 void setNbLinesToSkip(Integer nbLinesToSkip) { 425 this.nbLinesToSkip = nbLinesToSkip; 426 } 427 428 void setNumberSpaceIdentation(Integer numberSpaceIdentation) { 429 this.numberSpaceIdentation = numberSpaceIdentation; 430 } 431 432 void setNewPortionsCopyrightString(String portionsCopyrightString) { 433 this.newPortionsCopyrightLabel = portionsCopyrightString; 434 } 435 436 void setNewCopyrightOwnerString(String newCopyrightOwnerString) { 437 this.forgeRockCopyrightLabel = newCopyrightOwnerString; 438 } 439 440 void setNewCopyrightStartToken(String copyrightStartString) { 441 this.newCopyrightLabel = copyrightStartString; 442 } 443 444 void setCopyrightEndToken(String copyrightEndToken) { 445 this.forgerockCopyrightRegExp = copyrightEndToken; 446 } 447 448 void setDryRun(final boolean dryRun) { 449 this.dryRun = true; 450 } 451 452}