All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.relativitas.maven.plugins.formatter.FormatterMojo Maven / Gradle / Ivy

/**
 * Copyright 2010-2014. All work is copyrighted to their respective
 * author(s), unless otherwise stated.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.relativitas.maven.plugins.formatter;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.codehaus.plexus.components.io.fileselectors.FileSelector;
import org.codehaus.plexus.components.io.fileselectors.IncludeExcludeFileSelector;
import org.codehaus.plexus.components.io.resources.PlexusIoFileResource;
import org.codehaus.plexus.components.io.resources.PlexusIoFileResourceCollection;
import org.codehaus.plexus.components.io.resources.PlexusIoResource;
import org.codehaus.plexus.resource.ResourceManager;
import org.codehaus.plexus.resource.loader.FileResourceLoader;
import org.codehaus.plexus.resource.loader.ResourceNotFoundException;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.WriterFactory;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.ToolFactory;
import org.eclipse.jdt.core.formatter.CodeFormatter;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.TextEdit;
import org.xml.sax.SAXException;

/**
 * A Maven plugin mojo to format Java source code using the Eclipse code
 * formatter.
 * 
 * Mojo parameters allow customizing formatting by specifying the config XML
 * file, line endings, compiler version, and source code locations. Reformatting
 * source files is avoided using an md5 hash of the content, comparing to the
 * original hash to the hash after formatting and a cached hash.
 * 
 * @author jecki
 * @author Matt Blanchette
 */
@Mojo(name = "format", defaultPhase = LifecyclePhase.PROCESS_SOURCES)
public class FormatterMojo extends AbstractMojo {
	
	/** The Constant CACHE_PROPERTIES_FILENAME. */
	private static final String CACHE_PROPERTIES_FILENAME = "maven-java-formatter-cache.properties";
	
	/** The Constant DEFAULT_INCLUDES. */
	private static final String[] DEFAULT_INCLUDES = new String[] { "**/*.java" };

	/** The Constant LINE_ENDING_AUTO. */
	static final String LINE_ENDING_AUTO = "AUTO";
	
	/** The Constant LINE_ENDING_KEEP. */
	static final String LINE_ENDING_KEEP = "KEEP";
	
	/** The Constant LINE_ENDING_LF. */
	static final String LINE_ENDING_LF = "LF";
	
	/** The Constant LINE_ENDING_CRLF. */
	static final String LINE_ENDING_CRLF = "CRLF";
	
	/** The Constant LINE_ENDING_CR. */
	static final String LINE_ENDING_CR = "CR";

	/** The Constant LINE_ENDING_LF_CHAR. */
	static final String LINE_ENDING_LF_CHAR = "\n";
	
	/** The Constant LINE_ENDING_CRLF_CHARS. */
	static final String LINE_ENDING_CRLF_CHARS = "\r\n";
	
	/** The Constant LINE_ENDING_CR_CHAR. */
	static final String LINE_ENDING_CR_CHAR = "\r";

	/**
	 * ResourceManager for retrieving the configFile resource.
	 */
	@Component(role=ResourceManager.class)
	private ResourceManager resourceManager;

	/**
	 * Project's source directory as specified in the POM.
	 */
	@Parameter(property = "project.build.sourceDirectory", readonly = true, required = true)
	private File sourceDirectory;

	/**
	 * Project's test source directory as specified in the POM.
	 */
	@Parameter(property = "project.build.testSourceDirectory", readonly = true, required = true)
	private File testSourceDirectory;

	/**
	 * Project's target directory as specified in the POM.
	 */
	@Parameter(property = "project.build.directory", readonly = true, required = true)
	private File targetDirectory;

	/**
	 * Project's base directory.
	 */
	@Parameter(property = "project.basedir", readonly = true, required = true)
	private File basedir;

