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}