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.*;
029
030import java.io.BufferedReader;
031import java.io.File;
032import java.io.FileReader;
033import java.io.IOException;
034import java.util.Arrays;
035import java.util.Calendar;
036import java.util.LinkedList;
037import java.util.List;
038
039import org.apache.maven.plugin.AbstractMojo;
040import org.apache.maven.plugin.MojoExecutionException;
041import org.apache.maven.plugin.MojoFailureException;
042import org.apache.maven.plugins.annotations.Parameter;
043import org.apache.maven.project.MavenProject;
044import org.apache.maven.scm.ScmException;
045import org.apache.maven.scm.ScmFile;
046import org.apache.maven.scm.ScmFileSet;
047import org.apache.maven.scm.ScmFileStatus;
048import org.apache.maven.scm.command.status.StatusScmResult;
049import org.apache.maven.scm.manager.BasicScmManager;
050import org.apache.maven.scm.manager.NoSuchScmProviderException;
051import org.apache.maven.scm.manager.ScmManager;
052import org.apache.maven.scm.provider.ScmProvider;
053import org.apache.maven.scm.provider.git.gitexe.GitExeScmProvider;
054import org.apache.maven.scm.provider.svn.svnexe.SvnExeScmProvider;
055import org.apache.maven.scm.repository.ScmRepository;
056import org.apache.maven.scm.repository.ScmRepositoryException;
057
058/**
059 * Abstract class which is used for both copyright checks and updates.
060 */
061public abstract class CopyrightAbstractMojo extends AbstractMojo {
062
063    /** The Maven Project. */
064    @Parameter(required = true, property = "project", readonly = true)
065    private MavenProject project;
066
067    /**
068     * Copyright owner.
069     * This string token must be present on the same line with 'copyright' keyword and the current year.
070     */
071    @Parameter(required = true, defaultValue = "ForgeRock AS")
072    private String copyrightOwnerToken;
073
074    /** The path to the root of the Subversion workspace to check. */
075    @Parameter(required = true, defaultValue = "${basedir}")
076    private String scmWorkspaceRoot;
077
078    @Parameter(required = true, defaultValue = "${project.scm.connection}")
079    private String scmRepositoryUrl;
080
081    /** The file extensions to test. */
082    public static final List<String> CHECKED_EXTENSIONS = new LinkedList<>(Arrays.asList(
083            "bat", "c", "h", "html", "java", "ldif", "Makefile", "mc", "sh", "txt", "xml", "xsd", "xsl"));
084
085    private static final List<String> EXCLUDED_END_COMMENT_BLOCK_TOKEN = new LinkedList<>(Arrays.asList(
086                    "*/", "-->"));
087
088    private static final List<String> SUPPORTED_COMMENT_MIDDLE_BLOCK_TOKEN = new LinkedList<>(Arrays.asList(
089                    "*", "#", "rem", "!"));
090
091    private static final List<String> SUPPORTED_START_BLOCK_COMMENT_TOKEN = new LinkedList<>(Arrays.asList(
092                    "/*", "<!--"));
093
094    /** The string representation of the current year. */
095    Integer currentYear = Calendar.getInstance().get(Calendar.YEAR);
096
097    private final List<String> incorrectCopyrightFilePaths = new LinkedList<>();
098
099    /** The overall SCM Client Manager. */
100    private ScmManager scmManager;
101
102    private ScmRepository scmRepository;
103
104    List<String> getIncorrectCopyrightFilePaths() {
105        return incorrectCopyrightFilePaths;
106    }
107
108    private ScmManager getScmManager() throws MojoExecutionException {
109        if (scmManager == null) {
110            scmManager = new BasicScmManager();
111            String scmProviderID = getScmProviderID();
112            ScmProvider scmProvider;
113            if ("svn".equals(scmProviderID)) {
114                scmProvider = new SvnExeScmProvider();
115            } else if ("git".equals(scmProviderID)) {
116                scmProvider = new GitExeScmProvider();
117            } else {
118                throw new MojoExecutionException("Unsupported scm provider: " + scmProviderID + " or "
119                        + getIncorrectScmRepositoryUrlMsg());
120            }
121            scmManager.setScmProvider(scmProviderID, scmProvider);
122        }
123
124        return scmManager;
125    }
126
127    private String getScmProviderID() throws MojoExecutionException {
128        try {
129            return scmRepositoryUrl.split(":")[1];
130        } catch (Exception e) {
131            throw new MojoExecutionException(getIncorrectScmRepositoryUrlMsg(), e);
132        }
133    }
134
135    String getIncorrectScmRepositoryUrlMsg() {
136        return "the scmRepositoryUrl property with value '" + scmRepositoryUrl + "' is incorrect. "
137                + "The URL has to respect the format: scm:[provider]:[provider_specific_url]";
138    }
139
140    ScmRepository getScmRepository() throws MojoExecutionException {
141        if (scmRepository == null) {
142            try {
143                scmRepository = getScmManager().makeScmRepository(scmRepositoryUrl);
144            } catch (NoSuchScmProviderException e) {
145                throw new MojoExecutionException("Could not find a provider.", e);
146            } catch (ScmRepositoryException e) {
147                throw new MojoExecutionException("Error while connecting to the repository", e);
148            }
149        }
150
151        return scmRepository;
152    }
153
154    String getScmWorkspaceRoot() {
155        return scmWorkspaceRoot;
156    }
157
158    /** Performs a diff with current working directory state against remote HEAD revision. */
159    List<String> getChangedFiles() throws MojoExecutionException, MojoFailureException  {
160        try {
161            ScmFileSet workspaceFileSet = new ScmFileSet(new File(getScmWorkspaceRoot()));
162            StatusScmResult statusResult = getScmManager().status(getScmRepository(), workspaceFileSet);
163            if (!statusResult.isSuccess()) {
164                getLog().error("Impossible to perform scm status command because " + statusResult.getCommandOutput());
165                throw new MojoFailureException("SCM error");
166            }
167
168            List<ScmFile> scmFiles = statusResult.getChangedFiles();
169            List<String> changedFilePaths = new LinkedList<>();
170            for (ScmFile scmFile : scmFiles) {
171                if (scmFile.getStatus() != ScmFileStatus.UNKNOWN) {
172                    changedFilePaths.add(scmFile.getPath());
173                }
174            }
175
176            return changedFilePaths;
177        } catch (ScmException e) {
178            throw new MojoExecutionException("Encountered an error while examining modified files,  SCM status:  "
179                    + e.getMessage() + "No further checks will be performed.", e);
180        }
181    }
182
183    /** Examines the provided files list to determine whether each changed file copyright is acceptable. */
184    void checkCopyrights() throws MojoExecutionException, MojoFailureException {
185        for (String changedFileName : getChangedFiles()) {
186            File changedFile = new File(getScmWorkspaceRoot(), changedFileName);
187            if (!changedFile.exists() || !changedFile.isFile()) {
188                continue;
189            }
190
191            int lastPeriodPos = changedFileName.lastIndexOf('.');
192            if (lastPeriodPos > 0) {
193                String extension = changedFileName.substring(lastPeriodPos + 1);
194                if (!CHECKED_EXTENSIONS.contains(extension.toLowerCase())) {
195                    continue;
196                }
197            } else if (fileNameEquals("bin", changedFile.getParentFile())
198                    && fileNameEquals("resource", changedFile.getParentFile().getParentFile())) {
199                // ignore resource/bin directory.
200                continue;
201            }
202
203            if (!checkCopyrightForFile(changedFile)) {
204                incorrectCopyrightFilePaths.add(changedFile.getAbsolutePath());
205            }
206        }
207    }
208
209    private boolean fileNameEquals(String folderName, File file) {
210        return file != null && folderName.equals(file.getName());
211    }
212
213    /**
214     * Check to see whether the provided file has a comment line containing a
215     * copyright without the current year.
216     */
217    @SuppressWarnings("resource")
218    private boolean checkCopyrightForFile(File changedFile) throws MojoExecutionException {
219        BufferedReader reader = null;
220        try {
221            reader = new BufferedReader(new FileReader(changedFile));
222            String line;
223            while ((line = reader.readLine()) != null) {
224                String lowerLine = line.toLowerCase().trim();
225                if (isCommentLine(lowerLine)
226                        && lowerLine.contains("copyright")
227                        && line.contains(currentYear.toString())
228                        && line.contains(copyrightOwnerToken)) {
229                    reader.close();
230                    return true;
231                }
232            }
233
234            return false;
235        } catch (IOException ioe) {
236            throw new MojoExecutionException("Could not read file " + changedFile.getPath()
237                    + " to check copyright date. No further copyright date checking will be performed.");
238        } finally {
239            closeSilently(reader);
240        }
241    }
242
243    private String getCommentToken(String line, boolean includesStartBlock) {
244        List<String> supportedTokens = SUPPORTED_COMMENT_MIDDLE_BLOCK_TOKEN;
245        if (includesStartBlock) {
246            supportedTokens.addAll(SUPPORTED_START_BLOCK_COMMENT_TOKEN);
247        }
248
249        if (trimmedLineStartsWith(line, EXCLUDED_END_COMMENT_BLOCK_TOKEN) != null) {
250            return null;
251        }
252
253        return trimmedLineStartsWith(line, supportedTokens);
254    }
255
256    private String trimmedLineStartsWith(String line, List<String> supportedTokens) {
257        for (String token : supportedTokens) {
258            if (line.trim().startsWith(token)) {
259                return token;
260            }
261        }
262        return null;
263    }
264
265    boolean isNonEmptyCommentedLine(String line) {
266        String commentToken = getCommentTokenInBlock(line);
267        return commentToken == null || !commentToken.equals(line.trim());
268    }
269
270    String getCommentTokenInBlock(String line) {
271        return getCommentToken(line, false);
272    }
273
274    boolean isCommentLine(String line) {
275        return getCommentToken(line, true) != null;
276    }
277
278}