	/**
	 * Location of the Java source files to format. Defaults to source main and
	 * test directories if not set. Deprecated in version 0.3. Reintroduced in
	 * 0.4.
	 * 
	 * @since 0.4
	 */
	@Parameter
	private File[] directories;

	/**
	 * List of fileset patterns for Java source locations to include in
	 * formatting. Patterns are relative to the project source and test source
	 * directories. When not specified, the default include is
	 * **/*.java
	 * 
	 * @since 0.3
	 */
	@Parameter
	private String[] includes;

	/**
	 * List of fileset patterns for Java source locations to exclude from
	 * formatting. Patterns are relative to the project source and test source
	 * directories. When not specified, there is no default exclude.
	 * 
	 * @since 0.3
	 */
	@Parameter
	private String[] excludes;

	/**
	 * Java compiler source version.
	 */
	@Parameter(defaultValue = "1.5", property = "maven.compiler.source")
	private String compilerSource;

	/**
	 * Java compiler compliance version.
	 */
	@Parameter(defaultValue = "1.5", property = "maven.compiler.source")
	private String compilerCompliance;

	/**
	 * Java compiler target version.
	 */
	@Parameter(defaultValue = "1.5", property = "maven.compiler.target")
	private String compilerTargetPlatform;

	/**
	 * The file encoding used to read and write source files. When not specified
	 * and sourceEncoding also not set, default is platform file encoding.
	 * 
	 * @since 0.3
	 */
	@Parameter(defaultValue = "${project.build.sourceEncoding}")
	private String encoding;

