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}