org.codehaus.mojo.license.AbstractFileHeaderMojo Maven / Gradle / Ivy
Show all versions of license-maven-plugin Show documentation
package org.codehaus.mojo.license;
/*
* #%L
* License Maven Plugin
* %%
* Copyright (C) 2008 - 2012 CodeLutin, Codehaus, Tony Chemit
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Lesser Public License for more details.
*
* You should have received a copy of the GNU General Lesser Public
* License along with this program. If not, see
* .
* #L%
*/
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import freemarker.template.Template;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Parameter;
import org.codehaus.mojo.license.api.FreeMarkerHelper;
import org.codehaus.mojo.license.header.FileHeader;
import org.codehaus.mojo.license.header.FileHeaderProcessor;
import org.codehaus.mojo.license.header.InvalideFileHeaderException;
import org.codehaus.mojo.license.header.UpdateFileHeaderFilter;
import org.codehaus.mojo.license.header.transformer.FileHeaderTransformer;
import org.codehaus.mojo.license.header.transformer.JavaFileHeaderTransformer;
import org.codehaus.mojo.license.model.Copyright;
import org.codehaus.mojo.license.model.License;
import org.codehaus.mojo.license.utils.FileUtil;
import org.codehaus.mojo.license.utils.MojoHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract mojo for file-header operations (check, update, report,...).
*
* @author tchemit [email protected]
* @since 1.2
*/
public abstract class AbstractFileHeaderMojo extends AbstractLicenseNameMojo {
private static final Logger LOG = LoggerFactory.getLogger(AbstractFileHeaderMojo.class);
// ----------------------------------------------------------------------
// Mojo Parameters
// ----------------------------------------------------------------------
/**
* To overwrite the processStartTag used to build header model.
*
* See File header configuration.
*
* @since 1.1
*/
@Parameter(property = "license.processStartTag")
private String processStartTag;
/**
* To overwrite the processEndTag used to build header model.
*
* See File header configuration.
*
* @since 1.1
*/
@Parameter(property = "license.processEndTag")
private String processEndTag;
/**
* To overwrite the sectionDelimiter used to build header model.
*
* See File header configuration.
*
* @since 1.1
*/
@Parameter(property = "license.sectionDelimiter")
private String sectionDelimiter;
/**
* To specify a line separator to use.
*
* If not set, will use system property {@code line.separator}.
*/
@Parameter(property = "license.lineSeparator")
private String lineSeparator;
/**
* A flag to add svn:keywords on new header.
*
* Will add svn keywords :
*
Id, HeadURL
*
* Note: This parameter is used by the {@link #descriptionTemplate}, so if you change this
* template, the parameter could be no more used (depends what you put in your own template...).
*
* @since 1.0
*/
@Parameter(property = "license.addSvnKeyWords", defaultValue = "false")
private boolean addSvnKeyWords;
/**
* A flag to authorize update of the description part of the header.
*
* Note: By default, do NOT authorize it since description can change
* on each file).
*
* @since 1.0
*/
@Parameter(property = "license.canUpdateDescription", defaultValue = "false")
private boolean canUpdateDescription;
/**
* A flag to authorize update of the copyright part of the header.
*
* Note: By default, do NOT authorize it since copyright part should be
* handled by developpers (holder can change on each file for example).
*
* @since 1.0
*/
@Parameter(property = "license.canUpdateCopyright", defaultValue = "false")
private boolean canUpdateCopyright;
/**
* A flag to authorize update of the license part of the header.
*
* Note: By default, authorize it since license part should always be
* generated by the plugin.
*
* @since 1.0
*/
@Parameter(property = "license.canUpdateLicense", defaultValue = "true")
private boolean canUpdateLicense;
/**
* A tag to place on files that will be ignored by the plugin.
*
* Sometimes, it is necessary to do this when file is under a specific license.
*
* Note: If no sets, will use the default tag {@code %% Ignore-License}
*
* @since 1.0
*/
@Parameter(property = "license.ignoreTag")
private String ignoreTag;
/**
* A flag to add the license header in java files after the package statement.
*
* This is a practice used by many people (apache, codehaus, ...).
*
* Note: By default this property is then to {@code true} since it is a good practice.
*
* @since 1.2
*/
@Parameter(property = "license.addJavaLicenseAfterPackage", defaultValue = "true")
private boolean addJavaLicenseAfterPackage;
/**
* A flag to use for java comment start tag with no reformat syntax {@code /*-}.
*
* See http://www.oracle.com/technetwork/java/javase/documentation/codeconventions-141999.html#350
*
* @since 1.9
*/
@Parameter(property = "license.useJavaNoReformatCommentStartTag", defaultValue = "true")
private boolean useJavaNoReformatCommentStartTag;
/**
* A flag to indicate if there should be an empty line after the header.
*
* Checkstyle requires empty line between license header and package statement.
* If you are using addJavaLicenseAfterPackage=false it could make sense to set this to true.
*
* Note: By default this property is set to {@code false} to keep old behavior.
*
* @since 1.9
*/
@Parameter(property = "license.emptyLineAfterHeader", defaultValue = "false")
private boolean emptyLineAfterHeader;
/**
* A flag to indicate if trailing spaces should be trimmed.
*
* Checkstyle usually requires no trailing whitespace.
* If it is the case it could make sense to set this to true
*
* Note: By default this property is set to {@code false} to keep old behavior.
*
* @since 1.14
*/
@Parameter(property = "license.trimHeaderLine", defaultValue = "false")
private boolean trimHeaderLine;
/**
* A flag to ignore no files to scan.
*
* This flag will suppress the "No file to scan" warning. This will allow you to set the plug-in in the root pom of
* your project without getting a lot of warnings for aggregation modules / artifacts.
*
* Note: By default this property is set to {@code false} to keep old behavior.
*
* @since 1.9
*/
@Parameter(property = "license.ignoreNoFileToScan", defaultValue = "false")
private boolean ignoreNoFileToScan;
/**
* To specify the base dir from which we apply the license.
*
* Should be on form "root1,root2,rootn".
*
* By default, the main roots are "src, target/generated-sources, target/processed-sources".
*
* Note: If some of these roots do not exist, they will be simply
* ignored.
*
* Note: This parameter is not useable if you are still using a project file descriptor.
*
* @since 1.0
*/
@Parameter(property = "license.roots")
private String[] roots;
/**
* Specific files to includes, separated by a comma. By default, it is "** /*".
*
* Note: This parameter is not usable if you are still using a project file descriptor.
*
* @since 1.0
*/
@Parameter(property = "license.includes")
private String[] includes;
/**
* Specific files to excludes, separated by a comma.
* By default, those file types are excluded:
*
* - modelisation
* - images
*
*
* @since 1.0
*/
@Parameter(property = "license.excludes")
private String[] excludes;
/**
* To associate extra extension files to an existing comment style.
*
* Keys of the map are the extension of extra files to treat, and the value
* is the comment style you want to associate.
*
* For example, to treat file with extensions {@code java2} and {@code jdata}
* as {@code java} files (says using the {@code java} comment style, declare this
* in your plugin configuration :
*
* <extraExtensions>
* <java2>java</java2>
* <jdata>java</jdata>
* </extraExtensions>
*
*
* @since 1.0
*/
@Parameter
private Map extraExtensions;
/**
* To associate extra files to an existing comment style.
*
* Keys of the map are the name of extra files to treat, and the value
* is the comment style you want to associate.
*
* For example, to treat a file named {@code DockerFile} as {@code properties} files
* (says using the {@code properties} comment style, declare this in your plugin configuration :
*
* <extraFiles>
* <DockerFile>properties</DockerFile>
* </extraFiles>
*
*
* @since 1.11
*/
@Parameter
private Map extraFiles;
/**
* Template used to build the description section of the license header.
*
* (This template use freemarker).
*
* @since 1.1
*/
@Parameter(
property = "license.descriptionTemplate",
defaultValue = "/org/codehaus/mojo/license/default-file-header-description.ftl")
private String descriptionTemplate;
// ----------------------------------------------------------------------
// Plexus components
// ----------------------------------------------------------------------
/**
* All available header transformers.
*
* @since 1.0
*/
private final Map transformers;
// ----------------------------------------------------------------------
// Private fields
// ----------------------------------------------------------------------
/**
* timestamp used for generation.
*/
private long timestamp;
/**
* The dictionary of extension indexed by their associated comment style.
*
* @since 1.0
*/
private Map extensionToCommentStyle;
/**
* The freemarker template used to render the description section of the file header.
*
* @since 1.1
*/
private Template descriptionTemplate0;
/**
* set of processed files
*/
private Set processedFiles;
/**
* Dictionary of treated files indexed by their state.
*/
Map> result;
/**
* Dictionary of files to treat indexed by their CommentStyle.
*/
private Map> filesToTreatByCommentStyle;
/**
* Freemarker helper component.
*
* @since 1.0
*/
private FreeMarkerHelper freeMarkerHelper = FreeMarkerHelper.newDefaultHelper();
protected AbstractFileHeaderMojo(Map transformers) {
this.transformers = transformers;
}
// ----------------------------------------------------------------------
// Abstract Methods
// ----------------------------------------------------------------------
/**
* @return {@code true} if mojo must be a simple dry run (says do not modifiy any scanned files), {@code false}
* otherise.
*/
protected abstract boolean isDryRun();
/**
* @return {@code true} if mojo should fails if dryRun and there is some missing license header, {@code false}
* otherwise.
*/
protected abstract boolean isFailOnMissingHeader();
/**
* @return {@code true} if mojo should fails if dryRun and there is some obsolete license header, {@code false}
* otherwise.
*/
protected abstract boolean isFailOnNotUptodateHeader();
// ----------------------------------------------------------------------
// AbstractLicenseMojo Implementaton
// ----------------------------------------------------------------------
@Override
public void init() throws Exception {
if (StringUtils.isEmpty(ignoreTag)) {
// use default value
this.ignoreTag = "%" + "%Ignore-License";
}
if (!isDryRun()) {
if (isFailOnMissingHeader()) {
LOG.warn("The failOnMissingHeader has no effect if the property dryRun is not set.");
}
if (isFailOnNotUptodateHeader()) {
LOG.warn("The failOnNotUptodateHeader has no effect if the property dryRun is not set.");
}
}
if (isVerbose()) {
// print available comment styles (transformers)
StringBuilder buffer = new StringBuilder();
buffer.append("config - available comment styles :");
String commentFormat = "\n * %1$s (%2$s)";
for (String transformerName : transformers.keySet()) {
FileHeaderTransformer aTransformer = getTransformer(transformers, transformerName);
String str = String.format(commentFormat, aTransformer.getName(), aTransformer.getDescription());
buffer.append(str);
}
LOG.info("{}", buffer);
}
// set timestamp used for temporary files
this.timestamp = System.nanoTime();
super.init();
if (roots == null || roots.length == 0) {
roots = DEFAULT_ROOTS;
if (isVerbose()) {
LOG.info("Will use default roots {}", (Object) roots);
}
}
if (includes == null || includes.length == 0) {
includes = DEFAULT_INCLUDES;
if (isVerbose()) {
LOG.info("Will use default includes {}", (Object) includes);
}
}
if (excludes == null || excludes.length == 0) {
excludes = DEFAULT_EXCLUDES;
if (isVerbose()) {
LOG.info("Will use default excludes {}", (Object) excludes);
}
}
extensionToCommentStyle = new TreeMap<>();
processStartTag = cleanHeaderConfiguration(processStartTag, FileHeaderTransformer.DEFAULT_PROCESS_START_TAG);
if (isVerbose()) {
LOG.info("Will use processStartTag: {}", processStartTag);
}
processEndTag = cleanHeaderConfiguration(processEndTag, FileHeaderTransformer.DEFAULT_PROCESS_END_TAG);
if (isVerbose()) {
LOG.info("Will use processEndTag: {}", processEndTag);
}
sectionDelimiter = cleanHeaderConfiguration(sectionDelimiter, FileHeaderTransformer.DEFAULT_SECTION_DELIMITER);
if (isVerbose()) {
LOG.info("Will use sectionDelimiter: {}", sectionDelimiter);
}
// add default extensions from header transformers
for (Map.Entry entry : transformers.entrySet()) {
String commentStyle = entry.getKey();
FileHeaderTransformer aTransformer = entry.getValue();
aTransformer.setProcessStartTag(processStartTag);
aTransformer.setProcessEndTag(processEndTag);
aTransformer.setSectionDelimiter(sectionDelimiter);
aTransformer.setEmptyLineAfterHeader(emptyLineAfterHeader);
aTransformer.setTrimHeaderLine(trimHeaderLine);
aTransformer.setLineSeparator(lineSeparator);
if (aTransformer instanceof JavaFileHeaderTransformer) {
JavaFileHeaderTransformer javaFileHeaderTransformer = (JavaFileHeaderTransformer) aTransformer;
javaFileHeaderTransformer.setAddJavaLicenseAfterPackage(addJavaLicenseAfterPackage);
javaFileHeaderTransformer.setUseNoReformatCommentStartTag(useJavaNoReformatCommentStartTag);
}
String[] extensions = aTransformer.getDefaultAcceptedExtensions();
for (String extension : extensions) {
if (isVerbose()) {
LOG.info("Associate extension '{}' to comment style '{}'", extension, commentStyle);
}
extensionToCommentStyle.put(extension, commentStyle);
}
}
if (extraExtensions != null) {
// fill extra extensions for each transformer
for (Map.Entry entry : extraExtensions.entrySet()) {
String extension = entry.getKey();
if (extensionToCommentStyle.containsKey(extension)) {
// override existing extension mapping
LOG.warn(
"The extension '{}' is already accepted for comment style '{}'",
extension,
extensionToCommentStyle.get(extension));
}
String commentStyle = entry.getValue();
// check transformer exists
getTransformer(transformers, commentStyle);
if (isVerbose()) {
LOG.info("Associate extension '{}' to comment style '{}'", extension, commentStyle);
}
extensionToCommentStyle.put(extension, commentStyle);
}
}
if (extraFiles == null) {
extraFiles = Collections.emptyMap();
}
// get all files to treat indexed by their comment style
filesToTreatByCommentStyle = obtainFilesToProcessByCommentStyle(
extraFiles, roots, includes, excludes, extensionToCommentStyle, transformers);
// build the description template
if (isVerbose()) {
LOG.info("Use description template: {}", descriptionTemplate);
}
descriptionTemplate0 = freeMarkerHelper.getTemplate(descriptionTemplate);
}
@Override
public void doAction() throws Exception {
long t0 = System.nanoTime();
processedFiles = ConcurrentHashMap.newKeySet();
result = new EnumMap<>(FileState.class);
try {
for (Map.Entry> commentStyleFiles : filesToTreatByCommentStyle.entrySet()) {
String commentStyle = commentStyleFiles.getKey();
List files = commentStyleFiles.getValue();
processCommentStyle(commentStyle, files);
}
} finally {
checkResults(result);
int nbFiles = processedFiles.size();
if (nbFiles == 0 && !ignoreNoFileToScan) {
LOG.warn("No file to scan.");
} else {
String delay = MojoHelper.convertTime(System.nanoTime() - t0);
String message =
String.format("Scan %s file%s header done in %s.", nbFiles, nbFiles > 1 ? "s" : "", delay);
LOG.info(message);
}
Set states = result.keySet();
if (states.size() == 1 && states.contains(FileState.uptodate)) {
// all files where up to date
LOG.info("All files are up-to-date.");
} else {
StringBuilder buffer = new StringBuilder();
for (FileState state : FileState.values()) {
reportType(result, state, buffer);
}
LOG.info(buffer.toString());
}
}
}
// ----------------------------------------------------------------------
// Private Methods
// ----------------------------------------------------------------------
/**
* Checks the results of the mojo execution using the {@link #isFailOnMissingHeader()} and
* {@link #isFailOnNotUptodateHeader()}.
*
* @param result processed files by their status
* @throws MojoFailureException if check is not ok (some file with no header or to update)
*/
private void checkResults(Map> result) throws MojoFailureException {
Set states = result.keySet();
StringBuilder builder = new StringBuilder();
if (isDryRun() && isFailOnMissingHeader() && states.contains(FileState.add)) {
List files = FileUtil.orderFiles(result.get(FileState.add));
builder.append("There are ").append(files.size()).append(" file(s) with no header :");
for (File file : files) {
builder.append("\n").append(file);
}
}
if (isDryRun() && isFailOnNotUptodateHeader() && states.contains(FileState.update)) {
List files = FileUtil.orderFiles(result.get(FileState.update));
builder.append("\nThere are ").append(files.size()).append(" file(s) with header to update:");
for (File file : files) {
builder.append("\n").append(file);
}
}
String message = builder.toString();
if (StringUtils.isNotBlank(message)) {
throw new MojoFailureException(builder.toString());
}
}
/**
* Process a given comment style to all his detected files.
*
* @param commentStyle comment style to treat
* @param filesToTreat files using this comment style to treat
* @throws IOException if any IO error while processing files
*/
private void processCommentStyle(String commentStyle, List filesToTreat) throws IOException {
// obtain license from definition
License license = getLicense(getLicenseName(), true);
if (isVerbose()) {
LOG.info("Process header '{}'", commentStyle);
LOG.info(" - using {}", license.getDescription());
}
// use header transformer according to comment style given in header
FileHeaderTransformer transformer = getTransformer(transformers, commentStyle);
try {
filesToTreat.parallelStream().forEach(file -> {
try {
FileHeaderProcessor processor = getFileHeaderProcessor(license, transformer);
processFile(processor, file);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
} catch (UncheckedIOException e) {
throw e.getCause();
}
filesToTreat.clear();
}
private FileHeaderProcessor getFileHeaderProcessor(License license, FileHeaderTransformer transformer)
throws IOException {
// file header to use if no header is found on a file
FileHeader header = new FileHeader();
if (inceptionYear == null) {
LOG.warn("No inceptionYear defined (will use current year)");
}
Copyright copyright = getCopyright(copyrightStringFormat, getCopyrightOwners());
header.setCopyright(copyright);
String licenseContent = license.getHeaderContent(getEncoding());
if (license.isHeaderContentTemplateAware()) {
licenseContent = processLicenseContext(licenseContent);
}
header.setLicense(licenseContent);
UpdateFileHeaderFilter filter = new UpdateFileHeaderFilter();
filter.setUpdateCopyright(canUpdateCopyright);
filter.setUpdateDescription(canUpdateDescription);
filter.setUpdateLicense(canUpdateLicense);
// update processor filter
return new FileHeaderProcessor(filter, header, transformer);
}
/**
* Process the given file (will copy it, process the clone file and finally finalizeFile after process)...
*
* @param processor current file processor
* @param file original file to process
* @throws IOException if any IO error while processing this file
*/
private void processFile(FileHeaderProcessor processor, File file) throws IOException {
if (processedFiles.contains(file)) {
LOG.info(" - skip already processed file {}", file);
return;
}
// output file
File processFile = new File(file.getAbsolutePath() + "_" + timestamp);
boolean doFinalize = false;
try {
doFinalize = processFile(processor, file, processFile);
} catch (Exception e) {
LOG.warn(
"skip failed file: " + e.getMessage()
+ (e.getCause() == null
? ""
: " Cause : " + e.getCause().getMessage()),
e);
FileState.fail.addFile(file, result);
doFinalize = false;
} finally {
// whatever was the result, this file is treated.
processedFiles.add(file);
if (doFinalize) {
finalizeFile(file, processFile);
} else {
Files.deleteIfExists(processFile.toPath());
}
}
}
/**
* Process the given {@code file} and save the result in the given {@code processFile}.
*
* @param processor current file processor
* @param file the file to process
* @param processFile the output processed file
* @return {@code true} if prepareProcessFile can be finalize, otherwise need to be delete
* @throws java.io.IOException if any pb while treatment
*/
private boolean processFile(FileHeaderProcessor processor, File file, File processFile) throws IOException {
if (getLog().isDebugEnabled()) {
LOG.debug(" - process file {}", file);
LOG.debug(" - will process into file {}", processFile);
}
// update the file header description
Map descriptionParameters = new HashMap<>();
descriptionParameters.put("project", getProject());
descriptionParameters.put("addSvnKeyWords", addSvnKeyWords);
descriptionParameters.put("projectName", projectName);
descriptionParameters.put("inceptionYear", inceptionYear);
descriptionParameters.put("organizationName", organizationName);
descriptionParameters.put("file", file);
LOG.debug("Description parameters: {}", descriptionParameters);
String description = freeMarkerHelper.renderTemplate(descriptionTemplate0, descriptionParameters);
processor.updateDescription(description);
LOG.debug("header description : " + processor.getFileHeaderDescription());
String content;
try {
// check before all that file should not be skip by the ignoreTag
// this is a costy operation
// TODO-TC-20100411 We should process always from the read content not reading again from file
content = IOUtils.toString(file.toURI(), getEncoding());
} catch (IOException e) {
throw new IOException("Could not obtain content of file " + file);
}
// check that file is not marked to be ignored
if (content.contains(ignoreTag)) {
LOG.info(" - ignore file (detected {}) {}", ignoreTag, file);
FileState.ignore.addFile(file, result);
return false;
}
// process file to detect header
try {
processor.process(content, processFile, getEncoding());
} catch (IllegalStateException e) {
// could not obtain existing header
throw new InvalideFileHeaderException(
"Could not extract header on file " + file + " for reason " + e.getMessage());
} catch (Exception e) {
if (e instanceof InvalideFileHeaderException) {
throw (InvalideFileHeaderException) e;
}
throw new IOException("Could not process file " + file + " for reason " + e.getMessage());
}
if (processor.isTouched()) {
if (isVerbose()) {
LOG.info(" - header was updated for {}", file);
}
if (processor.isModified()) {
// header content has changed
// must copy back process file to file (if not dry run)
FileState.update.addFile(file, result);
return true;
}
FileState.uptodate.addFile(file, result);
return false;
}
// header was not fully (or not at all) detected in file
if (processor.isDetectHeader()) {
// file has not a valid header (found a start process atg, but
// not an ending one), can not do anything
throw new InvalideFileHeaderException("Could not find header end on file " + file);
}
// no header at all, add a new header
if (isVerbose()) {
LOG.info(" - adding license header on file {}", file);
}
// FIXME tchemit 20100409 xml files must add header after a xml prolog line
content = processor.addHeader(content);
if (!isDryRun()) {
FileUtil.printString(processFile, content, getEncoding());
}
FileState.add.addFile(file, result);
return true;
}
/**
* Finalize the process of a file.
*
* If ad DryRun then just remove processed file, else use process file as original file.
*
* @param file the original file
* @param processFile the processed file
* @throws IOException if any IO error while finalizing file
*/
private void finalizeFile(File file, File processFile) throws IOException {
if (isKeepBackup() && !isDryRun()) {
File backupFile = FileUtil.getBackupFile(file);
// always delete backup file, before the renaming
Files.deleteIfExists(backupFile.toPath());
LOG.debug(" - backup original file {}", file);
Files.copy(file.toPath(), backupFile.toPath(), StandardCopyOption.COPY_ATTRIBUTES);
}
if (isDryRun()) {
// dry run, delete temporary file
Files.deleteIfExists(processFile.toPath());
} else {
try {
// replace file with the updated one
String updatedContent = IOUtils.toString(processFile.toURI(), getEncoding());
FileUtil.printString(file, updatedContent, getEncoding());
Files.deleteIfExists(processFile.toPath());
} catch (IOException e) {
LOG.warn("Error updating {} -> {}", processFile, file, e);
}
}
}
}