	/**
	 * Sets the line-ending of files after formatting. Valid values are:
	 * 
    *
  • "AUTO" - Use line endings of current system
  • *
  • "KEEP" - Preserve line endings of files, default to AUTO if * ambiguous
  • *
  • "LF" - Use Unix and Mac style line endings
  • *
  • "CRLF" - Use DOS and Windows style line endings
  • *
  • "CR" - Use early Mac style line endings
  • *
* * @since 0.2.0 */ @Parameter(defaultValue = "AUTO") private String lineEnding; /** * File or classpath location of an Eclipse code formatter configuration xml * file to use in formatting. */ @Parameter private String configFile; /** * Sets whether compilerSource, compilerCompliance, and * compilerTargetPlatform values are used instead of those defined in the * configFile. * * @since 0.2.0 */ @Parameter(defaultValue = "false") private Boolean overrideConfigCompilerVersion; /** * Whether the formatting is skipped. * * @since 0.5 */ @Parameter(defaultValue = "false", alias = "skip", property = "formatter.skip") private Boolean skipFormatting; /** The formatter. */ private CodeFormatter formatter; /** The collection. */ private PlexusIoFileResourceCollection collection; /** * Execute. * * @throws MojoExecutionException the mojo execution exception * @see org.apache.maven.plugin.AbstractMojo#execute() */ @Override public void execute() throws MojoExecutionException { if (this.skipFormatting) { getLog().info("Formatting is skipped"); return; } long startClock = System.currentTimeMillis(); if (StringUtils.isEmpty(this.encoding)) { this.encoding = ReaderFactory.FILE_ENCODING; getLog().warn( "File encoding has not been set, using platform encoding (" + this.encoding + ") to format source files, i.e. build is platform dependent!"); } else { try { "Test Encoding".getBytes(this.encoding); } catch (UnsupportedEncodingException e) { throw new MojoExecutionException("Encoding '" + this.encoding + "' is not supported"); } getLog().info( "Using '" + this.encoding + "' encoding to format source files."); } if (!LINE_ENDING_AUTO.equals(this.lineEnding) && !LINE_ENDING_KEEP.equals(this.lineEnding) && !LINE_ENDING_LF.equals(this.lineEnding) && !LINE_ENDING_CRLF.equals(this.lineEnding) && !LINE_ENDING_CR.equals(this.lineEnding)) { throw new MojoExecutionException( "Unknown value for lineEnding parameter"); } createResourceCollection(); List files = new ArrayList(); try { if (this.directories != null) { for (File directory : this.directories) { if (directory.exists() && directory.isDirectory()) { this.collection.setBaseDir(directory); addCollectionFiles(files); } } } else { // Using defaults of source main and test dirs if (this.sourceDirectory != null && this.sourceDirectory.exists() && this.sourceDirectory.isDirectory()) { this.collection.setBaseDir(this.sourceDirectory); addCollectionFiles(files); } if (this.testSourceDirectory != null && this.testSourceDirectory.exists() && this.testSourceDirectory.isDirectory()) { this.collection.setBaseDir(this.testSourceDirectory); addCollectionFiles(files); } } } catch (IOException e) { throw new MojoExecutionException( "Unable to find files using includes/excludes", e); } int numberOfFiles = files.size(); Log log = getLog(); log.info("Number of files to be formatted: " + numberOfFiles); if (numberOfFiles > 0) { createCodeFormatter(); ResultCollector rc = new ResultCollector(); Properties hashCache = readFileHashCacheFile(); String basedirPath = getBasedirPath(); for (int i = 0, n = files.size(); i < n; i++) { File file = files.get(i); formatFile(file, rc, hashCache, basedirPath); } storeFileHashCache(hashCache); long endClock = System.currentTimeMillis(); log.info("Successfully formatted: " + rc.successCount + " file(s)"); log.info("Fail to format : " + rc.failCount + " file(s)"); log.info("Skipped : " + rc.skippedCount + " file(s)"); log.info("Approximate time taken: " + ((endClock - startClock) / 1000) + "s"); } } /** * Create a {@link PlexusIoFileResourceCollection} instance to be used by * this mojo. This collection uses the includes and excludes to find the * source files. */ void createResourceCollection() { this.collection = new PlexusIoFileResourceCollection(); if (this.includes != null && this.includes.length > 0) { this.collection.setIncludes(this.includes); } else { this.collection.setIncludes(DEFAULT_INCLUDES); } this.collection.setExcludes(this.excludes); this.collection.setIncludingEmptyDirectories(false); IncludeExcludeFileSelector fileSelector = new IncludeExcludeFileSelector(); fileSelector.setIncludes(DEFAULT_INCLUDES); this.collection.setFileSelectors(new FileSelector[] { fileSelector }); } /** * Add source files from the {@link PlexusIoFileResourceCollection} to the * files list. * * @param files the files * @throws IOException Signals that an I/O exception has occurred. */ void addCollectionFiles(List files) throws IOException { Iterator resources = this.collection.getResources(); while (resources.hasNext()) { PlexusIoFileResource resource = (PlexusIoFileResource) resources .next(); files.add(resource.getFile()); } } /** * Gets the basedir path. * * @return the basedir path */ private String getBasedirPath() { try { return this.basedir.getCanonicalPath(); } catch (Exception e) { return ""; } } /** * Store file hash cache. * * @param props the props */ private void storeFileHashCache(Properties props) { File cacheFile = new File(this.targetDirectory, CACHE_PROPERTIES_FILENAME); try { OutputStream out = new BufferedOutputStream(new FileOutputStream( cacheFile)); props.store(out, null); } catch (FileNotFoundException e) { getLog().warn("Cannot store file hash cache properties file", e); } catch (IOException e) { getLog().warn("Cannot store file hash cache properties file", e); } } /** * Read file hash cache file. * * @return the properties */ private Properties readFileHashCacheFile() { Properties props = new Properties(); Log log = getLog(); if (!this.targetDirectory.exists()) { this.targetDirectory.mkdirs(); } else if (!this.targetDirectory.isDirectory()) { log.warn("Something strange here as the " + "supposedly target directory is not a directory."); return props; } File cacheFile = new File(this.targetDirectory, CACHE_PROPERTIES_FILENAME); if (!cacheFile.exists()) { return props; } try { props.load(new BufferedInputStream(new FileInputStream(cacheFile))); } catch (FileNotFoundException e) { log.warn("Cannot load file hash cache properties file", e); } catch (IOException e) { log.warn("Cannot load file hash cache properties file", e); } return props; } /** * Format file. * * @param file the file * @param rc the rc * @param hashCache the hash cache * @param basedirPath the basedir path */ private void formatFile(File file, ResultCollector rc, Properties hashCache, String basedirPath) { try { doFormatFile(file, rc, hashCache, basedirPath); } catch (IOException e) { rc.failCount++; getLog().warn(e); } catch (MalformedTreeException e) { rc.failCount++; getLog().warn(e); } catch (BadLocationException e) { rc.failCount++; getLog().warn(e); } } /** * Format individual file. * * @param file the file * @param rc the rc * @param hashCache the hash cache * @param basedirPath the basedir path * @throws IOException Signals that an I/O exception has occurred. * @throws BadLocationException the bad location exception */ private void doFormatFile(File file, ResultCollector rc, Properties hashCache, String basedirPath) throws IOException, BadLocationException { Log log = getLog(); log.debug("Processing file: " + file); String code = readFileAsString(file); String originalHash = md5hash(code); String canonicalPath = file.getCanonicalPath(); String path = canonicalPath.substring(basedirPath.length()); String cachedHash = hashCache.getProperty(path); if (cachedHash != null && cachedHash.equals(originalHash)) { rc.skippedCount++; log.debug("File is already formatted."); return; } String lineSeparator = getLineEnding(code); TextEdit te = this.formatter.format(CodeFormatter.K_COMPILATION_UNIT + CodeFormatter.F_INCLUDE_COMMENTS, code, 0, code.length(), 0, lineSeparator); if (te == null) { rc.skippedCount++; log.debug("Code cannot be formatted. Possible cause " + "is unmatched source/target/compliance version."); return; } IDocument doc = new Document(code); te.apply(doc); String formattedCode = doc.get(); String formattedHash = md5hash(formattedCode); hashCache.setProperty(path, formattedHash); if (originalHash.equals(formattedHash)) { rc.skippedCount++; log.debug("Equal hash code. Not writing result to file."); return; } writeStringToFile(formattedCode, file); rc.successCount++; } /** * Md5hash. * * @param str the str * @return the string * @throws UnsupportedEncodingException the unsupported encoding exception */ private String md5hash(String str) throws UnsupportedEncodingException { return DigestUtils.md5Hex(str.getBytes(this.encoding)); } /** * Read the given file and return the content as a string. * * @param file the file * @return the string * @throws IOException Signals that an I/O exception has occurred. */ private String readFileAsString(File file) throws java.io.IOException { StringBuilder fileData = new StringBuilder(1000); BufferedReader reader = null; try { reader = new BufferedReader(ReaderFactory.newReader(file, this.encoding)); char[] buf = new char[1024]; int numRead = 0; while ((numRead = reader.read(buf)) != -1) { String readData = String.valueOf(buf, 0, numRead); fileData.append(readData); buf = new char[1024]; } } finally { IOUtil.close(reader); } return fileData.toString(); } /** * Write the given string to a file. * * @param str the str * @param file the file * @throws IOException Signals that an I/O exception has occurred. */ private void writeStringToFile(String str, File file) throws IOException { if (!file.exists() && file.isDirectory()) { return; } BufferedWriter bw = null; try { bw = new BufferedWriter(WriterFactory.newWriter(file, this.encoding)); bw.write(str); } finally { IOUtil.close(bw); } } /** * Create a {@link CodeFormatter} instance to be used by this mojo. * * @throws MojoExecutionException the mojo execution exception */ private void createCodeFormatter() throws MojoExecutionException { Map options = getFormattingOptions(); this.formatter = ToolFactory.createCodeFormatter(options); } /** * Return the options to be passed when creating {@link CodeFormatter} * instance. * * @return the formatting options * @throws MojoExecutionException the mojo execution exception */ private Map getFormattingOptions() throws MojoExecutionException { Map options = new HashMap(); options.put(JavaCore.COMPILER_SOURCE, this.compilerSource); options.put(JavaCore.COMPILER_COMPLIANCE, this.compilerCompliance); options.put(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, this.compilerTargetPlatform); if (this.configFile != null) { Map config = getOptionsFromConfigFile(); if (Boolean.TRUE.equals(this.overrideConfigCompilerVersion)) { config.remove(JavaCore.COMPILER_SOURCE); config.remove(JavaCore.COMPILER_COMPLIANCE); config.remove(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM); } options.putAll(config); } return options; } /** * Read config file and return the config as {@link Map}. * * @return the options from config file * @throws MojoExecutionException the mojo execution exception */ private Map getOptionsFromConfigFile() throws MojoExecutionException { InputStream configInput = null; try { this.resourceManager.addSearchPath(FileResourceLoader.ID, this.basedir.getAbsolutePath()); configInput = this.resourceManager.getResourceAsInputStream(this.configFile); } catch (ResourceNotFoundException e) { throw new MojoExecutionException("Config file [" + this.configFile + "] cannot be found", e); } if (configInput == null) { throw new MojoExecutionException("Config file [" + this.configFile + "] does not exist"); } try { ConfigReader configReader = new ConfigReader(); return configReader.read(configInput); } catch (IOException e) { throw new MojoExecutionException("Cannot read config file [" + this.configFile + "]", e); } catch (SAXException e) { throw new MojoExecutionException("Cannot parse config file [" + this.configFile + "]", e); } catch (ConfigReadException e) { throw new MojoExecutionException(e.getMessage(), e); } finally { if (configInput != null) { try { configInput.close(); } catch (IOException e) { // ignore } } } } /** * Returns the lineEnding parameter as characters when the value is known * (LF, CRLF, CR) or can be determined from the file text (KEEP). Otherwise * null is returned. * * @param fileDataString the file data string * @return the line ending */ String getLineEnding(String fileDataString) { String lineEnd = null; if (LINE_ENDING_KEEP.equals(this.lineEnding)) { lineEnd = determineLineEnding(fileDataString); } else if (LINE_ENDING_LF.equals(this.lineEnding)) { lineEnd = LINE_ENDING_LF_CHAR; } else if (LINE_ENDING_CRLF.equals(this.lineEnding)) { lineEnd = LINE_ENDING_CRLF_CHARS; } else if (LINE_ENDING_CR.equals(this.lineEnding)) { lineEnd = LINE_ENDING_CR_CHAR; } return lineEnd; } /** * Returns the most occurring line-ending characters in the file text or * null if no line-ending occurs the most. * * @param fileDataString the file data string * @return the string */ String determineLineEnding(String fileDataString) { int lfCount = 0; int crCount = 0; int crlfCount = 0; for (int i = 0; i < fileDataString.length(); i++) { char c = fileDataString.charAt(i); if (c == '\r') { if ((i + 1) < fileDataString.length() && fileDataString.charAt(i + 1) == '\n') { crlfCount++; i++; } else { crCount++; } } else if (c == '\n') { lfCount++; } } if (lfCount > crCount && lfCount > crlfCount) { return LINE_ENDING_LF_CHAR; } else if (crlfCount > lfCount && crlfCount > crCount) { return LINE_ENDING_CRLF_CHARS; } else if (crCount > lfCount && crCount > crlfCount) { return LINE_ENDING_CR_CHAR; } return null; } /** * The Class ResultCollector. */ private class ResultCollector { public ResultCollector() { // Prevent synthetic access } /** The success count. */ int successCount; /** The fail count. */ int failCount; /** The skipped count. */ int skippedCount; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy